feat: server management in app (#5628)

* start new server settings tabs

* update properties tab to match design

* better stying in general tab

* feat: add suffix input for hostname field

* implement tables for allocations and DNS records

* add tags for dns record type

* small gap adjustment

* polish advanced page

* adjust properties page hierarchy

* fix searching properties, empty state and projection radius appearing

* pnpm prepr

* update copy to match designs

* fix suffix input component

* style fixes and match heading size

* small fix

* fix search allocations placeholder

* adjust table styles

* move all installation settings helper text to below input

* update icon to use overflow menu buttons

* fix modal to be consistent

* open advanced properties when search

* remove other and custom properties, and update styles

* remove hide/show all java versions

* handle mc 26

* refactor: move server settings pages into /ui and add app ServerSettingsModal

* hook up server pages for app

* add server page header to app

* hook up server settings modal

* use large size

* fix card box shadow style

* fix hostname input for app

* fix app/website card containers

* implement external tabs for billing and admin billing

* fix save banner fixed to parent instead of page body

* remove unused prop to FriendsList causing warning in app

* fix client-only not available for app

* fix bottom cut off

* wire node auth

* implement full copy buttons

* dedup copy button tailwind styles

* fix hover class not working in @apply

* fix spacing

* fix error validation styles

* apply consistent styles and spacing

* feat: update hosting server card (#5609)

* fix type errors

* fix some stylesheets not imported for storybook

* add server listing stories

* add fix for frontend stylesheet imports

* remove props.

* convert copy code to use tailwind

* update server listing component styles

* update server info label styles

* start status/player count info label, more style updates and fixes

* add new server card buttons

* hook up server cards and implement updated styles

* hook up on download button

* fix tauri throwing error when api returns 204 No Content

* hook up purchase server modal in app

* fix upgrading state loading icon

* pnpm prepr

* filter out servers past 30 days after cancellation

* do not apply opacity on lock or spiner icons

* fix disabled server icon background

* update pending change stage

* handle known suspension states

* refactor: reduce code duplication for server listing

* update disabled state text color

* fix loading icon color

* clean up copy

* fix disabled opacity for server card

* update server listing files kept to be countdown

* implement resubscribe modal

* implement proper provisioning state for resubscribe

* fix duplicate attribute and pnpm prepr

* feat: add shared UI package auth DI

* feat: update purchase server flow (#5714)

* implement server list empty state component

* fix stories and adjust spacing

* implement select plan design refresh

* implement auth for empty server list

* use refs instead of reactive

* pnpm prepr

* fix auth usage for empty servers list

* move app auth provider setup to src/providers/setup

* pnpm prepr

* fix max height

* style fix

* fix getCreds no auth is blocking api client

* implement servers guest plan modal and signin which redirects back to modal's next step

* refactor guest plan select logic into provider

* implement sign in or create account popup

* remove force empty serverList

* add download button for suspended mod and generic

* add handling for when user logs out

* QA pass style fixes

* more consistent page styles

* fix duplicate export

* refactor: remove all fallback stuff from resubscribe modal

* implement shared download latest backup util

* i18n pass

* pnpm prepr

* fix region being selected if ping failed

* pnpm prepr

* feat: servers in app finalization (#5744)

* feat: start on shared console implementation into logs and overview pages

* fix: terminal gap issues

* feat: swap word wrap for full screen

* fix: stats cards alignment

* fix: stats

* feat: fix console clear + remove copy

* fix: lint

* fix: use reset not clear

* feat: shared server header & overview page for app and website (#5736)

* feat: implement shared server header for app and website

* feat: implement wrapped overview page with shared composable and hook it up

* pnpm prepr

* fix: bugs

* qa: cleanup

* feat: root.vue shared layout

* feat: delete old options pages + fix discovery frontend

* fix: discovery

* fix: misc style/layout issues

* fix page padding

* fix: modal height jankiness

* feat: implement server install content in app and server setup modal with DI

* fix: spacing

* remove servers in app feature flag

* Revert "remove servers in app feature flag"

This reverts commit 86e284c4bdd6fa42c3c8fbaf1efbec41f4d1c6d2.

* fix: qa

* feat: remove legacy components from apps/frontend/src/components/ui/servers

---------

Co-authored-by: Calum H. (IMB11) <contact@cal.engineer>

* qa pass (#5738)

* fix: qa

* feat: qa

* fix: server icon fetch fails due to global node auth race condition overriding each other

* fix: lint

* fix: server icon upload/sync and centralize logic

* fix: server settings modal not closing for server reset

* fix: better server sorting

* feat: copy address in server listing card

* fix: notification panel in modal and when overlapping with action bar

* fix: empty server list empty state flashing when refresh, fixed by adding isReady auth flag

* feat: use floating action bar for save banner

* fix: saving state in save bar

* fix: edit server icon styling

* fix: confirm modal to have consistent buttons

* feat: loading animation for server panel + caching improvements for app

* pnpm prepr

* feat: search page deduplication (#5754)

* fix: action bar behind modal

* fix: remove warning modal for stopping

* fix: server cards states

* we hate webkit we hate webkit

* fix: update allocation creation to not use modal

* fix: properties tab spacing and styles

* feat: add files tab copy

* fix: advanced properties icon

* fix: remove back to all servers link

* feat: add files tab link in copy

* fix: server header styles to be consistent with instance

* fix: add header icons back

* feat: update instance settings icon to be consistent

* fix: icon container

* feat: upload state persistence across tabs

* fix: server labels text wrapping

* fix: use surface-5 border

* fix: loading spinner showing with onboarding below

* feat: new server button shows purchase modal in website

* fix: billing page not showing quarterly interval

* fix: server downgrade not showing updated subscription notification

* fix: server settings invalidate saved state and remove server context provider since its already provided in the page

* pnpm prepr

* add stripe publishable key to app build

* feat: console highlighting

* fix: rename servers title to modrinth hosting

* feat: search fix

* fix: qa/styles

* fix: ip click active and remove power dont ask again

* fix: qa

* feat: highlighting fix console

* fix: disable conflicts action

* fix: error dismiss bug

* feat: modal clarification

* fix: files perms issue

* fix: lint

* feat: modal fix

* enable show uptime

* fix: add loading state to edit server icon

* fix: notification panel take in has sidebar from settings

* fix: consistency pass on app settings

* fix: consistency pass on instance settings

* pnpm prepr

* fix: nagivate to billing button in app to go to website

* fix: stripe return url in app causing app to open modrinth.com in tauri

* refactor: better show polling UI code

* fix: new server polling comparison to use server ids instead of length

* fix: buttonstyled story

* fix: button styling

* fix: content.vue regression

* feat: project url redirects

* fix: breadcrumbs

* fix: purchase with newly added card

* fix: console ordering problems

* fix: app-frontend missing env config and staging environment

* fix: log syncing for instances and server panel accidentally

* fix: QA issues

* fix: server page loading state

* fix: stats card logic

* fix: lint

* fix: qa

* fix: console height padding

* fix: terminal padding + loading indicator

* feat: update medal server listing styling

* fix: no upgrade button for medal server listing in app

* fix: go to overview instead of content tab after onboarding

* fix: qa

* fix: teleport modals to body

* fix: logs tab + qa

* fix: local storage for user preferences

* fix: qa loading indic

* feat: considitonal debug and trace

* fix: jump to top on install bug

* feat: swap out server hard drive icon to server stack icon

* feat: servers in app feature flag default true

* fix: highlight row ufll

* fix: webkit thing onto a tag

* fix: input field

* fix: clear fix

* fix: lint

* fix: fmt

* feat: improve share modal and bring it back for sharing log

* pnpm prepr

* fix: menu overflowing

* feat: remove servers in app feature flag

* fix: server stat charts no longer showing color

* fix: library nav no primary state

* fix: better modal height and width

* fix: highlighting bugs

* fix: empty states

* fix: delay import to fix overview page slow load on MacOS

* fix: medal server listing too bright on light mode

* fix: admon analysis + fix logs

* fix: bug

* fix: clear purchase intent from sign-in after closing modal

* performance: improve server manage stats loading by splitting reactivity

* fix: deploy + admon + disable highlighting

* fix: clippy

---------

Co-authored-by: tdgao <mr.trumgao@gmail.com>
Co-authored-by: Truman Gao <106889354+tdgao@users.noreply.github.com>

* feat: temp wrangler

* fix: lint

* fix: logs upload

* fix: console empty state and admon regressions

* fix: fields

* feat: log deleting + prefetch for Logs.vue

* feat: move delete before share

* feat: clear endpoint

* feat: we ball!

---------

Co-authored-by: Calum H. <calum@modrinth.com>
Co-authored-by: Calum H. (IMB11) <contact@cal.engineer>
This commit is contained in:
Truman Gao
2026-04-12 15:38:08 -06:00
committed by GitHub
parent a2a97d1313
commit 693a371d61
278 changed files with 15974 additions and 12608 deletions

View File

@@ -0,0 +1 @@
export * from './use-browse-search'

View File

@@ -0,0 +1,311 @@
import type { Labrinth } from '@modrinth/api-client'
import type { ComputedRef, Ref, ShallowRef } from 'vue'
import { computed, nextTick, ref, shallowRef, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useDebugLogger } from '#ui/composables/debug-logger'
import type { FilterType, FilterValue, ProjectType, SortType } from '#ui/utils/search'
import { useSearch } from '#ui/utils/search'
import { useServerSearch } from '#ui/utils/server-search'
import type { BrowseSearchResponse } from '../types'
export interface UseBrowseSearchOptions {
projectType: Ref<string>
tags: Ref<{
gameVersions: Labrinth.Tags.v2.GameVersion[]
loaders: Labrinth.Tags.v2.Loader[]
categories: Labrinth.Tags.v2.Category[]
}>
providedFilters?: ComputedRef<FilterValue[]>
search: (params: string) => Promise<BrowseSearchResponse>
persistentQueryParams: string[]
getExtraQueryParams?: () => Record<string, string | undefined>
maxResultsOptions?: ComputedRef<number[]>
displayMode?: Ref<'list' | 'grid' | 'gallery'> | ComputedRef<'list' | 'grid' | 'gallery'>
}
export interface BrowseSearchState {
query: Ref<string>
filters: ComputedRef<FilterType[]>
currentFilters: Ref<FilterValue[]>
toggledGroups: Ref<string[]>
overriddenProvidedFilterTypes: Ref<string[]>
serverFilterTypes: ComputedRef<FilterType[]>
serverCurrentFilters: Ref<FilterValue[]>
serverToggledGroups: Ref<string[]>
effectiveSortTypes: ComputedRef<readonly SortType[]>
effectiveCurrentSortType: Ref<SortType>
loading: Ref<boolean>
projectHits: ShallowRef<BrowseSearchResponse['projectHits']>
serverHits: ShallowRef<BrowseSearchResponse['serverHits']>
totalHits: Ref<number>
pageCount: ComputedRef<number>
maxResults: Ref<number>
currentPage: Ref<number>
isServerType: ComputedRef<boolean>
effectiveLayout: ComputedRef<'list' | 'grid'>
deprioritizedTags: ComputedRef<string[]>
excludeLoaders: ComputedRef<boolean>
refreshSearch: () => Promise<void>
setPage: (page: number) => Promise<void>
clearSearch: () => void
onFilterChange: () => void
}
const LOADER_FILTER_TYPES = [
'mod_loader',
'plugin_loader',
'modpack_loader',
'shader_loader',
'plugin_platform',
] as const
export function useBrowseSearch(options: UseBrowseSearchOptions): BrowseSearchState {
const debug = useDebugLogger('BrowseSearch')
const route = useRoute()
const router = useRouter()
debug('init, projectType:', options.projectType.value)
const projectTypes = computed(() => [options.projectType.value] as ProjectType[])
const isServerType = computed(() => options.projectType.value === 'server')
const {
query,
currentSortType,
currentFilters,
toggledGroups,
maxResults,
currentPage,
overriddenProvidedFilterTypes,
filters,
sortTypes,
requestParams,
createPageParams,
} = useSearch(projectTypes, options.tags, options.providedFilters ?? computed(() => []))
const {
serverCurrentSortType,
serverCurrentFilters,
serverToggledGroups,
serverSortTypes,
serverFilterTypes,
serverRequestParams,
createServerPageParams,
} = useServerSearch({ tags: options.tags, query, maxResults, currentPage })
const effectiveRequestParams = computed(() =>
isServerType.value ? serverRequestParams.value : requestParams.value,
)
const effectiveSortTypes = computed(() =>
isServerType.value ? (serverSortTypes as readonly SortType[]) : sortTypes,
)
const effectiveCurrentSortType = computed({
get: () => (isServerType.value ? serverCurrentSortType.value : currentSortType.value),
set: (v: SortType) => {
if (isServerType.value) serverCurrentSortType.value = v
else currentSortType.value = v
},
})
const effectiveMaxResultsOptions = computed(
() => options.maxResultsOptions?.value ?? [5, 10, 15, 20, 50, 100],
)
watch(effectiveMaxResultsOptions, (opts) => {
if (!opts.includes(maxResults.value)) {
maxResults.value = opts.reduce((prev, curr) =>
Math.abs(curr - maxResults.value) <= Math.abs(prev - maxResults.value) ? curr : prev,
)
}
})
const effectiveDisplayMode = computed(() => options.displayMode?.value ?? 'list')
const effectiveLayout = computed<'list' | 'grid'>(() =>
effectiveDisplayMode.value === 'grid' || effectiveDisplayMode.value === 'gallery'
? 'grid'
: 'list',
)
const selectedFilterTags = computed(() =>
currentFilters.value
.filter(
(f) =>
f.type.startsWith('category_') ||
LOADER_FILTER_TYPES.includes(f.type as (typeof LOADER_FILTER_TYPES)[number]),
)
.map((f) => f.option),
)
const excludeLoaders = computed(
() =>
currentFilters.value.some((f) =>
LOADER_FILTER_TYPES.includes(f.type as (typeof LOADER_FILTER_TYPES)[number]),
) || ['resourcepack', 'datapack'].includes(options.projectType.value),
)
const loadersNotForThisType = computed(
() =>
options.tags.value?.loaders
?.filter((loader) => !loader.supported_project_types.includes(options.projectType.value))
?.map((loader) => loader.name) ?? [],
)
const deprioritizedTags = computed(() => [
...selectedFilterTags.value,
...loadersNotForThisType.value,
])
const loading = ref(true)
const projectHits = shallowRef<BrowseSearchResponse['projectHits']>([])
const serverHits = shallowRef<BrowseSearchResponse['serverHits']>([])
const totalHits = ref(0)
const pageCount = computed(() => {
if (totalHits.value === 0) return 1
return Math.ceil(totalHits.value / maxResults.value)
})
let searchVersion = 0
let searchDebounceTimer: ReturnType<typeof setTimeout> | null = null
watch(effectiveRequestParams, (newVal, oldVal) => {
debug('effectiveRequestParams changed', {
from: oldVal?.substring(0, 80),
to: newVal?.substring(0, 80),
})
if (searchDebounceTimer) clearTimeout(searchDebounceTimer)
searchDebounceTimer = setTimeout(() => {
refreshSearch()
}, 200)
})
async function refreshSearch() {
const version = ++searchVersion
debug('refreshSearch start', {
version,
projectType: options.projectType.value,
params: effectiveRequestParams.value.substring(0, 100),
})
const currentHitsEmpty = isServerType.value
? serverHits.value.length === 0
: projectHits.value.length === 0
if (currentHitsEmpty) {
loading.value = true
}
try {
const response = await options.search(effectiveRequestParams.value)
if (version !== searchVersion) {
debug('refreshSearch stale, discarding', { version, current: searchVersion })
return
}
if (isServerType.value) {
serverHits.value = response.serverHits
} else {
projectHits.value = response.projectHits
}
totalHits.value = response.total_hits
debug('refreshSearch complete', {
version,
hits: response.total_hits,
projectHits: response.projectHits.length,
serverHits: response.serverHits.length,
})
updateUrlParams()
loading.value = false
} catch (err) {
debug('refreshSearch error', err)
console.error('Browse search error:', err)
if (version === searchVersion) {
loading.value = false
}
}
}
function updateUrlParams() {
debug('updateUrlParams', { path: route.path })
const persistentParams: Record<string, string | (string | null)[] | null | undefined> = {}
for (const [key, value] of Object.entries(route.query)) {
if (options.persistentQueryParams.includes(key)) {
persistentParams[key] = value
}
}
const extraParams = options.getExtraQueryParams?.() ?? {}
for (const [key, value] of Object.entries(extraParams)) {
if (value !== undefined) {
persistentParams[key] = value
}
}
const params = {
...persistentParams,
...(isServerType.value ? createServerPageParams() : createPageParams()),
}
router.replace({ path: route.path, query: params })
}
async function setPage(newPageNumber: number) {
currentPage.value = newPageNumber
await nextTick()
window.scrollTo({ top: 0, behavior: 'smooth' })
}
function clearSearch() {
query.value = ''
currentPage.value = 1
}
function onFilterChange() {
nextTick(() => window.scrollTo({ top: 0, behavior: 'smooth' }))
}
watch(
() => options.projectType.value,
(newType, oldType) => {
debug('projectType changed', { from: oldType, to: newType })
currentSortType.value = { display: 'Relevance', name: 'relevance' }
query.value = ''
},
)
return {
query,
filters,
currentFilters,
toggledGroups,
overriddenProvidedFilterTypes,
serverFilterTypes,
serverCurrentFilters,
serverToggledGroups,
effectiveSortTypes,
effectiveCurrentSortType,
loading,
projectHits,
serverHits,
totalHits,
pageCount,
maxResults,
currentPage,
isServerType,
effectiveLayout,
deprioritizedTags,
excludeLoaders,
refreshSearch,
setPage,
clearSearch,
onFilterChange,
}
}