feat: backups page cleanup before worlds (#5844)
* feat: card alignment + fix modals * feat: change admon title in restore alert modal * fix: lint * feat: backups queue api into api-client * feat: impl backup queue api endpoints into frontend * feat: ack fix * feat: bulk actions * feat: bulk delete impl * fix: lint * fix: align error states * fix: transition group * feat: ready for qa * fix: lint * feat: qa * feat: stacked admonitions component * fix: issues with stacking * feat: hook up admonition stacking + fix app csp for staging kyros nodes * fix: logs.vue * qa: close stack on admonitions click * fix: all problems with stacked admonitions * qa: admonition cleanup and copy overhaul draft * fix: qa issues padding * fix: padding bug * feat: qa * fix: intercom in app csp bug * fix: positioning intercom * feat: loading overlay on top of console + admon consistency changes * feat: scroll indicator fade in backup delete modal + admon timestamp fix * feat: move action bar behind modal * fix: lint + i18n * fix: server ping spam on filter (cache but clear on unmount) * fix: 1 admon fade in flicker issue * chore: temp staging undo * qa: changes * fix: lint * chore: revert staging to use staging * fix: scoping
This commit is contained in:
@@ -9,6 +9,7 @@ export * from './i18n-debug'
|
||||
export * from './page-leave-safety'
|
||||
export * from './scroll-indicator'
|
||||
export * from './server-backup'
|
||||
export * from './server-backups-queue'
|
||||
export * from './server-console'
|
||||
export * from './server-manage-core-runtime'
|
||||
export * from './sticky-observer'
|
||||
|
||||
@@ -20,7 +20,7 @@ export function useScrollIndicator(
|
||||
containerRef: Ref<HTMLElement | null>,
|
||||
options: ScrollIndicatorOptions = {},
|
||||
): ScrollIndicator {
|
||||
const { watchContent = true, debounceMs = 10, tolerance = 1, debug = false } = options
|
||||
const { watchContent = true, debounceMs = 0, tolerance = 1, debug = false } = options
|
||||
|
||||
const showTopFade = ref(false)
|
||||
const showBottomFade = ref(false)
|
||||
|
||||
133
packages/ui/src/composables/server-backups-queue.ts
Normal file
133
packages/ui/src/composables/server-backups-queue.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import type { Archon } from '@modrinth/api-client'
|
||||
import { useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||
import { computed, reactive, type Ref } from 'vue'
|
||||
|
||||
import { type BusyReason, injectModrinthClient } from '#ui/providers'
|
||||
|
||||
import { defineMessage } from './i18n'
|
||||
|
||||
type ProgressKey = `${string}:${'create' | 'restore'}`
|
||||
|
||||
export function useServerBackupsQueue(serverId: Ref<string>, worldId: Ref<string | null>) {
|
||||
const client = injectModrinthClient()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const queryKey = computed(() => ['backups', 'queue', serverId.value] as const)
|
||||
|
||||
const progressOverlay = reactive(new Map<ProgressKey, number>())
|
||||
const lastSeenState = new Map<ProgressKey, Archon.Websocket.v0.BackupState>()
|
||||
|
||||
const query = useQuery({
|
||||
queryKey,
|
||||
queryFn: () => client.archon.backups_queue_v1.list(serverId.value, worldId.value!),
|
||||
enabled: computed(() => !!worldId.value),
|
||||
refetchInterval: (q) => {
|
||||
const data = q.state.data as Archon.BackupsQueue.v1.BackupsQueueResponse | undefined
|
||||
return data?.active_operations?.length ? 3000 : false
|
||||
},
|
||||
})
|
||||
|
||||
const data = computed(() => query.data.value)
|
||||
const activeOperations = computed(() => data.value?.active_operations ?? [])
|
||||
const backups = computed(() =>
|
||||
[...(data.value?.backups ?? [])].sort(
|
||||
(a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime(),
|
||||
),
|
||||
)
|
||||
|
||||
const activeOperationByBackupId = computed(() => {
|
||||
const map = new Map<string, Archon.BackupsQueue.v1.ActiveOperation>()
|
||||
for (const op of activeOperations.value) map.set(op.backup_id, op)
|
||||
return map
|
||||
})
|
||||
const backupById = computed(() => {
|
||||
const map = new Map<string, Archon.BackupsQueue.v1.BackupQueueBackup>()
|
||||
for (const backup of backups.value) map.set(backup.id, backup)
|
||||
return map
|
||||
})
|
||||
|
||||
const hasActiveCreate = computed(() =>
|
||||
activeOperations.value.some((o) => o.operation_type === 'create' && !o.has_parent),
|
||||
)
|
||||
const hasActiveRestore = computed(() =>
|
||||
activeOperations.value.some((o) => o.operation_type === 'restore'),
|
||||
)
|
||||
const hasRunningCreate = computed(() =>
|
||||
activeOperations.value.some(
|
||||
(o) =>
|
||||
o.operation_type === 'create' &&
|
||||
!o.has_parent &&
|
||||
backupById.value.get(o.backup_id)?.status === 'in_progress',
|
||||
),
|
||||
)
|
||||
const hasRunningRestore = computed(() =>
|
||||
activeOperations.value.some(
|
||||
(o) =>
|
||||
o.operation_type === 'restore' &&
|
||||
backupById.value.get(o.backup_id)?.status === 'in_progress',
|
||||
),
|
||||
)
|
||||
|
||||
function handleWsBackupProgress(evt: Archon.Websocket.v0.WSBackupProgressEvent) {
|
||||
if (evt.task === 'file') return
|
||||
const key = `${evt.id}:${evt.task}` as ProgressKey
|
||||
|
||||
if (evt.state === 'ongoing') {
|
||||
progressOverlay.set(key, evt.progress)
|
||||
} else {
|
||||
progressOverlay.delete(key)
|
||||
}
|
||||
|
||||
const prev = lastSeenState.get(key)
|
||||
if (prev !== evt.state) {
|
||||
lastSeenState.set(key, evt.state)
|
||||
queryClient.invalidateQueries({ queryKey: queryKey.value })
|
||||
}
|
||||
}
|
||||
|
||||
function progressFor(backupId: string, kind: 'create' | 'restore'): number | undefined {
|
||||
return progressOverlay.get(`${backupId}:${kind}`)
|
||||
}
|
||||
|
||||
const busyReasons = computed<BusyReason[]>(() => {
|
||||
const reasons: BusyReason[] = []
|
||||
if (hasRunningCreate.value) {
|
||||
reasons.push({
|
||||
reason: defineMessage({
|
||||
id: 'servers.busy.backup-creating',
|
||||
defaultMessage: 'Backup creation in progress',
|
||||
}),
|
||||
})
|
||||
}
|
||||
if (hasRunningRestore.value) {
|
||||
reasons.push({
|
||||
reason: defineMessage({
|
||||
id: 'servers.busy.backup-restoring',
|
||||
defaultMessage: 'Backup restore in progress',
|
||||
}),
|
||||
})
|
||||
}
|
||||
return reasons
|
||||
})
|
||||
|
||||
async function invalidate() {
|
||||
await queryClient.invalidateQueries({ queryKey: queryKey.value })
|
||||
}
|
||||
|
||||
return {
|
||||
query,
|
||||
queryKey,
|
||||
data,
|
||||
activeOperations,
|
||||
activeOperationByBackupId,
|
||||
backups,
|
||||
hasActiveCreate,
|
||||
hasActiveRestore,
|
||||
hasRunningCreate,
|
||||
hasRunningRestore,
|
||||
progressFor,
|
||||
handleWsBackupProgress,
|
||||
busyReasons,
|
||||
invalidate,
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createGlobalState } from '@vueuse/core'
|
||||
import { type Ref, shallowRef, triggerRef } from 'vue'
|
||||
import { type Ref, ref, shallowRef, triggerRef } from 'vue'
|
||||
|
||||
import { detectLogLevel } from '../layouts/shared/console/composables/log-level'
|
||||
import type { Log4jEvent, LogLevel, LogLine } from '../layouts/shared/console/types'
|
||||
@@ -52,6 +52,8 @@ function groupContinuations(lines: LogLine[]): LogLine[] {
|
||||
|
||||
const batchTimeout = 300
|
||||
const initialBatchSize = 256
|
||||
const initialHydrationQuietMs = 700
|
||||
const initialHydrationMaxMs = 2000
|
||||
|
||||
const LogLevelCode = {
|
||||
None: 0,
|
||||
@@ -224,10 +226,43 @@ export function createConsoleState() {
|
||||
|
||||
let lineBuffer: LogLine[] = []
|
||||
let batchTimer: NodeJS.Timeout | null = null
|
||||
let initialHydrationQuietTimer: ReturnType<typeof setTimeout> | null = null
|
||||
let initialHydrationMaxTimer: ReturnType<typeof setTimeout> | null = null
|
||||
const isInitialLogHydrating = ref(false)
|
||||
|
||||
let wrapCount = 0
|
||||
let lastFlushMs = 0
|
||||
|
||||
const clearInitialHydrationTimers = (): void => {
|
||||
if (initialHydrationQuietTimer) {
|
||||
clearTimeout(initialHydrationQuietTimer)
|
||||
initialHydrationQuietTimer = null
|
||||
}
|
||||
if (initialHydrationMaxTimer) {
|
||||
clearTimeout(initialHydrationMaxTimer)
|
||||
initialHydrationMaxTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
const settleInitialLogHydration = (): void => {
|
||||
if (!isInitialLogHydrating.value) return
|
||||
clearInitialHydrationTimers()
|
||||
flushBuffer()
|
||||
isInitialLogHydrating.value = false
|
||||
}
|
||||
|
||||
const armInitialHydrationQuietTimer = (): void => {
|
||||
if (initialHydrationQuietTimer) clearTimeout(initialHydrationQuietTimer)
|
||||
initialHydrationQuietTimer = setTimeout(settleInitialLogHydration, initialHydrationQuietMs)
|
||||
}
|
||||
|
||||
const beginInitialLogHydration = (): void => {
|
||||
clearInitialHydrationTimers()
|
||||
isInitialLogHydrating.value = true
|
||||
armInitialHydrationQuietTimer()
|
||||
initialHydrationMaxTimer = setTimeout(settleInitialLogHydration, initialHydrationMaxMs)
|
||||
}
|
||||
|
||||
const flushBuffer = (): void => {
|
||||
if (lineBuffer.length === 0) return
|
||||
|
||||
@@ -269,6 +304,12 @@ export function createConsoleState() {
|
||||
}
|
||||
|
||||
const addLines = (lines: LogLine[]): void => {
|
||||
if (isInitialLogHydrating.value) {
|
||||
lineBuffer.push(...lines)
|
||||
armInitialHydrationQuietTimer()
|
||||
return
|
||||
}
|
||||
|
||||
if (output.value.length === 0 && lines.length >= initialBatchSize) {
|
||||
lineBuffer = lines
|
||||
flushBuffer()
|
||||
@@ -326,6 +367,8 @@ export function createConsoleState() {
|
||||
lineBuffer = []
|
||||
wsEventHistory.length = 0
|
||||
wrapCount = 0
|
||||
isInitialLogHydrating.value = false
|
||||
clearInitialHydrationTimers()
|
||||
if (batchTimer) {
|
||||
clearTimeout(batchTimer)
|
||||
batchTimer = null
|
||||
@@ -359,6 +402,8 @@ export function createConsoleState() {
|
||||
|
||||
return {
|
||||
output,
|
||||
isInitialLogHydrating,
|
||||
beginInitialLogHydration,
|
||||
addLines,
|
||||
addLog4jEvent,
|
||||
addLegacyLog,
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
} from '@modrinth/api-client'
|
||||
import type { Stats } from '@modrinth/utils'
|
||||
import type { ComputedRef, Ref } from 'vue'
|
||||
import { computed, reactive, ref, watch } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import type { FileOperation } from '../layouts/shared/files-tab/types'
|
||||
import { injectModrinthClient, provideModrinthServerContext } from '../providers'
|
||||
@@ -27,8 +27,7 @@ type UseServerManageCoreRuntimeOptions = {
|
||||
worldId: ReadableRef<string | null>
|
||||
server: ReadableRef<Archon.Servers.v0.Server | null | undefined>
|
||||
isSyncingContent: ReadableRef<boolean>
|
||||
markBackupCancelled?: (backupId: string) => void
|
||||
includeBackupBusyReasons?: boolean
|
||||
extraBusyReasons?: ComputedRef<BusyReason[]>
|
||||
setDisconnectedOnAuthIncorrect?: boolean
|
||||
syncUptimeFromState?: boolean
|
||||
incrementUptimeLocally?: boolean
|
||||
@@ -94,7 +93,6 @@ export function useServerManageCoreRuntime(options: UseServerManageCoreRuntimeOp
|
||||
const isServerRunning = computed(() => serverPowerState.value === 'running')
|
||||
const stats = ref<Stats>(createInitialStats())
|
||||
const uptimeSeconds = ref(0)
|
||||
const backupsState = reactive(new Map())
|
||||
const fsAuth = ref<{ url: string; token: string } | null>(null)
|
||||
const fsOps = ref<Archon.Websocket.v0.FilesystemOperation[]>([])
|
||||
const fsQueuedOps = ref<Archon.Websocket.v0.QueuedFilesystemOp[]>([])
|
||||
@@ -107,12 +105,6 @@ export function useServerManageCoreRuntime(options: UseServerManageCoreRuntimeOp
|
||||
let staleStatsTimeoutId: ReturnType<typeof setTimeout> | null = null
|
||||
let staleStatsIntervalId: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
const markBackupCancelled =
|
||||
options.markBackupCancelled ??
|
||||
((backupId: string) => {
|
||||
backupsState.delete(backupId)
|
||||
})
|
||||
|
||||
const busyReasons = computed<BusyReason[]>(() => {
|
||||
const reasons: BusyReason[] = []
|
||||
if (options.server.value?.status === 'installing') {
|
||||
@@ -131,28 +123,7 @@ export function useServerManageCoreRuntime(options: UseServerManageCoreRuntimeOp
|
||||
}),
|
||||
})
|
||||
}
|
||||
if (options.includeBackupBusyReasons) {
|
||||
for (const entry of backupsState.values()) {
|
||||
if (entry.create?.state === 'ongoing') {
|
||||
reasons.push({
|
||||
reason: defineMessage({
|
||||
id: 'servers.busy.backup-creating',
|
||||
defaultMessage: 'Backup creation in progress',
|
||||
}),
|
||||
})
|
||||
break
|
||||
}
|
||||
if (entry.restore?.state === 'ongoing') {
|
||||
reasons.push({
|
||||
reason: defineMessage({
|
||||
id: 'servers.busy.backup-restoring',
|
||||
defaultMessage: 'Backup restore in progress',
|
||||
}),
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if (options.extraBusyReasons) reasons.push(...options.extraBusyReasons.value)
|
||||
return reasons
|
||||
})
|
||||
|
||||
@@ -353,6 +324,7 @@ export function useServerManageCoreRuntime(options: UseServerManageCoreRuntimeOp
|
||||
isWsAuthIncorrect.value = false
|
||||
|
||||
modrinthServersConsole.clear()
|
||||
modrinthServersConsole.beginInitialLogHydration()
|
||||
|
||||
const baseSubscriptions: SocketUnsubscriber[] = [
|
||||
client.archon.sockets.on(targetServerId, 'log', handleLog),
|
||||
@@ -405,20 +377,6 @@ export function useServerManageCoreRuntime(options: UseServerManageCoreRuntimeOp
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => fsOps.value,
|
||||
(newOps) => {
|
||||
for (const op of newOps) {
|
||||
if (op.state === 'done' && op.id && !dismissedOpIds.value.has(op.id)) {
|
||||
setTimeout(() => {
|
||||
dismissOperation(op.id!, 'dismiss')
|
||||
}, 3000)
|
||||
}
|
||||
}
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
const refreshFsAuth = async () => {
|
||||
if (!options.serverId.value) {
|
||||
fsAuth.value = null
|
||||
@@ -440,8 +398,6 @@ export function useServerManageCoreRuntime(options: UseServerManageCoreRuntimeOp
|
||||
isServerRunning,
|
||||
stats,
|
||||
uptimeSeconds,
|
||||
backupsState,
|
||||
markBackupCancelled,
|
||||
isSyncingContent: options.isSyncingContent as Ref<boolean>,
|
||||
busyReasons,
|
||||
fsAuth,
|
||||
@@ -463,7 +419,6 @@ export function useServerManageCoreRuntime(options: UseServerManageCoreRuntimeOp
|
||||
|
||||
return {
|
||||
activeOperations,
|
||||
backupsState,
|
||||
busyReasons,
|
||||
cancelUpload,
|
||||
cleanupCoreRuntime,
|
||||
|
||||
Reference in New Issue
Block a user