You've already forked pages
forked from didirus/AstralRinth
feat: qa improvements for backups page (#4857)
* feat: fix backup action disabling logic * feat: allow actions when backup is being created * feat: qa fixes * feat: backups empty state * fix: lint * intl:extract --------- Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
This commit is contained in:
@@ -2558,9 +2558,6 @@
|
||||
"search.filter.locked.server.sync": {
|
||||
"message": "Sync with server"
|
||||
},
|
||||
"servers.backup.create.in-progress.tooltip": {
|
||||
"message": "Backup creation in progress"
|
||||
},
|
||||
"servers.backup.restore.in-progress.tooltip": {
|
||||
"message": "Backup restore in progress"
|
||||
},
|
||||
|
||||
@@ -120,7 +120,7 @@
|
||||
<div
|
||||
v-else-if="serverData"
|
||||
data-pyro-server-manager-root
|
||||
class="experimental-styles-within mobile-blurred-servericon relative mx-auto mb-6 box-border flex min-h-screen w-full min-w-0 max-w-[1280px] flex-col gap-6 px-6 transition-all duration-300"
|
||||
class="experimental-styles-within mobile-blurred-servericon relative mx-auto mb-12 box-border flex min-h-screen w-full min-w-0 max-w-[1280px] flex-col gap-6 px-6 transition-all duration-300"
|
||||
:style="{
|
||||
'--server-bg-image': serverData.image
|
||||
? `url(${serverData.image})`
|
||||
@@ -738,21 +738,9 @@ const handleBackupProgress = (data: Archon.Websocket.v0.WSBackupProgressEvent) =
|
||||
|
||||
if (backup?.ongoing && attempt < 3) {
|
||||
// retry 3 times max, archon is slow compared to ws state
|
||||
// jank as hell
|
||||
setTimeout(() => attemptCleanup(attempt + 1), 1000)
|
||||
return
|
||||
}
|
||||
|
||||
// clean up on success/3 attempts failed hope and pray
|
||||
const entry = backupsState.get(backupId)
|
||||
if (entry) {
|
||||
const { [data.task]: _, ...remaining } = entry
|
||||
if (Object.keys(remaining).length === 0) {
|
||||
backupsState.delete(backupId)
|
||||
} else {
|
||||
backupsState.set(backupId, remaining)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -981,7 +969,7 @@ export type BackupInProgressReason = {
|
||||
tooltip: MessageDescriptor
|
||||
}
|
||||
|
||||
const RestoreInProgressReason = {
|
||||
const restoreInProgressReason = {
|
||||
type: 'restore',
|
||||
tooltip: defineMessage({
|
||||
id: 'servers.backup.restore.in-progress.tooltip',
|
||||
@@ -989,21 +977,10 @@ const RestoreInProgressReason = {
|
||||
}),
|
||||
} satisfies BackupInProgressReason
|
||||
|
||||
const CreateInProgressReason = {
|
||||
type: 'create',
|
||||
tooltip: defineMessage({
|
||||
id: 'servers.backup.create.in-progress.tooltip',
|
||||
defaultMessage: 'Backup creation in progress',
|
||||
}),
|
||||
} satisfies BackupInProgressReason
|
||||
|
||||
const backupInProgress = computed(() => {
|
||||
for (const entry of backupsState.values()) {
|
||||
if (entry.create?.state === 'ongoing') {
|
||||
return CreateInProgressReason
|
||||
}
|
||||
if (entry.restore?.state === 'ongoing') {
|
||||
return RestoreInProgressReason
|
||||
return restoreInProgressReason
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
|
||||
@@ -76,12 +76,10 @@ export abstract class AbstractWebSocketClient {
|
||||
): () => void {
|
||||
const eventKey = `${serverId}:${eventType}` as keyof WSEventMap
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
this.emitter.on(eventKey, handler as any)
|
||||
this.emitter.on(eventKey, handler as () => void)
|
||||
|
||||
return () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
this.emitter.off(eventKey, handler as any)
|
||||
this.emitter.off(eventKey, handler as () => void)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@ type WSEventMap = {
|
||||
[K in Archon.Websocket.v0.WSEvent as `${string}:${K['event']}`]: K
|
||||
}
|
||||
|
||||
const NORMAL_CLOSURE = 1000
|
||||
|
||||
export class GenericWebSocketClient extends AbstractWebSocketClient {
|
||||
protected emitter = mitt<WSEventMap>()
|
||||
|
||||
@@ -55,7 +57,7 @@ export class GenericWebSocketClient extends AbstractWebSocketClient {
|
||||
}
|
||||
|
||||
ws.onclose = (event) => {
|
||||
if (event.code !== 1000) {
|
||||
if (event.code !== NORMAL_CLOSURE) {
|
||||
this.scheduleReconnect(serverId, auth)
|
||||
}
|
||||
}
|
||||
@@ -83,7 +85,7 @@ export class GenericWebSocketClient extends AbstractWebSocketClient {
|
||||
connection.socket.readyState === WebSocket.OPEN ||
|
||||
connection.socket.readyState === WebSocket.CONNECTING
|
||||
) {
|
||||
connection.socket.close(1000, 'Client disconnecting')
|
||||
connection.socket.close(NORMAL_CLOSURE, 'Client disconnecting')
|
||||
}
|
||||
|
||||
this.emitter.all.forEach((_handlers, type) => {
|
||||
|
||||
@@ -4,11 +4,11 @@ import {
|
||||
ClockIcon,
|
||||
DownloadIcon,
|
||||
EditIcon,
|
||||
LockIcon,
|
||||
LockOpenIcon,
|
||||
// LockIcon,
|
||||
// LockOpenIcon,
|
||||
MoreVerticalIcon,
|
||||
RotateCounterClockwiseIcon,
|
||||
ShieldIcon,
|
||||
// ShieldIcon,
|
||||
TrashIcon,
|
||||
UserRoundIcon,
|
||||
XIcon,
|
||||
@@ -25,7 +25,8 @@ import ProgressBar from '../../base/ProgressBar.vue'
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'download' | 'rename' | 'restore' | 'lock' | 'retry'): void
|
||||
// TODO: Re-add 'lock' when lock functionality is implemented
|
||||
(e: 'download' | 'rename' | 'restore' | 'retry'): void
|
||||
(e: 'delete', skipConfirmation?: boolean): void
|
||||
}>()
|
||||
|
||||
@@ -36,14 +37,14 @@ const props = withDefaults(
|
||||
kyrosUrl?: string
|
||||
jwt?: string
|
||||
showDebugInfo?: boolean
|
||||
disabled?: string
|
||||
restoreDisabled?: string
|
||||
}>(),
|
||||
{
|
||||
preview: false,
|
||||
kyrosUrl: undefined,
|
||||
jwt: undefined,
|
||||
showDebugInfo: false,
|
||||
disabled: undefined,
|
||||
restoreDisabled: undefined,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -93,7 +94,9 @@ const failedToRestore = computed(() => props.backup.task?.restore?.state === 'fa
|
||||
|
||||
const backupIcon = computed(() => {
|
||||
if (props.backup.automated) {
|
||||
return props.backup.locked ? ShieldIcon : ClockIcon
|
||||
// TODO: Re-add locked icon when lock functionality is implemented
|
||||
// return props.backup.locked ? ShieldIcon : ClockIcon
|
||||
return ClockIcon
|
||||
}
|
||||
return UserRoundIcon
|
||||
})
|
||||
@@ -112,7 +115,8 @@ const overflowMenuOptions = computed<OverflowOption[]>(() => {
|
||||
}
|
||||
|
||||
options.push({ id: 'rename', action: () => emit('rename') })
|
||||
options.push({ id: 'lock', action: () => emit('lock') })
|
||||
// TODO: Re-add when lock functionality is implemented
|
||||
// options.push({ id: 'lock', action: () => emit('lock') })
|
||||
|
||||
// Delete only available when not creating (has separate Cancel button)
|
||||
if (!creating.value) {
|
||||
@@ -121,7 +125,6 @@ const overflowMenuOptions = computed<OverflowOption[]>(() => {
|
||||
id: 'delete',
|
||||
color: 'red',
|
||||
action: () => emit('delete'),
|
||||
disabled: !!props.disabled,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -136,18 +139,19 @@ const overflowMenuOptions = computed<OverflowOption[]>(() => {
|
||||
// }
|
||||
|
||||
const messages = defineMessages({
|
||||
locked: {
|
||||
id: 'servers.backups.item.locked',
|
||||
defaultMessage: 'Locked',
|
||||
},
|
||||
lock: {
|
||||
id: 'servers.backups.item.lock',
|
||||
defaultMessage: 'Lock',
|
||||
},
|
||||
unlock: {
|
||||
id: 'servers.backups.item.unlock',
|
||||
defaultMessage: 'Unlock',
|
||||
},
|
||||
// TODO: Re-add when lock functionality is implemented
|
||||
// locked: {
|
||||
// id: 'servers.backups.item.locked',
|
||||
// defaultMessage: 'Locked',
|
||||
// },
|
||||
// lock: {
|
||||
// id: 'servers.backups.item.lock',
|
||||
// defaultMessage: 'Lock',
|
||||
// },
|
||||
// unlock: {
|
||||
// id: 'servers.backups.item.unlock',
|
||||
// defaultMessage: 'Unlock',
|
||||
// },
|
||||
restore: {
|
||||
id: 'servers.backups.item.restore',
|
||||
defaultMessage: 'Restore',
|
||||
@@ -219,9 +223,10 @@ const messages = defineMessages({
|
||||
>
|
||||
{{ formatMessage(messages.auto) }}
|
||||
</span>
|
||||
<span v-if="backup.locked" class="flex items-center gap-1 text-sm text-secondary">
|
||||
<!-- TODO: Re-add when lock functionality is implemented -->
|
||||
<!-- <span v-if="backup.locked" class="flex items-center gap-1 text-sm text-secondary">
|
||||
<LockIcon class="size-4" />
|
||||
</span>
|
||||
</span> -->
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 text-sm text-secondary">
|
||||
<template v-if="failedToCreate || failedToRestore">
|
||||
@@ -312,21 +317,22 @@ const messages = defineMessages({
|
||||
<template #rename>
|
||||
<EditIcon class="size-5" /> {{ formatMessage(messages.rename) }}
|
||||
</template>
|
||||
<template v-if="backup.locked" #lock>
|
||||
<!-- TODO: Re-add when lock functionality is implemented -->
|
||||
<!-- <template v-if="backup.locked" #lock>
|
||||
<LockOpenIcon class="size-5" /> {{ formatMessage(messages.unlock) }}
|
||||
</template>
|
||||
<template v-else #lock>
|
||||
<LockIcon class="size-5" /> {{ formatMessage(messages.lock) }}
|
||||
</template>
|
||||
</template> -->
|
||||
</OverflowMenu>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
<template v-else>
|
||||
<ButtonStyled color="brand" type="outlined">
|
||||
<button
|
||||
v-tooltip="props.disabled"
|
||||
v-tooltip="props.restoreDisabled"
|
||||
class="!border-[1px]"
|
||||
:disabled="!!props.disabled"
|
||||
:disabled="!!props.restoreDisabled"
|
||||
@click="() => emit('restore')"
|
||||
>
|
||||
<RotateCounterClockwiseIcon class="size-5" />
|
||||
@@ -342,12 +348,13 @@ const messages = defineMessages({
|
||||
<template #rename>
|
||||
<EditIcon class="size-5" /> {{ formatMessage(messages.rename) }}
|
||||
</template>
|
||||
<template v-if="backup.locked" #lock>
|
||||
<!-- TODO: Re-add when lock functionality is implemented -->
|
||||
<!-- <template v-if="backup.locked" #lock>
|
||||
<LockOpenIcon class="size-5" /> {{ formatMessage(messages.unlock) }}
|
||||
</template>
|
||||
<template v-else #lock>
|
||||
<LockIcon class="size-5" /> {{ formatMessage(messages.lock) }}
|
||||
</template>
|
||||
</template> -->
|
||||
<template #delete>
|
||||
<TrashIcon class="size-5" /> {{ formatMessage(commonMessages.deleteLabel) }}
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<NewModal ref="modal" header="Restore backup" fade="warning">
|
||||
<NewModal ref="modal" header="Restore backup" fade="danger">
|
||||
<div class="flex flex-col gap-6 max-w-[600px]">
|
||||
<Admonition v-if="ctx.isServerRunning.value" type="critical" header="Server is running">
|
||||
Stop the server before restoring a backup.
|
||||
|
||||
@@ -857,12 +857,6 @@
|
||||
"servers.backups.item.failed-to-restore-backup": {
|
||||
"defaultMessage": "Failed to restore from backup"
|
||||
},
|
||||
"servers.backups.item.lock": {
|
||||
"defaultMessage": "Lock"
|
||||
},
|
||||
"servers.backups.item.locked": {
|
||||
"defaultMessage": "Locked"
|
||||
},
|
||||
"servers.backups.item.manual-backup": {
|
||||
"defaultMessage": "Manual backup"
|
||||
},
|
||||
@@ -884,9 +878,6 @@
|
||||
"servers.backups.item.retry": {
|
||||
"defaultMessage": "Retry"
|
||||
},
|
||||
"servers.backups.item.unlock": {
|
||||
"defaultMessage": "Unlock"
|
||||
},
|
||||
"servers.notice.dismiss": {
|
||||
"defaultMessage": "Dismiss"
|
||||
},
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
<BackupRestoreModal ref="restoreBackupModal" />
|
||||
<BackupDeleteModal ref="deleteBackupModal" @delete="deleteBackup" />
|
||||
|
||||
<div class="mb-2 flex items-center align-middle justify-between">
|
||||
<div v-if="backupsData?.length" class="mb-2 flex items-center align-middle justify-between">
|
||||
<span class="text-2xl font-semibold text-contrast">Backups</span>
|
||||
<ButtonStyled color="brand">
|
||||
<button
|
||||
@@ -51,13 +51,89 @@
|
||||
<div
|
||||
v-if="groupedBackups.length === 0"
|
||||
key="empty"
|
||||
class="mt-6 flex items-center justify-center gap-2 text-center text-secondary"
|
||||
class="mt-6 flex flex-col items-center justify-center gap-2 text-center text-secondary"
|
||||
>
|
||||
<template v-if="server.used_backup_quota">
|
||||
<template v-if="!backupsData">
|
||||
<SpinnerIcon class="animate-spin" />
|
||||
Loading backups...
|
||||
</template>
|
||||
<template v-else>You don't have any backups yet.</template>
|
||||
<template v-else>
|
||||
<div class="mx-auto flex flex-col justify-center p-6 text-center">
|
||||
<div data-svg-wrapper>
|
||||
<svg
|
||||
viewBox="0 0 250 200"
|
||||
fill="none"
|
||||
class="h-[200px] w-[250px]"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M63 134H154C154.515 134 155.017 133.944 155.5 133.839C155.983 133.944 156.485 134 157 134H209C212.866 134 216 130.866 216 127C216 123.134 212.866 120 209 120H203C199.134 120 196 116.866 196 113C196 109.134 199.134 106 203 106H222C225.866 106 229 102.866 229 99C229 95.134 225.866 92 222 92H200C203.866 92 207 88.866 207 85C207 81.134 203.866 78 200 78H136C139.866 78 143 74.866 143 71C143 67.134 139.866 64 136 64H79C75.134 64 72 67.134 72 71C72 74.866 75.134 78 79 78H39C35.134 78 32 81.134 32 85C32 88.866 35.134 92 39 92H64C67.866 92 71 95.134 71 99C71 102.866 67.866 106 64 106H24C20.134 106 17 109.134 17 113C17 116.866 20.134 120 24 120H63C59.134 120 56 123.134 56 127C56 130.866 59.134 134 63 134ZM226 134C229.866 134 233 130.866 233 127C233 123.134 229.866 120 226 120C222.134 120 219 123.134 219 127C219 130.866 222.134 134 226 134Z"
|
||||
fill="var(--surface-2, #1D1F23)"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M113.119 112.307C113.04 112.86 113 113.425 113 114C113 120.627 118.373 126 125 126C131.627 126 137 120.627 137 114C137 113.425 136.96 112.86 136.881 112.307H166V139C166 140.657 164.657 142 163 142H87C85.3431 142 84 140.657 84 139V112.307H113.119Z"
|
||||
fill="var(--surface-1, #16181C)"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M138 112C138 119.18 132.18 125 125 125C117.82 125 112 119.18 112 112C112 111.767 112.006 111.536 112.018 111.307H84L93.5604 83.0389C93.9726 81.8202 95.1159 81 96.4023 81H153.598C154.884 81 156.027 81.8202 156.44 83.0389L166 111.307H137.982C137.994 111.536 138 111.767 138 112Z"
|
||||
fill="var(--surface-1, #16181C)"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M136.098 112.955C136.098 118.502 131.129 124 125 124C118.871 124 113.902 118.502 113.902 112.955C113.902 112.775 113.908 111.596 113.918 111.419H93L101.161 91.5755C101.513 90.6338 102.489 90 103.587 90H146.413C147.511 90 148.487 90.6338 148.839 91.5755L157 111.419H136.082C136.092 111.596 136.098 112.775 136.098 112.955Z"
|
||||
fill="var(--surface-2, #1D1F23)"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M85.25 111.512V138C85.25 138.966 86.0335 139.75 87 139.75H163C163.966 139.75 164.75 138.966 164.75 138V111.512L155.255 83.4393C155.015 82.7285 154.348 82.25 153.598 82.25H96.4023C95.6519 82.25 94.985 82.7285 94.7446 83.4393L85.25 111.512Z"
|
||||
stroke="var(--surface-4, #34363C)"
|
||||
stroke-width="2.5"
|
||||
/>
|
||||
<path
|
||||
d="M98 111C101.937 111 106.185 111 110.745 111C112.621 111 112.621 112.319 112.621 113C112.621 119.627 118.117 125 124.897 125C131.677 125 137.173 119.627 137.173 113C137.173 112.319 137.173 111 139.05 111H164M90.5737 111H93H90.5737Z"
|
||||
stroke="var(--surface-4, #34363C)"
|
||||
stroke-width="2.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M150.1 58.3027L139 70.7559M124.1 54V70.7559V54ZM98 58.3027L109.1 70.7559L98 58.3027Z"
|
||||
stroke="var(--surface-3, #27292E)"
|
||||
stroke-width="2.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex flex-col gap-4 -mt-4">
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<span class="text-lg text-contrast md:text-2xl">No backups yet</span>
|
||||
<span class="max-w-[256px] text-sm md:text-base leading-6 text-secondary">
|
||||
Create your first backup
|
||||
</span>
|
||||
</div>
|
||||
<ButtonStyled color="brand">
|
||||
<button
|
||||
v-tooltip="backupCreationDisabled"
|
||||
:disabled="!!backupCreationDisabled"
|
||||
class="w-min mx-auto"
|
||||
@click="showCreateModel"
|
||||
>
|
||||
<PlusIcon class="size-5" />
|
||||
Create backup
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div v-else key="list" class="flex flex-col gap-1.5">
|
||||
@@ -77,7 +153,7 @@
|
||||
v-for="backup in group.backups"
|
||||
:key="`backup-${backup.id}`"
|
||||
:backup="backup"
|
||||
:disabled="backupOperationDisabled"
|
||||
:restore-disabled="backupRestoreDisabled"
|
||||
:kyros-url="server.node?.instance"
|
||||
:jwt="server.node?.token"
|
||||
:show-debug-info="showDebugInfo"
|
||||
@@ -95,7 +171,7 @@
|
||||
"
|
||||
@delete="
|
||||
(skipConfirmation?: boolean) =>
|
||||
!skipConfirmation ? deleteBackup(backup) : deleteBackupModal?.show(backup)
|
||||
skipConfirmation ? deleteBackup(backup) : deleteBackupModal?.show(backup)
|
||||
"
|
||||
@retry="() => retryBackup(backup.id)"
|
||||
/>
|
||||
@@ -254,7 +330,7 @@ const groupedBackups = computed((): BackupGroup[] => {
|
||||
const diffDays = now.diff(created, 'day')
|
||||
|
||||
if (diffMinutes < 30 && isToday) {
|
||||
addToGroup('Just now', null, backup)
|
||||
addToGroup('Just now', CalendarIcon, backup)
|
||||
} else if (isToday) {
|
||||
addToGroup('Earlier today', CalendarIcon, backup)
|
||||
} else if (isYesterday) {
|
||||
@@ -276,25 +352,22 @@ const restoreBackupModal = ref<InstanceType<typeof BackupRestoreModal>>()
|
||||
const deleteBackupModal = ref<InstanceType<typeof BackupDeleteModal>>()
|
||||
// const backupSettingsModal = ref<InstanceType<typeof BackupSettingsModal>>()
|
||||
|
||||
const backupOperationDisabled = computed(() => {
|
||||
const backupRestoreDisabled = computed(() => {
|
||||
if (props.isServerRunning) {
|
||||
return 'Cannot perform backup operations while server is running'
|
||||
return 'Cannot restore backup while server is running'
|
||||
}
|
||||
for (const entry of backupsState.values()) {
|
||||
if (entry.create?.state === 'ongoing') {
|
||||
return 'Cannot perform backup operations while a backup is being created'
|
||||
return 'Cannot restore backup while a backup is being created'
|
||||
}
|
||||
if (entry.restore?.state === 'ongoing') {
|
||||
return 'Cannot perform backup operations while a backup is being restored'
|
||||
return 'Cannot restore backup while another restore is in progress'
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
|
||||
const backupCreationDisabled = computed(() => {
|
||||
if (props.isServerRunning) {
|
||||
return 'Cannot create backup while server is running'
|
||||
}
|
||||
if (
|
||||
server.value.used_backup_quota !== undefined &&
|
||||
server.value.backup_quota !== undefined &&
|
||||
@@ -311,6 +384,12 @@ const backupCreationDisabled = computed(() => {
|
||||
return 'Cannot create backup while a restore is in progress'
|
||||
}
|
||||
}
|
||||
|
||||
// also check API data for ongoing backups (before ws fires)
|
||||
if (backupsData.value?.some((backup) => backup.ongoing)) {
|
||||
return 'A backup is already in progress'
|
||||
}
|
||||
|
||||
if (server.value.status === 'installing') {
|
||||
return 'Cannot create backup while server is installing'
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user