This commit is contained in:
Calum H. (IMB11)
2026-06-26 17:09:23 +01:00
parent 417c6964f6
commit 4986bc194d
13 changed files with 99 additions and 216 deletions
@@ -54,7 +54,6 @@ export const DEFAULT_FEATURE_FLAGS = validateValues({
showModeratorProjectMemberUi: false, showModeratorProjectMemberUi: false,
showModeratorPrivateMessageHighlight: true, showModeratorPrivateMessageHighlight: true,
archonApiStaging: false, archonApiStaging: false,
showHostingAccessInstanceAuditLog: false,
versionDevInfoCollapsed: true, versionDevInfoCollapsed: true,
alwaysShowVersionDevInfo: false, alwaysShowVersionDevInfo: false,
} as const) } as const)
@@ -9,74 +9,22 @@ import { useQueryClient } from '@tanstack/vue-query'
const client = injectModrinthClient() const client = injectModrinthClient()
const { server, serverId } = injectModrinthServerContext() const { server, serverId } = injectModrinthServerContext()
const queryClient = useQueryClient() const queryClient = useQueryClient()
const flags = useFeatureFlags()
const ACTION_LOG_PAGE_SIZE = 200
const ACTION_LOG_SORT_DIRECTION = 'desc'
const actionLogDateFilter = defaultActionLogDateFilter()
await Promise.allSettled([ try {
queryClient.ensureQueryData({ await queryClient.ensureQueryData({
queryKey: ['servers', 'users', 'v1', serverId], queryKey: ['servers', 'users', 'v1', serverId],
queryFn: () => client.archon.server_users_v1.list(serverId), queryFn: () => client.archon.server_users_v1.list(serverId),
staleTime: 30_000, staleTime: 30_000,
}), })
queryClient.ensureQueryData({ } catch {
queryKey: ['servers', 'v1', 'detail', serverId], // Let mounted layouts' useQuery surface errors; do not fail route setup.
queryFn: () => client.archon.servers_v1.get(serverId), }
staleTime: 30_000,
}),
queryClient.ensureInfiniteQueryData({
queryKey: [
'servers',
'action-log',
'v1',
'infinite',
serverId,
null,
actionLogDateFilter.min_datetime,
actionLogDateFilter.max_datetime,
ACTION_LOG_SORT_DIRECTION,
],
queryFn: ({ pageParam = 0 }) => {
const offset = typeof pageParam === 'number' ? pageParam : 0
return client.archon.actions_v1.list(serverId, {
limit: ACTION_LOG_PAGE_SIZE,
offset,
order: ACTION_LOG_SORT_DIRECTION,
...actionLogDateFilter,
})
},
getNextPageParam: (lastPage) =>
typeof lastPage.next_offset === 'number' ? lastPage.next_offset : undefined,
initialPageParam: 0,
staleTime: 30_000,
}),
])
useHead({ useHead({
title: computed(() => `Access - ${server.value?.name ?? 'Server'} - Modrinth`), title: computed(() => `Access - ${server.value?.name ?? 'Server'} - Modrinth`),
}) })
function defaultActionLogDateFilter() {
const endDate = new Date()
const startDate = new Date(endDate)
startDate.setDate(startDate.getDate() - 6)
return {
min_datetime: startOfDay(startDate).toISOString(),
max_datetime: endOfDay(endDate).toISOString(),
}
}
function startOfDay(date: Date) {
return new Date(date.getFullYear(), date.getMonth(), date.getDate())
}
function endOfDay(date: Date) {
return new Date(date.getFullYear(), date.getMonth(), date.getDate(), 23, 59, 59, 999)
}
</script> </script>
<template> <template>
<ServersManageAccessPage :show-audit-log-instances="flags.showHostingAccessInstanceAuditLog" /> <ServersManageAccessPage />
</template> </template>
@@ -96,9 +96,13 @@ async function loadContentSummary(
index: number, index: number,
): Promise<ContentSummary> { ): Promise<ContentSummary> {
try { try {
const content = await client.archon.content_v1.getAddons(serverId, world.id, { const content = await queryClient.fetchQuery({
addons: true, queryKey: ['content', 'list', 'v1', serverId, world.id],
updates: false, queryFn: () =>
client.archon.content_v1.getAddons(serverId, world.id, {
from_modpack: false,
}),
staleTime: 0,
}) })
return { return {
@@ -106,13 +110,35 @@ async function loadContentSummary(
loader: content.modloader ?? world.content?.modloader ?? null, loader: content.modloader ?? world.content?.modloader ?? null,
loaderVersion: content.modloader_version ?? world.content?.modloader_version ?? null, loaderVersion: content.modloader_version ?? world.content?.modloader_version ?? null,
linkedModpack: getLinkedModpack(content.modpack), linkedModpack: getLinkedModpack(content.modpack),
installedContentCount: content.addons?.length ?? 0, installedContentCount: await getInstalledContentCount(world.id, content),
} }
} catch { } catch {
return createDummyContentSummary(world, index) return createDummyContentSummary(world, index)
} }
} }
async function getInstalledContentCount(
worldId: string,
content: Archon.Content.v1.Addons,
): Promise<number> {
const addonCount = content.addons?.length ?? 0
if (!content.modpack) return addonCount
try {
const modpackContent = await queryClient.fetchQuery({
queryKey: ['content', 'list', 'v1', serverId, worldId, 'modpack'],
queryFn: () =>
client.archon.content_v1.getAddons(serverId, worldId, {
from_modpack: true,
}),
staleTime: 0,
})
return addonCount + (modpackContent.addons?.length ?? 0)
} catch {
return addonCount
}
}
function toWorldSlot(world: Archon.Servers.v1.WorldFull, content: ContentSummary): WorldSlot { function toWorldSlot(world: Archon.Servers.v1.WorldFull, content: ContentSummary): WorldSlot {
return { return {
type: 'world', type: 'world',
@@ -17,6 +17,7 @@ const props = defineProps<{
ariaLabel?: string ariaLabel?: string
belowModal?: boolean belowModal?: boolean
hideWhenModalOpen?: boolean hideWhenModalOpen?: boolean
toolbarMaxWidth?: string
}>() }>()
const INTERCOM_BUBBLE_GAP = 8 const INTERCOM_BUBBLE_GAP = 8
@@ -46,6 +47,11 @@ const barStyle = computed(() => ({
'--floating-action-bar-left-offset': leftOffset.value, '--floating-action-bar-left-offset': leftOffset.value,
'--floating-action-bar-right-offset': rightOffset.value, '--floating-action-bar-right-offset': rightOffset.value,
})) }))
const toolbarStyle = computed(() =>
props.toolbarMaxWidth
? { '--floating-action-bar-toolbar-max-width': props.toolbarMaxWidth }
: undefined,
)
function checkCompact() { function checkCompact() {
const el = toolbarEl.value const el = toolbarEl.value
@@ -203,8 +209,9 @@ onUnmounted(() => {
ref="toolbarEl" ref="toolbarEl"
role="toolbar" role="toolbar"
:aria-label="ariaLabel" :aria-label="ariaLabel"
class="relative overflow-clip flex items-center gap-1.5 rounded-[20px] bg-surface-3 border border-surface-5 border-solid mx-auto md:max-w-[60vw] px-3 py-2.5 shadow-[0px_1px_3px_0px_rgba(0,0,0,0.3),0px_6px_10px_0px_rgba(0,0,0,0.15)]" class="floating-action-toolbar relative overflow-clip flex items-center gap-1.5 rounded-[20px] bg-surface-3 border border-surface-5 border-solid mx-auto px-3 py-2.5 shadow-[0px_1px_3px_0px_rgba(0,0,0,0.3),0px_6px_10px_0px_rgba(0,0,0,0.15)]"
:class="{ 'bar-compact': compact }" :class="{ 'bar-compact': compact }"
:style="toolbarStyle"
> >
<slot /> <slot />
</div> </div>
@@ -220,6 +227,12 @@ onUnmounted(() => {
transition: bottom 0.25s ease-in-out; transition: bottom 0.25s ease-in-out;
} }
@media (min-width: 768px) {
.floating-action-toolbar {
max-width: var(--floating-action-bar-toolbar-max-width, 60vw);
}
}
.floating-action-bar-enter-active { .floating-action-bar-enter-active {
transition: transition:
transform 0.25s cubic-bezier(0.15, 1.4, 0.64, 0.96), transform 0.25s cubic-bezier(0.15, 1.4, 0.64, 0.96),
@@ -5,7 +5,7 @@
class="flex flex-col gap-2" class="flex flex-col gap-2"
> >
<span class="font-semibold text-contrast"> <span class="font-semibold text-contrast">
{{ formatMessage(messages.worldNameLabel) }} <span class="text-red">*</span> {{ formatMessage(messages.worldNameLabel) }}
</span> </span>
<StyledInput <StyledInput
v-model="worldName" v-model="worldName"
@@ -31,42 +31,6 @@
row-key="id" row-key="id"
:row-transition-name="rowTransitionName" :row-transition-name="rowTransitionName"
> >
<template #header-world="{ column }">
<span class="inline-flex min-w-0 max-w-full items-center gap-1 font-semibold">
<span class="min-w-0 truncate">{{ column.label }}</span>
<Tooltip
theme="dismissable-prompt"
class="inline-flex shrink-0"
:triggers="['hover', 'focus']"
:popper-triggers="['hover', 'focus']"
popper-class="v-popper--interactive"
placement="top"
:delay="{ show: 200, hide: 100 }"
no-auto-focus
>
<button
type="button"
:aria-label="formatMessage(messages.instanceTooltipTitle)"
class="inline-flex cursor-help items-center justify-center border-0 bg-transparent p-0 text-secondary transition-colors hover:text-contrast"
>
<UnknownIcon class="size-4" aria-hidden="true" />
</button>
<template #popper>
<div class="grid !w-64 gap-1">
<h3 class="m-0 whitespace-nowrap text-base w-full font-bold text-contrast">
{{ formatMessage(messages.instanceTooltipTitle) }}
</h3>
<p
class="m-0 text-wrap text-sm w-full font-medium leading-tight text-secondary"
>
{{ formatMessage(messages.instanceTooltipDescription) }}
</p>
</div>
</template>
</Tooltip>
</span>
</template>
<template #cell-user="{ row: entry }"> <template #cell-user="{ row: entry }">
<AutoLink <AutoLink
v-tooltip="actorName(entry)" v-tooltip="actorName(entry)"
@@ -171,7 +135,7 @@
class="hidden min-h-14 bg-surface-3 @[800px]:grid @[800px]:h-14" class="hidden min-h-14 bg-surface-3 @[800px]:grid @[800px]:h-14"
:class=" :class="
showWorldColumn showWorldColumn
? '@[800px]:grid-cols-[18%_52%_20%_10%]' ? '@[800px]:grid-cols-[18%_46%_22%_14%]'
: '@[800px]:grid-cols-[26%_58%_16%]' : '@[800px]:grid-cols-[26%_58%_16%]'
" "
> >
@@ -185,37 +149,7 @@
v-if="showWorldColumn" v-if="showWorldColumn"
class="hidden items-center px-2 font-semibold text-secondary @[800px]:flex" class="hidden items-center px-2 font-semibold text-secondary @[800px]:flex"
> >
<span class="inline-flex min-w-0 max-w-full items-center gap-1 font-semibold"> <span class="min-w-0 truncate">{{ formatMessage(messages.worldColumn) }}</span>
<span class="min-w-0 truncate">{{ formatMessage(messages.worldColumn) }}</span>
<Tooltip
theme="dismissable-prompt"
class="inline-flex shrink-0"
:triggers="['hover', 'focus']"
:popper-triggers="['hover', 'focus']"
popper-class="v-popper--interactive"
placement="top"
:delay="{ show: 200, hide: 100 }"
no-auto-focus
>
<button
type="button"
:aria-label="formatMessage(messages.instanceTooltipTitle)"
class="inline-flex cursor-help items-center justify-center border-0 bg-transparent p-0 text-secondary transition-colors hover:text-contrast"
>
<UnknownIcon class="size-4" aria-hidden="true" />
</button>
<template #popper>
<div class="grid !w-64 gap-1">
<h3 class="m-0 whitespace-nowrap text-base font-bold text-contrast">
{{ formatMessage(messages.instanceTooltipTitle) }}
</h3>
<p class="m-0 text-wrap text-sm font-medium leading-tight text-secondary">
{{ formatMessage(messages.instanceTooltipDescription) }}
</p>
</div>
</template>
</Tooltip>
</span>
</div> </div>
<div <div
class="hidden items-center justify-end pl-2 pr-4 font-semibold text-secondary @[800px]:flex" class="hidden items-center justify-end pl-2 pr-4 font-semibold text-secondary @[800px]:flex"
@@ -250,8 +184,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { IntercomBubbleIcon, UnknownIcon } from '@modrinth/assets' import { IntercomBubbleIcon } from '@modrinth/assets'
import { Tooltip } from 'floating-vue'
import { computed, nextTick, onBeforeUnmount, onMounted, ref, useSlots, watch } from 'vue' import { computed, nextTick, onBeforeUnmount, onMounted, ref, useSlots, watch } from 'vue'
import { useFormatDateTime, useRelativeTime } from '../../../composables' import { useFormatDateTime, useRelativeTime } from '../../../composables'
@@ -324,15 +257,6 @@ const messages = defineMessages({
id: 'servers.audit-log.column.world', id: 'servers.audit-log.column.world',
defaultMessage: 'Instance', defaultMessage: 'Instance',
}, },
instanceTooltipTitle: {
id: 'servers.audit-log.column.world.tooltip-title',
defaultMessage: 'Coming soon!',
},
instanceTooltipDescription: {
id: 'servers.audit-log.column.world.tooltip-description',
defaultMessage:
'Server instances are contained environments with their own installed content and world files.',
},
eventColumn: { eventColumn: {
id: 'servers.audit-log.column.event', id: 'servers.audit-log.column.event',
defaultMessage: 'Actions', defaultMessage: 'Actions',
@@ -417,7 +341,7 @@ const columns = computed<TableColumn<AuditLogTableColumn>[]>(() => {
{ {
key: 'event', key: 'event',
label: formatMessage(messages.eventColumn), label: formatMessage(messages.eventColumn),
width: showWorldColumn.value ? '52%' : '58%', width: showWorldColumn.value ? '46%' : '58%',
}, },
] ]
@@ -425,7 +349,7 @@ const columns = computed<TableColumn<AuditLogTableColumn>[]>(() => {
tableColumns.push({ tableColumns.push({
key: 'world', key: 'world',
label: formatMessage(messages.worldColumn), label: formatMessage(messages.worldColumn),
width: '20%', width: '22%',
}) })
} }
@@ -434,7 +358,7 @@ const columns = computed<TableColumn<AuditLogTableColumn>[]>(() => {
label: formatMessage(messages.timeColumn), label: formatMessage(messages.timeColumn),
align: 'right', align: 'right',
enableSorting: true, enableSorting: true,
width: showWorldColumn.value ? '10%' : '16%', width: showWorldColumn.value ? '14%' : '16%',
}) })
return tableColumns return tableColumns
@@ -112,10 +112,6 @@ const props = withDefaults(
worlds?: HeaderWorld[] worlds?: HeaderWorld[]
powerDisabled?: boolean powerDisabled?: boolean
settingsLabel?: string settingsLabel?: string
showSettingsHint?: boolean
settingsHintTitle?: string
settingsHintDescription?: string
settingsHintDismissLabel?: string
actions?: HeaderAction[] actions?: HeaderAction[]
}>(), }>(),
{ {
@@ -131,17 +127,12 @@ const props = withDefaults(
worlds: () => [], worlds: () => [],
powerDisabled: false, powerDisabled: false,
settingsLabel: 'Server settings', settingsLabel: 'Server settings',
showSettingsHint: false,
settingsHintTitle: '',
settingsHintDescription: '',
settingsHintDismissLabel: "Don't show again",
actions: () => [], actions: () => [],
}, },
) )
const emit = defineEmits<{ const emit = defineEmits<{
openSettings: [] openSettings: []
dismissSettingsHint: []
}>() }>()
const client = injectModrinthClient() const client = injectModrinthClient()
@@ -408,16 +399,8 @@ const settingsAction = computed<HeaderAction>(() => ({
label: props.settingsLabel, label: props.settingsLabel,
icon: SettingsIcon, icon: SettingsIcon,
labelHidden: true, labelHidden: true,
tooltip: props.showSettingsHint ? undefined : props.settingsLabel, tooltip: props.settingsLabel,
onClick: () => emit('openSettings'), onClick: () => emit('openSettings'),
prompt: {
title: props.settingsHintTitle,
description: props.settingsHintDescription,
dismissLabel: props.settingsHintDismissLabel,
shown: props.showSettingsHint,
placement: 'bottom-end',
onDismiss: () => emit('dismissSettingsHint'),
},
})) }))
const headerActions = computed<HeaderAction[]>(() => [ const headerActions = computed<HeaderAction[]>(() => [
@@ -2,6 +2,7 @@
<FloatingActionBar <FloatingActionBar
:shown="shown" :shown="shown"
:aria-label="formatMessage(messages.ariaLabel)" :aria-label="formatMessage(messages.ariaLabel)"
toolbar-max-width="min(1152px, calc(100vw - 3rem))"
hide-when-modal-open hide-when-modal-open
> >
<div class="flex min-w-0 items-center gap-0.5"> <div class="flex min-w-0 items-center gap-0.5">
@@ -170,16 +170,16 @@ const messages = defineMessages({
}, },
resetWorldFilesButton: { resetWorldFilesButton: {
id: 'server.instance-settings.general.reset-world-files.button', id: 'server.instance-settings.general.reset-world-files.button',
defaultMessage: 'Reset world data', defaultMessage: 'Reset world',
}, },
resetWorldFilesDescription: { resetWorldFilesDescription: {
id: 'server.instance-settings.general.reset-world-files.description', id: 'server.instance-settings.general.reset-world-files.description',
defaultMessage: defaultMessage:
'Delete the current world data and generate a new one. Your content and files will stay the same.', 'Create a fresh world for this instance. Content, files, settings, and backups stay the same.',
}, },
resetWorldFilesModalTitle: { resetWorldFilesModalTitle: {
id: 'server.instance-settings.general.reset-world-files.modal.title', id: 'server.instance-settings.general.reset-world-files.modal.title',
defaultMessage: 'Reset world data', defaultMessage: 'Reset world',
}, },
resetWorldFilesModalDescription: { resetWorldFilesModalDescription: {
id: 'server.instance-settings.general.reset-world-files.modal.description', id: 'server.instance-settings.general.reset-world-files.modal.description',
@@ -200,12 +200,12 @@ const messages = defineMessages({
}, },
resetEverythingButton: { resetEverythingButton: {
id: 'server.instance-settings.general.reset-everything.button', id: 'server.instance-settings.general.reset-everything.button',
defaultMessage: 'Reset everything', defaultMessage: 'Reset instance',
}, },
resetEverythingDescription: { resetEverythingDescription: {
id: 'server.instance-settings.general.reset-everything.description', id: 'server.instance-settings.general.reset-everything.description',
defaultMessage: defaultMessage:
'Reset your instance completely. This removes world data, content, and any configuration. A backup of the previous instance will remain available.', 'Recreate this instance from scratch. World data, content, files, and settings are removed. Previous backups will remain available.',
}, },
deleteInstanceButton: { deleteInstanceButton: {
id: 'server.instance-settings.general.delete-instance.button', id: 'server.instance-settings.general.delete-instance.button',
@@ -214,7 +214,7 @@ const messages = defineMessages({
deleteInstanceDescription: { deleteInstanceDescription: {
id: 'server.instance-settings.general.delete-instance.description', id: 'server.instance-settings.general.delete-instance.description',
defaultMessage: defaultMessage:
'Permanently delete this instance, including its content, files, settings, and backups.', 'Permanently delete this instance. World data, content, files, settings, and backups will be removed.',
}, },
deleteInstanceModalTitle: { deleteInstanceModalTitle: {
id: 'server.instance-settings.general.delete-instance.modal.title', id: 'server.instance-settings.general.delete-instance.modal.title',
@@ -63,7 +63,7 @@
:has-more="hasMoreActionLogEntries" :has-more="hasMoreActionLogEntries"
:loading="isActionLogFiltering" :loading="isActionLogFiltering"
:loading-more="isLoadingMoreActionLogEntries" :loading-more="isLoadingMoreActionLogEntries"
:show-world-column="showAuditLogInstances" show-world-column
:suppress-row-transitions="isActionLogSortTransitioning" :suppress-row-transitions="isActionLogSortTransitioning"
@load-more="loadMoreActionLogEntries" @load-more="loadMoreActionLogEntries"
> >
@@ -145,14 +145,10 @@ type RoleFilter = ServerAccessRole | 'all'
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
showAuditLogInstances?: boolean
userProfileLink?: (username: string) => ServerAccessUserProfileLink userProfileLink?: (username: string) => ServerAccessUserProfileLink
}>(), }>(),
{ {},
showAuditLogInstances: false,
},
) )
const showAuditLogInstances = computed(() => props.showAuditLogInstances)
const INVITE_RESEND_COOLDOWN_SECONDS = 2 * 60 const INVITE_RESEND_COOLDOWN_SECONDS = 2 * 60
@@ -294,7 +290,6 @@ const {
client, client,
serverId, serverId,
serverFull, serverFull,
showAuditLogInstances,
addNotification, addNotification,
}) })
@@ -36,7 +36,6 @@ type UseAccessAuditLogOptions = {
client: AbstractModrinthClient client: AbstractModrinthClient
serverId: string serverId: string
serverFull: ComputedRef<Archon.Servers.v1.ServerFull | null> serverFull: ComputedRef<Archon.Servers.v1.ServerFull | null>
showAuditLogInstances: ComputedRef<boolean>
addNotification: AbstractWebNotificationManager['addNotification'] addNotification: AbstractWebNotificationManager['addNotification']
} }
@@ -47,7 +46,6 @@ export function useAccessAuditLog({
client, client,
serverId, serverId,
serverFull, serverFull,
showAuditLogInstances,
addNotification, addNotification,
}: UseAccessAuditLogOptions) { }: UseAccessAuditLogOptions) {
const { formatMessage } = useVIntl() const { formatMessage } = useVIntl()
@@ -68,9 +66,7 @@ export function useAccessAuditLog({
const worldOptions = computed( const worldOptions = computed(
() => serverFull.value?.worlds.map((world) => ({ id: world.id, name: world.name })) ?? [], () => serverFull.value?.worlds.map((world) => ({ id: world.id, name: world.name })) ?? [],
) )
const isAuditLogWorldFilterVisible = computed( const isAuditLogWorldFilterVisible = computed(() => worldOptions.value.length > 0)
() => showAuditLogInstances.value && worldOptions.value.length > 0,
)
const worldById = computed( const worldById = computed(
() => new Map(worldOptions.value.map((world) => [world.id, world] as const)), () => new Map(worldOptions.value.map((world) => [world.id, world] as const)),
@@ -130,12 +130,7 @@
:worlds="serverFull?.worlds ?? []" :worlds="serverFull?.worlds ?? []"
:power-disabled="!!installError" :power-disabled="!!installError"
:settings-label="formatMessage(messages.serverSettings)" :settings-label="formatMessage(messages.serverSettings)"
:show-settings-hint="showSettingsHint"
:settings-hint-title="formatMessage(settingsHintMessages.title)"
:settings-hint-description="formatMessage(settingsHintMessages.description)"
:settings-hint-dismiss-label="formatMessage(settingsHintMessages.dismiss)"
@open-settings="openServerSettingsModal()" @open-settings="openServerSettingsModal()"
@dismiss-settings-hint="dismissSettingsHint"
/> />
<ServerOnboardingPanelPage v-if="isOnboarding" :browse-modpacks="handleBrowseModpacks" /> <ServerOnboardingPanelPage v-if="isOnboarding" :browse-modpacks="handleBrowseModpacks" />
@@ -443,21 +438,6 @@ const leaveMessages = defineMessages({
}, },
}) })
const settingsHintMessages = defineMessages({
title: {
id: 'servers.manage.settings-hint.title',
defaultMessage: 'Your server settings have moved',
},
description: {
id: 'servers.manage.settings-hint.description',
defaultMessage: 'They can now be found here!',
},
dismiss: {
id: 'servers.manage.settings-hint.dismiss',
defaultMessage: "Don't show again",
},
})
const instancesHintMessages = defineMessages({ const instancesHintMessages = defineMessages({
title: { title: {
id: 'servers.manage.instances-hint.title', id: 'servers.manage.instances-hint.title',
@@ -712,14 +692,6 @@ const errorLog = ref('')
const errorLogFile = ref('') const errorLogFile = ref('')
const isOnboarding = computed(() => serverData.value?.flows?.intro) const isOnboarding = computed(() => serverData.value?.flows?.intro)
const SETTINGS_HINT_KEY = 'server-panel-settings-hint-dismissed'
const settingsHintDismissed = useStorage(SETTINGS_HINT_KEY, false)
const showSettingsHint = ref(!settingsHintDismissed.value)
function dismissSettingsHint() {
showSettingsHint.value = false
settingsHintDismissed.value = true
}
const INSTANCES_HINT_KEY = 'server-panel-instances-hint-dismissed' const INSTANCES_HINT_KEY = 'server-panel-instances-hint-dismissed'
const instancesHintDismissed = useStorage(INSTANCES_HINT_KEY, false) const instancesHintDismissed = useStorage(INSTANCES_HINT_KEY, false)
const showInstancesHint = ref(!instancesHintDismissed.value) const showInstancesHint = ref(!instancesHintDismissed.value)
@@ -201,9 +201,13 @@ async function loadContentSummary(
index: number, index: number,
): Promise<ContentSummary> { ): Promise<ContentSummary> {
try { try {
const content = await client.archon.content_v1.getAddons(serverId, world.id, { const content = await queryClient.fetchQuery({
addons: true, queryKey: ['content', 'list', 'v1', serverId, world.id],
updates: false, queryFn: () =>
client.archon.content_v1.getAddons(serverId, world.id, {
from_modpack: false,
}),
staleTime: 0,
}) })
return { return {
@@ -211,13 +215,35 @@ async function loadContentSummary(
loader: content.modloader ?? world.content?.modloader ?? null, loader: content.modloader ?? world.content?.modloader ?? null,
loaderVersion: content.modloader_version ?? world.content?.modloader_version ?? null, loaderVersion: content.modloader_version ?? world.content?.modloader_version ?? null,
linkedModpack: getLinkedModpack(content.modpack), linkedModpack: getLinkedModpack(content.modpack),
installedContentCount: content.addons?.length ?? 0, installedContentCount: await getInstalledContentCount(world.id, content),
} }
} catch { } catch {
return createDummyContentSummary(world, index) return createDummyContentSummary(world, index)
} }
} }
async function getInstalledContentCount(
worldId: string,
content: Archon.Content.v1.Addons,
): Promise<number> {
const addonCount = content.addons?.length ?? 0
if (!content.modpack) return addonCount
try {
const modpackContent = await queryClient.fetchQuery({
queryKey: ['content', 'list', 'v1', serverId, worldId, 'modpack'],
queryFn: () =>
client.archon.content_v1.getAddons(serverId, worldId, {
from_modpack: true,
}),
staleTime: 0,
})
return addonCount + (modpackContent.addons?.length ?? 0)
} catch {
return addonCount
}
}
function toWorldSlot(world: Archon.Servers.v1.WorldFull, content: ContentSummary): WorldSlot { function toWorldSlot(world: Archon.Servers.v1.WorldFull, content: ContentSummary): WorldSlot {
return { return {
type: 'world', type: 'world',