Skip to content

Commit 6333cca

Browse files
committed
feat: implement shared server header for app and website
1 parent 18b8f6a commit 6333cca

File tree

10 files changed

+718
-471
lines changed

10 files changed

+718
-471
lines changed

apps/app-frontend/src/pages/hosting/manage/Index.vue

Lines changed: 108 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -5,78 +5,27 @@
55
</div>
66
<div v-else-if="hasError" class="py-8 text-secondary">Failed to load server.</div>
77
<template v-else>
8-
<!-- TODO-SERVERS: make a shared server header component for website and app to share -->
9-
<RouterLink
10-
to="/hosting/manage"
11-
class="breadcrumb goto-link mt-6 mb-4 flex w-fit items-center"
8+
<ServerManageHeader
9+
:server="server"
10+
:server-image="serverImage"
11+
:server-project="serverProject"
12+
:server-project-link="serverProjectLink"
13+
breadcrumb-class="breadcrumb goto-link mt-6 mb-4 flex w-fit items-center"
14+
header-class="mb-4"
15+
:show-uptime="false"
1216
>
13-
<LeftArrowIcon />
14-
All servers
15-
</RouterLink>
16-
<ContentPageHeader class="mb-4">
17-
<template #icon>
18-
<ServerIcon
19-
:image="
20-
server.is_medal
21-
? 'https://cdn-raw.modrinth.com/medal_icon.webp'
22-
: (serverImage ?? undefined)
23-
"
24-
/>
25-
</template>
26-
<template #title>
27-
{{ server.name || 'Server' }}
28-
</template>
29-
<template #stats>
30-
<div
31-
v-if="server.flows?.intro"
32-
class="flex items-center gap-2 font-semibold text-secondary"
33-
>
34-
<SettingsIcon />
35-
Configuring server...
36-
</div>
37-
<div v-else class="flex flex-wrap items-center gap-2">
38-
<div v-if="server.loader" class="flex items-center gap-2 font-medium capitalize">
39-
<LoaderIcon :loader="server.loader" class="flex shrink-0 [&&]:size-5" />
40-
{{ server.loader }} {{ server.mc_version }}
41-
</div>
42-
43-
<div
44-
v-if="server.loader && server.net?.domain"
45-
class="h-1.5 w-1.5 rounded-full bg-surface-5"
46-
/>
47-
48-
<div
49-
v-if="server.net?.domain"
50-
v-tooltip="'Copy server address'"
51-
class="flex cursor-pointer items-center gap-2 font-medium hover:underline"
52-
@click="copyServerAddress"
53-
>
54-
<LinkIcon class="flex size-5 shrink-0" />
55-
{{ server.net.domain }}.modrinth.gg
56-
</div>
57-
58-
<div
59-
v-if="serverProject && (server.loader || server.net?.domain)"
60-
class="h-1.5 w-1.5 rounded-full bg-surface-5"
61-
/>
62-
63-
<div v-if="serverProject" class="flex items-center gap-1.5 font-medium text-primary">
64-
Linked to
65-
<Avatar :src="serverProject.icon_url" :alt="serverProject.title" size="24px" />
66-
<RouterLink :to="serverProjectLink" class="truncate text-primary hover:underline">
67-
{{ serverProject.title }}
68-
</RouterLink>
69-
</div>
70-
</div>
71-
</template>
7217
<template #actions>
73-
<ButtonStyled circular size="large">
74-
<button v-tooltip="'Server settings'" @click="openServerSettingsModal">
75-
<SettingsIcon />
76-
</button>
77-
</ButtonStyled>
18+
<div class="flex gap-2" v-if="isConnected && !server.flows?.intro">
19+
<PanelServerActionButton />
20+
<ButtonStyled circular size="large">
21+
<button v-tooltip="'Server settings'" @click="openServerSettingsModal">
22+
<SettingsIcon />
23+
</button>
24+
</ButtonStyled>
25+
<PanelServerOverflowMenu :show-copy-id-action="themeStore.devMode" />
26+
</div>
7827
</template>
79-
</ContentPageHeader>
28+
</ServerManageHeader>
8029

8130
<div class="mb-4">
8231
<NavTabs :links="tabs" />
@@ -103,38 +52,30 @@
10352

10453
<script setup lang="ts">
10554
import { type Archon, clearNodeAuthState, setNodeAuthState } from '@modrinth/api-client'
55+
import { BoxesIcon, DatabaseBackupIcon, FolderOpenIcon, SettingsIcon } from '@modrinth/assets'
10656
import {
107-
BoxesIcon,
108-
DatabaseBackupIcon,
109-
FolderOpenIcon,
110-
LeftArrowIcon,
111-
LinkIcon,
112-
SettingsIcon,
113-
} from '@modrinth/assets'
114-
import {
115-
Avatar,
11657
type BusyReason,
11758
ButtonStyled,
118-
ContentPageHeader,
11959
defineMessage,
12060
injectModrinthClient,
121-
injectNotificationManager,
122-
LoaderIcon,
12361
LoadingIndicator,
12462
NavTabs,
63+
PanelServerActionButton,
64+
PanelServerOverflowMenu,
12565
provideModrinthServerContext,
126-
ServerIcon,
66+
ServerManageHeader,
12767
} from '@modrinth/ui'
12868
import { useQuery, useQueryClient } from '@tanstack/vue-query'
12969
import { computed, onUnmounted, reactive, ref, watch } from 'vue'
130-
import { RouterLink, useRoute } from 'vue-router'
70+
import { useRoute } from 'vue-router'
13171
13272
import ServerSettingsModal from '@/components/ui/modal/ServerSettingsModal.vue'
73+
import { useTheming } from '@/store/theme'
13374
13475
const route = useRoute()
76+
const themeStore = useTheming()
13577
const client = injectModrinthClient()
13678
const queryClient = useQueryClient()
137-
const { addNotification } = injectNotificationManager()
13879
13980
const serverId = computed(() => {
14081
const rawId = route.params.id
@@ -196,11 +137,13 @@ const serverProjectLink = computed(() => {
196137
return `/project/${serverProject.value.slug ?? serverProject.value.id}`
197138
})
198139
199-
const isConnected = ref(true)
140+
const isConnected = ref(false)
200141
const powerState = ref<Archon.Websocket.v0.PowerState>('stopped')
201142
const isServerRunning = computed(() => powerState.value === 'running')
202143
const backupsState = reactive(new Map())
203144
const isSyncingContent = ref(false)
145+
const socketUnsubscribers = ref<(() => void)[]>([])
146+
const connectedSocketServerId = ref<string | null>(null)
204147
205148
const busyReasons = computed<BusyReason[]>(() => {
206149
const reasons: BusyReason[] = []
@@ -277,30 +220,98 @@ function markBackupCancelled(backupId: string) {
277220
backupsState.delete(backupId)
278221
}
279222
280-
function copyServerAddress() {
281-
if (!server.value?.net?.domain) return
282-
navigator.clipboard.writeText(server.value.net.domain + '.modrinth.gg')
283-
addNotification({
284-
title: 'Server address copied',
285-
text: "Your server's address has been copied to your clipboard.",
286-
type: 'success',
287-
})
288-
}
289-
290223
function openServerSettingsModal() {
291224
if (!serverId.value) return
292225
serverSettingsModal.value?.show({ serverId: serverId.value })
293226
}
294227
228+
function setPowerState(state: Archon.Websocket.v0.PowerState) {
229+
powerState.value = state
230+
}
231+
232+
function handlePowerState(data: Archon.Websocket.v0.WSPowerStateEvent) {
233+
setPowerState(data.state)
234+
}
235+
236+
function handleState(data: Archon.Websocket.v0.WSStateEvent) {
237+
const powerMap: Record<Archon.Websocket.v0.FlattenedPowerState, Archon.Websocket.v0.PowerState> =
238+
{
239+
not_ready: 'stopped',
240+
starting: 'starting',
241+
running: 'running',
242+
stopping: 'stopping',
243+
idle:
244+
data.was_oom || (data.exit_code != null && data.exit_code !== 0) ? 'crashed' : 'stopped',
245+
}
246+
setPowerState(powerMap[data.power_variant])
247+
}
248+
249+
function disconnectSocket(targetServerId?: string) {
250+
for (const unsub of socketUnsubscribers.value) unsub()
251+
socketUnsubscribers.value = []
252+
253+
if (targetServerId) {
254+
client.archon.sockets.disconnect(targetServerId)
255+
}
256+
connectedSocketServerId.value = null
257+
isConnected.value = false
258+
setPowerState('stopped')
259+
}
260+
261+
async function connectSocket(targetServerId: string) {
262+
if (connectedSocketServerId.value === targetServerId && isConnected.value) {
263+
return
264+
}
265+
disconnectSocket(targetServerId)
266+
267+
try {
268+
await client.archon.sockets.safeConnect(targetServerId, { force: true })
269+
connectedSocketServerId.value = targetServerId
270+
isConnected.value = true
271+
socketUnsubscribers.value = [
272+
client.archon.sockets.on(targetServerId, 'state', handleState),
273+
client.archon.sockets.on(targetServerId, 'power-state', handlePowerState),
274+
client.archon.sockets.on(targetServerId, 'auth-incorrect', () => {
275+
isConnected.value = false
276+
}),
277+
client.archon.sockets.on(targetServerId, 'auth-ok', () => {
278+
isConnected.value = true
279+
}),
280+
]
281+
} catch (error) {
282+
console.error('[hosting/manage] Failed to connect server socket:', error)
283+
isConnected.value = false
284+
}
285+
}
286+
295287
watch(
296288
() => serverId.value,
297-
() => {
289+
(newServerId, oldServerId) => {
290+
if (oldServerId && oldServerId !== newServerId) {
291+
disconnectSocket(oldServerId)
292+
}
298293
fsAuth.value = null
299294
void refreshFsAuth().catch(() => {})
300295
},
301296
{ immediate: true },
302297
)
303298
299+
watch(
300+
() => [serverId.value, serverQuery.data.value] as const,
301+
([currentServerId, currentServer]) => {
302+
if (!currentServerId || !currentServer) return
303+
if (currentServer.status === 'suspended' || currentServer.node === null) {
304+
disconnectSocket(currentServerId)
305+
return
306+
}
307+
if (connectedSocketServerId.value === currentServerId && isConnected.value) {
308+
return
309+
}
310+
void connectSocket(currentServerId)
311+
},
312+
{ immediate: true },
313+
)
314+
304315
provideModrinthServerContext({
305316
get serverId() {
306317
return serverId.value
@@ -323,6 +334,7 @@ provideModrinthServerContext({
323334
setNodeAuthState(() => fsAuth.value, refreshFsAuth)
324335
325336
onUnmounted(() => {
337+
disconnectSocket(serverId.value || undefined)
326338
clearNodeAuthState()
327339
})
328340

0 commit comments

Comments
 (0)