Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
003249e
start new server settings tabs
tdgao Mar 21, 2026
ef452a2
update properties tab to match design
tdgao Mar 21, 2026
debbe66
Merge branch 'main' into truman/new-server-settings
tdgao Mar 24, 2026
05995c3
better stying in general tab
tdgao Mar 24, 2026
e83df0b
feat: add suffix input for hostname field
tdgao Mar 24, 2026
4d38ac4
implement tables for allocations and DNS records
tdgao Mar 24, 2026
88f1ec6
add tags for dns record type
tdgao Mar 24, 2026
92e5be7
small gap adjustment
tdgao Mar 24, 2026
e8347a9
polish advanced page
tdgao Mar 24, 2026
d55df3b
adjust properties page hierarchy
tdgao Mar 24, 2026
affe960
fix searching properties, empty state and projection radius appearing
tdgao Mar 24, 2026
7589703
pnpm prepr
tdgao Mar 24, 2026
45b090e
update copy to match designs
tdgao Mar 25, 2026
e2a4730
fix suffix input component
tdgao Mar 25, 2026
003df72
style fixes and match heading size
tdgao Mar 25, 2026
b226a23
small fix
tdgao Mar 25, 2026
6491d8a
fix search allocations placeholder
tdgao Mar 25, 2026
9a1b43a
adjust table styles
tdgao Mar 25, 2026
2d4ed90
move all installation settings helper text to below input
tdgao Mar 25, 2026
2a6e83b
update icon to use overflow menu buttons
tdgao Mar 25, 2026
9905533
fix modal to be consistent
tdgao Mar 25, 2026
292c555
open advanced properties when search
tdgao Mar 25, 2026
5428bfc
remove other and custom properties, and update styles
tdgao Mar 25, 2026
9cacac8
remove hide/show all java versions
tdgao Mar 26, 2026
3aad5dd
Merge branch 'main' into truman/new-server-settings
tdgao Mar 27, 2026
314bc1a
handle mc 26
tdgao Mar 27, 2026
a618a34
refactor: move server settings pages into /ui and add app ServerSetti…
tdgao Mar 27, 2026
df75595
hook up server pages for app
tdgao Mar 27, 2026
530b481
add server page header to app
tdgao Mar 27, 2026
86d7f5d
hook up server settings modal
tdgao Mar 27, 2026
aaa69f1
use large size
tdgao Mar 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
256 changes: 256 additions & 0 deletions apps/app-frontend/src/components/ui/modal/ServerSettingsModal.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
<script setup lang="ts">
import {
type Archon,
clearNodeAuthState,
type Labrinth,
setNodeAuthState,
} from '@modrinth/api-client'
import { ChevronRightIcon } from '@modrinth/assets'
import {
type BusyReason,
commonMessages,
defineMessage,
defineMessages,
injectModrinthClient,
injectNotificationManager,
provideModrinthServerContext,
provideServerSettings,
ServerSettingsAdminBillingPage,
ServerSettingsAdvancedPage,
ServerSettingsBillingPage,
ServerSettingsGeneralPage,
ServerSettingsInstallationPage,
ServerSettingsNetworkPage,
ServerSettingsPropertiesPage,
serverSettingsTabDefinitions,
TabbedModal,
type TabbedModalTab,
useVIntl,
} from '@modrinth/ui'
import { useQueryClient } from '@tanstack/vue-query'
import { computed, nextTick, onUnmounted, reactive, ref } from 'vue'

import { get_user } from '@/helpers/cache'
import { get as getCreds } from '@/helpers/mr_auth'

type ShowOptions = {
serverId: string
tabIndex?: number
}

const { formatMessage } = useVIntl()
const queryClient = useQueryClient()
const client = injectModrinthClient()
const { addNotification } = injectNotificationManager()

const messages = defineMessages({
failedToLoadServer: {
id: 'app.server-settings.failed-to-load-server',
defaultMessage: 'Failed to load server settings',
},
})

const modal = ref<InstanceType<typeof TabbedModal> | null>(null)

const currentServerId = ref('')
const worldId = ref<string | null>(null)
const server = ref<Archon.Servers.v0.Server>({} as Archon.Servers.v0.Server)

const currentUserId = ref<string | null>(null)
const currentUserRole = ref<string | null>(null)

const isApp = ref(true)

function browseModpacks() {
// Stub for app browse-modpacks flow. Intentionally no-op for now.
}

const isConnected = ref(true)
const powerState = ref<Archon.Websocket.v0.PowerState>('stopped')
const isServerRunning = computed(() => powerState.value === 'running')
const backupsState = reactive(new Map())
const isSyncingContent = ref(false)

const busyReasons = computed<BusyReason[]>(() => {
const reasons: BusyReason[] = []
if (server.value?.status === 'installing') {
reasons.push({
reason: defineMessage({
id: 'servers.busy.installing',
defaultMessage: 'Server is installing',
}),
})
}
if (isSyncingContent.value) {
reasons.push({
reason: defineMessage({
id: 'servers.busy.syncing-content',
defaultMessage: 'Content sync in progress',
}),
})
}
return reasons
})

const fsAuth = ref<{ url: string; token: string } | null>(null)
const fsOps = ref<Archon.Websocket.v0.FilesystemOperation[]>([])
const fsQueuedOps = ref<Archon.Websocket.v0.QueuedFilesystemOp[]>([])

async function refreshFsAuth() {
if (!currentServerId.value) {
fsAuth.value = null
return
}
fsAuth.value = await queryClient.fetchQuery({
queryKey: ['servers', 'filesystem-auth', currentServerId.value],
queryFn: () => client.archon.servers_v0.getFilesystemAuth(currentServerId.value),
})
}

function markBackupCancelled(_backupId: string) {}

const serverSettingsTabComponentMap = {
general: ServerSettingsGeneralPage,
installation: ServerSettingsInstallationPage,
network: ServerSettingsNetworkPage,
properties: ServerSettingsPropertiesPage,
advanced: ServerSettingsAdvancedPage,
billing: ServerSettingsBillingPage,
'admin-billing': ServerSettingsAdminBillingPage,
} as const

provideServerSettings({
isApp,
currentUserId,
currentUserRole,
browseModpacks,
})

provideModrinthServerContext({
get serverId() {
return currentServerId.value
},
worldId,
server,
isConnected,
powerState,
isServerRunning,
backupsState,
markBackupCancelled,
isSyncingContent,
busyReasons,
fsAuth,
fsOps,
fsQueuedOps,
refreshFsAuth,
})

const ownerId = computed(() => server.value?.owner_id ?? 'Ghost')
const isOwner = computed(() => currentUserId.value != null && currentUserId.value === ownerId.value)
const isAdmin = computed(() => currentUserRole.value === 'admin')

const tabs = computed<TabbedModalTab[]>(() =>
serverSettingsTabDefinitions.map((tab) => ({
name: defineMessage({
id: `server.settings.tabs.${tab.id}`,
defaultMessage: tab.label,
}),
icon: tab.icon,
content: serverSettingsTabComponentMap[tab.id],
shown: tab.shown
? tab.shown({
serverId: currentServerId.value,
ownerId: ownerId.value,
serverStatus: server.value?.status,
isOwner: isOwner.value,
isAdmin: isAdmin.value,
})
: true,
})),
)

async function resolveViewer() {
currentUserId.value = null
currentUserRole.value = null

const credentials = await getCreds().catch(() => null)
if (!credentials?.user_id) {
return
}

currentUserId.value = credentials.user_id

const user = await get_user(credentials.user_id, 'bypass').catch(() => null)
const typedUser = user as Labrinth.Users.v2.User | null
currentUserRole.value = typedUser?.role ?? null
}

async function show({ serverId, tabIndex }: ShowOptions) {
try {
const [serverData, serverFull] = await Promise.all([
queryClient.fetchQuery({
queryKey: ['servers', 'detail', serverId],
queryFn: () => client.archon.servers_v0.get(serverId),
}),
queryClient.fetchQuery({
queryKey: ['servers', 'v1', 'detail', serverId],
queryFn: () => client.archon.servers_v1.get(serverId),
}),
resolveViewer(),
])

currentServerId.value = serverId
server.value = serverData
const activeWorld = serverFull.worlds.find((world) => world.is_active)
worldId.value = activeWorld?.id ?? serverFull.worlds[0]?.id ?? null

setNodeAuthState(() => fsAuth.value, refreshFsAuth)
await refreshFsAuth().catch(() => {})

modal.value?.show()
const visibleTabsCount = tabs.value.filter((tab) => tab.shown !== false).length
const requestedTab = tabIndex ?? 0
const clampedTab = Math.min(Math.max(requestedTab, 0), Math.max(visibleTabsCount - 1, 0))
nextTick(() => modal.value?.setTab(clampedTab))
} catch (error) {
console.error(error)
addNotification({
type: 'error',
title: formatMessage(messages.failedToLoadServer),
})
}
}

function hide() {
modal.value?.hide()
}

function onHide() {
clearNodeAuthState()
}

onUnmounted(() => {
clearNodeAuthState()
})

defineExpose({ show, hide })
</script>

<template>
<TabbedModal
ref="modal"
:tabs="tabs"
:on-hide="onHide"
:max-width="'min(980px, calc(95vw - 10rem))'"
:width="'min(980px, calc(95vw - 10rem))'"
>
<template #title>
<span class="flex items-center gap-2 text-lg font-semibold text-primary">
{{ server.name || 'Server' }} <ChevronRightIcon />
<span class="font-extrabold text-contrast">{{
formatMessage(commonMessages.settingsLabel)
}}</span>
</span>
</template>
</TabbedModal>
</template>
9 changes: 9 additions & 0 deletions apps/app-frontend/src/locales/en-US/index.json
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,9 @@
"app.modal.update-to-play.update-required-description": {
"message": "An update is required to play {name}. Please update to the latest version to launch the game."
},
"app.server-settings.failed-to-load-server": {
"message": "Failed to load server settings"
},
"app.settings.developer-mode-enabled": {
"message": "Developer mode enabled."
},
Expand Down Expand Up @@ -598,5 +601,11 @@
},
"search.filter.locked.server-loader.title": {
"message": "Loader is provided by the server"
},
"servers.busy.installing": {
"message": "Server is installing"
},
"servers.busy.syncing-content": {
"message": "Content sync in progress"
}
}
9 changes: 9 additions & 0 deletions apps/app-frontend/src/pages/hosting/manage/Backups.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<script setup lang="ts">
import { injectModrinthServerContext, ServersManageBackupsPage } from '@modrinth/ui'

const { isServerRunning } = injectModrinthServerContext()
</script>

<template>
<ServersManageBackupsPage :is-server-running="isServerRunning" />
</template>
7 changes: 7 additions & 0 deletions apps/app-frontend/src/pages/hosting/manage/Content.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<script setup lang="ts">
import { ServersManageContentPage } from '@modrinth/ui'
</script>

<template>
<ServersManageContentPage />
</template>
7 changes: 7 additions & 0 deletions apps/app-frontend/src/pages/hosting/manage/Files.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<script setup lang="ts">
import { ServersManageFilesPage } from '@modrinth/ui'
</script>

<template>
<ServersManageFilesPage />
</template>
Loading
Loading