You've already forked AstralRinth
forked from didirus/AstralRinth
backup page fixes and new impls for new apis (#3437)
* wip: backup page fixes and new impls for new apis * wip: more progress on backup fixes, almost done * lint * Backups cleanup * Don't show create warning if creating * Fix ongoing state * Download support * Support ready * Disable auto backup button * Use auth param for download of backups * Disable install buttons when backup is in progress, add retrying * Make prepare button have immediate feedback, don't refresh backups in all cases * Intl:extract & rebase fixes * Updated changelog and fix lint --------- Co-authored-by: Prospector <prospectordev@gmail.com>
This commit is contained in:
@@ -1,29 +1,32 @@
|
||||
<template>
|
||||
<NewModal ref="modal" header="Creating backup" @show="focusInput">
|
||||
<div class="flex flex-col gap-2 md:w-[600px]">
|
||||
<div class="font-semibold text-contrast">Name</div>
|
||||
<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="e.g. Before 1.21"
|
||||
maxlength="64"
|
||||
:placeholder="`Backup #${newBackupAmount}`"
|
||||
maxlength="48"
|
||||
/>
|
||||
<div class="flex items-center gap-2">
|
||||
<InfoIcon class="hidden sm:block" />
|
||||
<span class="text-sm text-secondary">
|
||||
If left empty, the backup name will default to
|
||||
<span class="font-semibold"> Backup #{{ newBackupAmount }}</span>
|
||||
<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="mb-1 mt-4 flex justify-start gap-4">
|
||||
<div class="mt-2 flex justify-start gap-2">
|
||||
<ButtonStyled color="brand">
|
||||
<button :disabled="isCreating" @click="createBackup">
|
||||
<button :disabled="isCreating || nameExists" @click="createBackup">
|
||||
<PlusIcon />
|
||||
Create backup
|
||||
</button>
|
||||
@@ -41,24 +44,30 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, nextTick, computed } from "vue";
|
||||
import { ButtonStyled, NewModal } from "@modrinth/ui";
|
||||
import { PlusIcon, XIcon, InfoIcon } from "@modrinth/assets";
|
||||
import { IssuesIcon, PlusIcon, XIcon } from "@modrinth/assets";
|
||||
|
||||
const props = defineProps<{
|
||||
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(["backupCreated"]);
|
||||
|
||||
const modal = ref<InstanceType<typeof NewModal>>();
|
||||
const input = ref<HTMLInputElement>();
|
||||
const isCreating = ref(false);
|
||||
const isRateLimited = ref(false);
|
||||
const backupError = ref<string | null>(null);
|
||||
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) => backup.name.trim().toLowerCase() === trimmedName.value.toLowerCase(),
|
||||
);
|
||||
});
|
||||
|
||||
const focusInput = () => {
|
||||
nextTick(() => {
|
||||
setTimeout(() => {
|
||||
@@ -67,30 +76,38 @@ const focusInput = () => {
|
||||
});
|
||||
};
|
||||
|
||||
function show() {
|
||||
backupName.value = "";
|
||||
isCreating.value = false;
|
||||
modal.value?.show();
|
||||
}
|
||||
|
||||
const hideModal = () => {
|
||||
modal.value?.hide();
|
||||
backupName.value = "";
|
||||
};
|
||||
|
||||
const createBackup = async () => {
|
||||
if (!backupName.value.trim()) {
|
||||
if (backupName.value.trim().length === 0) {
|
||||
backupName.value = `Backup #${newBackupAmount.value}`;
|
||||
}
|
||||
|
||||
isCreating.value = true;
|
||||
isRateLimited.value = false;
|
||||
try {
|
||||
await props.server.backups?.create(backupName.value);
|
||||
await props.server.refresh();
|
||||
await props.server.backups?.create(trimmedName.value);
|
||||
hideModal();
|
||||
emit("backupCreated", { success: true, message: "Backup created successfully" });
|
||||
await props.server.refresh();
|
||||
} catch (error) {
|
||||
if (error instanceof PyroFetchError && error.statusCode === 429) {
|
||||
isRateLimited.value = true;
|
||||
backupError.value = "You're creating backups too fast.";
|
||||
addNotification({
|
||||
type: "error",
|
||||
title: "Error creating backup",
|
||||
text: "You're creating backups too fast.",
|
||||
});
|
||||
} else {
|
||||
backupError.value = error instanceof Error ? error.message : String(error);
|
||||
emit("backupCreated", { success: false, message: backupError.value });
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
addNotification({ type: "error", title: "Error creating backup", text: message });
|
||||
}
|
||||
} finally {
|
||||
isCreating.value = false;
|
||||
@@ -98,7 +115,7 @@ const createBackup = async () => {
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
show: () => modal.value?.show(),
|
||||
show,
|
||||
hide: hideModal,
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,86 +1,45 @@
|
||||
<template>
|
||||
<NewModal ref="modal" danger header="Deleting backup">
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="relative flex w-full flex-col gap-2 rounded-2xl bg-[#0e0e0ea4] p-6">
|
||||
<div class="text-2xl font-extrabold text-contrast">
|
||||
{{ backupName }}
|
||||
</div>
|
||||
<div class="flex gap-2 font-semibold text-contrast">
|
||||
<CalendarIcon />
|
||||
{{ formattedDate }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-1 mt-4 flex justify-end gap-4">
|
||||
<ButtonStyled color="red">
|
||||
<button :disabled="isDeleting" @click="deleteBackup">
|
||||
<TrashIcon />
|
||||
Delete backup
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled type="transparent">
|
||||
<button @click="hideModal">Cancel</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</NewModal>
|
||||
<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 { ref } from "vue";
|
||||
import { ButtonStyled, NewModal } from "@modrinth/ui";
|
||||
import { TrashIcon, CalendarIcon } from "@modrinth/assets";
|
||||
import { ConfirmModal } from "@modrinth/ui";
|
||||
import type { Server } from "~/composables/pyroServers";
|
||||
import BackupItem from "~/components/ui/servers/BackupItem.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
server: Server<["general", "mods", "backups", "network", "startup", "ws", "fs"]>;
|
||||
backupId: string;
|
||||
backupName: string;
|
||||
backupCreatedAt: string;
|
||||
defineProps<{
|
||||
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(["backupDeleted"]);
|
||||
const emit = defineEmits<{
|
||||
(e: "delete", backup: Backup | undefined): void;
|
||||
}>();
|
||||
|
||||
const modal = ref<InstanceType<typeof NewModal>>();
|
||||
const isDeleting = ref(false);
|
||||
const backupError = ref<string | null>(null);
|
||||
const modal = ref<InstanceType<typeof ConfirmModal>>();
|
||||
const currentBackup = ref<Backup | undefined>(undefined);
|
||||
|
||||
const formattedDate = computed(() => {
|
||||
return new Date(props.backupCreatedAt).toLocaleString("en-US", {
|
||||
month: "numeric",
|
||||
day: "numeric",
|
||||
year: "2-digit",
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
hour12: true,
|
||||
});
|
||||
});
|
||||
|
||||
const hideModal = () => {
|
||||
modal.value?.hide();
|
||||
};
|
||||
|
||||
const deleteBackup = async () => {
|
||||
if (!props.backupId) {
|
||||
emit("backupDeleted", { success: false, message: "No backup selected" });
|
||||
return;
|
||||
}
|
||||
|
||||
isDeleting.value = true;
|
||||
try {
|
||||
await props.server.backups?.delete(props.backupId);
|
||||
await props.server.refresh();
|
||||
hideModal();
|
||||
emit("backupDeleted", { success: true, message: "Backup deleted successfully" });
|
||||
} catch (error) {
|
||||
backupError.value = error instanceof Error ? error.message : String(error);
|
||||
emit("backupDeleted", { success: false, message: backupError.value });
|
||||
} finally {
|
||||
isDeleting.value = false;
|
||||
}
|
||||
};
|
||||
function show(backup: Backup) {
|
||||
currentBackup.value = backup;
|
||||
modal.value?.show();
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
show: () => modal.value?.show(),
|
||||
hide: hideModal,
|
||||
show,
|
||||
});
|
||||
</script>
|
||||
|
||||
320
apps/frontend/src/components/ui/servers/BackupItem.vue
Normal file
320
apps/frontend/src/components/ui/servers/BackupItem.vue
Normal file
@@ -0,0 +1,320 @@
|
||||
<script setup lang="ts">
|
||||
import dayjs from "dayjs";
|
||||
import {
|
||||
MoreVerticalIcon,
|
||||
HistoryIcon,
|
||||
DownloadIcon,
|
||||
SpinnerIcon,
|
||||
EditIcon,
|
||||
LockIcon,
|
||||
TrashIcon,
|
||||
FolderArchiveIcon,
|
||||
BotIcon,
|
||||
XIcon,
|
||||
LockOpenIcon,
|
||||
RotateCounterClockwiseIcon,
|
||||
} from "@modrinth/assets";
|
||||
import { ButtonStyled, commonMessages, OverflowMenu, ProgressBar } from "@modrinth/ui";
|
||||
import { defineMessages, useVIntl } from "@vintl/vintl";
|
||||
import { ref } from "vue";
|
||||
import type { Backup } from "~/composables/pyroServers.ts";
|
||||
|
||||
const flags = useFeatureFlags();
|
||||
const { formatMessage } = useVIntl();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "prepare" | "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 preparedDownloadStates = ["ready", "done"];
|
||||
const inactiveStates = ["failed", "cancelled"];
|
||||
|
||||
const hasPreparedDownload = computed(() =>
|
||||
preparedDownloadStates.includes(props.backup.task?.file?.state ?? ""),
|
||||
);
|
||||
|
||||
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 initiatedPrepare = ref(false);
|
||||
|
||||
const preparingFile = computed(() => {
|
||||
const task = props.backup.task?.file;
|
||||
return (
|
||||
initiatedPrepare.value || (task && task.progress < 1 && !inactiveStates.includes(task.state))
|
||||
);
|
||||
});
|
||||
|
||||
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",
|
||||
},
|
||||
preparingDownload: {
|
||||
id: "servers.backups.item.preparing-download",
|
||||
defaultMessage: "Preparing download...",
|
||||
},
|
||||
prepareDownload: {
|
||||
id: "servers.backups.item.prepare-download",
|
||||
defaultMessage: "Prepare download",
|
||||
},
|
||||
alreadyPreparing: {
|
||||
id: "servers.backups.item.already-preparing",
|
||||
defaultMessage: "Already preparing backup for download",
|
||||
},
|
||||
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 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
|
||||
v-if="hasPreparedDownload"
|
||||
:class="{
|
||||
disabled: !kyrosUrl || !jwt,
|
||||
}"
|
||||
:href="`https://${kyrosUrl}/modrinth/v0/backups/${backup.id}/download?auth=${jwt}`"
|
||||
@click="() => emit('download')"
|
||||
>
|
||||
<DownloadIcon />
|
||||
{{ formatMessage(commonMessages.downloadButton) }}
|
||||
</a>
|
||||
<button
|
||||
v-else
|
||||
:disabled="!!preparingFile"
|
||||
@click="
|
||||
() => {
|
||||
initiatedPrepare = true;
|
||||
emit('prepare');
|
||||
}
|
||||
"
|
||||
>
|
||||
<SpinnerIcon v-if="preparingFile" class="animate-spin" />
|
||||
<DownloadIcon v-else />
|
||||
{{
|
||||
formatMessage(preparingFile ? messages.preparingDownload : messages.prepareDownload)
|
||||
}}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled circular type="transparent">
|
||||
<OverflowMenu
|
||||
:options="[
|
||||
{ id: 'rename', action: () => emit('rename') },
|
||||
{
|
||||
id: 'restore',
|
||||
action: () => emit('restore'),
|
||||
disabled: !!restoring || !!preparingFile,
|
||||
},
|
||||
{ id: 'lock', action: () => emit('lock') },
|
||||
{ divider: true },
|
||||
{
|
||||
id: 'delete',
|
||||
color: 'red',
|
||||
action: () => emit('delete'),
|
||||
disabled: !!restoring || !!preparingFile,
|
||||
},
|
||||
]"
|
||||
>
|
||||
<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,24 +1,41 @@
|
||||
<template>
|
||||
<NewModal ref="modal" header="Renaming backup" @show="focusInput">
|
||||
<div class="flex flex-col gap-2 md:w-[600px]">
|
||||
<div class="font-semibold text-contrast">Name</div>
|
||||
<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="e.g. Before 1.21"
|
||||
: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="mb-1 mt-4 flex justify-start gap-4">
|
||||
<div class="mt-2 flex justify-start gap-2">
|
||||
<ButtonStyled color="brand">
|
||||
<button :disabled="isRenaming" @click="renameBackup">
|
||||
<SaveIcon />
|
||||
Rename backup
|
||||
<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="hideModal">
|
||||
<button @click="hide">
|
||||
<XIcon />
|
||||
Cancel
|
||||
</button>
|
||||
@@ -28,23 +45,38 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, nextTick } from "vue";
|
||||
import { ref, nextTick, computed } from "vue";
|
||||
import { ButtonStyled, NewModal } from "@modrinth/ui";
|
||||
import { SaveIcon, XIcon } from "@modrinth/assets";
|
||||
import { SpinnerIcon, SaveIcon, XIcon, IssuesIcon } from "@modrinth/assets";
|
||||
import type { Server } from "~/composables/pyroServers";
|
||||
|
||||
const props = defineProps<{
|
||||
server: Server<["general", "mods", "backups", "network", "startup", "ws", "fs"]>;
|
||||
currentBackupId: string;
|
||||
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(["backupRenamed"]);
|
||||
|
||||
const modal = ref<InstanceType<typeof NewModal>>();
|
||||
const input = ref<HTMLInputElement>();
|
||||
const backupName = ref("");
|
||||
const originalName = ref("");
|
||||
const isRenaming = ref(false);
|
||||
const backupError = ref<string | null>(null);
|
||||
|
||||
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.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(() => {
|
||||
@@ -54,33 +86,55 @@ const focusInput = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const hideModal = () => {
|
||||
backupName.value = "";
|
||||
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 (!backupName.value.trim() || !props.currentBackupId) {
|
||||
emit("backupRenamed", { success: false, message: "Backup name cannot be empty" });
|
||||
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 {
|
||||
await props.server.backups?.rename(props.currentBackupId, backupName.value);
|
||||
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();
|
||||
hideModal();
|
||||
emit("backupRenamed", { success: true, message: "Backup renamed successfully" });
|
||||
} catch (error) {
|
||||
backupError.value = error instanceof Error ? error.message : String(error);
|
||||
emit("backupRenamed", { success: false, message: backupError.value });
|
||||
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: () => modal.value?.show(),
|
||||
hide: hideModal,
|
||||
show,
|
||||
hide,
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,82 +1,58 @@
|
||||
<template>
|
||||
<NewModal ref="modal" header="Restoring backup">
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="relative flex w-full flex-col gap-2 rounded-2xl bg-bg p-6">
|
||||
<div class="text-2xl font-extrabold text-contrast">
|
||||
{{ backupName }}
|
||||
</div>
|
||||
<div class="flex gap-2 font-semibold text-contrast">
|
||||
<CalendarIcon />
|
||||
{{ formattedDate }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-1 mt-4 flex justify-end gap-4">
|
||||
<ButtonStyled color="brand">
|
||||
<button :disabled="isRestoring" @click="restoreBackup">Restore backup</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled type="transparent">
|
||||
<button @click="hideModal">Cancel</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</NewModal>
|
||||
<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 { ref } from "vue";
|
||||
import { ButtonStyled, NewModal } from "@modrinth/ui";
|
||||
import { CalendarIcon } from "@modrinth/assets";
|
||||
import { ConfirmModal, NewModal } from "@modrinth/ui";
|
||||
import type { Server } from "~/composables/pyroServers";
|
||||
import BackupItem from "~/components/ui/servers/BackupItem.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
server: Server<["general", "mods", "backups", "network", "startup", "ws", "fs"]>;
|
||||
backupId: string;
|
||||
backupName: string;
|
||||
backupCreatedAt: string;
|
||||
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(["backupRestored"]);
|
||||
|
||||
const modal = ref<InstanceType<typeof NewModal>>();
|
||||
const isRestoring = ref(false);
|
||||
const backupError = ref<string | null>(null);
|
||||
const currentBackup = ref<Backup | null>(null);
|
||||
|
||||
const formattedDate = computed(() => {
|
||||
return new Date(props.backupCreatedAt).toLocaleString("en-US", {
|
||||
month: "numeric",
|
||||
day: "numeric",
|
||||
year: "2-digit",
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
hour12: true,
|
||||
});
|
||||
});
|
||||
|
||||
const hideModal = () => {
|
||||
modal.value?.hide();
|
||||
};
|
||||
function show(backup: Backup) {
|
||||
currentBackup.value = backup;
|
||||
modal.value?.show();
|
||||
}
|
||||
|
||||
const restoreBackup = async () => {
|
||||
if (!props.backupId) {
|
||||
emit("backupRestored", { success: false, message: "No backup selected" });
|
||||
if (!currentBackup.value) {
|
||||
addNotification({
|
||||
type: "error",
|
||||
title: "Failed to restore backup",
|
||||
text: "Current backup is null",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
isRestoring.value = true;
|
||||
try {
|
||||
await props.server.backups?.restore(props.backupId);
|
||||
hideModal();
|
||||
emit("backupRestored", { success: true, message: "Backup restored successfully" });
|
||||
await props.server.backups?.restore(currentBackup.value.id);
|
||||
} catch (error) {
|
||||
backupError.value = error instanceof Error ? error.message : String(error);
|
||||
emit("backupRestored", { success: false, message: backupError.value });
|
||||
} finally {
|
||||
isRestoring.value = false;
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
addNotification({ type: "error", title: "Failed to restore backup", text: message });
|
||||
}
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
show: () => modal.value?.show(),
|
||||
hide: hideModal,
|
||||
show,
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -42,6 +42,9 @@
|
||||
:column="true"
|
||||
class="mb-6 flex flex-col gap-2"
|
||||
/>
|
||||
<div v-if="flags.advancedDebugInfo" class="markdown-body">
|
||||
<pre>{{ serverData }}</pre>
|
||||
</div>
|
||||
<ButtonStyled type="standard" color="brand" @click="closeDetailsModal">
|
||||
<button class="w-full">Close</button>
|
||||
</ButtonStyled>
|
||||
|
||||
@@ -67,37 +67,27 @@
|
||||
Removes all data on your server, including your worlds, mods, and configuration files,
|
||||
then reinstalls it with the selected version.
|
||||
</div>
|
||||
<div class="font-bold">This does not affect your backups, which are stored off-site.</div>
|
||||
</div>
|
||||
|
||||
<div class="flex w-full flex-col gap-2 rounded-2xl bg-table-alternateRow p-4">
|
||||
<div class="flex w-full flex-row items-center justify-between">
|
||||
<label class="w-full text-lg font-bold text-contrast" for="backup-server-mrpack">
|
||||
Backup server
|
||||
</label>
|
||||
<input
|
||||
id="backup-server-mrpack"
|
||||
v-model="backupServer"
|
||||
class="switch stylized-toggle shrink-0"
|
||||
type="checkbox"
|
||||
/>
|
||||
</div>
|
||||
<div>Creates a backup of your server before proceeding.</div>
|
||||
</div>
|
||||
<BackupWarning :backup-link="`/servers/manage/${props.server?.serverId}/backups`" />
|
||||
</div>
|
||||
<div class="mt-4 flex justify-start gap-4">
|
||||
<ButtonStyled :color="isDangerous ? 'red' : 'brand'">
|
||||
<button :disabled="canInstall" @click="handleReinstall">
|
||||
<button
|
||||
v-tooltip="backupInProgress ? formatMessage(backupInProgress.tooltip) : undefined"
|
||||
:disabled="canInstall || backupInProgress"
|
||||
@click="handleReinstall"
|
||||
>
|
||||
<RightArrowIcon />
|
||||
{{
|
||||
isBackingUp
|
||||
? "Backing up..."
|
||||
: isMrpackModalSecondPhase
|
||||
? "Erase and install"
|
||||
: loadingServerCheck
|
||||
? "Loading..."
|
||||
: isDangerous
|
||||
? "Erase and install"
|
||||
: "Install"
|
||||
isMrpackModalSecondPhase
|
||||
? "Erase and install"
|
||||
: loadingServerCheck
|
||||
? "Loading..."
|
||||
: isDangerous
|
||||
? "Erase and install"
|
||||
: "Install"
|
||||
}}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
@@ -124,12 +114,14 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ButtonStyled, NewModal } from "@modrinth/ui";
|
||||
import { BackupWarning, ButtonStyled, NewModal } from "@modrinth/ui";
|
||||
import { UploadIcon, RightArrowIcon, XIcon, ServerIcon } from "@modrinth/assets";
|
||||
import type { Server } from "~/composables/pyroServers";
|
||||
import type { BackupInProgressReason } from "~/pages/servers/manage/[id].vue";
|
||||
|
||||
const props = defineProps<{
|
||||
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
|
||||
backupInProgress?: BackupInProgressReason;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -139,9 +131,7 @@ const emit = defineEmits<{
|
||||
const mrpackModal = ref();
|
||||
const isMrpackModalSecondPhase = ref(false);
|
||||
const hardReset = ref(false);
|
||||
const backupServer = ref(false);
|
||||
const isLoading = ref(false);
|
||||
const isBackingUp = ref(false);
|
||||
const loadingServerCheck = ref(false);
|
||||
const mrpackFile = ref<File | null>(null);
|
||||
|
||||
@@ -156,64 +146,12 @@ const uploadMrpack = (event: Event) => {
|
||||
mrpackFile.value = target.files[0];
|
||||
};
|
||||
|
||||
const performBackup = async (): Promise<boolean> => {
|
||||
try {
|
||||
const date = new Date();
|
||||
const format = date.toLocaleString(navigator.language || "en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
second: "numeric",
|
||||
timeZoneName: "short",
|
||||
});
|
||||
const backupName = `Reinstallation - ${format}`;
|
||||
isLoading.value = true;
|
||||
const backupId = await props.server.backups?.create(backupName);
|
||||
isBackingUp.value = true;
|
||||
let attempts = 0;
|
||||
while (true) {
|
||||
attempts++;
|
||||
if (attempts > 100) {
|
||||
addNotification({
|
||||
group: "server",
|
||||
title: "Backup Failed",
|
||||
text: "An unexpected error occurred while backing up. Please try again later.",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
await props.server.refresh(["backups"]);
|
||||
const backups = await props.server.backups?.data;
|
||||
const backup = backupId ? backups?.find((x) => x.id === backupId) : undefined;
|
||||
if (backup && !backup.ongoing) {
|
||||
isBackingUp.value = false;
|
||||
break;
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 5000));
|
||||
}
|
||||
return true;
|
||||
} catch {
|
||||
addNotification({
|
||||
group: "server",
|
||||
title: "Backup Failed",
|
||||
text: "An unexpected error occurred while backing up. Please try again later.",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleReinstall = async () => {
|
||||
if (hardReset.value && !backupServer.value && !isMrpackModalSecondPhase.value) {
|
||||
if (hardReset.value && !isMrpackModalSecondPhase.value) {
|
||||
isMrpackModalSecondPhase.value = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (backupServer.value && !(await performBackup())) {
|
||||
isLoading.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
isLoading.value = true;
|
||||
|
||||
try {
|
||||
@@ -259,7 +197,6 @@ const handleReinstall = async () => {
|
||||
|
||||
const onShow = () => {
|
||||
hardReset.value = false;
|
||||
backupServer.value = false;
|
||||
isMrpackModalSecondPhase.value = false;
|
||||
loadingServerCheck.value = false;
|
||||
isLoading.value = false;
|
||||
|
||||
@@ -20,9 +20,7 @@
|
||||
}"
|
||||
>
|
||||
{{
|
||||
backupServer
|
||||
? "A backup will be created before proceeding with the reinstallation, then all data will be erased from your server. Are you sure you want to continue?"
|
||||
: "This will reinstall your server and erase all data. Are you sure you want to continue?"
|
||||
"This will reinstall your server and erase all data. Are you sure you want to continue?"
|
||||
}}
|
||||
</p>
|
||||
<div v-if="!isSecondPhase" class="flex flex-col gap-4">
|
||||
@@ -131,41 +129,28 @@
|
||||
Removes all data on your server, including your worlds, mods, and configuration files,
|
||||
then reinstalls it with the selected version.
|
||||
</div>
|
||||
<div class="font-bold">This does not affect your backups, which are stored off-site.</div>
|
||||
</div>
|
||||
|
||||
<div class="flex w-full flex-col gap-2 rounded-2xl bg-table-alternateRow p-4">
|
||||
<div class="flex w-full flex-row items-center justify-between">
|
||||
<label class="w-full text-lg font-bold text-contrast" for="backup-server">
|
||||
Backup server
|
||||
</label>
|
||||
<input
|
||||
id="backup-server"
|
||||
v-model="backupServer"
|
||||
class="switch stylized-toggle shrink-0"
|
||||
type="checkbox"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
Creates a backup of your server before proceeding with the installation or
|
||||
reinstallation.
|
||||
</div>
|
||||
</div>
|
||||
<BackupWarning :backup-link="`/servers/manage/${props.server?.serverId}/backups`" />
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex justify-start gap-4">
|
||||
<ButtonStyled :color="isDangerous ? 'red' : 'brand'">
|
||||
<button :disabled="canInstall" @click="handleReinstall">
|
||||
<button
|
||||
v-tooltip="backupInProgress ? formatMessage(backupInProgress.tooltip) : undefined"
|
||||
:disabled="canInstall || !!backupInProgress"
|
||||
@click="handleReinstall"
|
||||
>
|
||||
<RightArrowIcon />
|
||||
{{
|
||||
isBackingUp
|
||||
? "Backing up..."
|
||||
: isLoading
|
||||
? "Installing..."
|
||||
: isSecondPhase
|
||||
? "Erase and install"
|
||||
: hardReset
|
||||
? "Continue"
|
||||
: "Install"
|
||||
isLoading
|
||||
? "Installing..."
|
||||
: isSecondPhase
|
||||
? "Erase and install"
|
||||
: hardReset
|
||||
? "Continue"
|
||||
: "Install"
|
||||
}}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
@@ -192,10 +177,13 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ButtonStyled, NewModal } from "@modrinth/ui";
|
||||
import { BackupWarning, ButtonStyled, NewModal } from "@modrinth/ui";
|
||||
import { RightArrowIcon, XIcon, ServerIcon, DropdownIcon } from "@modrinth/assets";
|
||||
import type { Server } from "~/composables/pyroServers";
|
||||
import type { Loaders } from "~/types/servers";
|
||||
import type { BackupInProgressReason } from "~/pages/servers/manage/[id].vue";
|
||||
|
||||
const { formatMessage } = useVIntl();
|
||||
|
||||
interface LoaderVersion {
|
||||
id: string;
|
||||
@@ -213,6 +201,7 @@ type VersionCache = Record<string, any>;
|
||||
const props = defineProps<{
|
||||
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
|
||||
currentLoader: Loaders | undefined;
|
||||
backupInProgress?: BackupInProgressReason;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -222,9 +211,7 @@ const emit = defineEmits<{
|
||||
const versionSelectModal = ref();
|
||||
const isSecondPhase = ref(false);
|
||||
const hardReset = ref(false);
|
||||
const backupServer = ref(false);
|
||||
const isLoading = ref(false);
|
||||
const isBackingUp = ref(false);
|
||||
const loadingServerCheck = ref(false);
|
||||
const serverCheckError = ref("");
|
||||
|
||||
@@ -413,69 +400,12 @@ const canInstall = computed(() => {
|
||||
return conds || !selectedLoaderVersion.value;
|
||||
});
|
||||
|
||||
const performBackup = async (): Promise<boolean> => {
|
||||
try {
|
||||
const date = new Date();
|
||||
const format = date.toLocaleString(navigator.language || "en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
second: "numeric",
|
||||
timeZoneName: "short",
|
||||
});
|
||||
const backupName = `Reinstallation - ${format}`;
|
||||
isLoading.value = true;
|
||||
const backupId = await props.server.backups?.create(backupName);
|
||||
isBackingUp.value = true;
|
||||
let attempts = 0;
|
||||
while (true) {
|
||||
attempts++;
|
||||
if (attempts > 100) {
|
||||
addNotification({
|
||||
group: "server",
|
||||
title: "Backup Failed",
|
||||
text: "An unexpected error occurred while backing up. Please try again later.",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
await props.server.refresh(["backups"]);
|
||||
const backups = await props.server.backups?.data;
|
||||
const backup = backupId ? backups?.find((x) => x.id === backupId) : undefined;
|
||||
if (backup && !backup.ongoing) {
|
||||
isBackingUp.value = false;
|
||||
break;
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 5000));
|
||||
}
|
||||
return true;
|
||||
} catch {
|
||||
addNotification({
|
||||
group: "server",
|
||||
title: "Backup Failed",
|
||||
text: "An unexpected error occurred while backing up. Please try again later.",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleReinstall = async () => {
|
||||
if (hardReset.value && !isSecondPhase.value) {
|
||||
isSecondPhase.value = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (backupServer.value) {
|
||||
isBackingUp.value = true;
|
||||
if (!(await performBackup())) {
|
||||
isBackingUp.value = false;
|
||||
isLoading.value = false;
|
||||
return;
|
||||
}
|
||||
isBackingUp.value = false;
|
||||
}
|
||||
|
||||
isLoading.value = true;
|
||||
|
||||
try {
|
||||
@@ -522,7 +452,6 @@ const onShow = () => {
|
||||
|
||||
const onHide = () => {
|
||||
hardReset.value = false;
|
||||
backupServer.value = false;
|
||||
isSecondPhase.value = false;
|
||||
serverCheckError.value = "";
|
||||
loadingServerCheck.value = false;
|
||||
|
||||
@@ -21,7 +21,12 @@
|
||||
</div>
|
||||
|
||||
<div class="h-full w-full">
|
||||
<NuxtPage :route="props.route" :server="props.server" @reinstall="onReinstall" />
|
||||
<NuxtPage
|
||||
:route="route"
|
||||
:server="server"
|
||||
:backup-in-progress="backupInProgress"
|
||||
@reinstall="onReinstall"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -30,13 +35,15 @@
|
||||
import { RightArrowIcon } from "@modrinth/assets";
|
||||
import type { RouteLocationNormalized } from "vue-router";
|
||||
import type { Server } from "~/composables/pyroServers";
|
||||
import type { BackupInProgressReason } from "~/pages/servers/manage/[id].vue";
|
||||
|
||||
const emit = defineEmits(["reinstall"]);
|
||||
|
||||
const props = defineProps<{
|
||||
defineProps<{
|
||||
navLinks: { label: string; href: string; icon: Component; external?: boolean }[];
|
||||
route: RouteLocationNormalized;
|
||||
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
|
||||
backupInProgress?: BackupInProgressReason;
|
||||
}>();
|
||||
|
||||
const onReinstall = (...args: any[]) => {
|
||||
|
||||
Reference in New Issue
Block a user