pocketbase/ui/src/components/settings/BackupsList.svelte

225 lines
7.0 KiB
Svelte

<script>
import { onMount } from "svelte";
import { slide } from "svelte/transition";
import ApiClient from "@/utils/ApiClient";
import CommonHelper from "@/utils/CommonHelper";
import tooltip from "@/actions/tooltip";
import { confirm } from "@/stores/confirmation";
import { addSuccessToast } from "@/stores/toasts";
import BackupCreatePanel from "@/components/settings/BackupCreatePanel.svelte";
import BackupRestorePanel from "@/components/settings/BackupRestorePanel.svelte";
let createPanel;
let restorePanel;
let backups = [];
let isLoading = false;
let isDownloading = {};
let isDeleting = {};
let canBackup = true;
loadBackups();
loadCanBackup();
export async function loadBackups() {
isLoading = true;
try {
backups = await ApiClient.backups.getFullList();
// sort backups DESC by their modified date
backups.sort((a, b) => {
if (a.modified < b.modified) {
return 1;
}
if (a.modified > b.modified) {
return -1;
}
return 0;
});
isLoading = false;
} catch (err) {
if (!err.isAbort) {
ApiClient.error(err);
isLoading = false;
}
}
}
async function download(name) {
if (isDownloading[name]) {
return;
}
isDownloading[name] = true;
try {
const token = await ApiClient.getAdminFileToken();
const url = ApiClient.backups.getDownloadUrl(token, name);
CommonHelper.download(url);
} catch (err) {
if (!err.isAbort) {
ApiClient.error(err);
}
}
delete isDownloading[name];
isDownloading = isDownloading;
}
function deleteConfirm(name) {
confirm(`Do you really want to delete ${name}?`, () => deleteBackup(name));
}
async function deleteBackup(name) {
if (isDeleting[name]) {
return;
}
isDeleting[name] = true;
try {
await ApiClient.backups.delete(name);
CommonHelper.removeByKey(backups, "name", name);
loadBackups();
addSuccessToast(`Successfully deleted ${name}.`);
} catch (err) {
if (!err.isAbort) {
ApiClient.error(err);
}
}
delete isDeleting[name];
isDeleting = isDeleting;
}
async function loadCanBackup() {
try {
const health = await ApiClient.health.check({ $autoCancel: false });
const oldCanBackup = canBackup;
canBackup = health?.data?.canBackup || false;
// reload backups list
if (oldCanBackup != canBackup && canBackup) {
loadBackups();
}
} catch (_) {}
}
onMount(() => {
let canBackupIntervalId = setInterval(() => {
loadCanBackup();
}, 3000);
return () => {
clearInterval(canBackupIntervalId);
};
});
</script>
<div class="list list-compact">
<div class="list-content">
{#if isLoading}
{#each Array(backups.length || 1) as i}
<div class="list-item list-item-loader">
<span class="skeleton-loader" />
</div>
{/each}
{:else}
{#each backups as backup (backup.key)}
<div class="list-item" transition:slide|local={{ duration: 150 }}>
<i class="ri-folder-zip-line" />
<div class="content">
<span class="name backup-name">{backup.key}</span>
<span class="size txt-hint txt-nowrap">
({CommonHelper.formattedFileSize(backup.size)})
</span>
</div>
<div class="actions nonintrusive">
<button
type="button"
class="btn btn-sm btn-circle btn-hint btn-transparent"
class:btn-loading={isDownloading[backup.key]}
disabled={isDeleting[backup.key] || isDownloading[backup.key]}
aria-label="Download"
use:tooltip={"Download"}
on:click|preventDefault={() => download(backup.key)}
>
<i class="ri-download-line" />
</button>
<button
type="button"
class="btn btn-sm btn-circle btn-hint btn-transparent"
disabled={isDeleting[backup.key]}
aria-label="Restore"
use:tooltip={"Restore"}
on:click|preventDefault={() => restorePanel.show(backup.key)}
>
<i class="ri-restart-line" />
</button>
<button
type="button"
class="btn btn-sm btn-circle btn-hint btn-transparent"
class:btn-loading={isDeleting[backup.key]}
disabled={isDeleting[backup.key]}
aria-label="Delete"
use:tooltip={"Delete"}
on:click|preventDefault={() => deleteConfirm(backup.key)}
>
<i class="ri-delete-bin-7-line" />
</button>
</div>
</div>
{:else}
<div class="list-item list-item-placeholder">
<span class="txt">No backups yet.</span>
</div>
{/each}
{/if}
</div>
<div class="list-item list-item-btn">
<button
type="button"
class="btn btn-block btn-transparent"
disabled={isLoading || !canBackup}
on:click={() => createPanel?.show()}
>
{#if canBackup}
<i class="ri-play-circle-line" />
<span class="txt">Initialize new backup</span>
{:else}
<span class="loader loader-sm" />
<span class="txt">Backup/restore operation is in process</span>
{/if}
</button>
</div>
</div>
<BackupCreatePanel
bind:this={createPanel}
on:submit={() => {
loadBackups();
}}
/>
<BackupRestorePanel bind:this={restorePanel} />
<style lang="scss">
.list-content {
overflow: auto;
max-height: 342px;
.list-item {
min-height: 49px;
}
}
.backup-name {
max-width: 300px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
</style>