-
+
-
+
-
-
-
- {{ formatLoaderLabel(props.server.loader) }} {{ props.server.mc_version }}
-
-
-
-
-
-
- {{ props.server.net.domain }}.modrinth.gg
-
-
-
-
-
-
- {{ formattedUptime }}
-
-
-
-
+
+
+
+
+
+ {{ item.label }}
+
+
+
+ {{ item.label }}
+
+
+
Linked to
import type { Archon } from '@modrinth/api-client'
import { NuxtModrinthClient } from '@modrinth/api-client'
-import { LinkIcon, SettingsIcon, TimerIcon } from '@modrinth/assets'
+import { GlobeIcon, LinkIcon, SettingsIcon, TimerIcon } from '@modrinth/assets'
import { useStorage } from '@vueuse/core'
+import type { Component } from 'vue'
import { computed } from 'vue'
import { AutoLink, Avatar, ContentPageHeader, ServerIcon } from '#ui/components'
@@ -102,12 +85,20 @@ type ServerProjectSummary = {
icon_url?: string | null
}
+type HeaderStat = {
+ id: string
+ label: string
+ icon: Component
+ copyable?: boolean
+}
+
const props = withDefaults(
defineProps<{
server: Archon.Servers.v0.Server | null | undefined
serverImage?: string | null
serverProject?: ServerProjectSummary | null
serverProjectLink?: string
+ activeWorldName?: string | null
uptimeSeconds?: number
showUptime?: boolean
backHref?: string
@@ -118,6 +109,7 @@ const props = withDefaults(
serverImage: null,
serverProject: null,
serverProjectLink: '',
+ activeWorldName: null,
uptimeSeconds: 0,
showUptime: true,
backHref: '/hosting/manage',
@@ -158,6 +150,57 @@ const formattedUptime = computed(() => {
return formatted.trim()
})
+const serverAddress = computed(() => {
+ const domain = props.server?.net?.domain
+ if (domain) return `${domain}.modrinth.gg`
+
+ const ip = props.server?.net?.ip
+ if (!ip) return null
+ const port = props.server?.net?.port
+ return port ? `${ip}:${port}` : ip
+})
+
+const showAddress = computed(
+ () => !!serverAddress.value && (!props.server?.net?.domain || !userPreferences.value.hideSubdomainLabel),
+)
+
+const headerStats = computed(() => {
+ const stats: HeaderStat[] = []
+ const worldName = props.activeWorldName
+ if (worldName) {
+ stats.push({
+ id: 'world',
+ label: worldName,
+ icon: GlobeIcon,
+ })
+ }
+ if (props.server?.loader) {
+ stats.push({
+ id: 'loader',
+ label: props.server.mc_version
+ ? `${formatLoaderLabel(props.server.loader)} ${props.server.mc_version}`
+ : formatLoaderLabel(props.server.loader),
+ icon: LoaderIcon,
+ })
+ }
+ if (showAddress.value && serverAddress.value) {
+ stats.push({
+ id: 'address',
+ label: serverAddress.value,
+ icon: LinkIcon,
+ copyable: true,
+ })
+ }
+ if (showUptime.value) {
+ stats.push({
+ id: 'uptime',
+ label: formattedUptime.value,
+ icon: TimerIcon,
+ })
+ }
+ return stats
+})
+
const showProject = computed(() => !!props.serverProject)
const serverProjectLink = computed(() => {
@@ -171,8 +214,8 @@ const serverProjectLink = computed(() => {
})
function copyServerAddress() {
- if (!props.server?.net?.domain) return
- navigator.clipboard.writeText(`${props.server.net.domain}.modrinth.gg`)
+ if (!serverAddress.value) return
+ navigator.clipboard.writeText(serverAddress.value)
addNotification({
title: 'Server address copied',
text: "Your server's address has been copied to your clipboard.",
diff --git a/packages/ui/src/composables/server-backups-queue.ts b/packages/ui/src/composables/server-backups-queue.ts
index e05a716626..db1071b8b4 100644
--- a/packages/ui/src/composables/server-backups-queue.ts
+++ b/packages/ui/src/composables/server-backups-queue.ts
@@ -12,7 +12,7 @@ export function useServerBackupsQueue(serverId: Ref, worldId: Ref ['backups', 'queue', serverId.value] as const)
+ const queryKey = computed(() => ['backups', 'queue', serverId.value, worldId.value] as const)
const progressOverlay = reactive(new Map())
const lastSeenState = new Map()
diff --git a/packages/ui/src/layouts/shared/server-settings/pages/installation.vue b/packages/ui/src/layouts/shared/server-settings/pages/installation.vue
index 15035cd77a..c2e7bf186e 100644
--- a/packages/ui/src/layouts/shared/server-settings/pages/installation.vue
+++ b/packages/ui/src/layouts/shared/server-settings/pages/installation.vue
@@ -227,6 +227,7 @@ const resetServerDisabledTooltip = computed(() => {
})
const installationSettingsLayout = ref>()
const setupModal = ref>()
+const contentListQueryKey = computed(() => ['content', 'list', 'v1', serverId, worldId.value])
function showResetServerModal() {
if (resetServerDisabled.value) return
@@ -237,13 +238,13 @@ async function invalidateServerState() {
debug('invalidateServerState: starting')
await Promise.all([
queryClient.invalidateQueries({ queryKey: ['servers', 'detail', serverId] }),
- queryClient.invalidateQueries({ queryKey: ['content', 'list', 'v1', serverId] }),
+ queryClient.invalidateQueries({ queryKey: contentListQueryKey.value }),
])
debug('invalidateServerState: complete')
}
const addonsQuery = useQuery({
- queryKey: computed(() => ['content', 'list', 'v1', serverId]),
+ queryKey: contentListQueryKey,
queryFn: () =>
client.archon.content_v1.getAddons(serverId, worldId.value!, { from_modpack: false }),
enabled: computed(() => worldId.value !== null),
@@ -670,7 +671,7 @@ provideInstallationSettings({
const previousData = addonsQuery.data.value
if (previousData) {
debug('unlinkModpack: optimistically removing modpack from cache')
- queryClient.setQueryData(['content', 'list', 'v1', serverId], {
+ queryClient.setQueryData(contentListQueryKey.value, {
...previousData,
modpack: null,
})
@@ -682,7 +683,7 @@ provideInstallationSettings({
} catch (err) {
debug('unlinkModpack: failed, reverting cache', err)
if (previousData) {
- queryClient.setQueryData(['content', 'list', 'v1', serverId], previousData)
+ queryClient.setQueryData(contentListQueryKey.value, previousData)
}
addNotification({
type: 'error',
@@ -695,7 +696,7 @@ provideInstallationSettings({
queryKey: ['servers', 'detail', serverId],
}),
queryClient.invalidateQueries({
- queryKey: ['content', 'list', 'v1', serverId],
+ queryKey: contentListQueryKey.value,
}),
])
debug('unlinkModpack: invalidation complete')
diff --git a/packages/ui/src/layouts/shared/server-settings/pages/properties.vue b/packages/ui/src/layouts/shared/server-settings/pages/properties.vue
index e40efe2538..96653f2328 100644
--- a/packages/ui/src/layouts/shared/server-settings/pages/properties.vue
+++ b/packages/ui/src/layouts/shared/server-settings/pages/properties.vue
@@ -317,8 +317,10 @@ const { canUseAdvancedSettings, canUsePowerActions, permissionDeniedMessage } =
const advancedActionTooltip = computed(() =>
canUseAdvancedSettings.value ? undefined : permissionDeniedMessage.value,
)
-const filesTabLink = computed(
- () => `/hosting/manage/${encodeURIComponent(serverId)}/files?path=/&editing=server.properties`,
+const filesTabLink = computed(() =>
+ worldId.value
+ ? `/hosting/manage/${encodeURIComponent(serverId)}/worlds/${encodeURIComponent(worldId.value)}/files?path=/&editing=server.properties`
+ : `/hosting/manage/${encodeURIComponent(serverId)}/worlds`,
)
const serverSettings = injectServerSettings(null)
diff --git a/packages/ui/src/layouts/wrapped/hosting/manage/[id]/onboarding.vue b/packages/ui/src/layouts/wrapped/hosting/manage/[id]/onboarding.vue
index bcdebf6392..e195214fcf 100644
--- a/packages/ui/src/layouts/wrapped/hosting/manage/[id]/onboarding.vue
+++ b/packages/ui/src/layouts/wrapped/hosting/manage/[id]/onboarding.vue
@@ -272,7 +272,11 @@ async function finalizeSetup() {
client.archon.servers_v1.endIntro(serverId).then(() => {
queryClient.invalidateQueries({ queryKey: ['servers', 'detail', serverId] })
})
- await router.push(`/hosting/manage/${serverId}/`)
+ await router.push(
+ worldId.value
+ ? `/hosting/manage/${encodeURIComponent(serverId)}/worlds/${encodeURIComponent(worldId.value)}`
+ : `/hosting/manage/${encodeURIComponent(serverId)}/worlds`,
+ )
}
/** Map UI loader names to API Modloader values */
diff --git a/packages/ui/src/layouts/wrapped/hosting/manage/backups.vue b/packages/ui/src/layouts/wrapped/hosting/manage/backups.vue
index b6e8da3f92..a1279db3e8 100644
--- a/packages/ui/src/layouts/wrapped/hosting/manage/backups.vue
+++ b/packages/ui/src/layouts/wrapped/hosting/manage/backups.vue
@@ -362,7 +362,7 @@ const filterPillOptions = computed(() => [
])
const client = injectModrinthClient()
const queryClient = useQueryClient()
-const { server, worldId, busyReasons } = injectModrinthServerContext()
+const { server, serverId, worldId, busyReasons } = injectModrinthServerContext()
const props = defineProps<{
isServerRunning: boolean
@@ -371,9 +371,7 @@ const props = defineProps<{
}>()
const route = useRoute()
-const serverId = route.params.id as string
const BACKUP_HIGHLIGHT_DURATION_MS = 5_000
-
defineEmits(['onDownload'])
const { backups, invalidate, activeOperationByBackupId, hasActiveCreate, hasActiveRestore, query } =
diff --git a/packages/ui/src/layouts/wrapped/hosting/manage/content.vue b/packages/ui/src/layouts/wrapped/hosting/manage/content.vue
index 12814ff74d..0c8dd1c03e 100644
--- a/packages/ui/src/layouts/wrapped/hosting/manage/content.vue
+++ b/packages/ui/src/layouts/wrapped/hosting/manage/content.vue
@@ -4,7 +4,7 @@ import { ClipboardCopyIcon } from '@modrinth/assets'
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
import { useIntervalFn } from '@vueuse/core'
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
-import { useRoute, useRouter } from 'vue-router'
+import { useRouter } from 'vue-router'
import ReadyTransition from '#ui/components/base/ReadyTransition.vue'
import { useUploadSessionUpload } from '#ui/composables/hosting/kyros-session-upload'
@@ -115,7 +115,7 @@ const messages = defineMessages({
})
const client = injectModrinthClient()
-const { server, worldId, busyReasons, isSyncingContent, uploadState, cancelUpload } =
+const { server, serverId, worldId, busyReasons, isSyncingContent, uploadState, cancelUpload } =
injectModrinthServerContext()
const contentUploadSession = useUploadSessionUpload({
client,
@@ -127,10 +127,8 @@ const contentUploadSession = useUploadSessionUpload({
const { addNotification } = injectNotificationManager()
const { openServerSettings, browseServerContent } = injectServerSettingsModal()
const { canSetup, permissionDeniedMessage } = useServerPermissions()
-const route = useRoute()
const router = useRouter()
const queryClient = useQueryClient()
-const serverId = route.params.id as string
const type = computed(() => {
const loader = server.value?.loader?.toLowerCase()
@@ -139,8 +137,15 @@ const type = computed(() => {
return 'mod'
})
-const queryKey = computed(() => ['content', 'list', 'v1', serverId])
-const modpackContentQueryKey = computed(() => ['content', 'list', 'v1', serverId, 'modpack'])
+const queryKey = computed(() => ['content', 'list', 'v1', serverId, worldId.value])
+const modpackContentQueryKey = computed(() => [
+ 'content',
+ 'list',
+ 'v1',
+ serverId,
+ worldId.value,
+ 'modpack',
+])
function getContentOwnerAvatarUrl(owner: ContentOwnerAvatarSource) {
const ownerId = owner.type === 'user' ? owner.name || owner.id : owner.id
diff --git a/packages/ui/src/layouts/wrapped/hosting/manage/root.vue b/packages/ui/src/layouts/wrapped/hosting/manage/root.vue
index 8f66ff1eb6..f3de11aa2b 100644
--- a/packages/ui/src/layouts/wrapped/hosting/manage/root.vue
+++ b/packages/ui/src/layouts/wrapped/hosting/manage/root.vue
@@ -92,7 +92,17 @@
-
+
+
+
@@ -305,8 +316,8 @@
@@ -27,6 +27,7 @@
import type { Archon } from '@modrinth/api-client'
import { useQuery } from '@tanstack/vue-query'
import { computed } from 'vue'
+import { useRouter } from 'vue-router'
import { defineMessages, useVIntl } from '#ui/composables/i18n'
import {
@@ -84,6 +85,7 @@ const client = injectModrinthClient()
const { serverId, server, isServerRunning } = injectModrinthServerContext()
const { openServerSettings } = injectServerSettingsModal()
const { formatMessage } = useVIntl()
+const router = useRouter()
const worldsQuery = useQuery({
queryKey: computed(() => ['servers', 'worlds', 'summary', 'v1', serverId]),
@@ -272,7 +274,13 @@ function createDummyWorldSlots(): WorldSlot[] {
]
}
-function handleEditWorld() {
+function handleEditWorld(worldId: string) {
+ router.push(
+ `/hosting/manage/${encodeURIComponent(serverId)}/worlds/${encodeURIComponent(worldId)}`,
+ )
+}
+
+function handleWorldSettings() {
openServerSettings({ tabId: 'installation' })
}
diff --git a/packages/ui/src/layouts/wrapped/index.ts b/packages/ui/src/layouts/wrapped/index.ts
index 38994b8993..b8b9ae3c45 100644
--- a/packages/ui/src/layouts/wrapped/index.ts
+++ b/packages/ui/src/layouts/wrapped/index.ts
@@ -6,4 +6,5 @@ export { default as ServersManageFilesPage } from './hosting/manage/files.vue'
export { default as ServersManagePageIndex } from './hosting/manage/index.vue'
export { default as ServersManageOverviewPage } from './hosting/manage/overview.vue'
export { default as ServersManageRootLayout } from './hosting/manage/root.vue'
+export { default as ServersManageWorldRootLayout } from './hosting/manage/world-root.vue'
export { default as ServersManageWorldsPage } from './hosting/manage/worlds.vue'
diff --git a/packages/ui/src/stories/servers/ServerPanelAdmonitions.stories.ts b/packages/ui/src/stories/servers/ServerPanelAdmonitions.stories.ts
index aeccb82689..d6b5c9257c 100644
--- a/packages/ui/src/stories/servers/ServerPanelAdmonitions.stories.ts
+++ b/packages/ui/src/stories/servers/ServerPanelAdmonitions.stories.ts
@@ -25,7 +25,7 @@ const meta = {
setup() {
const router = useRouter()
onMounted(() => {
- router.replace('/hosting/manage/demo-server/content')
+ router.replace('/hosting/manage/demo-server/worlds/demo-world')
})
const server = ref({