mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
add: multiSelectTable to database backups.
This commit is contained in:
@@ -23,6 +23,7 @@
|
||||
allowSelection = true,
|
||||
confirmDeletion = true,
|
||||
showSuccessNotification = true,
|
||||
computeKey = 'multiSelectionTable',
|
||||
header,
|
||||
children,
|
||||
onDelete,
|
||||
@@ -33,6 +34,7 @@
|
||||
resource: string;
|
||||
allowSelection?: boolean;
|
||||
confirmDeletion?: boolean;
|
||||
computeKey?: string | number;
|
||||
showSuccessNotification?: boolean;
|
||||
columns: Array<TableColumn> | number;
|
||||
header: Snippet<[root: TableRootProps]>;
|
||||
@@ -72,90 +74,92 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<Table.Root let:root {columns} {allowSelection} bind:selectedRows>
|
||||
<svelte:fragment slot="header" let:root>
|
||||
{@render header?.(root)}
|
||||
</svelte:fragment>
|
||||
|
||||
{@render children(root)}
|
||||
</Table.Root>
|
||||
|
||||
{#if allowSelection && selectedRows.length > 0}
|
||||
<FloatingActionBar>
|
||||
<svelte:fragment slot="start">
|
||||
<Badge content={selectedRows.length.toString()} />
|
||||
<span>
|
||||
{selectedRows.length > 1 ? getPluralResource() : resource}
|
||||
selected
|
||||
</span>
|
||||
{#key computeKey}
|
||||
<Table.Root let:root {columns} {allowSelection} bind:selectedRows>
|
||||
<svelte:fragment slot="header" let:root>
|
||||
{@render header?.(root)}
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="end">
|
||||
<Button
|
||||
text
|
||||
on:click={() => {
|
||||
onCancel?.();
|
||||
selectedRows = [];
|
||||
}}>Cancel</Button>
|
||||
<Button
|
||||
secondary
|
||||
on:click={async () => {
|
||||
if (confirmDeletion) {
|
||||
showConfirmDeletion = true;
|
||||
} else {
|
||||
const state = await onDelete?.(selectedRows);
|
||||
if (typeof state === 'string') {
|
||||
// user should handle error on their own!
|
||||
|
||||
{@render children(root)}
|
||||
</Table.Root>
|
||||
|
||||
{#if allowSelection && selectedRows.length > 0}
|
||||
<FloatingActionBar>
|
||||
<svelte:fragment slot="start">
|
||||
<Badge content={selectedRows.length.toString()} />
|
||||
<span>
|
||||
{selectedRows.length > 1 ? getPluralResource() : resource}
|
||||
selected
|
||||
</span>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="end">
|
||||
<Button
|
||||
text
|
||||
on:click={() => {
|
||||
onCancel?.();
|
||||
selectedRows = [];
|
||||
}}>Cancel</Button>
|
||||
<Button
|
||||
secondary
|
||||
on:click={async () => {
|
||||
if (confirmDeletion) {
|
||||
showConfirmDeletion = true;
|
||||
} else {
|
||||
notifySuccess();
|
||||
selectedRows = [];
|
||||
const state = await onDelete?.(selectedRows);
|
||||
if (typeof state === 'string') {
|
||||
// user should handle error on their own!
|
||||
} else {
|
||||
notifySuccess();
|
||||
selectedRows = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
}}>Delete</Button>
|
||||
</svelte:fragment>
|
||||
</FloatingActionBar>
|
||||
{/if}
|
||||
}}>Delete</Button>
|
||||
</svelte:fragment>
|
||||
</FloatingActionBar>
|
||||
{/if}
|
||||
|
||||
{#if allowSelection}
|
||||
<Confirm
|
||||
submissionLoader
|
||||
confirmDeletion
|
||||
error={onDeleteError}
|
||||
disabled={disableModal}
|
||||
title="Delete {resource}s"
|
||||
bind:open={showConfirmDeletion}
|
||||
onSubmit={async () => {
|
||||
disableModal = true;
|
||||
onDeleteError = null;
|
||||
{#if allowSelection}
|
||||
<Confirm
|
||||
submissionLoader
|
||||
confirmDeletion
|
||||
error={onDeleteError}
|
||||
disabled={disableModal}
|
||||
title="Delete {resource}s"
|
||||
bind:open={showConfirmDeletion}
|
||||
onSubmit={async () => {
|
||||
disableModal = true;
|
||||
onDeleteError = null;
|
||||
|
||||
const state = await onDelete?.(selectedRows);
|
||||
if (typeof state === 'string') {
|
||||
disableModal = false;
|
||||
onDeleteError = state || `Failed to delete ${resource}s`;
|
||||
} else {
|
||||
notifySuccess();
|
||||
selectedRows = [];
|
||||
disableModal = false;
|
||||
showConfirmDeletion = false;
|
||||
}
|
||||
}}>
|
||||
<Typography.Text>
|
||||
{@const selectionCount = selectedRows.length}
|
||||
{#if deleteContent}
|
||||
<!-- some show extra info -->
|
||||
{@render deleteContent(selectionCount)}
|
||||
{:else}
|
||||
Are you sure you want to delete <strong>{selectionCount}</strong>
|
||||
{selectionCount > 1 ? getPluralResource() : resource}?
|
||||
{/if}
|
||||
</Typography.Text>
|
||||
const state = await onDelete?.(selectedRows);
|
||||
if (typeof state === 'string') {
|
||||
disableModal = false;
|
||||
onDeleteError = state || `Failed to delete ${resource}s`;
|
||||
} else {
|
||||
notifySuccess();
|
||||
selectedRows = [];
|
||||
disableModal = false;
|
||||
showConfirmDeletion = false;
|
||||
}
|
||||
}}>
|
||||
<Typography.Text>
|
||||
{@const selectionCount = selectedRows.length}
|
||||
{#if deleteContent}
|
||||
<!-- some show extra info -->
|
||||
{@render deleteContent(selectionCount)}
|
||||
{:else}
|
||||
Are you sure you want to delete <strong>{selectionCount}</strong>
|
||||
{selectionCount > 1 ? getPluralResource() : resource}?
|
||||
{/if}
|
||||
</Typography.Text>
|
||||
|
||||
<Typography.Text variant="m-500">
|
||||
{#if deleteContentNotice}
|
||||
<!-- some show extra info -->
|
||||
{@render deleteContentNotice()}
|
||||
{:else}
|
||||
This action is irreversible.
|
||||
{/if}
|
||||
</Typography.Text>
|
||||
</Confirm>
|
||||
{/if}
|
||||
<Typography.Text variant="m-500">
|
||||
{#if deleteContentNotice}
|
||||
<!-- some show extra info -->
|
||||
{@render deleteContentNotice()}
|
||||
{:else}
|
||||
This action is irreversible.
|
||||
{/if}
|
||||
</Typography.Text>
|
||||
</Confirm>
|
||||
{/if}
|
||||
{/key}
|
||||
|
||||
+3
-4
@@ -69,11 +69,11 @@
|
||||
await sdk
|
||||
.forProject(page.params.region, page.params.project)
|
||||
.backups.createArchive(['databases'], data.database.$id);
|
||||
await invalidate(Dependencies.BACKUPS);
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: 'Database backup has started'
|
||||
});
|
||||
invalidate(Dependencies.BACKUPS);
|
||||
trackEvent('click_manual_submit');
|
||||
showFeedbackNotification();
|
||||
} catch (error) {
|
||||
@@ -86,7 +86,7 @@
|
||||
}
|
||||
};
|
||||
|
||||
const trackEvents = (policies) => {
|
||||
const trackEvents = (policies: UserBackupPolicy[]) => {
|
||||
policies.forEach((policy) => {
|
||||
let actualDay = null;
|
||||
const monthlyBackupFrequency = policy.monthlyBackupFrequency;
|
||||
@@ -139,7 +139,6 @@
|
||||
? `Backup policies have been created`
|
||||
: `<b>${totalPolicies[0].label}</b> policy has been created`;
|
||||
|
||||
// TODO: html isn't yet supported on Toast.
|
||||
addNotification({
|
||||
isHtml: true,
|
||||
type: 'success',
|
||||
@@ -148,7 +147,7 @@
|
||||
|
||||
trackEvents(totalPolicies);
|
||||
|
||||
invalidate(Dependencies.BACKUPS);
|
||||
await invalidate(Dependencies.BACKUPS);
|
||||
showFeedbackNotification();
|
||||
} catch (err) {
|
||||
addNotification({
|
||||
|
||||
+211
-208
@@ -1,5 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { Card, Modal } from '$lib/components';
|
||||
import {
|
||||
Card,
|
||||
Confirm,
|
||||
type DeleteOperationState,
|
||||
Modal,
|
||||
MultiSelectionTable
|
||||
} from '$lib/components';
|
||||
import { Button, InputCheckbox, InputText } from '$lib/elements/forms';
|
||||
import RestoreModal from './restoreModal.svelte';
|
||||
import type { PageData } from './$types';
|
||||
@@ -11,17 +17,14 @@
|
||||
import { ID } from '@appwrite.io/console';
|
||||
import { columns } from './store';
|
||||
import { database } from '../store';
|
||||
import type { BackupArchive } from '$lib/sdk/backups';
|
||||
import type { BackupArchive, BackupPolicy } from '$lib/sdk/backups';
|
||||
import { Click, trackEvent } from '$lib/actions/analytics';
|
||||
import { copy } from '$lib/helpers/copy';
|
||||
import { LabelCard } from '$lib/components/index.js';
|
||||
import DualTimeView from '$lib/components/dualTimeView.svelte';
|
||||
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import {
|
||||
ActionMenu,
|
||||
Badge,
|
||||
FloatingActionBar,
|
||||
Icon,
|
||||
Layout,
|
||||
Popover,
|
||||
@@ -40,24 +43,26 @@
|
||||
} from '@appwrite.io/pink-icons-svelte';
|
||||
import { capitalize } from '$lib/helpers/string';
|
||||
import Ellipse from './components/Ellipse.svelte';
|
||||
import Confirm from '$lib/components/confirm.svelte';
|
||||
import { page } from '$app/state';
|
||||
|
||||
export let data: PageData;
|
||||
const {
|
||||
data
|
||||
}: {
|
||||
data: PageData;
|
||||
} = $props();
|
||||
|
||||
let showDelete = false;
|
||||
let selectedBackup: BackupArchive = null;
|
||||
let showDelete = $state(false);
|
||||
let selectedBackup: BackupArchive | null = $state(null);
|
||||
|
||||
let showDropdown = [];
|
||||
let selectedBackups: string[] = [];
|
||||
|
||||
let showRestore = false;
|
||||
let showCustomId = false;
|
||||
let newDatabaseInfo: { name: string; id: string } = { name: null, id: null };
|
||||
let showRestore = $state(false);
|
||||
let showCustomId = $state(false);
|
||||
let newDatabaseInfo: { name: string; id: string } = $state({ name: null, id: null });
|
||||
|
||||
let confirmSameDbRestore = false;
|
||||
let selectedRestoreOption = 'new';
|
||||
let restoreOptions = [
|
||||
let confirmSameDbRestore = $state(false);
|
||||
let selectedRestoreOption = $state('new');
|
||||
const restoreOptions = [
|
||||
{
|
||||
id: 'new',
|
||||
title: 'Restore in new database',
|
||||
@@ -70,89 +75,21 @@
|
||||
}
|
||||
];
|
||||
|
||||
const deleteBackups = async () => {
|
||||
if (!selectedBackups.length && selectedBackup) {
|
||||
selectedBackups.push(selectedBackup.$id);
|
||||
}
|
||||
|
||||
const message = `${selectedBackups.length} backup${selectedBackups.length > 1 ? 's have been' : ''} deleted`;
|
||||
|
||||
const promises = selectedBackups.map((archiveId) =>
|
||||
sdk.forProject(page.params.region, page.params.project).backups.deleteArchive(archiveId)
|
||||
const disableRestoreButton = $derived.by(() => {
|
||||
return (
|
||||
(selectedRestoreOption === 'new' &&
|
||||
(!newDatabaseInfo.name || $database.$id === newDatabaseInfo.id)) ||
|
||||
(selectedRestoreOption === 'same' && !confirmSameDbRestore)
|
||||
);
|
||||
});
|
||||
|
||||
try {
|
||||
await Promise.all(promises);
|
||||
addNotification({
|
||||
message,
|
||||
type: 'success'
|
||||
});
|
||||
invalidate(Dependencies.BACKUPS);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
} finally {
|
||||
showDelete = false;
|
||||
selectedBackup = null;
|
||||
selectedBackups = [];
|
||||
}
|
||||
};
|
||||
|
||||
const restoreBackup = async () => {
|
||||
if (selectedRestoreOption === 'same') {
|
||||
newDatabaseInfo.id = $database.$id;
|
||||
newDatabaseInfo.name = $database.name;
|
||||
}
|
||||
|
||||
try {
|
||||
await sdk
|
||||
.forProject(page.params.region, page.params.project)
|
||||
.backups.createRestoration(
|
||||
selectedBackup.$id,
|
||||
['databases'],
|
||||
newDatabaseInfo.id ?? ID.unique(),
|
||||
newDatabaseInfo.name
|
||||
);
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: 'Database restore initiated'
|
||||
});
|
||||
|
||||
invalidate(Dependencies.BACKUPS);
|
||||
trackEvent('backup_restore_submit', {
|
||||
newDatabaseName: newDatabaseInfo.name
|
||||
});
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
} finally {
|
||||
showRestore = false;
|
||||
}
|
||||
};
|
||||
|
||||
const policyDetails = (policyId: string | null) =>
|
||||
data.policies.policies.find((policy) => policy.$id === policyId);
|
||||
|
||||
const cleanBackupName = (backup: BackupArchive) =>
|
||||
toLocaleDateTime(backup.$createdAt).replaceAll(',', '');
|
||||
|
||||
$: if (!showRestore && !showDelete) {
|
||||
showCustomId = false;
|
||||
selectedBackup = null;
|
||||
|
||||
confirmSameDbRestore = false;
|
||||
selectedRestoreOption = 'new';
|
||||
newDatabaseInfo = { name: null, id: null };
|
||||
function getPolicyDetails(policyId: string | null): BackupPolicy | null {
|
||||
return data.policies.policies.find((policy) => policy.$id === policyId);
|
||||
}
|
||||
|
||||
$: disableButton =
|
||||
(selectedRestoreOption === 'new' &&
|
||||
(!newDatabaseInfo.name || $database.$id === newDatabaseInfo.id)) ||
|
||||
(selectedRestoreOption === 'same' && !confirmSameDbRestore);
|
||||
function getCleanBackupName(backup: BackupArchive): string {
|
||||
return toLocaleDateTime(backup.$createdAt).replaceAll(',', '');
|
||||
}
|
||||
|
||||
function getBackupStatus(backup: BackupArchive) {
|
||||
switch (backup.status) {
|
||||
@@ -169,142 +106,208 @@
|
||||
return 'waiting';
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteBackups(selectedRows: string[]): Promise<DeleteOperationState> {
|
||||
const promises = selectedRows.map((archiveId) => {
|
||||
return sdk
|
||||
.forProject(page.params.region, page.params.project)
|
||||
.backups.deleteArchive(archiveId);
|
||||
});
|
||||
|
||||
try {
|
||||
await Promise.all(promises);
|
||||
await invalidate(Dependencies.BACKUPS);
|
||||
|
||||
if (selectedBackup) {
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: '1 backup deleted'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
if (selectedBackup) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
} else {
|
||||
return error.message;
|
||||
}
|
||||
} finally {
|
||||
if (selectedBackup) {
|
||||
showDelete = false;
|
||||
selectedBackup = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function restoreBackup() {
|
||||
if (selectedRestoreOption === 'same') {
|
||||
newDatabaseInfo.id = $database.$id;
|
||||
newDatabaseInfo.name = $database.name;
|
||||
}
|
||||
|
||||
try {
|
||||
await sdk
|
||||
.forProject(page.params.region, page.params.project)
|
||||
.backups.createRestoration(
|
||||
selectedBackup.$id,
|
||||
['databases'],
|
||||
newDatabaseInfo.id ?? ID.unique(),
|
||||
newDatabaseInfo.name
|
||||
);
|
||||
await invalidate(Dependencies.BACKUPS);
|
||||
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: 'Database restore initiated'
|
||||
});
|
||||
trackEvent('backup_restore_submit', { newDatabaseName: newDatabaseInfo.name });
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
} finally {
|
||||
showRestore = false;
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (!showRestore && !showDelete) {
|
||||
showCustomId = false;
|
||||
selectedBackup = null;
|
||||
|
||||
confirmSameDbRestore = false;
|
||||
selectedRestoreOption = 'new';
|
||||
newDatabaseInfo = { name: null, id: null };
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<Table.Root let:root allowSelection columns={$columns} bind:selectedRows={selectedBackups}>
|
||||
<svelte:fragment slot="header" let:root>
|
||||
<MultiSelectionTable
|
||||
resource="backup"
|
||||
columns={$columns}
|
||||
onDelete={deleteBackups}
|
||||
computeKey={data.backups.archives.length}>
|
||||
{#snippet header(root)}
|
||||
{#each $columns as column}
|
||||
<Table.Header.Cell column={column.id} {root}>{column.title}</Table.Header.Cell>
|
||||
{/each}
|
||||
</svelte:fragment>
|
||||
{/snippet}
|
||||
|
||||
{#each data.backups.archives as backup, index}
|
||||
{@const policy = policyDetails(backup.policyId)}
|
||||
{@const retainedUntil = new Date(
|
||||
new Date(policy?.$createdAt).getTime() + policy?.retention * 24 * 60 * 60 * 1000
|
||||
)}
|
||||
{@const formattedRetainedUntil = `${retainedUntil.getDate()} ${retainedUntil.toLocaleString('en-US', { month: 'short' })}, ${retainedUntil.getFullYear()} ${retainedUntil.toLocaleTimeString('en-US', { hour12: false })}`}
|
||||
<Table.Row.Base id={backup.$id} {root}>
|
||||
<Table.Cell column="backups" {root}>
|
||||
<DualTimeView time={backup.$createdAt}>
|
||||
{cleanBackupName(backup)}
|
||||
</DualTimeView>
|
||||
</Table.Cell>
|
||||
<Table.Cell column="size" {root}>
|
||||
{#if backup.status === 'completed'}
|
||||
{calculateSize(backup.size)}
|
||||
{:else}
|
||||
-
|
||||
{/if}
|
||||
</Table.Cell>
|
||||
<Table.Cell column="status" {root}>
|
||||
{@const backupStatus = getBackupStatus(backup)}
|
||||
<Status status={backupStatus} label={capitalize(backupStatus)} />
|
||||
<!--{#if backup.status === 'Failed'}-->
|
||||
<!-- <span class="u-underline">Get support</span>-->
|
||||
<!--{/if}-->
|
||||
</Table.Cell>
|
||||
<Table.Cell column="policy" {root}>
|
||||
<div class="u-flex u-cross-baseline">
|
||||
<Tooltip maxWidth="fit-content">
|
||||
<span>
|
||||
{policy?.name || 'Manual'}
|
||||
</span>
|
||||
<span slot="tooltip"
|
||||
>{policy
|
||||
? `Retained until: ${formattedRetainedUntil}`
|
||||
: `Retained forever`}</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Table.Cell>
|
||||
<Table.Cell column="actions" {root}>
|
||||
<div class="action-cell u-flex u-main-end u-width-full-line">
|
||||
<Popover let:toggle padding="m" placement="bottom-end">
|
||||
<Button extraCompact on:click={toggle}>
|
||||
<Icon icon={IconDotsHorizontal} />
|
||||
</Button>
|
||||
<svelte:fragment slot="tooltip" let:toggle>
|
||||
<ActionMenu.Root width="180px" noPadding>
|
||||
{#if backup.status === 'completed'}
|
||||
{#snippet children(root)}
|
||||
{#each data.backups.archives as backup, index}
|
||||
{@const policy = getPolicyDetails(backup.policyId)}
|
||||
{@const retainedUntil = new Date(
|
||||
new Date(policy?.$createdAt).getTime() + policy?.retention * 24 * 60 * 60 * 1000
|
||||
)}
|
||||
{@const formattedRetainedUntil = `${retainedUntil.getDate()} ${retainedUntil.toLocaleString('en-US', { month: 'short' })}, ${retainedUntil.getFullYear()} ${retainedUntil.toLocaleTimeString('en-US', { hour12: false })}`}
|
||||
<Table.Row.Base id={backup.$id} {root}>
|
||||
<Table.Cell column="backups" {root}>
|
||||
<DualTimeView time={backup.$createdAt}>
|
||||
{getCleanBackupName(backup)}
|
||||
</DualTimeView>
|
||||
</Table.Cell>
|
||||
<Table.Cell column="size" {root}>
|
||||
{#if backup.status === 'completed'}
|
||||
{calculateSize(backup.size)}
|
||||
{:else}
|
||||
-
|
||||
{/if}
|
||||
</Table.Cell>
|
||||
<Table.Cell column="status" {root}>
|
||||
{@const backupStatus = getBackupStatus(backup)}
|
||||
<Status status={backupStatus} label={capitalize(backupStatus)} />
|
||||
<!--{#if backup.status === 'Failed'}-->
|
||||
<!-- <span class="u-underline">Get support</span>-->
|
||||
<!--{/if}-->
|
||||
</Table.Cell>
|
||||
<Table.Cell column="policy" {root}>
|
||||
<div class="u-flex u-cross-baseline">
|
||||
<Tooltip maxWidth="fit-content">
|
||||
<span>
|
||||
{policy?.name || 'Manual'}
|
||||
</span>
|
||||
<span slot="tooltip"
|
||||
>{policy
|
||||
? `Retained until: ${formattedRetainedUntil}`
|
||||
: `Retained forever`}</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Table.Cell>
|
||||
<Table.Cell column="actions" {root}>
|
||||
<div class="action-cell u-flex u-main-end u-width-full-line">
|
||||
<Popover let:toggle padding="m" placement="bottom-end">
|
||||
<Button extraCompact on:click={toggle}>
|
||||
<Icon icon={IconDotsHorizontal} />
|
||||
</Button>
|
||||
<svelte:fragment slot="tooltip" let:toggle>
|
||||
<ActionMenu.Root width="180px" noPadding>
|
||||
{#if backup.status === 'completed'}
|
||||
<ActionMenu.Item.Button
|
||||
trailingIcon={IconRefresh}
|
||||
on:click={(e) => {
|
||||
toggle(e);
|
||||
showRestore = true;
|
||||
selectedBackup = backup;
|
||||
showDropdown[index] = false;
|
||||
trackEvent(Click.BackupRestoreClick);
|
||||
}}>
|
||||
Restore
|
||||
</ActionMenu.Item.Button>
|
||||
{/if}
|
||||
<ActionMenu.Item.Button
|
||||
trailingIcon={IconRefresh}
|
||||
trailingIcon={IconDuplicate}
|
||||
on:click={(e) => {
|
||||
toggle(e);
|
||||
showRestore = true;
|
||||
copy(backup.$id);
|
||||
showDropdown[index] = false;
|
||||
trackEvent(Click.BackupCopyIdClick);
|
||||
}}>
|
||||
Copy ID
|
||||
</ActionMenu.Item.Button>
|
||||
<ActionMenu.Item.Button
|
||||
status="danger"
|
||||
trailingIcon={IconTrash}
|
||||
on:click={(e) => {
|
||||
toggle(e);
|
||||
showDelete = true;
|
||||
selectedBackup = backup;
|
||||
showDropdown[index] = false;
|
||||
trackEvent(Click.BackupRestoreClick);
|
||||
trackEvent(Click.BackupDeleteClick);
|
||||
}}>
|
||||
Restore
|
||||
Delete
|
||||
</ActionMenu.Item.Button>
|
||||
{/if}
|
||||
<ActionMenu.Item.Button
|
||||
trailingIcon={IconDuplicate}
|
||||
on:click={(e) => {
|
||||
toggle(e);
|
||||
copy(backup.$id);
|
||||
showDropdown[index] = false;
|
||||
trackEvent(Click.BackupCopyIdClick);
|
||||
}}>
|
||||
Copy ID
|
||||
</ActionMenu.Item.Button>
|
||||
<ActionMenu.Item.Button
|
||||
status="danger"
|
||||
trailingIcon={IconTrash}
|
||||
on:click={(e) => {
|
||||
toggle(e);
|
||||
showDelete = true;
|
||||
selectedBackup = backup;
|
||||
showDropdown[index] = false;
|
||||
trackEvent(Click.BackupDeleteClick);
|
||||
}}>
|
||||
Delete
|
||||
</ActionMenu.Item.Button>
|
||||
</ActionMenu.Root>
|
||||
</svelte:fragment>
|
||||
</Popover>
|
||||
</div>
|
||||
</Table.Cell>
|
||||
</Table.Row.Base>
|
||||
{/each}
|
||||
</Table.Root>
|
||||
|
||||
{#if selectedBackups.length > 0}
|
||||
<FloatingActionBar>
|
||||
<svelte:fragment slot="start">
|
||||
<Badge content={selectedBackups.length.toString()} />
|
||||
<span>
|
||||
{selectedBackups.length > 1 ? 'backups' : 'backup'}
|
||||
selected
|
||||
</span>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="end">
|
||||
<Button text on:click={() => (selectedBackups = [])}>Cancel</Button>
|
||||
<Button secondary on:click={() => (showDelete = true)}>Delete</Button>
|
||||
</svelte:fragment>
|
||||
</FloatingActionBar>
|
||||
{/if}
|
||||
</ActionMenu.Root>
|
||||
</svelte:fragment>
|
||||
</Popover>
|
||||
</div>
|
||||
</Table.Cell>
|
||||
</Table.Row.Base>
|
||||
{/each}
|
||||
{/snippet}
|
||||
</MultiSelectionTable>
|
||||
|
||||
<!-- this is for single backup delete -->
|
||||
<Confirm
|
||||
title="Delete {selectedBackups.length ? 'backups' : 'backup'}"
|
||||
title="Delete backup"
|
||||
bind:open={showDelete}
|
||||
onSubmit={deleteBackups}>
|
||||
onSubmit={async () => {
|
||||
await deleteBackups([selectedBackup.$id]);
|
||||
}}>
|
||||
<Typography.Text>
|
||||
Are you sure you want to delete
|
||||
{#if selectedBackups.length}
|
||||
<b>{selectedBackups.length}</b> {selectedBackups.length > 1 ? 'backups' : 'backup'}?
|
||||
{:else}
|
||||
the <b>{cleanBackupName(selectedBackup)}</b> backup?
|
||||
{/if}
|
||||
<br />This action is irreversible.
|
||||
Are you sure you want to delete the <b>{getCleanBackupName(selectedBackup)}</b> backup?
|
||||
</Typography.Text>
|
||||
|
||||
<Typography.Text variant="m-500">This action is irreversible.</Typography.Text>
|
||||
</Confirm>
|
||||
|
||||
<Modal title="Restore backup" bind:show={showRestore} onSubmit={restoreBackup}>
|
||||
<Card radius="m" padding="s">
|
||||
<Layout.Stack gap="xxs">
|
||||
<Typography.Text variant="m-500">
|
||||
{cleanBackupName(selectedBackup)}
|
||||
{getCleanBackupName(selectedBackup)}
|
||||
</Typography.Text>
|
||||
|
||||
<Typography.Caption variant="500">
|
||||
@@ -374,6 +377,6 @@
|
||||
|
||||
<svelte:fragment slot="footer">
|
||||
<Button text on:click={() => (showRestore = false)}>Cancel</Button>
|
||||
<Button submit disabled={disableButton}>Restore</Button>
|
||||
<Button submit disabled={disableRestoreButton}>Restore</Button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
|
||||
Reference in New Issue
Block a user