Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 40 additions & 23 deletions apps/frontend/src/components/ui/servers/BackupCreateModal.vue
Original file line number Diff line number Diff line change
@@ -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>
Expand All @@ -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(() => {
Expand All @@ -67,38 +76,46 @@ 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;
}
};

defineExpose({
show: () => modal.value?.show(),
show,
hide: hideModal,
});
</script>
101 changes: 30 additions & 71 deletions apps/frontend/src/components/ui/servers/BackupDeleteModal.vue
Original file line number Diff line number Diff line change
@@ -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 modal = ref<InstanceType<typeof NewModal>>();
const isDeleting = ref(false);
const backupError = ref<string | 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();
};
const emit = defineEmits<{
(e: "delete", backup: Backup | undefined): void;
}>();

const deleteBackup = async () => {
if (!props.backupId) {
emit("backupDeleted", { success: false, message: "No backup selected" });
return;
}
const modal = ref<InstanceType<typeof ConfirmModal>>();
const currentBackup = ref<Backup | undefined>(undefined);

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>
Loading
Loading