mirror of
https://github.com/modrinth/code.git
synced 2026-09-03 13:36:48 +00:00
feat: reshuffle layout for worlds
This commit is contained in:
@@ -495,6 +495,11 @@ const sidebarOverlayScrollbarsOptions = Object.freeze({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
router.beforeEach(async (to) => {
|
||||||
|
const redirect = await resolveLegacyServerWorldTabRedirect(to)
|
||||||
|
if (redirect) return redirect
|
||||||
|
})
|
||||||
|
|
||||||
router.beforeEach(() => {
|
router.beforeEach(() => {
|
||||||
suspensePending = false
|
suspensePending = false
|
||||||
if (routerToken) loading.end(routerToken)
|
if (routerToken) loading.end(routerToken)
|
||||||
@@ -526,6 +531,50 @@ function onSuspensePending() {
|
|||||||
suspenseToken = loading.begin()
|
suspenseToken = loading.begin()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function resolveLegacyServerWorldTabRedirect(to) {
|
||||||
|
if (!['ServerManageContent', 'ServerManageFiles', 'ServerManageBackups'].includes(to.name)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const serverId = getRouteParam(to.params.id)
|
||||||
|
if (!serverId) return null
|
||||||
|
|
||||||
|
const tabPath =
|
||||||
|
to.name === 'ServerManageFiles' ? '/files' : to.name === 'ServerManageBackups' ? '/backups' : ''
|
||||||
|
const worldsPath = `/hosting/manage/${encodeURIComponent(serverId)}/worlds`
|
||||||
|
|
||||||
|
try {
|
||||||
|
const serverFull = await tauriApiClient.archon.servers_v1.get(serverId)
|
||||||
|
const world = serverFull.worlds.find((item) => item.is_active) ?? serverFull.worlds[0]
|
||||||
|
if (world) {
|
||||||
|
return {
|
||||||
|
path: `${worldsPath}/${encodeURIComponent(world.id)}${tabPath}`,
|
||||||
|
query: to.query,
|
||||||
|
hash: to.hash,
|
||||||
|
replace: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return {
|
||||||
|
path: worldsPath,
|
||||||
|
query: to.query,
|
||||||
|
hash: to.hash,
|
||||||
|
replace: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
path: worldsPath,
|
||||||
|
query: to.query,
|
||||||
|
hash: to.hash,
|
||||||
|
replace: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRouteParam(param) {
|
||||||
|
return Array.isArray(param) ? param[0] : param
|
||||||
|
}
|
||||||
|
|
||||||
function onSuspenseResolve() {
|
function onSuspenseResolve() {
|
||||||
if (suspenseToken) {
|
if (suspenseToken) {
|
||||||
loading.end(suspenseToken)
|
loading.end(suspenseToken)
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ const queryClient = useQueryClient()
|
|||||||
if (worldId.value) {
|
if (worldId.value) {
|
||||||
try {
|
try {
|
||||||
await queryClient.ensureQueryData({
|
await queryClient.ensureQueryData({
|
||||||
queryKey: ['backups', 'list', serverId],
|
queryKey: ['backups', 'list', serverId, worldId.value],
|
||||||
queryFn: () => client.archon.backups_v1.list(serverId, worldId.value!),
|
queryFn: () => client.archon.backups_v1.list(serverId, worldId.value!),
|
||||||
staleTime: 30_000,
|
staleTime: 30_000,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ const queryClient = useQueryClient()
|
|||||||
if (worldId.value) {
|
if (worldId.value) {
|
||||||
try {
|
try {
|
||||||
await queryClient.ensureQueryData({
|
await queryClient.ensureQueryData({
|
||||||
queryKey: ['content', 'list', 'v1', serverId],
|
queryKey: ['content', 'list', 'v1', serverId, worldId.value],
|
||||||
queryFn: () =>
|
queryFn: () =>
|
||||||
client.archon.content_v1.getAddons(serverId, worldId.value!, { from_modpack: false }),
|
client.archon.content_v1.getAddons(serverId, worldId.value!, { from_modpack: false }),
|
||||||
staleTime: 30_000,
|
staleTime: 30_000,
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ watch(
|
|||||||
breadcrumbs.setName('Server', server.name)
|
breadcrumbs.setName('Server', server.name)
|
||||||
breadcrumbs.setContext({
|
breadcrumbs.setContext({
|
||||||
name: server.name,
|
name: server.name,
|
||||||
link: `/hosting/manage/${serverId.value}/content`,
|
link: `/hosting/manage/${serverId.value}/worlds`,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<template>
|
||||||
|
<ServersManageWorldRootLayout>
|
||||||
|
<RouterView v-slot="{ Component }">
|
||||||
|
<template v-if="Component">
|
||||||
|
<Suspense>
|
||||||
|
<component :is="Component" />
|
||||||
|
</Suspense>
|
||||||
|
</template>
|
||||||
|
</RouterView>
|
||||||
|
</ServersManageWorldRootLayout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ServersManageWorldRootLayout } from '@modrinth/ui'
|
||||||
|
import { RouterView } from 'vue-router'
|
||||||
|
</script>
|
||||||
@@ -4,6 +4,7 @@ import Content from './Content.vue'
|
|||||||
import Files from './Files.vue'
|
import Files from './Files.vue'
|
||||||
import Index from './Index.vue'
|
import Index from './Index.vue'
|
||||||
import Overview from './Overview.vue'
|
import Overview from './Overview.vue'
|
||||||
|
import World from './World.vue'
|
||||||
import Worlds from './Worlds.vue'
|
import Worlds from './Worlds.vue'
|
||||||
|
|
||||||
export { Access, Backups, Content, Files, Index, Overview, Worlds }
|
export { Access, Backups, Content, Files, Index, Overview, World, Worlds }
|
||||||
|
|||||||
@@ -252,7 +252,7 @@ export function createServerInstallContent(opts: {
|
|||||||
if (serverFlowFrom.value === 'reset-server') {
|
if (serverFlowFrom.value === 'reset-server') {
|
||||||
return `/hosting/manage/${sid}?openSettings=installation`
|
return `/hosting/manage/${sid}?openSettings=installation`
|
||||||
}
|
}
|
||||||
return `/hosting/manage/${sid}/content`
|
return getServerWorldContentPath(sid, effectiveServerWorldId.value)
|
||||||
})
|
})
|
||||||
const serverBackLabel = computed(() => {
|
const serverBackLabel = computed(() => {
|
||||||
if (serverFlowFrom.value === 'onboarding') return 'Back to setup'
|
if (serverFlowFrom.value === 'onboarding') return 'Back to setup'
|
||||||
@@ -556,7 +556,7 @@ export function createServerInstallContent(opts: {
|
|||||||
|
|
||||||
if (serverFlowFrom.value === 'onboarding') {
|
if (serverFlowFrom.value === 'onboarding') {
|
||||||
await client.archon.servers_v1.endIntro(sid)
|
await client.archon.servers_v1.endIntro(sid)
|
||||||
await router.push(`/hosting/manage/${sid}/content`)
|
await router.push(getServerWorldContentPath(sid, wid))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -571,6 +571,11 @@ export function createServerInstallContent(opts: {
|
|||||||
serverContentProjectIds.value = new Set([...serverContentProjectIds.value, id])
|
serverContentProjectIds.value = new Set([...serverContentProjectIds.value, id])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getServerWorldContentPath(serverId: string, worldId: string | null) {
|
||||||
|
const base = `/hosting/manage/${encodeURIComponent(serverId)}/worlds`
|
||||||
|
return worldId ? `${base}/${encodeURIComponent(worldId)}` : base
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
serverIdQuery,
|
serverIdQuery,
|
||||||
worldIdQuery,
|
worldIdQuery,
|
||||||
|
|||||||
@@ -65,6 +65,40 @@ export default new createRouter({
|
|||||||
breadcrumb: [{ name: '?Server' }],
|
breadcrumb: [{ name: '?Server' }],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'worlds/:world_id',
|
||||||
|
name: 'ServerManageWorld',
|
||||||
|
component: Hosting.World,
|
||||||
|
meta: {
|
||||||
|
breadcrumb: [{ name: '?Server' }],
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
name: 'ServerManageWorldContent',
|
||||||
|
component: Hosting.Content,
|
||||||
|
meta: {
|
||||||
|
breadcrumb: [{ name: '?Server' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'files',
|
||||||
|
name: 'ServerManageWorldFiles',
|
||||||
|
component: Hosting.Files,
|
||||||
|
meta: {
|
||||||
|
breadcrumb: [{ name: '?Server' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'backups',
|
||||||
|
name: 'ServerManageWorldBackups',
|
||||||
|
component: Hosting.Backups,
|
||||||
|
meta: {
|
||||||
|
breadcrumb: [{ name: '?Server' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'files',
|
path: 'files',
|
||||||
name: 'ServerManageFiles',
|
name: 'ServerManageFiles',
|
||||||
|
|||||||
@@ -187,7 +187,10 @@ export function useServerInstallContent({
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const contentQueryKey = computed(() => ['content', 'list', currentServerId.value ?? ''] as const)
|
const contentQueryKey = computed(
|
||||||
|
() =>
|
||||||
|
['content', 'list', 'v1', currentServerId.value ?? '', currentWorldId.value ?? null] as const,
|
||||||
|
)
|
||||||
const { data: serverContentData, error: serverContentError } = useQuery({
|
const { data: serverContentData, error: serverContentError } = useQuery({
|
||||||
queryKey: contentQueryKey,
|
queryKey: contentQueryKey,
|
||||||
queryFn: () =>
|
queryFn: () =>
|
||||||
@@ -659,7 +662,7 @@ export function useServerInstallContent({
|
|||||||
if (fromContext.value === 'onboarding') {
|
if (fromContext.value === 'onboarding') {
|
||||||
await client.archon.servers_v1.endIntro(currentServerId.value)
|
await client.archon.servers_v1.endIntro(currentServerId.value)
|
||||||
queryClient.invalidateQueries({ queryKey: ['servers', 'detail', currentServerId.value] })
|
queryClient.invalidateQueries({ queryKey: ['servers', 'detail', currentServerId.value] })
|
||||||
navigateTo(`/hosting/manage/${currentServerId.value}/content`)
|
navigateTo(getServerWorldContentPath(currentServerId.value, currentWorldId.value ?? null))
|
||||||
} else {
|
} else {
|
||||||
navigateTo(`/hosting/manage/${currentServerId.value}?openSettings=installation`)
|
navigateTo(`/hosting/manage/${currentServerId.value}?openSettings=installation`)
|
||||||
}
|
}
|
||||||
@@ -675,9 +678,14 @@ export function useServerInstallContent({
|
|||||||
if (fromContext.value === 'onboarding') return `/hosting/manage/${id}?resumeModal=setup-type`
|
if (fromContext.value === 'onboarding') return `/hosting/manage/${id}?resumeModal=setup-type`
|
||||||
if (fromContext.value === 'reset-server')
|
if (fromContext.value === 'reset-server')
|
||||||
return `/hosting/manage/${id}?openSettings=installation`
|
return `/hosting/manage/${id}?openSettings=installation`
|
||||||
return `/hosting/manage/${id}/content`
|
return getServerWorldContentPath(id, currentWorldId.value)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function getServerWorldContentPath(serverId: string, worldId: string | null) {
|
||||||
|
const base = `/hosting/manage/${encodeURIComponent(serverId)}/worlds`
|
||||||
|
return worldId ? `${base}/${encodeURIComponent(worldId)}` : base
|
||||||
|
}
|
||||||
|
|
||||||
const serverBackLabel = computed(() => {
|
const serverBackLabel = computed(() => {
|
||||||
if (fromContext.value === 'onboarding') return formatMessage(messages.backToSetup)
|
if (fromContext.value === 'onboarding') return formatMessage(messages.backToSetup)
|
||||||
if (fromContext.value === 'reset-server') return formatMessage(messages.cancelReset)
|
if (fromContext.value === 'reset-server') return formatMessage(messages.cancelReset)
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { createModrinthClient } from '~/helpers/api.ts'
|
||||||
|
|
||||||
|
export default defineNuxtRouteMiddleware(async (to) => {
|
||||||
|
const match = to.path.match(/^\/hosting\/manage\/([^/]+)\/(content|files|backups)\/?$/)
|
||||||
|
if (!match) return
|
||||||
|
|
||||||
|
const serverId = decodeURIComponent(match[1])
|
||||||
|
const tab = match[2]
|
||||||
|
const worldsPath = `/hosting/manage/${encodeURIComponent(serverId)}/worlds`
|
||||||
|
const tabPath = tab === 'content' ? '' : `/${tab}`
|
||||||
|
const auth = await useAuth()
|
||||||
|
|
||||||
|
if (auth.value.token) {
|
||||||
|
try {
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
const client = createModrinthClient(auth, {
|
||||||
|
apiBaseUrl: config.public.apiBaseUrl.replace('/v2/', '/'),
|
||||||
|
archonBaseUrl: config.public.pyroBaseUrl.replace('/v2/', '/'),
|
||||||
|
rateLimitKey: config.rateLimitKey,
|
||||||
|
})
|
||||||
|
const serverFull = await client.archon.servers_v1.get(serverId)
|
||||||
|
const world = serverFull.worlds.find((item) => item.is_active) ?? serverFull.worlds[0]
|
||||||
|
|
||||||
|
if (world) {
|
||||||
|
return navigateTo(
|
||||||
|
{
|
||||||
|
path: `${worldsPath}/${encodeURIComponent(world.id)}${tabPath}`,
|
||||||
|
query: to.query,
|
||||||
|
hash: to.hash,
|
||||||
|
},
|
||||||
|
{ replace: true },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return navigateTo({ path: worldsPath, query: to.query, hash: to.hash }, { replace: true })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return navigateTo({ path: worldsPath, query: to.query, hash: to.hash }, { replace: true })
|
||||||
|
})
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ServersManageWorldRootLayout } from '@modrinth/ui'
|
||||||
|
|
||||||
|
const route = useNativeRoute()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ServersManageWorldRootLayout>
|
||||||
|
<NuxtPage :route="route" />
|
||||||
|
</ServersManageWorldRootLayout>
|
||||||
|
</template>
|
||||||
+1
-1
@@ -14,7 +14,7 @@ const flags = useFeatureFlags()
|
|||||||
if (worldId.value) {
|
if (worldId.value) {
|
||||||
try {
|
try {
|
||||||
await queryClient.ensureQueryData({
|
await queryClient.ensureQueryData({
|
||||||
queryKey: ['backups', 'list', serverId],
|
queryKey: ['backups', 'list', serverId, worldId.value],
|
||||||
queryFn: () => client.archon.backups_v1.list(serverId, worldId.value!),
|
queryFn: () => client.archon.backups_v1.list(serverId, worldId.value!),
|
||||||
staleTime: 30_000,
|
staleTime: 30_000,
|
||||||
})
|
})
|
||||||
+1
-1
@@ -38,7 +38,7 @@ const contentWorldId = await getContentWorldId()
|
|||||||
if (contentWorldId) {
|
if (contentWorldId) {
|
||||||
try {
|
try {
|
||||||
const content = await queryClient.ensureQueryData({
|
const content = await queryClient.ensureQueryData({
|
||||||
queryKey: ['content', 'list', 'v1', serverId],
|
queryKey: ['content', 'list', 'v1', serverId, contentWorldId],
|
||||||
queryFn: () =>
|
queryFn: () =>
|
||||||
client.archon.content_v1.getAddons(serverId, contentWorldId, { from_modpack: false }),
|
client.archon.content_v1.getAddons(serverId, contentWorldId, { from_modpack: false }),
|
||||||
staleTime: 30_000,
|
staleTime: 30_000,
|
||||||
@@ -77,7 +77,7 @@ onMounted(() => {
|
|||||||
isClient.value = true
|
isClient.value = true
|
||||||
})
|
})
|
||||||
|
|
||||||
const { serverId } = injectModrinthServerContext()
|
const { serverId, worldId } = injectModrinthServerContext()
|
||||||
const { featureFlags } = injectPageContext()
|
const { featureFlags } = injectPageContext()
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
@@ -190,7 +190,9 @@ const metrics = computed(() => {
|
|||||||
showGraph: false,
|
showGraph: false,
|
||||||
chartOptions: null as ReturnType<typeof buildChartOptions> | null,
|
chartOptions: null as ReturnType<typeof buildChartOptions> | null,
|
||||||
series: null as { name: string; data: number[] }[] | null,
|
series: null as { name: string; data: number[] }[] | null,
|
||||||
link: `/hosting/manage/${encodeURIComponent(serverId)}/files`,
|
link: worldId.value
|
||||||
|
? `/hosting/manage/${encodeURIComponent(serverId)}/worlds/${encodeURIComponent(worldId.value)}/files`
|
||||||
|
: `/hosting/manage/${encodeURIComponent(serverId)}/worlds`,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (props.loading) {
|
if (props.loading) {
|
||||||
|
|||||||
@@ -195,6 +195,13 @@ async function show({ serverId, tabIndex, tabId }: ShowOptions) {
|
|||||||
queryKey: ['servers', 'properties', 'v1', targetServerId, worldId.value],
|
queryKey: ['servers', 'properties', 'v1', targetServerId, worldId.value],
|
||||||
queryFn: () => client.archon.properties_v1.getProperties(targetServerId, worldId.value!),
|
queryFn: () => client.archon.properties_v1.getProperties(targetServerId, worldId.value!),
|
||||||
})
|
})
|
||||||
|
queryClient.prefetchQuery({
|
||||||
|
queryKey: ['content', 'list', 'v1', targetServerId, worldId.value],
|
||||||
|
queryFn: () =>
|
||||||
|
client.archon.content_v1.getAddons(targetServerId, worldId.value!, {
|
||||||
|
from_modpack: false,
|
||||||
|
}),
|
||||||
|
})
|
||||||
queryClient.prefetchQuery({
|
queryClient.prefetchQuery({
|
||||||
queryKey: ['servers', 'startup', 'v1', targetServerId, worldId.value],
|
queryKey: ['servers', 'startup', 'v1', targetServerId, worldId.value],
|
||||||
queryFn: () => client.archon.options_v1.getStartup(targetServerId, worldId.value!),
|
queryFn: () => client.archon.options_v1.getStartup(targetServerId, worldId.value!),
|
||||||
|
|||||||
@@ -55,8 +55,12 @@ const messages = defineMessages({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const isOnContentTab = computed(() => route.path.includes('/content'))
|
|
||||||
const isOnFilesTab = computed(() => route.path.includes('/files'))
|
const isOnFilesTab = computed(() => route.path.includes('/files'))
|
||||||
|
const isOnContentTab = computed(
|
||||||
|
() =>
|
||||||
|
route.path.includes('/content') ||
|
||||||
|
(!!route.params.world_id && !isOnFilesTab.value && !route.path.includes('/backups')),
|
||||||
|
)
|
||||||
|
|
||||||
const bannerCoversInstalling = computed(
|
const bannerCoversInstalling = computed(
|
||||||
() =>
|
() =>
|
||||||
|
|||||||
@@ -103,12 +103,12 @@ const props = withDefaults(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
const backupsQueryKey = ['backups', 'queue', ctx.serverId]
|
const backupsQueryKey = computed(() => ['backups', 'queue', ctx.serverId, ctx.worldId.value])
|
||||||
|
|
||||||
const createMutation = useMutation({
|
const createMutation = useMutation({
|
||||||
mutationFn: (name: string) =>
|
mutationFn: (name: string) =>
|
||||||
client.archon.backups_queue_v1.create(ctx.serverId, ctx.worldId.value!, { name }),
|
client.archon.backups_queue_v1.create(ctx.serverId, ctx.worldId.value!, { name }),
|
||||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: backupsQueryKey }),
|
onSuccess: () => queryClient.invalidateQueries({ queryKey: backupsQueryKey.value }),
|
||||||
})
|
})
|
||||||
|
|
||||||
const modal = ref<InstanceType<typeof NewModal>>()
|
const modal = ref<InstanceType<typeof NewModal>>()
|
||||||
|
|||||||
@@ -85,12 +85,12 @@ const props = withDefaults(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
const backupsQueryKey = ['backups', 'queue', ctx.serverId]
|
const backupsQueryKey = computed(() => ['backups', 'queue', ctx.serverId, ctx.worldId.value])
|
||||||
|
|
||||||
const renameMutation = useMutation({
|
const renameMutation = useMutation({
|
||||||
mutationFn: ({ backupId, name }: { backupId: string; name: string }) =>
|
mutationFn: ({ backupId, name }: { backupId: string; name: string }) =>
|
||||||
client.archon.backups_v1.rename(ctx.serverId, ctx.worldId.value!, backupId, { name }),
|
client.archon.backups_v1.rename(ctx.serverId, ctx.worldId.value!, backupId, { name }),
|
||||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: backupsQueryKey }),
|
onSuccess: () => queryClient.invalidateQueries({ queryKey: backupsQueryKey.value }),
|
||||||
})
|
})
|
||||||
|
|
||||||
const modal = ref<InstanceType<typeof NewModal>>()
|
const modal = ref<InstanceType<typeof NewModal>>()
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ const props = withDefaults(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
const backupsQueryKey = ['backups', 'queue', ctx.serverId]
|
const backupsQueryKey = computed(() => ['backups', 'queue', ctx.serverId, ctx.worldId.value])
|
||||||
|
|
||||||
function safetyBackupName(backupName: string) {
|
function safetyBackupName(backupName: string) {
|
||||||
const base = `Before restoring "${backupName}"`
|
const base = `Before restoring "${backupName}"`
|
||||||
@@ -84,7 +84,7 @@ function safetyBackupName(backupName: string) {
|
|||||||
const restoreMutation = useMutation({
|
const restoreMutation = useMutation({
|
||||||
mutationFn: ({ backupId, name }: { backupId: string; name: string }) =>
|
mutationFn: ({ backupId, name }: { backupId: string; name: string }) =>
|
||||||
client.archon.backups_queue_v1.restore(ctx.serverId, ctx.worldId.value!, backupId, { name }),
|
client.archon.backups_queue_v1.restore(ctx.serverId, ctx.worldId.value!, backupId, { name }),
|
||||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: backupsQueryKey }),
|
onSuccess: () => queryClient.invalidateQueries({ queryKey: backupsQueryKey.value }),
|
||||||
})
|
})
|
||||||
|
|
||||||
const modal = ref<InstanceType<typeof NewModal>>()
|
const modal = ref<InstanceType<typeof NewModal>>()
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="contents">
|
<div class="contents">
|
||||||
<div class="flex flex-row items-center gap-2 rounded-lg">
|
<div class="flex flex-row items-center gap-2 rounded-lg">
|
||||||
<ButtonStyled v-if="isInstalling" type="standard" color="brand" size="large">
|
<ButtonStyled v-if="isInstalling" type="standard" color="brand" :size="size">
|
||||||
<button disabled class="flex-shrink-0">
|
<button disabled class="flex-shrink-0">
|
||||||
<LoaderCircleIcon class="size-5 animate-spin" /> Installing...
|
<LoaderCircleIcon class="size-5 animate-spin" /> Installing...
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
|
|
||||||
<template v-else-if="showRestartButton">
|
<template v-else-if="showRestartButton">
|
||||||
<ButtonStyled type="standard" color="orange" size="large">
|
<ButtonStyled type="standard" color="orange" :size="size">
|
||||||
<button v-tooltip="busyTooltip" :disabled="!canTakeAction" @click="handlePrimaryAction">
|
<button v-tooltip="busyTooltip" :disabled="!canTakeAction" @click="handlePrimaryAction">
|
||||||
<UpdatedIcon />
|
<UpdatedIcon />
|
||||||
<span>{{ primaryActionText }}</span>
|
<span>{{ primaryActionText }}</span>
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
|
|
||||||
<JoinedButtons
|
<JoinedButtons
|
||||||
color="red"
|
color="red"
|
||||||
size="large"
|
:size="size"
|
||||||
:actions="stopSplitActions"
|
:actions="stopSplitActions"
|
||||||
:primary-disabled="!canTakeAction"
|
:primary-disabled="!canTakeAction"
|
||||||
:dropdown-disabled="!canKill"
|
:dropdown-disabled="!canKill"
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
<template v-else-if="isStopping">
|
<template v-else-if="isStopping">
|
||||||
<JoinedButtons
|
<JoinedButtons
|
||||||
color="red"
|
color="red"
|
||||||
size="large"
|
:size="size"
|
||||||
:actions="stopSplitActions"
|
:actions="stopSplitActions"
|
||||||
:primary-disabled="true"
|
:primary-disabled="true"
|
||||||
:dropdown-disabled="!canKill"
|
:dropdown-disabled="!canKill"
|
||||||
@@ -49,10 +49,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<ButtonStyled type="standard" color="brand" size="large">
|
<ButtonStyled type="standard" color="brand" :size="size">
|
||||||
<button v-tooltip="busyTooltip" :disabled="!canTakeAction" @click="handlePrimaryAction">
|
<button v-tooltip="busyTooltip" :disabled="!canTakeAction" @click="handlePrimaryAction">
|
||||||
<PlayIcon />
|
<PlayIcon />
|
||||||
<span>{{ primaryActionText }}</span>
|
<span>{{ startActionText }}</span>
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</template>
|
</template>
|
||||||
@@ -77,9 +77,13 @@ import { useServerPowerAction } from './use-server-power-action'
|
|||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
|
size?: 'standard' | 'large' | 'small'
|
||||||
|
startLabel?: string
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
disabled: false,
|
disabled: false,
|
||||||
|
size: 'large',
|
||||||
|
startLabel: 'Start',
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -97,6 +101,11 @@ const {
|
|||||||
disabled: computed(() => props.disabled),
|
disabled: computed(() => props.disabled),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const size = computed(() => props.size)
|
||||||
|
const startActionText = computed(() =>
|
||||||
|
primaryActionText.value === 'Start' ? props.startLabel : primaryActionText.value,
|
||||||
|
)
|
||||||
|
|
||||||
const stopSplitActions = computed<JoinedButtonAction[]>(() => [
|
const stopSplitActions = computed<JoinedButtonAction[]>(() => [
|
||||||
{
|
{
|
||||||
id: 'stop',
|
id: 'stop',
|
||||||
|
|||||||
@@ -18,46 +18,28 @@
|
|||||||
<SettingsIcon />
|
<SettingsIcon />
|
||||||
Configuring server...
|
Configuring server...
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="flex flex-wrap items-center gap-2">
|
<div v-else class="flex min-w-0 flex-wrap items-center gap-2">
|
||||||
<div v-if="props.server?.loader" class="flex items-center gap-2 font-medium">
|
<template v-for="(item, index) in headerStats" :key="item.id">
|
||||||
<LoaderIcon :loader="props.server.loader" class="flex shrink-0 [&&]:size-5" />
|
<div v-if="index > 0" class="h-1.5 w-1.5 rounded-full bg-surface-5" />
|
||||||
{{ formatLoaderLabel(props.server.loader) }} {{ props.server.mc_version }}
|
<button
|
||||||
</div>
|
v-if="item.copyable"
|
||||||
|
v-tooltip="'Copy server address'"
|
||||||
<div
|
class="m-0 flex min-w-0 cursor-pointer items-center gap-2 border-0 bg-transparent p-0 font-medium text-secondary hover:underline text-nowrap"
|
||||||
v-if="
|
type="button"
|
||||||
props.server?.loader &&
|
@click="copyServerAddress"
|
||||||
props.server?.net?.domain &&
|
>
|
||||||
!userPreferences.hideSubdomainLabel
|
<component :is="item.icon" class="flex size-5 shrink-0" />
|
||||||
"
|
<span class="truncate">{{ item.label }}</span>
|
||||||
class="h-1.5 w-1.5 rounded-full bg-surface-5"
|
</button>
|
||||||
/>
|
<div v-else class="flex min-w-0 items-center gap-2 font-medium text-secondary text-nowrap">
|
||||||
|
<component :is="item.icon" class="flex size-5 shrink-0" />
|
||||||
<div
|
<span class="truncate">{{ item.label }}</span>
|
||||||
v-if="props.server?.net?.domain && !userPreferences.hideSubdomainLabel"
|
</div>
|
||||||
v-tooltip="'Copy server address'"
|
</template>
|
||||||
class="flex cursor-pointer items-center gap-2 font-medium hover:underline text-nowrap"
|
<div v-if="showProject && headerStats.length > 0" class="h-1.5 w-1.5 rounded-full bg-surface-5" />
|
||||||
@click="copyServerAddress"
|
|
||||||
>
|
|
||||||
<LinkIcon class="flex size-5 shrink-0" />
|
|
||||||
{{ props.server.net.domain }}.modrinth.gg
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="showUptime" class="h-1.5 w-1.5 rounded-full bg-surface-5" />
|
|
||||||
|
|
||||||
<div v-if="showUptime" class="flex items-center gap-2 font-medium">
|
|
||||||
<TimerIcon class="flex size-5 shrink-0" />
|
|
||||||
{{ formattedUptime }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-if="showProject && (props.server?.loader || props.server?.net?.domain || showUptime)"
|
|
||||||
class="h-1.5 w-1.5 rounded-full bg-surface-5"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="showProject"
|
v-if="showProject"
|
||||||
class="flex items-center gap-1.5 font-medium text-primary text-nowrap"
|
class="flex min-w-0 items-center gap-1.5 font-medium text-primary text-nowrap"
|
||||||
>
|
>
|
||||||
Linked to
|
Linked to
|
||||||
<Avatar
|
<Avatar
|
||||||
@@ -81,8 +63,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Archon } from '@modrinth/api-client'
|
import type { Archon } from '@modrinth/api-client'
|
||||||
import { NuxtModrinthClient } 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 { useStorage } from '@vueuse/core'
|
||||||
|
import type { Component } from 'vue'
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
|
||||||
import { AutoLink, Avatar, ContentPageHeader, ServerIcon } from '#ui/components'
|
import { AutoLink, Avatar, ContentPageHeader, ServerIcon } from '#ui/components'
|
||||||
@@ -102,12 +85,20 @@ type ServerProjectSummary = {
|
|||||||
icon_url?: string | null
|
icon_url?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type HeaderStat = {
|
||||||
|
id: string
|
||||||
|
label: string
|
||||||
|
icon: Component
|
||||||
|
copyable?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
server: Archon.Servers.v0.Server | null | undefined
|
server: Archon.Servers.v0.Server | null | undefined
|
||||||
serverImage?: string | null
|
serverImage?: string | null
|
||||||
serverProject?: ServerProjectSummary | null
|
serverProject?: ServerProjectSummary | null
|
||||||
serverProjectLink?: string
|
serverProjectLink?: string
|
||||||
|
activeWorldName?: string | null
|
||||||
uptimeSeconds?: number
|
uptimeSeconds?: number
|
||||||
showUptime?: boolean
|
showUptime?: boolean
|
||||||
backHref?: string
|
backHref?: string
|
||||||
@@ -118,6 +109,7 @@ const props = withDefaults(
|
|||||||
serverImage: null,
|
serverImage: null,
|
||||||
serverProject: null,
|
serverProject: null,
|
||||||
serverProjectLink: '',
|
serverProjectLink: '',
|
||||||
|
activeWorldName: null,
|
||||||
uptimeSeconds: 0,
|
uptimeSeconds: 0,
|
||||||
showUptime: true,
|
showUptime: true,
|
||||||
backHref: '/hosting/manage',
|
backHref: '/hosting/manage',
|
||||||
@@ -158,6 +150,57 @@ const formattedUptime = computed(() => {
|
|||||||
return formatted.trim()
|
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<HeaderStat[]>(() => {
|
||||||
|
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 showProject = computed(() => !!props.serverProject)
|
||||||
|
|
||||||
const serverProjectLink = computed(() => {
|
const serverProjectLink = computed(() => {
|
||||||
@@ -171,8 +214,8 @@ const serverProjectLink = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
function copyServerAddress() {
|
function copyServerAddress() {
|
||||||
if (!props.server?.net?.domain) return
|
if (!serverAddress.value) return
|
||||||
navigator.clipboard.writeText(`${props.server.net.domain}.modrinth.gg`)
|
navigator.clipboard.writeText(serverAddress.value)
|
||||||
addNotification({
|
addNotification({
|
||||||
title: 'Server address copied',
|
title: 'Server address copied',
|
||||||
text: "Your server's address has been copied to your clipboard.",
|
text: "Your server's address has been copied to your clipboard.",
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export function useServerBackupsQueue(serverId: Ref<string>, worldId: Ref<string
|
|||||||
const client = injectModrinthClient()
|
const client = injectModrinthClient()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
const queryKey = computed(() => ['backups', 'queue', serverId.value] as const)
|
const queryKey = computed(() => ['backups', 'queue', serverId.value, worldId.value] as const)
|
||||||
|
|
||||||
const progressOverlay = reactive(new Map<ProgressKey, number>())
|
const progressOverlay = reactive(new Map<ProgressKey, number>())
|
||||||
const lastSeenState = new Map<ProgressKey, Archon.Websocket.v0.BackupState>()
|
const lastSeenState = new Map<ProgressKey, Archon.Websocket.v0.BackupState>()
|
||||||
|
|||||||
@@ -227,6 +227,7 @@ const resetServerDisabledTooltip = computed(() => {
|
|||||||
})
|
})
|
||||||
const installationSettingsLayout = ref<InstanceType<typeof InstallationSettingsLayout>>()
|
const installationSettingsLayout = ref<InstanceType<typeof InstallationSettingsLayout>>()
|
||||||
const setupModal = ref<InstanceType<typeof ServerSetupModal>>()
|
const setupModal = ref<InstanceType<typeof ServerSetupModal>>()
|
||||||
|
const contentListQueryKey = computed(() => ['content', 'list', 'v1', serverId, worldId.value])
|
||||||
|
|
||||||
function showResetServerModal() {
|
function showResetServerModal() {
|
||||||
if (resetServerDisabled.value) return
|
if (resetServerDisabled.value) return
|
||||||
@@ -237,13 +238,13 @@ async function invalidateServerState() {
|
|||||||
debug('invalidateServerState: starting')
|
debug('invalidateServerState: starting')
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
queryClient.invalidateQueries({ queryKey: ['servers', 'detail', serverId] }),
|
queryClient.invalidateQueries({ queryKey: ['servers', 'detail', serverId] }),
|
||||||
queryClient.invalidateQueries({ queryKey: ['content', 'list', 'v1', serverId] }),
|
queryClient.invalidateQueries({ queryKey: contentListQueryKey.value }),
|
||||||
])
|
])
|
||||||
debug('invalidateServerState: complete')
|
debug('invalidateServerState: complete')
|
||||||
}
|
}
|
||||||
|
|
||||||
const addonsQuery = useQuery({
|
const addonsQuery = useQuery({
|
||||||
queryKey: computed(() => ['content', 'list', 'v1', serverId]),
|
queryKey: contentListQueryKey,
|
||||||
queryFn: () =>
|
queryFn: () =>
|
||||||
client.archon.content_v1.getAddons(serverId, worldId.value!, { from_modpack: false }),
|
client.archon.content_v1.getAddons(serverId, worldId.value!, { from_modpack: false }),
|
||||||
enabled: computed(() => worldId.value !== null),
|
enabled: computed(() => worldId.value !== null),
|
||||||
@@ -670,7 +671,7 @@ provideInstallationSettings({
|
|||||||
const previousData = addonsQuery.data.value
|
const previousData = addonsQuery.data.value
|
||||||
if (previousData) {
|
if (previousData) {
|
||||||
debug('unlinkModpack: optimistically removing modpack from cache')
|
debug('unlinkModpack: optimistically removing modpack from cache')
|
||||||
queryClient.setQueryData(['content', 'list', 'v1', serverId], {
|
queryClient.setQueryData(contentListQueryKey.value, {
|
||||||
...previousData,
|
...previousData,
|
||||||
modpack: null,
|
modpack: null,
|
||||||
})
|
})
|
||||||
@@ -682,7 +683,7 @@ provideInstallationSettings({
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
debug('unlinkModpack: failed, reverting cache', err)
|
debug('unlinkModpack: failed, reverting cache', err)
|
||||||
if (previousData) {
|
if (previousData) {
|
||||||
queryClient.setQueryData(['content', 'list', 'v1', serverId], previousData)
|
queryClient.setQueryData(contentListQueryKey.value, previousData)
|
||||||
}
|
}
|
||||||
addNotification({
|
addNotification({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
@@ -695,7 +696,7 @@ provideInstallationSettings({
|
|||||||
queryKey: ['servers', 'detail', serverId],
|
queryKey: ['servers', 'detail', serverId],
|
||||||
}),
|
}),
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: ['content', 'list', 'v1', serverId],
|
queryKey: contentListQueryKey.value,
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
debug('unlinkModpack: invalidation complete')
|
debug('unlinkModpack: invalidation complete')
|
||||||
|
|||||||
@@ -317,8 +317,10 @@ const { canUseAdvancedSettings, canUsePowerActions, permissionDeniedMessage } =
|
|||||||
const advancedActionTooltip = computed(() =>
|
const advancedActionTooltip = computed(() =>
|
||||||
canUseAdvancedSettings.value ? undefined : permissionDeniedMessage.value,
|
canUseAdvancedSettings.value ? undefined : permissionDeniedMessage.value,
|
||||||
)
|
)
|
||||||
const filesTabLink = computed(
|
const filesTabLink = computed(() =>
|
||||||
() => `/hosting/manage/${encodeURIComponent(serverId)}/files?path=/&editing=server.properties`,
|
worldId.value
|
||||||
|
? `/hosting/manage/${encodeURIComponent(serverId)}/worlds/${encodeURIComponent(worldId.value)}/files?path=/&editing=server.properties`
|
||||||
|
: `/hosting/manage/${encodeURIComponent(serverId)}/worlds`,
|
||||||
)
|
)
|
||||||
const serverSettings = injectServerSettings(null)
|
const serverSettings = injectServerSettings(null)
|
||||||
|
|
||||||
|
|||||||
@@ -272,7 +272,11 @@ async function finalizeSetup() {
|
|||||||
client.archon.servers_v1.endIntro(serverId).then(() => {
|
client.archon.servers_v1.endIntro(serverId).then(() => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['servers', 'detail', serverId] })
|
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 */
|
/** Map UI loader names to API Modloader values */
|
||||||
|
|||||||
@@ -362,7 +362,7 @@ const filterPillOptions = computed<FilterPillOption[]>(() => [
|
|||||||
])
|
])
|
||||||
const client = injectModrinthClient()
|
const client = injectModrinthClient()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const { server, worldId, busyReasons } = injectModrinthServerContext()
|
const { server, serverId, worldId, busyReasons } = injectModrinthServerContext()
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
isServerRunning: boolean
|
isServerRunning: boolean
|
||||||
@@ -371,9 +371,7 @@ const props = defineProps<{
|
|||||||
}>()
|
}>()
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const serverId = route.params.id as string
|
|
||||||
const BACKUP_HIGHLIGHT_DURATION_MS = 5_000
|
const BACKUP_HIGHLIGHT_DURATION_MS = 5_000
|
||||||
|
|
||||||
defineEmits(['onDownload'])
|
defineEmits(['onDownload'])
|
||||||
|
|
||||||
const { backups, invalidate, activeOperationByBackupId, hasActiveCreate, hasActiveRestore, query } =
|
const { backups, invalidate, activeOperationByBackupId, hasActiveCreate, hasActiveRestore, query } =
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { ClipboardCopyIcon } from '@modrinth/assets'
|
|||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||||
import { useIntervalFn } from '@vueuse/core'
|
import { useIntervalFn } from '@vueuse/core'
|
||||||
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
|
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 ReadyTransition from '#ui/components/base/ReadyTransition.vue'
|
||||||
import { useUploadSessionUpload } from '#ui/composables/hosting/kyros-session-upload'
|
import { useUploadSessionUpload } from '#ui/composables/hosting/kyros-session-upload'
|
||||||
@@ -115,7 +115,7 @@ const messages = defineMessages({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const client = injectModrinthClient()
|
const client = injectModrinthClient()
|
||||||
const { server, worldId, busyReasons, isSyncingContent, uploadState, cancelUpload } =
|
const { server, serverId, worldId, busyReasons, isSyncingContent, uploadState, cancelUpload } =
|
||||||
injectModrinthServerContext()
|
injectModrinthServerContext()
|
||||||
const contentUploadSession = useUploadSessionUpload({
|
const contentUploadSession = useUploadSessionUpload({
|
||||||
client,
|
client,
|
||||||
@@ -127,10 +127,8 @@ const contentUploadSession = useUploadSessionUpload({
|
|||||||
const { addNotification } = injectNotificationManager()
|
const { addNotification } = injectNotificationManager()
|
||||||
const { openServerSettings, browseServerContent } = injectServerSettingsModal()
|
const { openServerSettings, browseServerContent } = injectServerSettingsModal()
|
||||||
const { canSetup, permissionDeniedMessage } = useServerPermissions()
|
const { canSetup, permissionDeniedMessage } = useServerPermissions()
|
||||||
const route = useRoute()
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const serverId = route.params.id as string
|
|
||||||
|
|
||||||
const type = computed(() => {
|
const type = computed(() => {
|
||||||
const loader = server.value?.loader?.toLowerCase()
|
const loader = server.value?.loader?.toLowerCase()
|
||||||
@@ -139,8 +137,15 @@ const type = computed(() => {
|
|||||||
return 'mod'
|
return 'mod'
|
||||||
})
|
})
|
||||||
|
|
||||||
const queryKey = computed(() => ['content', 'list', 'v1', serverId])
|
const queryKey = computed(() => ['content', 'list', 'v1', serverId, worldId.value])
|
||||||
const modpackContentQueryKey = computed(() => ['content', 'list', 'v1', serverId, 'modpack'])
|
const modpackContentQueryKey = computed(() => [
|
||||||
|
'content',
|
||||||
|
'list',
|
||||||
|
'v1',
|
||||||
|
serverId,
|
||||||
|
worldId.value,
|
||||||
|
'modpack',
|
||||||
|
])
|
||||||
|
|
||||||
function getContentOwnerAvatarUrl(owner: ContentOwnerAvatarSource) {
|
function getContentOwnerAvatarUrl(owner: ContentOwnerAvatarSource) {
|
||||||
const ownerId = owner.type === 'user' ? owner.name || owner.id : owner.id
|
const ownerId = owner.type === 'user' ? owner.name || owner.id : owner.id
|
||||||
|
|||||||
@@ -92,7 +92,17 @@
|
|||||||
</template>
|
</template>
|
||||||
</ErrorInformationCard>
|
</ErrorInformationCard>
|
||||||
</div>
|
</div>
|
||||||
<!-- SERVER START -->
|
<div
|
||||||
|
v-else-if="serverData && isWorldDetailRoute"
|
||||||
|
data-pyro-world-manager-root
|
||||||
|
class="experimental-styles-within relative mx-auto box-border flex w-full min-w-0 flex-col px-6 transition-all duration-300"
|
||||||
|
:class="[
|
||||||
|
'server-panel-' + revealState,
|
||||||
|
isNuxt ? 'min-h-[100svh] max-w-[1280px] pb-16' : 'min-h-[calc(100svh-100px)] pb-6',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<slot :on-reinstall="onReinstall" :on-reinstall-failed="onReinstallFailed" />
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else-if="serverData"
|
v-else-if="serverData"
|
||||||
data-pyro-server-manager-root
|
data-pyro-server-manager-root
|
||||||
@@ -115,6 +125,7 @@
|
|||||||
:server="serverData"
|
:server="serverData"
|
||||||
:server-image="serverImage"
|
:server-image="serverImage"
|
||||||
:server-project="serverProject"
|
:server-project="serverProject"
|
||||||
|
:active-world-name="activeWorldName"
|
||||||
:uptime-seconds="showUptime ? uptimeSeconds : undefined"
|
:uptime-seconds="showUptime ? uptimeSeconds : undefined"
|
||||||
>
|
>
|
||||||
<template #actions>
|
<template #actions>
|
||||||
@@ -305,8 +316,8 @@
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="showAdvancedDebugInfo"
|
v-if="showAdvancedDebugInfo && !isWorldDetailRoute"
|
||||||
class="relative mx-auto mt-6 box-border w-full min-w-0 max-w-[1280px] px-6"
|
class="experimental-styles-within relative mx-auto mt-6 box-border w-full min-w-0 max-w-[1280px] px-6"
|
||||||
>
|
>
|
||||||
<h2 class="m-0 text-lg font-extrabold text-contrast">
|
<h2 class="m-0 text-lg font-extrabold text-contrast">
|
||||||
{{ formatMessage(messages.serverDataTitle) }}
|
{{ formatMessage(messages.serverDataTitle) }}
|
||||||
@@ -334,12 +345,9 @@
|
|||||||
import type { Archon, Labrinth } from '@modrinth/api-client'
|
import type { Archon, Labrinth } from '@modrinth/api-client'
|
||||||
import { getNodeWebSocketUrl, ModrinthApiError } from '@modrinth/api-client'
|
import { getNodeWebSocketUrl, ModrinthApiError } from '@modrinth/api-client'
|
||||||
import {
|
import {
|
||||||
BoxesIcon,
|
|
||||||
CheckIcon,
|
CheckIcon,
|
||||||
CopyIcon,
|
CopyIcon,
|
||||||
DatabaseBackupIcon,
|
|
||||||
FileIcon,
|
FileIcon,
|
||||||
FolderOpenIcon,
|
|
||||||
IssuesIcon,
|
IssuesIcon,
|
||||||
LayoutTemplateIcon,
|
LayoutTemplateIcon,
|
||||||
LoaderCircleIcon,
|
LoaderCircleIcon,
|
||||||
@@ -624,22 +632,10 @@ const messages = defineMessages({
|
|||||||
id: 'servers.manage.nav.overview',
|
id: 'servers.manage.nav.overview',
|
||||||
defaultMessage: 'Overview',
|
defaultMessage: 'Overview',
|
||||||
},
|
},
|
||||||
contentNav: {
|
|
||||||
id: 'servers.manage.nav.content',
|
|
||||||
defaultMessage: 'Content',
|
|
||||||
},
|
|
||||||
worldsNav: {
|
worldsNav: {
|
||||||
id: 'servers.manage.nav.worlds',
|
id: 'servers.manage.nav.worlds',
|
||||||
defaultMessage: 'Worlds',
|
defaultMessage: 'Worlds',
|
||||||
},
|
},
|
||||||
filesNav: {
|
|
||||||
id: 'servers.manage.nav.files',
|
|
||||||
defaultMessage: 'Files',
|
|
||||||
},
|
|
||||||
backupsNav: {
|
|
||||||
id: 'servers.manage.nav.backups',
|
|
||||||
defaultMessage: 'Backups',
|
|
||||||
},
|
|
||||||
errorDismissingNotice: {
|
errorDismissingNotice: {
|
||||||
id: 'servers.manage.notice.dismiss-error',
|
id: 'servers.manage.notice.dismiss-error',
|
||||||
defaultMessage: 'Error dismissing notice',
|
defaultMessage: 'Error dismissing notice',
|
||||||
@@ -773,11 +769,29 @@ const { data: serverFull } = useQuery({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const worldId = computed(() => {
|
const worldId = computed(() => {
|
||||||
|
const routeWorldId = getRouteParam(route.params.world_id)
|
||||||
|
if (routeWorldId) return routeWorldId
|
||||||
if (!serverFull.value) return null
|
if (!serverFull.value) return null
|
||||||
const activeWorld = serverFull.value.worlds.find((w) => w.is_active)
|
const activeWorld = serverFull.value.worlds.find((w) => w.is_active)
|
||||||
return activeWorld?.id ?? serverFull.value.worlds[0]?.id ?? null
|
return activeWorld?.id ?? serverFull.value.worlds[0]?.id ?? null
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const isWorldDetailRoute = computed(() => !!getRouteParam(route.params.world_id))
|
||||||
|
const activeWorldName = computed(() => {
|
||||||
|
if (!serverFull.value) return null
|
||||||
|
const activeWorld = serverFull.value.worlds.find((world) => world.is_active)
|
||||||
|
return activeWorld?.name ?? serverFull.value.worlds[0]?.name ?? null
|
||||||
|
})
|
||||||
|
|
||||||
|
function getRouteParam(param: string | string[] | undefined): string | null {
|
||||||
|
if (Array.isArray(param)) return param[0] ?? null
|
||||||
|
return param ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
function getWorldPath(targetWorldId: string) {
|
||||||
|
return `/hosting/manage/${encodeURIComponent(props.serverId)}/worlds/${encodeURIComponent(targetWorldId)}`
|
||||||
|
}
|
||||||
|
|
||||||
const { handleWsBackupProgress, busyReasons: backupsBusy } = useServerBackupsQueue(
|
const { handleWsBackupProgress, busyReasons: backupsBusy } = useServerBackupsQueue(
|
||||||
computed(() => props.serverId),
|
computed(() => props.serverId),
|
||||||
worldId,
|
worldId,
|
||||||
@@ -1032,37 +1046,19 @@ watch(serverData, (data) => {
|
|||||||
const navLinks = computed<Tab[]>(() => [
|
const navLinks = computed<Tab[]>(() => [
|
||||||
{
|
{
|
||||||
label: formatMessage(messages.overviewNav),
|
label: formatMessage(messages.overviewNav),
|
||||||
href: `/hosting/manage/${props.serverId}`,
|
href: `/hosting/manage/${encodeURIComponent(props.serverId)}`,
|
||||||
icon: LayoutTemplateIcon,
|
icon: LayoutTemplateIcon,
|
||||||
subpages: [],
|
subpages: [],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
label: formatMessage(messages.contentNav),
|
|
||||||
href: `/hosting/manage/${props.serverId}/content`,
|
|
||||||
icon: BoxesIcon,
|
|
||||||
subpages: ['mods', 'datapacks'],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
label: formatMessage(messages.worldsNav),
|
label: formatMessage(messages.worldsNav),
|
||||||
href: `/hosting/manage/${props.serverId}/worlds`,
|
href: `/hosting/manage/${encodeURIComponent(props.serverId)}/worlds`,
|
||||||
icon: WorldIcon,
|
icon: WorldIcon,
|
||||||
subpages: [],
|
subpages: [],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
label: formatMessage(messages.filesNav),
|
|
||||||
href: `/hosting/manage/${props.serverId}/files`,
|
|
||||||
icon: FolderOpenIcon,
|
|
||||||
subpages: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: formatMessage(messages.backupsNav),
|
|
||||||
href: `/hosting/manage/${props.serverId}/backups`,
|
|
||||||
icon: DatabaseBackupIcon,
|
|
||||||
subpages: [],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
label: 'Access',
|
label: 'Access',
|
||||||
href: `/hosting/manage/${props.serverId}/access`,
|
href: `/hosting/manage/${encodeURIComponent(props.serverId)}/access`,
|
||||||
icon: UsersIcon,
|
icon: UsersIcon,
|
||||||
subpages: [],
|
subpages: [],
|
||||||
},
|
},
|
||||||
@@ -1219,7 +1215,7 @@ const handleFilesystemOps = (data: Archon.Websocket.v0.WSFilesystemOpsEvent) =>
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleNewMod = () => {
|
const handleNewMod = () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['content', 'list'] })
|
queryClient.invalidateQueries({ queryKey: ['content', 'list', 'v1', props.serverId] })
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleInstallationResult = async (data: Archon.Websocket.v0.WSInstallationResultEvent) => {
|
const handleInstallationResult = async (data: Archon.Websocket.v0.WSInstallationResultEvent) => {
|
||||||
@@ -1305,7 +1301,7 @@ const onReinstall = async (
|
|||||||
|
|
||||||
debug('[root.vue] onReinstall: triggering immediate invalidation')
|
debug('[root.vue] onReinstall: triggering immediate invalidation')
|
||||||
queryClient.invalidateQueries({ queryKey: ['servers', 'detail', props.serverId] })
|
queryClient.invalidateQueries({ queryKey: ['servers', 'detail', props.serverId] })
|
||||||
queryClient.invalidateQueries({ queryKey: ['content', 'list'] })
|
queryClient.invalidateQueries({ queryKey: ['content', 'list', 'v1', props.serverId] })
|
||||||
}
|
}
|
||||||
|
|
||||||
const onReinstallFailed = () => {
|
const onReinstallFailed = () => {
|
||||||
@@ -1354,7 +1350,7 @@ async function invalidateAfterInstall() {
|
|||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: ['servers', 'startup', 'v1', props.serverId],
|
queryKey: ['servers', 'startup', 'v1', props.serverId],
|
||||||
}),
|
}),
|
||||||
queryClient.invalidateQueries({ queryKey: ['content', 'list'] }),
|
queryClient.invalidateQueries({ queryKey: ['content', 'list', 'v1', props.serverId] }),
|
||||||
])
|
])
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
console.error('Error refreshing data after installation:', err)
|
console.error('Error refreshing data after installation:', err)
|
||||||
@@ -1478,7 +1474,10 @@ const copyServerDebugInfo = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const openInstallLog = () => {
|
const openInstallLog = () => {
|
||||||
const url = `/hosting/manage/${props.serverId}/files?editing=${encodeURIComponent(errorLogFile.value)}`
|
const filesPath = worldId.value
|
||||||
|
? `${getWorldPath(worldId.value)}/files`
|
||||||
|
: `/hosting/manage/${encodeURIComponent(props.serverId)}/worlds`
|
||||||
|
const url = `${filesPath}?editing=${encodeURIComponent(errorLogFile.value)}`
|
||||||
window.history.pushState({}, '', url)
|
window.history.pushState({}, '', url)
|
||||||
window.dispatchEvent(new PopStateEvent('popstate'))
|
window.dispatchEvent(new PopStateEvent('popstate'))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,213 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex min-h-[36rem] flex-col gap-6 text-primary">
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<RouterLink
|
||||||
|
:to="worldsPath"
|
||||||
|
class="flex w-fit items-center gap-1 text-base font-medium text-blue hover:underline"
|
||||||
|
>
|
||||||
|
<ChevronLeftIcon class="size-4" aria-hidden="true" />
|
||||||
|
{{ formatMessage(messages.allWorlds) }}
|
||||||
|
</RouterLink>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
||||||
|
<div class="flex min-w-0 flex-col gap-1">
|
||||||
|
<h1 class="m-0 truncate text-2xl font-semibold leading-8 text-contrast">
|
||||||
|
{{ worldName }}
|
||||||
|
</h1>
|
||||||
|
<div class="flex flex-wrap items-center gap-2 text-base font-medium text-secondary">
|
||||||
|
<template v-for="(item, index) in worldMetadata" :key="item">
|
||||||
|
<span>{{ item }}</span>
|
||||||
|
<BulletDivider v-if="index < worldMetadata.length - 1" />
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex shrink-0 items-center gap-2">
|
||||||
|
<PanelServerActionButton size="standard" start-label="Start world" />
|
||||||
|
<ButtonStyled circular>
|
||||||
|
<button
|
||||||
|
v-tooltip="formatMessage(messages.worldSettings)"
|
||||||
|
@click="openServerSettings({ tabId: 'installation' })"
|
||||||
|
>
|
||||||
|
<SettingsIcon aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="h-px w-full bg-surface-5" />
|
||||||
|
|
||||||
|
<NavTabs :links="worldTabLinks" replace />
|
||||||
|
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { Archon } from '@modrinth/api-client'
|
||||||
|
import {
|
||||||
|
BoxesIcon,
|
||||||
|
ChevronLeftIcon,
|
||||||
|
DatabaseBackupIcon,
|
||||||
|
FolderOpenIcon,
|
||||||
|
SettingsIcon,
|
||||||
|
} from '@modrinth/assets'
|
||||||
|
import { useQuery } from '@tanstack/vue-query'
|
||||||
|
import { computed, watch } from 'vue'
|
||||||
|
import { RouterLink, useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
import BulletDivider from '#ui/components/base/BulletDivider.vue'
|
||||||
|
import ButtonStyled from '#ui/components/base/ButtonStyled.vue'
|
||||||
|
import NavTabs from '#ui/components/base/NavTabs.vue'
|
||||||
|
import { PanelServerActionButton } from '#ui/components/servers/server-header'
|
||||||
|
import { useRelativeTime } from '#ui/composables'
|
||||||
|
import { defineMessages, useVIntl } from '#ui/composables/i18n'
|
||||||
|
import {
|
||||||
|
injectModrinthClient,
|
||||||
|
injectModrinthServerContext,
|
||||||
|
injectServerSettingsModal,
|
||||||
|
} from '#ui/providers'
|
||||||
|
import { formatLoaderLabel } from '#ui/utils/loaders'
|
||||||
|
|
||||||
|
interface Tab {
|
||||||
|
label: string
|
||||||
|
href: string
|
||||||
|
icon?: object
|
||||||
|
subpages?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
allWorlds: {
|
||||||
|
id: 'servers.manage.world.all-worlds',
|
||||||
|
defaultMessage: 'All worlds',
|
||||||
|
},
|
||||||
|
contentNav: {
|
||||||
|
id: 'servers.manage.nav.content',
|
||||||
|
defaultMessage: 'Content',
|
||||||
|
},
|
||||||
|
filesNav: {
|
||||||
|
id: 'servers.manage.nav.files',
|
||||||
|
defaultMessage: 'Files',
|
||||||
|
},
|
||||||
|
backupsNav: {
|
||||||
|
id: 'servers.manage.nav.backups',
|
||||||
|
defaultMessage: 'Backups',
|
||||||
|
},
|
||||||
|
worldFallbackName: {
|
||||||
|
id: 'servers.manage.world.fallback-name',
|
||||||
|
defaultMessage: 'World',
|
||||||
|
},
|
||||||
|
lastActive: {
|
||||||
|
id: 'servers.manage.world.last-active',
|
||||||
|
defaultMessage: 'Last active {time}',
|
||||||
|
},
|
||||||
|
worldSettings: {
|
||||||
|
id: 'servers.manage.world.settings',
|
||||||
|
defaultMessage: 'World settings',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const client = injectModrinthClient()
|
||||||
|
const { serverId, server, worldId, isServerRunning } = injectModrinthServerContext()
|
||||||
|
const { openServerSettings } = injectServerSettingsModal()
|
||||||
|
const { formatMessage } = useVIntl()
|
||||||
|
const formatRelativeTime = useRelativeTime()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const { data: serverFull } = useQuery({
|
||||||
|
queryKey: computed(() => ['servers', 'v1', 'detail', serverId]),
|
||||||
|
queryFn: () => client.archon.servers_v1.get(serverId),
|
||||||
|
staleTime: 30_000,
|
||||||
|
})
|
||||||
|
|
||||||
|
const worldsPath = computed(() => `/hosting/manage/${encodeURIComponent(serverId)}/worlds`)
|
||||||
|
const worldPath = computed(() =>
|
||||||
|
worldId.value ? `${worldsPath.value}/${encodeURIComponent(worldId.value)}` : worldsPath.value,
|
||||||
|
)
|
||||||
|
|
||||||
|
const currentWorld = computed(() => {
|
||||||
|
const id = worldId.value
|
||||||
|
if (!id) return null
|
||||||
|
return serverFull.value?.worlds.find((world) => world.id === id) ?? null
|
||||||
|
})
|
||||||
|
|
||||||
|
const worldName = computed(
|
||||||
|
() => currentWorld.value?.name ?? server.value?.name ?? formatMessage(messages.worldFallbackName),
|
||||||
|
)
|
||||||
|
|
||||||
|
const worldMetadata = computed(() =>
|
||||||
|
[gameVersionLabel.value, loaderLabel.value, lastActiveLabel.value].filter(
|
||||||
|
(item): item is string => !!item,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
const gameVersionLabel = computed(() => {
|
||||||
|
const version = currentWorld.value?.content?.game_version ?? server.value?.mc_version
|
||||||
|
return version ? `MC ${version}` : null
|
||||||
|
})
|
||||||
|
|
||||||
|
const loaderLabel = computed(() => {
|
||||||
|
const loader = currentWorld.value?.content?.modloader ?? server.value?.loader?.toLowerCase()
|
||||||
|
if (!loader) return null
|
||||||
|
const loaderVersion =
|
||||||
|
currentWorld.value?.content?.modloader_version ?? server.value?.loader_version ?? null
|
||||||
|
return [formatLoaderLabel(loader), loaderVersion].filter(Boolean).join(' ')
|
||||||
|
})
|
||||||
|
|
||||||
|
const lastActiveLabel = computed(() => {
|
||||||
|
const latestBackup = currentWorld.value ? latestDate(currentWorld.value.backups) : null
|
||||||
|
const lastActiveAt =
|
||||||
|
latestBackup ??
|
||||||
|
(currentWorld.value?.is_active && isServerRunning.value
|
||||||
|
? new Date().toISOString()
|
||||||
|
: currentWorld.value?.created_at)
|
||||||
|
|
||||||
|
return lastActiveAt
|
||||||
|
? formatMessage(messages.lastActive, { time: formatRelativeTime(lastActiveAt) })
|
||||||
|
: null
|
||||||
|
})
|
||||||
|
|
||||||
|
const worldTabLinks = computed<Tab[]>(() => [
|
||||||
|
{
|
||||||
|
label: formatMessage(messages.contentNav),
|
||||||
|
href: worldPath.value,
|
||||||
|
icon: BoxesIcon,
|
||||||
|
subpages: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: formatMessage(messages.filesNav),
|
||||||
|
href: `${worldPath.value}/files`,
|
||||||
|
icon: FolderOpenIcon,
|
||||||
|
subpages: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: formatMessage(messages.backupsNav),
|
||||||
|
href: `${worldPath.value}/backups`,
|
||||||
|
icon: DatabaseBackupIcon,
|
||||||
|
subpages: [],
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [serverFull.value, currentWorld.value, worldId.value] as const,
|
||||||
|
([full, world, id]) => {
|
||||||
|
if (full && id && !world) {
|
||||||
|
router.replace(worldsPath.value)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
function latestDate(backups: Archon.Servers.v1.WorldFull['backups']): string | null {
|
||||||
|
let latest = 0
|
||||||
|
let latestIso: string | null = null
|
||||||
|
for (const backup of backups) {
|
||||||
|
const timestamp = new Date(backup.created_at).getTime()
|
||||||
|
if (!Number.isFinite(timestamp) || timestamp <= latest) continue
|
||||||
|
latest = timestamp
|
||||||
|
latestIso = backup.created_at
|
||||||
|
}
|
||||||
|
return latestIso
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
:world="world"
|
:world="world"
|
||||||
@create="handleCreateWorld"
|
@create="handleCreateWorld"
|
||||||
@edit="handleEditWorld"
|
@edit="handleEditWorld"
|
||||||
@settings="handleEditWorld"
|
@settings="handleWorldSettings"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -27,6 +27,7 @@
|
|||||||
import type { Archon } from '@modrinth/api-client'
|
import type { Archon } from '@modrinth/api-client'
|
||||||
import { useQuery } from '@tanstack/vue-query'
|
import { useQuery } from '@tanstack/vue-query'
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
import { defineMessages, useVIntl } from '#ui/composables/i18n'
|
import { defineMessages, useVIntl } from '#ui/composables/i18n'
|
||||||
import {
|
import {
|
||||||
@@ -84,6 +85,7 @@ const client = injectModrinthClient()
|
|||||||
const { serverId, server, isServerRunning } = injectModrinthServerContext()
|
const { serverId, server, isServerRunning } = injectModrinthServerContext()
|
||||||
const { openServerSettings } = injectServerSettingsModal()
|
const { openServerSettings } = injectServerSettingsModal()
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
const worldsQuery = useQuery({
|
const worldsQuery = useQuery({
|
||||||
queryKey: computed(() => ['servers', 'worlds', 'summary', 'v1', serverId]),
|
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' })
|
openServerSettings({ tabId: 'installation' })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 ServersManagePageIndex } from './hosting/manage/index.vue'
|
||||||
export { default as ServersManageOverviewPage } from './hosting/manage/overview.vue'
|
export { default as ServersManageOverviewPage } from './hosting/manage/overview.vue'
|
||||||
export { default as ServersManageRootLayout } from './hosting/manage/root.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'
|
export { default as ServersManageWorldsPage } from './hosting/manage/worlds.vue'
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ const meta = {
|
|||||||
setup() {
|
setup() {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
router.replace('/hosting/manage/demo-server/content')
|
router.replace('/hosting/manage/demo-server/worlds/demo-world')
|
||||||
})
|
})
|
||||||
|
|
||||||
const server = ref({
|
const server = ref({
|
||||||
|
|||||||
Reference in New Issue
Block a user