refactor: align files tab with content tab design (#5621)
* fix: files.vue bugs before styling changes * feat: move files tab to shared layout structure * fix: qa * fix: qa * fix: bugs * fix: lint * fix: admonition cleanup with progress + actions * fix: cleanup * fix: modals * fix: admon title * fix: i18n standard * fix: lint + i18n pass * fix: remove transition * fix: type errors * feat: files tab in app * fix: qa * fix: backup item minmax * fix: use ContentPageHeader for server panel * fix: lint * fix: lint * fix: lint * feat: page leave safety * fix: lint * fix: cargo fmt fix * fix: blank in prod * fix: content card table stuff * Revert "fix: blank in prod" This reverts commit 74758fe185cf85a4a20355857f889cb091b97ace. * fix: import * feat: browse worlds/servers flow * fix: worlds tab parity with content tab * fix: perf bug + shader filter pill copy * feat: singleplayer filter * fix: ordering * fix: breadcrumbs * fix: lint * fix: qa * feat: store server proj id when adding to a non-linked instance * fix: lint * fix: i18n + qa * fix: conflict * qa: already installed modal + placeholders not server-specific * fix: qa * fix: add + edit server modals * fix: qa * fix: security * fix: devin flags * fix: lint * chore: change file to break build cache * fix: admon * fix: import path stuff * feat: qa * fix: fmt fmt idiot --------- Signed-off-by: Calum H. <calum@modrinth.com>
This commit is contained in:
@@ -1,12 +1,27 @@
|
||||
<script setup lang="ts">
|
||||
import type { Archon } from '@modrinth/api-client'
|
||||
import {
|
||||
CheckCircleIcon,
|
||||
ClockIcon,
|
||||
InfoIcon,
|
||||
RotateCounterClockwiseIcon,
|
||||
TriangleAlertIcon,
|
||||
XIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||
import { computed, reactive, watch } from 'vue'
|
||||
|
||||
import { useRelativeTime } from '../../../composables'
|
||||
import { defineMessages, useVIntl } from '../../../composables/i18n'
|
||||
import { injectModrinthClient, injectModrinthServerContext } from '../../../providers'
|
||||
import type { BackupProgressEntry } from '../../../providers/server-context'
|
||||
import BackupProgressAdmonition from './BackupProgressAdmonition.vue'
|
||||
import { commonMessages } from '../../../utils'
|
||||
import Admonition from '../../base/Admonition.vue'
|
||||
import ButtonStyled from '../../base/ButtonStyled.vue'
|
||||
import ProgressBar from '../../base/ProgressBar.vue'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
const relativeTime = useRelativeTime()
|
||||
const client = injectModrinthClient()
|
||||
const queryClient = useQueryClient()
|
||||
const { serverId, worldId, backupsState, markBackupCancelled } = injectModrinthServerContext()
|
||||
@@ -81,7 +96,6 @@ const admonitions = computed<AdmonitionEntry[]>(() => {
|
||||
const result: AdmonitionEntry[] = []
|
||||
const seenIds = new Set<string>()
|
||||
|
||||
// 1. Active WS entries (real-time progress from backupsState)
|
||||
for (const [id, entry] of backupsState.entries()) {
|
||||
const backup = findBackup(id)
|
||||
seenIds.add(id)
|
||||
@@ -115,7 +129,6 @@ const admonitions = computed<AdmonitionEntry[]>(() => {
|
||||
}
|
||||
}
|
||||
|
||||
// 2. REST-based entries for pending/in_progress backups without WS data yet
|
||||
if (backupsList.value) {
|
||||
for (const backup of backupsList.value) {
|
||||
if (seenIds.has(backup.id)) continue
|
||||
@@ -136,7 +149,6 @@ const admonitions = computed<AdmonitionEntry[]>(() => {
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Terminal entries (snapshotted before cleanup)
|
||||
for (const [key, entry] of terminalEntries.entries()) {
|
||||
if (dismissedIds.has(key)) continue
|
||||
if (result.some((r) => r.key === key)) continue
|
||||
@@ -177,6 +189,128 @@ function handleDismiss(key: string) {
|
||||
dismissedIds.add(key)
|
||||
terminalEntries.delete(key)
|
||||
}
|
||||
|
||||
function getAdmonitionType(state: Archon.Backups.v1.BackupState): 'info' | 'critical' | 'success' {
|
||||
if (state === 'failed') return 'critical'
|
||||
if (state === 'done') return 'success'
|
||||
return 'info'
|
||||
}
|
||||
|
||||
function getIcon(state: Archon.Backups.v1.BackupState) {
|
||||
if (state === 'failed') return TriangleAlertIcon
|
||||
if (state === 'done') return CheckCircleIcon
|
||||
return InfoIcon
|
||||
}
|
||||
|
||||
function getButtonColor(state: Archon.Backups.v1.BackupState): 'red' | 'green' | 'blue' {
|
||||
if (state === 'failed') return 'red'
|
||||
if (state === 'done') return 'green'
|
||||
return 'blue'
|
||||
}
|
||||
|
||||
function isQueued(item: AdmonitionEntry) {
|
||||
return item.state === 'ongoing' && item.progress === 0
|
||||
}
|
||||
|
||||
function isInProgress(item: AdmonitionEntry) {
|
||||
return item.state === 'ongoing' && item.progress > 0
|
||||
}
|
||||
|
||||
function getTitle(item: AdmonitionEntry) {
|
||||
if (item.type === 'create') {
|
||||
if (isQueued(item)) return formatMessage(messages.backupQueuedTitle)
|
||||
if (isInProgress(item)) return formatMessage(messages.creatingBackupTitle)
|
||||
if (item.state === 'failed') return formatMessage(messages.backupFailedTitle)
|
||||
}
|
||||
if (isQueued(item)) return formatMessage(messages.restoreQueuedTitle)
|
||||
if (isInProgress(item)) return formatMessage(messages.restoringBackupTitle)
|
||||
if (item.state === 'done') return formatMessage(messages.restoreSuccessfulTitle)
|
||||
if (item.state === 'failed') return formatMessage(messages.restoreFailedTitle)
|
||||
return ''
|
||||
}
|
||||
|
||||
function getDescription(item: AdmonitionEntry) {
|
||||
const backupName = item.name ?? formatMessage(messages.fallbackName)
|
||||
if (item.type === 'create') {
|
||||
if (isQueued(item)) return formatMessage(messages.backupQueuedDescription, { backupName })
|
||||
if (isInProgress(item)) return formatMessage(messages.creatingBackupDescription, { backupName })
|
||||
if (item.state === 'failed')
|
||||
return formatMessage(messages.backupFailedDescription, { backupName })
|
||||
}
|
||||
if (isQueued(item)) return formatMessage(messages.restoreQueuedDescription, { backupName })
|
||||
if (isInProgress(item)) return formatMessage(messages.restoringBackupDescription, { backupName })
|
||||
if (item.state === 'done')
|
||||
return formatMessage(messages.restoreSuccessfulDescription, { backupName })
|
||||
if (item.state === 'failed')
|
||||
return formatMessage(messages.restoreFailedDescription, { backupName })
|
||||
return ''
|
||||
}
|
||||
|
||||
const messages = defineMessages({
|
||||
fallbackName: {
|
||||
id: 'servers.backups.admonition.fallback-name',
|
||||
defaultMessage: 'Your backup',
|
||||
},
|
||||
backupQueuedTitle: {
|
||||
id: 'servers.backups.admonition.backup-queued.title',
|
||||
defaultMessage: 'Backup queued',
|
||||
},
|
||||
backupQueuedDescription: {
|
||||
id: 'servers.backups.admonition.backup-queued.description',
|
||||
defaultMessage: '{backupName} is queued and will start shortly.',
|
||||
},
|
||||
creatingBackupTitle: {
|
||||
id: 'servers.backups.admonition.creating-backup.title',
|
||||
defaultMessage: 'Creating backup',
|
||||
},
|
||||
creatingBackupDescription: {
|
||||
id: 'servers.backups.admonition.creating-backup.description',
|
||||
defaultMessage:
|
||||
'Saving world data and server configuration for {backupName}. This can take a few minutes.',
|
||||
},
|
||||
backupFailedTitle: {
|
||||
id: 'servers.backups.admonition.backup-failed.title',
|
||||
defaultMessage: 'Backup failed',
|
||||
},
|
||||
backupFailedDescription: {
|
||||
id: 'servers.backups.admonition.backup-failed.description',
|
||||
defaultMessage:
|
||||
'Something went wrong while creating {backupName}. Please try again or contact support if the issue continues.',
|
||||
},
|
||||
restoreQueuedTitle: {
|
||||
id: 'servers.backups.admonition.restore-queued.title',
|
||||
defaultMessage: 'Restoring from backup queued',
|
||||
},
|
||||
restoreQueuedDescription: {
|
||||
id: 'servers.backups.admonition.restore-queued.description',
|
||||
defaultMessage: 'Restoring from {backupName} is queued and will start shortly.',
|
||||
},
|
||||
restoringBackupTitle: {
|
||||
id: 'servers.backups.admonition.restoring-backup.title',
|
||||
defaultMessage: 'Restoring from backup',
|
||||
},
|
||||
restoringBackupDescription: {
|
||||
id: 'servers.backups.admonition.restoring-backup.description',
|
||||
defaultMessage: 'Restoring your server from {backupName}. This may take a couple of minutes.',
|
||||
},
|
||||
restoreSuccessfulTitle: {
|
||||
id: 'servers.backups.admonition.restore-successful.title',
|
||||
defaultMessage: 'Restoring from backup successful',
|
||||
},
|
||||
restoreSuccessfulDescription: {
|
||||
id: 'servers.backups.admonition.restore-successful.description',
|
||||
defaultMessage: 'Your server has been restored to {backupName} and is ready to start.',
|
||||
},
|
||||
restoreFailedTitle: {
|
||||
id: 'servers.backups.admonition.restore-failed.title',
|
||||
defaultMessage: 'Restoring from backup failed',
|
||||
},
|
||||
restoreFailedDescription: {
|
||||
id: 'servers.backups.admonition.restore-failed.description',
|
||||
defaultMessage:
|
||||
'Something went wrong while restoring from {backupName}. Please try again or contact support if the issue continues.',
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -186,18 +320,55 @@ function handleDismiss(key: string) {
|
||||
tag="div"
|
||||
class="flex flex-col gap-3"
|
||||
>
|
||||
<BackupProgressAdmonition
|
||||
v-for="item in admonitions"
|
||||
:key="item.key"
|
||||
:type="item.type"
|
||||
:state="item.state"
|
||||
:progress="item.progress"
|
||||
:backup-name="item.name"
|
||||
:created-at="item.createdAt"
|
||||
@cancel="handleCancel(item.backupId)"
|
||||
@retry="handleRetry(item.backupId, item.key)"
|
||||
@dismiss="handleDismiss(item.key)"
|
||||
/>
|
||||
<Admonition v-for="item in admonitions" :key="item.key" :type="getAdmonitionType(item.state)">
|
||||
<template #icon="{ iconClass }">
|
||||
<component :is="getIcon(item.state)" :class="iconClass" />
|
||||
</template>
|
||||
<template #header>
|
||||
<div class="flex items-center gap-2">
|
||||
<span>{{ getTitle(item) }}</span>
|
||||
<div v-if="item.createdAt" class="flex items-center gap-1.5 text-secondary">
|
||||
<ClockIcon class="size-4" />
|
||||
<span class="font-medium">{{ relativeTime(item.createdAt) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
{{ getDescription(item) }}
|
||||
<template #top-right-actions>
|
||||
<ButtonStyled v-if="isQueued(item) || isInProgress(item)" type="outlined" color="blue">
|
||||
<button class="!border" @click="handleCancel(item.backupId)">
|
||||
{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-if="item.state === 'failed'" color="red">
|
||||
<button @click="handleRetry(item.backupId, item.key)">
|
||||
<RotateCounterClockwiseIcon class="size-5" />
|
||||
{{ formatMessage(commonMessages.retryButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled
|
||||
v-if="item.state === 'failed' || item.state === 'done'"
|
||||
circular
|
||||
type="transparent"
|
||||
hover-color-fill="background"
|
||||
:color="getButtonColor(item.state)"
|
||||
>
|
||||
<button @click="handleDismiss(item.key)">
|
||||
<XIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
<template v-if="isInProgress(item)" #progress>
|
||||
<div class="pl-9">
|
||||
<ProgressBar
|
||||
:progress="item.progress"
|
||||
color="blue"
|
||||
:waiting="item.progress === 0"
|
||||
full-width
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Admonition>
|
||||
</TransitionGroup>
|
||||
</template>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user