You've already forked AstralRinth
forked from didirus/AstralRinth
feat: ws client & new backups frontend (#4813)
* feat: ws client * feat: v1 backups endpoints * feat: migrate backups page to api-client and new DI ctx * feat: switch to ws client via api-client * fix: disgust * fix: stats * fix: console * feat: v0 backups api * feat: migrate backups.vue to page system w/ components to ui pkgs * feat: polish backups frontend * feat: pending refactor for ws handling of backups * fix: vue shit * fix: cancel logic fix * fix: qa issues * fix: alignment issues for backups page * fix: bar positioning * feat: finish QA * fix: icons * fix: lint & i18n * fix: clear comment * lint --------- Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
This commit is contained in:
@@ -1,126 +0,0 @@
|
||||
<template>
|
||||
<NewModal ref="modal" header="Creating backup" @show="focusInput">
|
||||
<div class="flex flex-col gap-2 md:w-[600px]">
|
||||
<label for="backup-name-input">
|
||||
<span class="text-lg font-semibold text-contrast"> Name </span>
|
||||
</label>
|
||||
<input
|
||||
id="backup-name-input"
|
||||
ref="input"
|
||||
v-model="backupName"
|
||||
type="text"
|
||||
class="bg-bg-input w-full rounded-lg p-4"
|
||||
:placeholder="`Backup #${newBackupAmount}`"
|
||||
maxlength="48"
|
||||
/>
|
||||
<div v-if="nameExists && !isCreating" class="flex items-center gap-1">
|
||||
<IssuesIcon class="hidden text-orange sm:block" />
|
||||
<span class="text-sm text-orange">
|
||||
You already have a backup named '<span class="font-semibold">{{ trimmedName }}</span
|
||||
>'
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="isRateLimited" class="mt-2 text-sm text-red">
|
||||
You're creating backups too fast. Please wait a moment before trying again.
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2 flex justify-start gap-2">
|
||||
<ButtonStyled color="brand">
|
||||
<button :disabled="isCreating || nameExists" @click="createBackup">
|
||||
<PlusIcon />
|
||||
Create backup
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<button @click="hideModal">
|
||||
<XIcon />
|
||||
Cancel
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</NewModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { IssuesIcon, PlusIcon, XIcon } from '@modrinth/assets'
|
||||
import { ButtonStyled, injectNotificationManager, NewModal } from '@modrinth/ui'
|
||||
import { ModrinthServersFetchError, type ServerBackup } from '@modrinth/utils'
|
||||
import { computed, nextTick, ref } from 'vue'
|
||||
|
||||
import type { ModrinthServer } from '~/composables/servers/modrinth-servers.ts'
|
||||
|
||||
const { addNotification } = injectNotificationManager()
|
||||
|
||||
const props = defineProps<{
|
||||
server: ModrinthServer
|
||||
}>()
|
||||
|
||||
const modal = ref<InstanceType<typeof NewModal>>()
|
||||
const input = ref<HTMLInputElement>()
|
||||
const isCreating = ref(false)
|
||||
const isRateLimited = ref(false)
|
||||
const backupName = ref('')
|
||||
const newBackupAmount = computed(() =>
|
||||
props.server.backups?.data?.length === undefined ? 1 : props.server.backups?.data?.length + 1,
|
||||
)
|
||||
|
||||
const trimmedName = computed(() => backupName.value.trim())
|
||||
|
||||
const nameExists = computed(() => {
|
||||
if (!props.server.backups?.data) return false
|
||||
return props.server.backups.data.some(
|
||||
(backup: ServerBackup) => backup.name.trim().toLowerCase() === trimmedName.value.toLowerCase(),
|
||||
)
|
||||
})
|
||||
|
||||
const focusInput = () => {
|
||||
nextTick(() => {
|
||||
setTimeout(() => {
|
||||
input.value?.focus()
|
||||
}, 100)
|
||||
})
|
||||
}
|
||||
|
||||
function show() {
|
||||
backupName.value = ''
|
||||
isCreating.value = false
|
||||
modal.value?.show()
|
||||
}
|
||||
|
||||
const hideModal = () => {
|
||||
modal.value?.hide()
|
||||
}
|
||||
|
||||
const createBackup = async () => {
|
||||
if (backupName.value.trim().length === 0) {
|
||||
backupName.value = `Backup #${newBackupAmount.value}`
|
||||
}
|
||||
|
||||
isCreating.value = true
|
||||
isRateLimited.value = false
|
||||
try {
|
||||
await props.server.backups?.create(trimmedName.value)
|
||||
hideModal()
|
||||
await props.server.refresh()
|
||||
} catch (error) {
|
||||
if (error instanceof ModrinthServersFetchError && error?.statusCode === 429) {
|
||||
isRateLimited.value = true
|
||||
addNotification({
|
||||
type: 'error',
|
||||
title: 'Error creating backup',
|
||||
text: "You're creating backups too fast.",
|
||||
})
|
||||
} else {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
addNotification({ type: 'error', title: 'Error creating backup', text: message })
|
||||
}
|
||||
} finally {
|
||||
isCreating.value = false
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
show,
|
||||
hide: hideModal,
|
||||
})
|
||||
</script>
|
||||
@@ -1,42 +0,0 @@
|
||||
<template>
|
||||
<ConfirmModal
|
||||
ref="modal"
|
||||
danger
|
||||
title="Are you sure you want to delete this backup?"
|
||||
proceed-label="Delete backup"
|
||||
:confirmation-text="currentBackup?.name ?? 'null'"
|
||||
has-to-type
|
||||
@proceed="emit('delete', currentBackup)"
|
||||
>
|
||||
<BackupItem
|
||||
v-if="currentBackup"
|
||||
:backup="currentBackup"
|
||||
preview
|
||||
class="border-px border-solid border-button-border"
|
||||
/>
|
||||
</ConfirmModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ConfirmModal } from '@modrinth/ui'
|
||||
import type { Backup } from '@modrinth/utils'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import BackupItem from '~/components/ui/servers/BackupItem.vue'
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'delete', backup: Backup | undefined): void
|
||||
}>()
|
||||
|
||||
const modal = ref<InstanceType<typeof ConfirmModal>>()
|
||||
const currentBackup = ref<Backup | undefined>(undefined)
|
||||
|
||||
function show(backup: Backup) {
|
||||
currentBackup.value = backup
|
||||
modal.value?.show()
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
show,
|
||||
})
|
||||
</script>
|
||||
@@ -1,277 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
BotIcon,
|
||||
DownloadIcon,
|
||||
EditIcon,
|
||||
FolderArchiveIcon,
|
||||
HistoryIcon,
|
||||
LockIcon,
|
||||
LockOpenIcon,
|
||||
MoreVerticalIcon,
|
||||
RotateCounterClockwiseIcon,
|
||||
SpinnerIcon,
|
||||
TrashIcon,
|
||||
XIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { ButtonStyled, commonMessages, OverflowMenu, ProgressBar } from '@modrinth/ui'
|
||||
import type { Backup } from '@modrinth/utils'
|
||||
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||
import dayjs from 'dayjs'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const flags = useFeatureFlags()
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'download' | 'rename' | 'restore' | 'lock' | 'retry'): void
|
||||
(e: 'delete', skipConfirmation?: boolean): void
|
||||
}>()
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
backup: Backup
|
||||
preview?: boolean
|
||||
kyrosUrl?: string
|
||||
jwt?: string
|
||||
}>(),
|
||||
{
|
||||
preview: false,
|
||||
kyrosUrl: undefined,
|
||||
jwt: undefined,
|
||||
},
|
||||
)
|
||||
|
||||
const backupQueued = computed(
|
||||
() =>
|
||||
props.backup.task?.create?.progress === 0 ||
|
||||
(props.backup.ongoing && !props.backup.task?.create),
|
||||
)
|
||||
const automated = computed(() => props.backup.automated)
|
||||
const failedToCreate = computed(() => props.backup.interrupted)
|
||||
|
||||
const inactiveStates = ['failed', 'cancelled']
|
||||
|
||||
const creating = computed(() => {
|
||||
const task = props.backup.task?.create
|
||||
if (task && task.progress < 1 && !inactiveStates.includes(task.state)) {
|
||||
return task
|
||||
}
|
||||
if (props.backup.ongoing) {
|
||||
return {
|
||||
progress: 0,
|
||||
state: 'ongoing',
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
|
||||
const restoring = computed(() => {
|
||||
const task = props.backup.task?.restore
|
||||
if (task && task.progress < 1 && !inactiveStates.includes(task.state)) {
|
||||
return task
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
|
||||
const failedToRestore = computed(() => props.backup.task?.restore?.state === 'failed')
|
||||
|
||||
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',
|
||||
},
|
||||
restore: {
|
||||
id: 'servers.backups.item.restore',
|
||||
defaultMessage: 'Restore',
|
||||
},
|
||||
rename: {
|
||||
id: 'servers.backups.item.rename',
|
||||
defaultMessage: 'Rename',
|
||||
},
|
||||
queuedForBackup: {
|
||||
id: 'servers.backups.item.queued-for-backup',
|
||||
defaultMessage: 'Queued for backup',
|
||||
},
|
||||
creatingBackup: {
|
||||
id: 'servers.backups.item.creating-backup',
|
||||
defaultMessage: 'Creating backup...',
|
||||
},
|
||||
restoringBackup: {
|
||||
id: 'servers.backups.item.restoring-backup',
|
||||
defaultMessage: 'Restoring from backup...',
|
||||
},
|
||||
failedToCreateBackup: {
|
||||
id: 'servers.backups.item.failed-to-create-backup',
|
||||
defaultMessage: 'Failed to create backup',
|
||||
},
|
||||
failedToRestoreBackup: {
|
||||
id: 'servers.backups.item.failed-to-restore-backup',
|
||||
defaultMessage: 'Failed to restore from backup',
|
||||
},
|
||||
automated: {
|
||||
id: 'servers.backups.item.automated',
|
||||
defaultMessage: 'Automated',
|
||||
},
|
||||
retry: {
|
||||
id: 'servers.backups.item.retry',
|
||||
defaultMessage: 'Retry',
|
||||
},
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
:class="
|
||||
preview
|
||||
? 'grid-cols-[min-content_1fr_1fr] sm:grid-cols-[min-content_3fr_2fr_1fr] md:grid-cols-[auto_3fr_2fr_1fr]'
|
||||
: 'grid-cols-[min-content_1fr_1fr] sm:grid-cols-[min-content_3fr_2fr_1fr] md:grid-cols-[auto_3fr_2fr_1fr_2fr]'
|
||||
"
|
||||
class="grid items-center gap-4 rounded-2xl bg-bg-raised px-4 py-3"
|
||||
>
|
||||
<div
|
||||
class="flex h-12 w-12 items-center justify-center rounded-xl border-[1px] border-solid border-button-border bg-button-bg"
|
||||
>
|
||||
<SpinnerIcon
|
||||
v-if="creating"
|
||||
class="h-6 w-6 animate-spin"
|
||||
:class="{ 'text-orange': backupQueued, 'text-green': !backupQueued }"
|
||||
/>
|
||||
<FolderArchiveIcon v-else class="h-6 w-6" />
|
||||
</div>
|
||||
<div class="col-span-2 flex flex-col gap-1 sm:col-span-1">
|
||||
<span class="font-bold text-contrast">
|
||||
{{ backup.name }}
|
||||
</span>
|
||||
<div class="flex flex-wrap items-center gap-2 text-sm">
|
||||
<span v-if="backup.locked" class="flex items-center gap-1 text-sm text-secondary">
|
||||
<LockIcon /> {{ formatMessage(messages.locked) }}
|
||||
</span>
|
||||
<span v-if="automated && backup.locked">•</span>
|
||||
<span v-if="automated" class="flex items-center gap-1 text-secondary">
|
||||
<BotIcon /> {{ formatMessage(messages.automated) }}
|
||||
</span>
|
||||
<span v-if="(failedToCreate || failedToRestore) && (automated || backup.locked)">•</span>
|
||||
<span
|
||||
v-if="failedToCreate || failedToRestore"
|
||||
class="flex items-center gap-1 text-sm text-red"
|
||||
>
|
||||
<XIcon />
|
||||
{{
|
||||
formatMessage(
|
||||
failedToCreate ? messages.failedToCreateBackup : messages.failedToRestoreBackup,
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="creating" class="col-span-2 flex flex-col gap-3">
|
||||
<span v-if="backupQueued" class="text-orange">
|
||||
{{ formatMessage(messages.queuedForBackup) }}
|
||||
</span>
|
||||
<span v-else class="text-green"> {{ formatMessage(messages.creatingBackup) }} </span>
|
||||
<ProgressBar
|
||||
:progress="creating.progress"
|
||||
:color="backupQueued ? 'orange' : 'green'"
|
||||
:waiting="creating.progress === 0"
|
||||
class="max-w-full"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="restoring" class="col-span-2 flex flex-col gap-3 text-purple">
|
||||
{{ formatMessage(messages.restoringBackup) }}
|
||||
<ProgressBar
|
||||
:progress="restoring.progress"
|
||||
color="purple"
|
||||
:waiting="restoring.progress === 0"
|
||||
class="max-w-full"
|
||||
/>
|
||||
</div>
|
||||
<template v-else>
|
||||
<div class="col-span-2">
|
||||
{{ dayjs(backup.created_at).format('MMMM D, YYYY [at] h:mm A') }}
|
||||
</div>
|
||||
<div v-if="false">{{ 245 }} MiB</div>
|
||||
</template>
|
||||
<div
|
||||
v-if="!preview"
|
||||
class="col-span-full flex justify-normal gap-2 md:col-span-1 md:justify-end"
|
||||
>
|
||||
<template v-if="failedToCreate">
|
||||
<ButtonStyled>
|
||||
<button @click="() => emit('retry')">
|
||||
<RotateCounterClockwiseIcon />
|
||||
{{ formatMessage(messages.retry) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<button @click="() => emit('delete', true)">
|
||||
<TrashIcon />
|
||||
Remove
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
<ButtonStyled v-else-if="creating">
|
||||
<button @click="() => emit('delete')">
|
||||
<XIcon />
|
||||
{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<template v-else>
|
||||
<ButtonStyled>
|
||||
<a
|
||||
:class="{
|
||||
disabled: !kyrosUrl || !jwt,
|
||||
}"
|
||||
:href="`https://${kyrosUrl}/modrinth/v0/backups/${backup.id}/download?auth=${jwt}`"
|
||||
@click="() => emit('download')"
|
||||
>
|
||||
<DownloadIcon />
|
||||
{{ formatMessage(commonMessages.downloadButton) }}
|
||||
</a>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled circular type="transparent">
|
||||
<OverflowMenu
|
||||
:options="[
|
||||
{ id: 'rename', action: () => emit('rename') },
|
||||
{
|
||||
id: 'restore',
|
||||
action: () => emit('restore'),
|
||||
disabled: !!restoring,
|
||||
},
|
||||
{ id: 'lock', action: () => emit('lock') },
|
||||
{ divider: true },
|
||||
{
|
||||
id: 'delete',
|
||||
color: 'red',
|
||||
action: () => emit('delete'),
|
||||
disabled: !!restoring,
|
||||
},
|
||||
]"
|
||||
>
|
||||
<MoreVerticalIcon />
|
||||
<template #rename> <EditIcon /> {{ formatMessage(messages.rename) }} </template>
|
||||
<template #restore> <HistoryIcon /> {{ formatMessage(messages.restore) }} </template>
|
||||
<template v-if="backup.locked" #lock>
|
||||
<LockOpenIcon /> {{ formatMessage(messages.unlock) }}
|
||||
</template>
|
||||
<template v-else #lock> <LockIcon /> {{ formatMessage(messages.lock) }} </template>
|
||||
<template #delete>
|
||||
<TrashIcon /> {{ formatMessage(commonMessages.deleteLabel) }}
|
||||
</template>
|
||||
</OverflowMenu>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
</div>
|
||||
<pre
|
||||
v-if="!preview && flags.advancedDebugInfo"
|
||||
class="col-span-full m-0 rounded-xl bg-button-bg text-xs"
|
||||
>{{ backup }}</pre
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,144 +0,0 @@
|
||||
<template>
|
||||
<NewModal ref="modal" header="Renaming backup" @show="focusInput">
|
||||
<div class="flex flex-col gap-2 md:w-[600px]">
|
||||
<label for="backup-name-input">
|
||||
<span class="text-lg font-semibold text-contrast"> Name </span>
|
||||
</label>
|
||||
<input
|
||||
id="backup-name-input"
|
||||
ref="input"
|
||||
v-model="backupName"
|
||||
type="text"
|
||||
class="bg-bg-input w-full rounded-lg p-4"
|
||||
:placeholder="`Backup #${backupNumber}`"
|
||||
maxlength="48"
|
||||
/>
|
||||
<div v-if="nameExists" class="flex items-center gap-1">
|
||||
<IssuesIcon class="hidden text-orange sm:block" />
|
||||
<span class="text-sm text-orange">
|
||||
You already have a backup named '<span class="font-semibold">{{ trimmedName }}</span
|
||||
>'
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2 flex justify-start gap-2">
|
||||
<ButtonStyled color="brand">
|
||||
<button :disabled="isRenaming || nameExists" @click="renameBackup">
|
||||
<template v-if="isRenaming">
|
||||
<SpinnerIcon class="animate-spin" />
|
||||
Renaming...
|
||||
</template>
|
||||
<template v-else>
|
||||
<SaveIcon />
|
||||
Save changes
|
||||
</template>
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<button @click="hide">
|
||||
<XIcon />
|
||||
Cancel
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</NewModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { IssuesIcon, SaveIcon, SpinnerIcon, XIcon } from '@modrinth/assets'
|
||||
import { ButtonStyled, injectNotificationManager, NewModal } from '@modrinth/ui'
|
||||
import type { Backup } from '@modrinth/utils'
|
||||
import { computed, nextTick, ref } from 'vue'
|
||||
|
||||
import type { ModrinthServer } from '~/composables/servers/modrinth-servers.ts'
|
||||
|
||||
const { addNotification } = injectNotificationManager()
|
||||
|
||||
const props = defineProps<{
|
||||
server: ModrinthServer
|
||||
}>()
|
||||
|
||||
const modal = ref<InstanceType<typeof NewModal>>()
|
||||
const input = ref<HTMLInputElement>()
|
||||
const backupName = ref('')
|
||||
const originalName = ref('')
|
||||
const isRenaming = ref(false)
|
||||
|
||||
const currentBackup = ref<Backup | null>(null)
|
||||
|
||||
const trimmedName = computed(() => backupName.value.trim())
|
||||
|
||||
const nameExists = computed(() => {
|
||||
if (!props.server.backups?.data || trimmedName.value === originalName.value || isRenaming.value) {
|
||||
return false
|
||||
}
|
||||
|
||||
return props.server.backups.data.some(
|
||||
(backup: Backup) => backup.name.trim().toLowerCase() === trimmedName.value.toLowerCase(),
|
||||
)
|
||||
})
|
||||
|
||||
const backupNumber = computed(
|
||||
() => (props.server.backups?.data?.findIndex((b) => b.id === currentBackup.value?.id) ?? 0) + 1,
|
||||
)
|
||||
|
||||
const focusInput = () => {
|
||||
nextTick(() => {
|
||||
setTimeout(() => {
|
||||
input.value?.focus()
|
||||
}, 100)
|
||||
})
|
||||
}
|
||||
|
||||
function show(backup: Backup) {
|
||||
currentBackup.value = backup
|
||||
backupName.value = backup.name
|
||||
originalName.value = backup.name
|
||||
isRenaming.value = false
|
||||
modal.value?.show()
|
||||
}
|
||||
|
||||
function hide() {
|
||||
modal.value?.hide()
|
||||
}
|
||||
|
||||
const renameBackup = async () => {
|
||||
if (!currentBackup.value) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
title: 'Error renaming backup',
|
||||
text: 'Current backup is null',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (trimmedName.value === originalName.value) {
|
||||
hide()
|
||||
return
|
||||
}
|
||||
|
||||
isRenaming.value = true
|
||||
try {
|
||||
let newName = trimmedName.value
|
||||
|
||||
if (newName.length === 0) {
|
||||
newName = `Backup #${backupNumber.value}`
|
||||
}
|
||||
|
||||
await props.server.backups?.rename(currentBackup.value.id, newName)
|
||||
hide()
|
||||
await props.server.refresh()
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
addNotification({ type: 'error', title: 'Error renaming backup', text: message })
|
||||
} finally {
|
||||
hide()
|
||||
isRenaming.value = false
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
show,
|
||||
hide,
|
||||
})
|
||||
</script>
|
||||
@@ -1,63 +0,0 @@
|
||||
<template>
|
||||
<ConfirmModal
|
||||
ref="modal"
|
||||
danger
|
||||
title="Are you sure you want to restore from this backup?"
|
||||
proceed-label="Restore from backup"
|
||||
description="This will **overwrite all files on your server** and replace them with the files from the backup."
|
||||
@proceed="restoreBackup"
|
||||
>
|
||||
<BackupItem
|
||||
v-if="currentBackup"
|
||||
:backup="currentBackup"
|
||||
preview
|
||||
class="border-px border-solid border-button-border"
|
||||
/>
|
||||
</ConfirmModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { NewModal } from '@modrinth/ui'
|
||||
import { ConfirmModal, injectNotificationManager } from '@modrinth/ui'
|
||||
import type { Backup } from '@modrinth/utils'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import BackupItem from '~/components/ui/servers/BackupItem.vue'
|
||||
import type { ModrinthServer } from '~/composables/servers/modrinth-servers.ts'
|
||||
|
||||
const { addNotification } = injectNotificationManager()
|
||||
|
||||
const props = defineProps<{
|
||||
server: ModrinthServer
|
||||
}>()
|
||||
|
||||
const modal = ref<InstanceType<typeof NewModal>>()
|
||||
const currentBackup = ref<Backup | null>(null)
|
||||
|
||||
function show(backup: Backup) {
|
||||
currentBackup.value = backup
|
||||
modal.value?.show()
|
||||
}
|
||||
|
||||
const restoreBackup = async () => {
|
||||
if (!currentBackup.value) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
title: 'Failed to restore backup',
|
||||
text: 'Current backup is null',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await props.server.backups?.restore(currentBackup.value.id)
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
addNotification({ type: 'error', title: 'Failed to restore backup', text: message })
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
show,
|
||||
})
|
||||
</script>
|
||||
Reference in New Issue
Block a user