feat: implement improved flow for server panel edit installation (#5711)
* feat: implement improved flow for server panel edit installation * feat: installation form finalized * feat: error state for InstallingBanner * feat: action button refactor + save banner text fix * fix: lint * fix: content card alignment * feat: better copy * fix: lint * fix: hide shift click + fix NeoForge chip * fix: lint
This commit is contained in:
@@ -172,17 +172,7 @@
|
||||
</template>
|
||||
<template #actions>
|
||||
<div v-if="isConnected && !serverData.flows?.intro" class="flex gap-2">
|
||||
<PanelServerActionButton
|
||||
:is-online="isServerRunning"
|
||||
:is-actioning="isActioning"
|
||||
:is-installing="serverData.status === 'installing'"
|
||||
:disabled="isActioning || !!error"
|
||||
:server-name="serverData.name"
|
||||
:server-data="serverData"
|
||||
:uptime-seconds="uptimeSeconds"
|
||||
:busy-reason="busyReasons.length > 0 ? formatMessage(busyReasons[0].reason) : undefined"
|
||||
@action="sendPowerAction"
|
||||
/>
|
||||
<PanelServerActionButton :disabled="!!error" :uptime-seconds="uptimeSeconds" />
|
||||
</div>
|
||||
</template>
|
||||
</ContentPageHeader>
|
||||
@@ -320,12 +310,14 @@
|
||||
>
|
||||
<InstallingBanner
|
||||
v-if="
|
||||
(serverData.status === 'installing' || isSyncingContent) &&
|
||||
(serverData.status === 'installing' || isSyncingContent || contentError) &&
|
||||
syncProgress?.phase !== 'Analyzing'
|
||||
"
|
||||
data-pyro-server-installing
|
||||
class="mb-4"
|
||||
:progress="syncProgress"
|
||||
:content-error="contentError"
|
||||
@retry="handleContentRetry"
|
||||
>
|
||||
<template #icon>
|
||||
<ServerIcon :image="serverImage" class="!h-6 !w-6" />
|
||||
@@ -398,9 +390,8 @@ import {
|
||||
ServerNotice,
|
||||
ServerOnboardingPanelPage,
|
||||
useDebugLogger,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import type { PowerAction, Stats } from '@modrinth/utils'
|
||||
import type { Stats } from '@modrinth/utils'
|
||||
import { useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||
import { useTimeoutFn } from '@vueuse/core'
|
||||
import DOMPurify from 'dompurify'
|
||||
@@ -416,7 +407,6 @@ import { useServerProject } from '~/composables/servers/use-server-project.ts'
|
||||
import { useModrinthServersConsole } from '~/store/console.ts'
|
||||
|
||||
const { addNotification } = injectNotificationManager()
|
||||
const { formatMessage } = useVIntl()
|
||||
const client = injectModrinthClient()
|
||||
|
||||
const isReconnecting = ref(false)
|
||||
@@ -505,7 +495,6 @@ const modrinthServersConsole = useModrinthServersConsole()
|
||||
const queryClient = useQueryClient()
|
||||
const cpuData = ref<number[]>([])
|
||||
const ramData = ref<number[]>([])
|
||||
const isActioning = ref(false)
|
||||
const isServerRunning = computed(() => serverPowerState.value === 'running')
|
||||
const serverPowerState = ref<Archon.Websocket.v0.PowerState>('stopped')
|
||||
const powerStateDetails = ref<{ oom_killed?: boolean; exit_code?: number }>()
|
||||
@@ -518,6 +507,7 @@ const markBackupCancelled = (backupId: string) => {
|
||||
|
||||
// Parthenon state event
|
||||
const syncProgress = ref<Archon.Websocket.v0.SyncContentProgress | null>(null)
|
||||
const contentError = ref<Archon.Websocket.v0.SyncContentError | null>(null)
|
||||
const syncProgressActive = ref(false)
|
||||
const isAwaitingPostInstallRefresh = ref(false)
|
||||
const { start: startSyncHide, stop: cancelSyncHide } = useTimeoutFn(
|
||||
@@ -845,6 +835,7 @@ const handleState = (data: Archon.Websocket.v0.WSStateEvent) => {
|
||||
serverStatus: serverData.value?.status,
|
||||
})
|
||||
syncProgress.value = data.progress
|
||||
contentError.value = data.content_error
|
||||
|
||||
// Sync power state from the state event
|
||||
const powerMap: Record<Archon.Websocket.v0.FlattenedPowerState, Archon.Websocket.v0.PowerState> =
|
||||
@@ -878,6 +869,7 @@ const handleState = (data: Archon.Websocket.v0.WSStateEvent) => {
|
||||
hasSeenInstallProgress = true
|
||||
} else if (
|
||||
data.progress == null &&
|
||||
data.content_error == null &&
|
||||
serverData.value.status === 'installing' &&
|
||||
hasSeenInstallProgress
|
||||
) {
|
||||
@@ -889,6 +881,18 @@ const handleState = (data: Archon.Websocket.v0.WSStateEvent) => {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleContentRetry() {
|
||||
if (!worldId.value) return
|
||||
try {
|
||||
await client.archon.content_v1.repair(serverId, worldId.value)
|
||||
} catch (err) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
text: err instanceof Error ? err.message : 'Failed to retry installation',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleUptime = (data: Archon.Websocket.v0.WSUptimeEvent) => {
|
||||
stopUptimeUpdates()
|
||||
uptimeSeconds.value = data.uptime
|
||||
@@ -1219,43 +1223,6 @@ const updateGraphData = (dataArray: number[], newValue: number): number[] => {
|
||||
return updated
|
||||
}
|
||||
|
||||
const toAdverb = (word: string) => {
|
||||
if (word.endsWith('p')) {
|
||||
return word + 'ping'
|
||||
}
|
||||
if (word.endsWith('e')) {
|
||||
return word.slice(0, -1) + 'ing'
|
||||
}
|
||||
if (word.endsWith('ie')) {
|
||||
return word.slice(0, -2) + 'ying'
|
||||
}
|
||||
return word + 'ing'
|
||||
}
|
||||
|
||||
const sendPowerAction = async (action: PowerAction) => {
|
||||
const actionName = action.charAt(0).toUpperCase() + action.slice(1)
|
||||
try {
|
||||
isActioning.value = true
|
||||
await client.archon.servers_v0.power(serverId, action)
|
||||
} catch (error) {
|
||||
console.error(`Error ${toAdverb(actionName)} server:`, error)
|
||||
notifyError(
|
||||
`Error ${toAdverb(actionName)} server`,
|
||||
'An error occurred while performing this action.',
|
||||
)
|
||||
} finally {
|
||||
isActioning.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const notifyError = (title: string, text: string) => {
|
||||
addNotification({
|
||||
title,
|
||||
text,
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
|
||||
const nodeUnavailableDetails = computed(() => [
|
||||
{
|
||||
label: 'Server ID',
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
@proceed="confirmResetToOnboarding"
|
||||
/>
|
||||
|
||||
<InstallationSettingsLayout ref="installationSettingsLayout">
|
||||
<InstallationSettingsLayout ref="installationSettingsLayout" @reset-server="setupModal?.show()">
|
||||
<template #extra>
|
||||
<div class="flex flex-col gap-2.5">
|
||||
<span class="text-lg font-semibold text-contrast">{{
|
||||
@@ -455,6 +455,9 @@ provideInstallationSettings({
|
||||
debug('save: called with', { platform, gameVersion, loaderVersionId })
|
||||
const currentPlatform = server.value?.loader?.toLowerCase() ?? 'vanilla'
|
||||
const platformChanged = platform !== currentPlatform
|
||||
const gameVersionChanged = gameVersion !== (server.value?.mc_version ?? '')
|
||||
const loaderVersionChanged =
|
||||
loaderVersionId !== null && loaderVersionId !== (server.value?.loader_version ?? '')
|
||||
|
||||
let resolvedLoaderVersion = loaderVersionId
|
||||
if (!resolvedLoaderVersion && platform !== 'vanilla') {
|
||||
@@ -465,12 +468,12 @@ provideInstallationSettings({
|
||||
debug('save: emitting reinstall before API call')
|
||||
emit(
|
||||
'reinstall',
|
||||
platformChanged
|
||||
platformChanged || loaderVersionChanged
|
||||
? { loader: platform, lVersion: resolvedLoaderVersion, mVersion: gameVersion }
|
||||
: { mVersion: gameVersion },
|
||||
)
|
||||
try {
|
||||
if (platformChanged) {
|
||||
if (platformChanged || loaderVersionChanged) {
|
||||
const request: Archon.Content.v1.InstallWorldContent = {
|
||||
content_variant: 'bare',
|
||||
loader: toApiLoader(platform),
|
||||
@@ -478,9 +481,9 @@ provideInstallationSettings({
|
||||
game_version: gameVersion || undefined,
|
||||
soft_override: true,
|
||||
}
|
||||
debug('save: platform changed, calling installContent', request)
|
||||
debug('save: platform/loader version changed, calling installContent', request)
|
||||
await client.archon.content_v1.installContent(serverId, worldId.value!, request)
|
||||
} else {
|
||||
} else if (gameVersionChanged) {
|
||||
debug('save: game version only, calling applyGameVersionUpdate', gameVersion)
|
||||
await client.archon.content_v1.applyGameVersionUpdate(serverId, worldId.value!, gameVersion)
|
||||
}
|
||||
@@ -662,8 +665,66 @@ provideInstallationSettings({
|
||||
isApp: false,
|
||||
showModpackVersionActions: computed(() => modpack.value?.spec.platform === 'modrinth'),
|
||||
|
||||
lockPlatform: true,
|
||||
hideLoaderVersion: true,
|
||||
lockPlatform: false,
|
||||
hideLoaderVersion: false,
|
||||
|
||||
async disableAllContent() {
|
||||
debug('disableAllContent: fetching all addons')
|
||||
const addons = await client.archon.content_v1.getAddons(serverId, worldId.value!)
|
||||
const items = (addons.addons ?? [])
|
||||
.filter((a) => !a.disabled)
|
||||
.map((a) => ({ kind: a.kind, filename: a.filename }))
|
||||
if (items.length > 0) {
|
||||
debug('disableAllContent: disabling', items.length, 'addons')
|
||||
await client.archon.content_v1.disableAddons(serverId, worldId.value!, items)
|
||||
}
|
||||
debug('disableAllContent: done')
|
||||
},
|
||||
|
||||
async disableIncompatibleContent(diffs) {
|
||||
debug('disableIncompatibleContent: processing', diffs.length, 'diffs')
|
||||
const addons = await client.archon.content_v1.getAddons(serverId, worldId.value!)
|
||||
const removedFiles = new Set(diffs.filter((d) => d.type === 'removed').map((d) => d.fileName))
|
||||
const items = (addons.addons ?? [])
|
||||
.filter((a) => !a.disabled && removedFiles.has(a.filename))
|
||||
.map((a) => ({ kind: a.kind, filename: a.filename }))
|
||||
if (items.length > 0) {
|
||||
debug('disableIncompatibleContent: disabling', items.length, 'addons')
|
||||
await client.archon.content_v1.disableAddons(serverId, worldId.value!, items)
|
||||
}
|
||||
debug('disableIncompatibleContent: done')
|
||||
},
|
||||
|
||||
async saveWithoutAutoFix(platform, gameVersion, loaderVersionId) {
|
||||
debug('saveWithoutAutoFix: called with', { platform, gameVersion, loaderVersionId })
|
||||
let resolvedLoaderVersion = loaderVersionId
|
||||
if (!resolvedLoaderVersion && platform !== 'vanilla') {
|
||||
const versions = getLoaderVersionsForGameVersion(platform, gameVersion)
|
||||
resolvedLoaderVersion = versions[0]?.id ?? null
|
||||
}
|
||||
emit('reinstall', { loader: platform, lVersion: resolvedLoaderVersion, mVersion: gameVersion })
|
||||
try {
|
||||
const request: Archon.Content.v1.InstallWorldContent = {
|
||||
content_variant: 'bare',
|
||||
loader: toApiLoader(platform),
|
||||
version: resolvedLoaderVersion ?? '',
|
||||
game_version: gameVersion || undefined,
|
||||
soft_override: true,
|
||||
}
|
||||
debug('saveWithoutAutoFix: calling installContent', request)
|
||||
await client.archon.content_v1.installContent(serverId, worldId.value!, request)
|
||||
debug('saveWithoutAutoFix: succeeded, invalidating')
|
||||
invalidateServerState()
|
||||
} catch (err) {
|
||||
debug('saveWithoutAutoFix: failed', err)
|
||||
emit('reinstall-failed')
|
||||
addNotification({
|
||||
type: 'error',
|
||||
text: err instanceof Error ? err.message : formatMessage(messages.failedToSaveSettings),
|
||||
})
|
||||
throw err
|
||||
}
|
||||
},
|
||||
|
||||
async previewSave(_platform, gameVersion, _loaderVersionId, signal) {
|
||||
const result = await client.archon.content_v1.getUpdateGameVersionPreview(
|
||||
|
||||
Reference in New Issue
Block a user