mirror of
https://github.com/appwrite/console.git
synced 2026-04-07 19:17:46 +00:00
Merge pull request #1740 from appwrite/backups-to-pink2
Backups to pink2
This commit is contained in:
@@ -12,6 +12,7 @@
|
||||
import { base } from '$app/paths';
|
||||
import { getProjectId } from '$lib/helpers/project';
|
||||
import { toLocaleDate } from '$lib/helpers/date';
|
||||
import { Typography } from '@appwrite.io/pink-svelte';
|
||||
|
||||
const backupRestoreItems: {
|
||||
archives: Map<string, BackupArchive>;
|
||||
@@ -146,7 +147,9 @@
|
||||
<section class="upload-box">
|
||||
<header class="upload-box-header">
|
||||
<h4 class="upload-box-title">
|
||||
<span class="text">{titleText} ({items.size})</span>
|
||||
<Typography.Text variant="m-500">
|
||||
{titleText} ({items.size})
|
||||
</Typography.Text>
|
||||
</h4>
|
||||
<button
|
||||
class="upload-box-button"
|
||||
@@ -172,13 +175,13 @@
|
||||
<section class="progress-bar u-width-full-line">
|
||||
<div
|
||||
class="progress-bar-top-line u-flex u-gap-8 u-main-space-between">
|
||||
<span class="body-text-2">
|
||||
<Typography.Text>
|
||||
{text(item.status, key)}
|
||||
</span>
|
||||
</Typography.Text>
|
||||
|
||||
<span class="backup-name">
|
||||
<Typography.Caption variant="400">
|
||||
{backupName(item, key)}
|
||||
</span>
|
||||
</Typography.Caption>
|
||||
</div>
|
||||
<div
|
||||
class="progress-bar-container"
|
||||
@@ -195,7 +198,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
<style lang="scss">
|
||||
.upload-box-title {
|
||||
font-size: 11px;
|
||||
}
|
||||
@@ -211,13 +214,17 @@
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.backup-name {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 130%;
|
||||
font-style: normal;
|
||||
letter-spacing: -0.12px;
|
||||
color: var(--mid-neutrals-50, #818186);
|
||||
font-family: var(--font-family-sansSerif, Inter);
|
||||
.progress-bar-container {
|
||||
height: 4px;
|
||||
|
||||
&::before {
|
||||
height: 4px;
|
||||
background-color: var(--bgcolor-neutral-invert);
|
||||
}
|
||||
|
||||
&.is-danger::before {
|
||||
height: 4px;
|
||||
background-color: var(--bgcolor-error);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -77,7 +77,9 @@
|
||||
|
||||
<div class="aw-stripe-container" data-private>
|
||||
{#if isLoading}
|
||||
<Spinner />
|
||||
<div class="loader-element">
|
||||
<Spinner />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="stripe-element" bind:this={element}>
|
||||
@@ -99,5 +101,11 @@
|
||||
.stripe-element {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.loader-element {
|
||||
width: 100%;
|
||||
align-self: center;
|
||||
justify-items: end;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { Button, InputChoice, InputText } from '$lib/elements/forms';
|
||||
import { Button, InputText } from '$lib/elements/forms';
|
||||
import type { PaymentList, PaymentMethodData } from '$lib/sdk/billing';
|
||||
import { hasStripePublicKey, isCloud } from '$lib/system';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
@@ -15,6 +15,11 @@
|
||||
|
||||
let confirm = false;
|
||||
let checkboxId = `delete_${title.replaceAll(' ', '_').toLowerCase()}`;
|
||||
|
||||
// reset checkbox status
|
||||
$: if (open && confirmDeletion) {
|
||||
confirm = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<Form isModal {onSubmit}>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
export let show = false;
|
||||
export let error: string = null;
|
||||
export let dismissible = true;
|
||||
export let size: 's' | 'm' | 'l' = 'm';
|
||||
export let onSubmit: (e: SubmitEvent) => Promise<void> | void = function () {
|
||||
return;
|
||||
};
|
||||
@@ -28,7 +29,7 @@
|
||||
</script>
|
||||
|
||||
<Form isModal {onSubmit} bind:this={formComponent}>
|
||||
<Modal {title} bind:open={show} {hideFooter} {dismissible}>
|
||||
<Modal {title} bind:open={show} {hideFooter} {dismissible} {size}>
|
||||
<slot slot="description" name="description" />
|
||||
{#if error}
|
||||
<div bind:this={alert}>
|
||||
@@ -50,3 +51,10 @@
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
</Form>
|
||||
|
||||
<style>
|
||||
/* temporary fix to modal width */
|
||||
:global(dialog section) {
|
||||
max-width: 100% !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { DropList } from '$lib/components';
|
||||
import { SelectSearchCheckbox } from '..';
|
||||
import { Icon } from '@appwrite.io/pink-svelte';
|
||||
import { IconChevronDown, IconChevronUp } from '@appwrite.io/pink-icons-svelte';
|
||||
|
||||
type Option = {
|
||||
value: string;
|
||||
@@ -60,16 +62,11 @@
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<input
|
||||
class="tags-input-text u-cursor-text"
|
||||
{placeholder}
|
||||
bind:value={search}
|
||||
bind:this={input} />
|
||||
<span
|
||||
class:icon-cheveron-up={show}
|
||||
class:icon-cheveron-down={!show}
|
||||
class="chevron-icon u-position-absolute u-inset-inline-end-12"
|
||||
aria-hidden="true"></span>
|
||||
<div class="input">
|
||||
<input {placeholder} bind:value={search} bind:this={input} />
|
||||
|
||||
<Icon size="m" icon={show ? IconChevronUp : IconChevronDown} />
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<svelte:fragment slot="list">
|
||||
@@ -88,13 +85,41 @@
|
||||
</DropList>
|
||||
|
||||
<style>
|
||||
@media (max-width: 768px) {
|
||||
.chevron-icon {
|
||||
inset-block-start: 0.25rem !important;
|
||||
}
|
||||
.tags-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.tags-input {
|
||||
padding-right: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.input {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition: all 0.15s ease-in-out;
|
||||
border: var(--border-width-s) solid var(--border-neutral);
|
||||
border-radius: var(--border-radius-s);
|
||||
background-color: var(--p-input-background-color);
|
||||
padding-inline: var(--space-6);
|
||||
outline-offset: calc(var(--border-width-s) * -1);
|
||||
--p-input-background-color: var(--input-background-color, var(--bgcolor-neutral-default));
|
||||
}
|
||||
.input input {
|
||||
inline-size: 100%;
|
||||
padding-block: var(--space-3);
|
||||
padding-inline: 0;
|
||||
border: none;
|
||||
display: block;
|
||||
line-height: 140%;
|
||||
background: none;
|
||||
}
|
||||
.input input::placeholder {
|
||||
color: var(--fgcolor-neutral-tertiary);
|
||||
}
|
||||
.input:focus-within {
|
||||
outline: var(--border-width-l) solid var(--border-focus);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -8,4 +8,6 @@
|
||||
export let description: string = undefined;
|
||||
</script>
|
||||
|
||||
<Selector.Switch {id} {description} {label} {disabled} bind:checked={value} on:invalid on:change />
|
||||
<Selector.Switch {id} {description} {label} {disabled} bind:checked={value} on:invalid on:change>
|
||||
<slot name="description" slot="description" />
|
||||
</Selector.Switch>
|
||||
|
||||
@@ -19,7 +19,7 @@ const userPreferences = () => get(user)?.prefs;
|
||||
const notificationPrefs = (): Record<string, NotificationPrefItem> => {
|
||||
const prefs = userPreferences();
|
||||
|
||||
// due to php backend, empty object can be returnd as an empty array
|
||||
// due to php backend, empty object can be returned as an empty array.
|
||||
if (!prefs?.notificationPrefs || Array.isArray(prefs.notificationPrefs)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -5,18 +5,23 @@
|
||||
export let title: string;
|
||||
export let type: 'info' | 'success' | 'warning' | 'error' | 'default' = 'info';
|
||||
|
||||
let container;
|
||||
let container: HTMLElement | null = null;
|
||||
|
||||
function setNavigationHeight() {
|
||||
const alertHeight = container ? container.getBoundingClientRect().height : 0;
|
||||
const header: HTMLHeadingElement = document.querySelector('main > header');
|
||||
const sidebar: HTMLElement = document.querySelector('main > div > nav');
|
||||
const contentSection: HTMLElement = document.querySelector('main > div > section');
|
||||
|
||||
if (header) {
|
||||
header.style.top = `${alertHeight}px`;
|
||||
}
|
||||
if (sidebar) {
|
||||
sidebar.style.top = `${alertHeight + ($isTabletViewport ? 0 : header.getBoundingClientRect().height)}px`;
|
||||
}
|
||||
if (contentSection) {
|
||||
contentSection.style.paddingBlockStart = `${alertHeight}px`;
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
@@ -30,6 +35,7 @@
|
||||
</script>
|
||||
|
||||
<svelte:window on:resize={setNavigationHeight} />
|
||||
|
||||
<section
|
||||
bind:this={container}
|
||||
class="alert is-action is-action-and-top-sticky u-sep-block-end"
|
||||
|
||||
@@ -107,7 +107,7 @@
|
||||
</svelte:fragment>
|
||||
</CardContainer>
|
||||
{:else}
|
||||
<Empty single on:click={createOrg}>
|
||||
<Empty single on:click={createOrg} target="organization">
|
||||
<p>Create a new organization</p>
|
||||
</Empty>
|
||||
{/if}
|
||||
|
||||
@@ -25,7 +25,7 @@ if (isCloud) {
|
||||
},
|
||||
learnMore: {
|
||||
text: 'Learn more',
|
||||
link: () => 'http://appwrite.io/docs/products/databases/backups'
|
||||
link: () => 'https://appwrite.io/docs/products/databases/backups'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -122,6 +122,7 @@
|
||||
<style>
|
||||
.layout-level-progress-bars {
|
||||
gap: 1rem;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { Alert, CustomId, Modal } from '$lib/components';
|
||||
import { CustomId, Modal } from '$lib/components';
|
||||
import { Button, InputText } from '$lib/elements/forms';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
@@ -12,7 +12,7 @@
|
||||
import { upgradeURL } from '$lib/stores/billing';
|
||||
import CreatePolicy from './database-[database]/backups/createPolicy.svelte';
|
||||
import { cronExpression, type UserBackupPolicy } from '$lib/helpers/backups';
|
||||
import { Icon, Tag } from '@appwrite.io/pink-svelte';
|
||||
import { Alert, Icon, Tag } from '@appwrite.io/pink-svelte';
|
||||
import { IconPencil } from '@appwrite.io/pink-icons-svelte';
|
||||
|
||||
export let showCreate = false;
|
||||
@@ -122,33 +122,30 @@
|
||||
}}><Icon icon={IconPencil} /> Database ID</Tag>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<CustomId bind:show={showCustomId} name="Database" bind:id autofocus={false} />
|
||||
|
||||
{#if isCloud}
|
||||
<div class="u-flex-vertical u-gap-24 u-padding-block-start-24">
|
||||
{#if $organization?.billingPlan === BillingPlan.FREE}
|
||||
{#if showPlanUpgradeAlert}
|
||||
<Alert
|
||||
type="warning"
|
||||
dismissible
|
||||
on:dismiss={() => (showPlanUpgradeAlert = false)}>
|
||||
<svelte:fragment slot="title">
|
||||
This database won't be backed up
|
||||
</svelte:fragment>
|
||||
Upgrade your plan to ensure your data stays safe and backed up.
|
||||
<svelte:fragment slot="buttons">
|
||||
<Button href={$upgradeURL} text>Upgrade plan</Button>
|
||||
</svelte:fragment>
|
||||
</Alert>
|
||||
{/if}
|
||||
{:else}
|
||||
<CreatePolicy
|
||||
bind:totalPolicies
|
||||
bind:isShowing={showCreate}
|
||||
title="Backup policies"
|
||||
subtitle="Protect your data and ensure quick recovery by adding backup policies." />
|
||||
{#if $organization?.billingPlan === BillingPlan.FREE}
|
||||
{#if showPlanUpgradeAlert}
|
||||
<Alert.Inline
|
||||
dismissible
|
||||
title="This database won't be backed up"
|
||||
status="warning"
|
||||
on:dismiss={() => (showPlanUpgradeAlert = false)}>
|
||||
Upgrade your plan to ensure your data stays safe and backed up.
|
||||
<svelte:fragment slot="actions">
|
||||
<Button compact href={$upgradeURL}>Upgrade plan</Button>
|
||||
</svelte:fragment>
|
||||
</Alert.Inline>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<CreatePolicy
|
||||
bind:totalPolicies
|
||||
bind:isShowing={showCreate}
|
||||
title="Backup policies"
|
||||
subtitle="Protect your data and ensure quick recovery by adding backup policies." />
|
||||
{/if}
|
||||
{/if}
|
||||
<svelte:fragment slot="footer">
|
||||
<Button secondary on:click={() => (showCreate = false)}>Cancel</Button>
|
||||
|
||||
+21
-13
@@ -21,6 +21,7 @@
|
||||
import { showCreateBackup, showCreatePolicy } from './store';
|
||||
import { getProjectId } from '$lib/helpers/project';
|
||||
import { trackEvent } from '$lib/actions/analytics';
|
||||
import { Layout, Typography } from '@appwrite.io/pink-svelte';
|
||||
|
||||
let policyCreateError: string;
|
||||
let totalPolicies: UserBackupPolicy[] = [];
|
||||
@@ -132,6 +133,7 @@
|
||||
? `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',
|
||||
@@ -168,15 +170,15 @@
|
||||
<Container size="xxl">
|
||||
<div class="u-flex u-gap-32 u-flex-vertical-mobile">
|
||||
{#if !isDisabled}
|
||||
<div class="u-flex-vertical policies-holder-card">
|
||||
<div class="u-flex-vertical u-gap-16 policies-holder-card">
|
||||
<ContainerHeader
|
||||
title="Policies"
|
||||
buttonText="Create policy"
|
||||
buttonEvent="create_backup"
|
||||
buttonType="secondary"
|
||||
buttonDisabled={isDisabled}
|
||||
maxPolicies={$currentPlan.backupPolicies}
|
||||
policiesCreated={data.policies.total}
|
||||
maxPolicies={$currentPlan.backupPolicies}
|
||||
buttonMethod={() => {
|
||||
$showCreatePolicy = true;
|
||||
trackEvent('click_policy_create');
|
||||
@@ -188,7 +190,7 @@
|
||||
lastBackupDates={data.lastBackupDates} />
|
||||
</div>
|
||||
|
||||
<div class="u-flex-vertical u-width-full-line u-overflow-x-auto">
|
||||
<div class="u-flex-vertical u-gap-16 u-width-full-line u-overflow-x-auto">
|
||||
<ContainerHeader
|
||||
title="Backups"
|
||||
buttonText="Manual backup"
|
||||
@@ -201,7 +203,7 @@
|
||||
}} />
|
||||
|
||||
{#if data.backups.total}
|
||||
<div class="u-padding-block-start-8">
|
||||
<Layout.Stack gap="xxl">
|
||||
<Table {data} />
|
||||
|
||||
{#if data.backups.total > 6}
|
||||
@@ -211,11 +213,10 @@
|
||||
offset={data.offset}
|
||||
total={data.backups.total} />
|
||||
{/if}
|
||||
</div>
|
||||
</Layout.Stack>
|
||||
{:else}
|
||||
<div class="u-flex u-flex-vertical u-gap-16">
|
||||
<article
|
||||
class="empty card u-width-full-line common-section u-margin-block-start-24">
|
||||
<article class="empty card u-width-full-line common-section">
|
||||
No backups yet
|
||||
</article>
|
||||
</div>
|
||||
@@ -242,12 +243,19 @@
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
|
||||
<Modal title="Create manual backup" bind:show={$showCreateBackup} onSubmit={createManualBackup}>
|
||||
<p class="text" data-private>
|
||||
Manual backups are <b>retained forever</b> unless manually deleted. Use for major data
|
||||
changes or rollback safeguards.
|
||||
<Modal
|
||||
size="s"
|
||||
title="Create manual backup"
|
||||
bind:show={$showCreateBackup}
|
||||
onSubmit={createManualBackup}>
|
||||
<Typography.Text variant="m-400">
|
||||
Manual backups are <b>retained forever</b> unless manually deleted. Use for major data changes
|
||||
or rollback safeguards.
|
||||
</Typography.Text>
|
||||
|
||||
<Typography.Text variant="m-500">
|
||||
<b>Depending on the size of your data, this may take a while.</b>
|
||||
</p>
|
||||
</Typography.Text>
|
||||
|
||||
<svelte:fragment slot="footer">
|
||||
<Button text on:click={() => ($showCreateBackup = false)}>Cancel</Button>
|
||||
@@ -264,7 +272,7 @@
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.policies-holder-card {
|
||||
max-width: 21.5rem;
|
||||
min-width: 330px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
+3
-2
@@ -1,5 +1,5 @@
|
||||
import { getLimit, getPage, getView, pageToOffset, View } from '$lib/helpers/load';
|
||||
import { CARD_LIMIT, Dependencies } from '$lib/constants';
|
||||
import { Dependencies, PAGE_LIMIT } from '$lib/constants';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { Query } from '@appwrite.io/console';
|
||||
import type { BackupArchive, BackupArchiveList, BackupPolicyList } from '$lib/sdk/backups';
|
||||
@@ -7,8 +7,9 @@ import { isCloud } from '$lib/system';
|
||||
|
||||
export const load = async ({ params, url, route, depends }) => {
|
||||
depends(Dependencies.BACKUPS);
|
||||
|
||||
const page = getPage(url);
|
||||
const limit = getLimit(url, route, CARD_LIMIT);
|
||||
const limit = getLimit(url, route, PAGE_LIMIT);
|
||||
const view = getView(url, route, View.Grid);
|
||||
const offset = pageToOffset(page, limit);
|
||||
|
||||
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
<script lang="ts">
|
||||
export let size: 'm' | 's' = 'm';
|
||||
export let color: string | undefined = 'currentColor';
|
||||
|
||||
const sizes = {
|
||||
m: { width: 8, height: 8, cx: 4, cy: 4, r: 4 },
|
||||
s: { width: 4, height: 4, cx: 2, cy: 2, r: 2 }
|
||||
};
|
||||
|
||||
const { width, height, cx, cy, r } = sizes[size] || sizes.m;
|
||||
</script>
|
||||
|
||||
<span class="ellipse" class:m={size === 'm'} class:s={size === 's'}>
|
||||
<span class="ellipse-wrapper">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{width}
|
||||
{height}
|
||||
viewBox={`0 0 ${width} ${height}`}
|
||||
fill="none">
|
||||
<circle {cx} {cy} {r} fill={color ?? 'currentColor'} />
|
||||
</svg>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<style>
|
||||
.ellipse {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.ellipse-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.ellipse svg {
|
||||
display: inline;
|
||||
vertical-align: middle;
|
||||
}
|
||||
</style>
|
||||
+34
-34
@@ -1,11 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { DropList } from '$lib/components';
|
||||
import { Pill } from '$lib/elements';
|
||||
import { DropList } from '$lib/components';
|
||||
import { wizard } from '$lib/stores/wizard';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import SupportWizard from '$routes/(console)/supportWizard.svelte';
|
||||
import { Icon } from '@appwrite.io/pink-svelte';
|
||||
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
|
||||
import { IconInfo, IconPlus } from '@appwrite.io/pink-icons-svelte';
|
||||
import { Badge, Icon, Layout, Typography } from '@appwrite.io/pink-svelte';
|
||||
|
||||
export let isFlex = true;
|
||||
export let title: string;
|
||||
@@ -25,14 +25,27 @@
|
||||
class:is-disabled={buttonDisabled}
|
||||
class:u-flex={isFlex}
|
||||
class="u-gap-12 common-section u-main-space-between u-flex-wrap">
|
||||
<div class="u-flex u-cross-child-center u-cross-center u-gap-12">
|
||||
<div class="body-text-1 u-bold backups-title">{title}</div>
|
||||
<Layout.Stack
|
||||
direction="row"
|
||||
gap="m"
|
||||
alignContent="center"
|
||||
alignItems="center"
|
||||
justifyContent="space-between">
|
||||
<Layout.Stack direction="row" gap="xs">
|
||||
<Typography.Text variant="m-500">{title}</Typography.Text>
|
||||
{#if title === 'Policies'}
|
||||
<Badge size="xs" variant="secondary" content={policiesCreated.toString()} />
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
|
||||
{#if title === 'Policies' && policiesCreated >= maxPolicies}
|
||||
<div style="height: 40px; padding-block-start: 4px">
|
||||
<div style:height="40px;" style:padding-block-start="4px">
|
||||
<DropList bind:show={showDropdown} width="16">
|
||||
<Pill button on:click={() => (showDropdown = true)}>
|
||||
<span class="icon-info" />{policiesCreated}/{maxPolicies} created
|
||||
<Pill disabled={buttonDisabled} button on:click={() => (showDropdown = true)}>
|
||||
<Layout.Stack direction="row" gap="xs" alignItems="center" inline>
|
||||
<Icon icon={IconInfo} size="s" />
|
||||
{policiesCreated}/{maxPolicies} created
|
||||
</Layout.Stack>
|
||||
</Pill>
|
||||
<svelte:fragment slot="list">
|
||||
<slot name="tooltip">
|
||||
@@ -51,36 +64,23 @@
|
||||
</DropList>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if title === 'Backups' || policiesCreated < maxPolicies}
|
||||
<Button
|
||||
event={buttonEvent}
|
||||
on:click={buttonMethod}
|
||||
disabled={buttonDisabled}
|
||||
text={buttonType === 'text'}
|
||||
secondary={buttonType === 'secondary'}>
|
||||
<Icon icon={IconPlus} slot="start" size="s" />
|
||||
{buttonText}
|
||||
</Button>
|
||||
{/if}
|
||||
{#if title === 'Backups' || policiesCreated < maxPolicies}
|
||||
<Button
|
||||
event={buttonEvent}
|
||||
on:click={buttonMethod}
|
||||
disabled={buttonDisabled}
|
||||
text={buttonType === 'text'}
|
||||
secondary={buttonType === 'secondary'}>
|
||||
<Icon icon={IconPlus} slot="start" size="s" />
|
||||
{buttonText}
|
||||
</Button>
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
</header>
|
||||
|
||||
<style>
|
||||
.is-disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
:global(.theme-light) .backups-title {
|
||||
--p-body-text-color: #373b4d;
|
||||
color: var(--p-body-text-color);
|
||||
}
|
||||
|
||||
:global(.theme-dark) .backups-title {
|
||||
color: hsl(var(--color-neutral-5));
|
||||
}
|
||||
|
||||
:global(.small-radius-border-button) {
|
||||
border-radius: var(--border-radius-small) !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
+123
-126
@@ -23,10 +23,13 @@
|
||||
import { InputNumber } from '$lib/elements/forms/index.js';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
import { Card } from '$lib/components';
|
||||
import { wizard } from '$lib/stores/wizard';
|
||||
import SupportWizard from '$routes/(console)/supportWizard.svelte';
|
||||
|
||||
import { Card, Icon, Layout, Link, Tag, Typography } from '@appwrite.io/pink-svelte';
|
||||
import { IconPencil, IconTrash } from '@appwrite.io/pink-icons-svelte';
|
||||
import { isSmallViewport } from '$lib/stores/viewport';
|
||||
|
||||
export let isShowing: boolean;
|
||||
export let isFromBackupsTab: boolean = false;
|
||||
export let title: string | undefined = undefined;
|
||||
@@ -100,9 +103,8 @@
|
||||
showCustomPolicy = false;
|
||||
};
|
||||
|
||||
const markPolicyChecked = (event: Event, policy: UserBackupPolicy) => {
|
||||
const isChecked = (event.target as HTMLInputElement).checked;
|
||||
|
||||
const markPolicyChecked = (event: CustomEvent, policy: UserBackupPolicy) => {
|
||||
const isChecked = event.detail as boolean;
|
||||
presetPolicies.update((all) => {
|
||||
return all.map((p) => {
|
||||
if (p.label === policy.label) {
|
||||
@@ -168,6 +170,11 @@
|
||||
customRetention = { ...selectedOption, number: customRetention.number };
|
||||
}
|
||||
}
|
||||
|
||||
let frequencyOptions = ['hourly', 'daily', 'weekly', 'monthly'].map((freq) => ({
|
||||
value: freq,
|
||||
label: freq.charAt(0).toUpperCase() + freq.slice(1)
|
||||
}));
|
||||
</script>
|
||||
|
||||
<div class="u-flex-vertical u-gap-16">
|
||||
@@ -191,85 +198,82 @@
|
||||
{@const dailyPolicy = $presetPolicies[1]}
|
||||
|
||||
{#if isFromBackupsTab}
|
||||
<div class="u-flex-vertical u-gap-8">
|
||||
<Card
|
||||
isTile
|
||||
class="restore-modal-inner-card"
|
||||
style="border-radius: var(--border-radius-small, 8px); padding: 1rem;">
|
||||
<div class="u-flex u-flex-vertical u-gap-4">
|
||||
<span class="body-text-2 u-bold darker-neutral-color">
|
||||
Daily backup
|
||||
</span>
|
||||
<span>Runs every day and is retained for 7 days</span>
|
||||
</div>
|
||||
</Card>
|
||||
<span>
|
||||
<button
|
||||
type="button"
|
||||
<Layout.Stack gap="m">
|
||||
<Card.Base padding="s" radius="s">
|
||||
<Layout.Stack gap="xs">
|
||||
<Typography.Text variant="m-600">Daily backup</Typography.Text>
|
||||
Runs every day and is retained for 7 days
|
||||
</Layout.Stack>
|
||||
</Card.Base>
|
||||
|
||||
<Layout.Stack gap="xxs" direction="row" alignItems="center">
|
||||
<Button
|
||||
extraCompact
|
||||
class="u-underline cursor-pointer"
|
||||
on:click={() => {
|
||||
isShowing = false;
|
||||
$showCreatePolicy = false;
|
||||
wizard.start(SupportWizard);
|
||||
}}>Contact support</button> to upgrade your plan and add customized backup
|
||||
}}>Contact support</Button> to upgrade your plan and add customized backup
|
||||
policies.
|
||||
</span>
|
||||
</div>
|
||||
</Layout.Stack>
|
||||
</Layout.Stack>
|
||||
{:else}
|
||||
<div class="u-flex u-gap-8 body-text-2">
|
||||
<Layout.Stack gap="m">
|
||||
<InputSwitch
|
||||
id="daily_backup"
|
||||
label="Daily backups"
|
||||
on:change={(event) => markPolicyChecked(event, dailyPolicy)}>
|
||||
<svelte:fragment slot="description">
|
||||
<span>
|
||||
Daily backups are retained for 7 days.
|
||||
<button
|
||||
type="button"
|
||||
class="u-underline cursor-pointer"
|
||||
on:click={() => {
|
||||
isShowing = false;
|
||||
wizard.start(SupportWizard);
|
||||
}}>Contact support</button>
|
||||
to upgrade your plan and add customized backup policies.
|
||||
</span>
|
||||
</svelte:fragment>
|
||||
<Typography.Text variant="m-400" slot="description">
|
||||
Daily backups are retained for 7 days.
|
||||
|
||||
<Link.Button
|
||||
on:click={() => {
|
||||
isShowing = false;
|
||||
wizard.start(SupportWizard);
|
||||
}}>Contact support</Link.Button>
|
||||
to upgrade your plan and add customized backup policies.
|
||||
</Typography.Text>
|
||||
</InputSwitch>
|
||||
</div>
|
||||
</Layout.Stack>
|
||||
{/if}
|
||||
{:else}
|
||||
<!-- show 2 preset and create custom policy button on Scale & up -->
|
||||
<div class="u-flex-vertical u-gap-12">
|
||||
<Layout.Stack gap="m">
|
||||
<div class="grid-1-1 u-gap-12">
|
||||
{#each $presetPolicies as policy, index (index)}
|
||||
<label for={index.toString()} class="card preset-label-card is-allow-focus">
|
||||
<div class="u-flex u-gap-8 body-text-2">
|
||||
<Layout.Stack gap="s" direction="row">
|
||||
<InputCheckbox
|
||||
id={index.toString()}
|
||||
on:change={(event) => markPolicyChecked(event, policy)} />
|
||||
|
||||
<div class="u-flex-vertical u-gap-4">
|
||||
<h3 class="u-bold">{policy.label}</h3>
|
||||
<Layout.Stack gap="xxs">
|
||||
<Typography.Text variant="m-600"
|
||||
>{policy.label}</Typography.Text>
|
||||
{policy.description}
|
||||
</div>
|
||||
</div>
|
||||
</Layout.Stack>
|
||||
</Layout.Stack>
|
||||
</label>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if listOfCustomPolicies.length}
|
||||
<div class="u-flex-vertical u-gap-8">
|
||||
<Layout.Stack gap="s">
|
||||
{#each listOfCustomPolicies as policy}
|
||||
<div class="card custom-policy-card">
|
||||
<div class="u-flex-vertical u-gap-4 body-text-2">
|
||||
<div class="u-flex u-main-space-between">
|
||||
<h3 class="u-bold">{policy.label}</h3>
|
||||
<Card.Base padding="s">
|
||||
<Layout.Stack gap="xs">
|
||||
<Layout.Stack direction="row" justifyContent="space-between">
|
||||
<Typography.Text style="width: inherit" variant="m-600"
|
||||
>{policy.label}</Typography.Text>
|
||||
|
||||
<div class="u-flex u-gap-8">
|
||||
<Layout.Stack
|
||||
direction="row"
|
||||
gap="s"
|
||||
justifyContent="flex-end">
|
||||
<Button
|
||||
text
|
||||
noMargin
|
||||
class="icon-pencil height-fit-content"
|
||||
extraCompact
|
||||
class="height-fit-content"
|
||||
on:click={() => {
|
||||
policyInEdit = policy.id;
|
||||
backupPolicyName = policy.label;
|
||||
@@ -293,55 +297,56 @@
|
||||
(p) => policy.id !== p.id
|
||||
)
|
||||
];
|
||||
}} />
|
||||
}}>
|
||||
<Icon icon={IconPencil} size="s" />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
text
|
||||
noMargin
|
||||
class="icon-trash height-fit-content"
|
||||
extraCompact
|
||||
class="height-fit-content"
|
||||
on:click={() => {
|
||||
listOfCustomPolicies = [
|
||||
...listOfCustomPolicies.filter(
|
||||
(p) => policy.id !== p.id
|
||||
)
|
||||
];
|
||||
}} />
|
||||
</div>
|
||||
</div>
|
||||
}}>
|
||||
<Icon icon={IconTrash} size="s" />
|
||||
</Button>
|
||||
</Layout.Stack>
|
||||
</Layout.Stack>
|
||||
|
||||
<span>{customPolicyDescription(policy)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Layout.Stack>
|
||||
</Card.Base>
|
||||
{/each}
|
||||
</div>
|
||||
</Layout.Stack>
|
||||
{/if}
|
||||
|
||||
{#if showCustomPolicy || policyInEdit}
|
||||
<section
|
||||
bind:this={customPolicySection}
|
||||
class="modal is-inner-modal u-width-full-line">
|
||||
<Card.Base
|
||||
variant="secondary"
|
||||
padding="s"
|
||||
--input-background-color="var(--bgcolor-neutral-primary)">
|
||||
<div class="modal-form">
|
||||
<div class="u-flex-vertical u-gap-24">
|
||||
<div class="u-flex-vertical u-gap-4">
|
||||
<InputSelect
|
||||
label="Frequency"
|
||||
id="policyFrequency"
|
||||
placeholder="Select frequency"
|
||||
bind:value={policyFrequency}
|
||||
options={['hourly', 'daily', 'weekly', 'monthly'].map(
|
||||
(freq) => ({
|
||||
value: freq,
|
||||
label: freq.charAt(0).toUpperCase() + freq.slice(1)
|
||||
})
|
||||
)}
|
||||
required />
|
||||
{#if policyFrequency === 'hourly'}
|
||||
<span>{formPolicyDescription()}</span>
|
||||
{/if}
|
||||
<Layout.Grid columns={4}>
|
||||
{#each frequencyOptions as frequency}
|
||||
<Tag
|
||||
size="s"
|
||||
selected={frequency.value === policyFrequency}
|
||||
on:click={() => {
|
||||
policyFrequency = frequency.value;
|
||||
}}>
|
||||
{frequency.label}
|
||||
</Tag>
|
||||
{/each}
|
||||
</Layout.Grid>
|
||||
</div>
|
||||
|
||||
{#if policyFrequency !== 'hourly'}
|
||||
<div class="u-flex-vertical u-gap-8">
|
||||
<Layout.Stack gap="s">
|
||||
<div class="time-holder">
|
||||
{#if policyFrequency === 'monthly'}
|
||||
<InputSelect
|
||||
@@ -349,11 +354,11 @@
|
||||
label="Monthly timing"
|
||||
bind:value={monthlyBackupFrequency}
|
||||
placeholder="End of month (28th)"
|
||||
fullWidth
|
||||
options={backupFrequencies[policyFrequency]} />
|
||||
{:else if policyFrequency === 'weekly'}
|
||||
<div class="u-flex-vertical u-width-full-line">
|
||||
<Label>Timing</Label>
|
||||
<Layout.Stack gap="s">
|
||||
<Label class="timing-label">Timing</Label>
|
||||
|
||||
<InputSelectCheckbox
|
||||
name="Timing"
|
||||
bind:tags={daysSelectionArray}
|
||||
@@ -366,17 +371,19 @@
|
||||
option.value
|
||||
)
|
||||
}))} />
|
||||
</div>
|
||||
</Layout.Stack>
|
||||
{/if}
|
||||
|
||||
<div
|
||||
class="input-time"
|
||||
class:hide={policyFrequency === 'monthly' ||
|
||||
policyFrequency === 'weekly'}
|
||||
class:u-margin-block-start-4={policyFrequency ===
|
||||
'monthly' || policyFrequency === 'weekly'}>
|
||||
class:u-margin-block-start-32={!$isSmallViewport &&
|
||||
(policyFrequency === 'monthly' ||
|
||||
policyFrequency === 'weekly')}>
|
||||
<InputTime
|
||||
id="time"
|
||||
step={null}
|
||||
bind:value={selectedTime}
|
||||
label={['daily'].includes(policyFrequency)
|
||||
? 'Timing'
|
||||
@@ -384,13 +391,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span>{formPolicyDescription()}</span>
|
||||
</div>
|
||||
<Typography.Text>{formPolicyDescription()}</Typography.Text>
|
||||
</Layout.Stack>
|
||||
{/if}
|
||||
|
||||
<div class="u-flex-vertical u-gap-8">
|
||||
<InputSelect
|
||||
fullWidth
|
||||
id="retention"
|
||||
label="Keep for"
|
||||
placeholder="3 months"
|
||||
@@ -410,7 +416,6 @@
|
||||
</div>
|
||||
|
||||
<InputSelect
|
||||
fullWidth
|
||||
id="retention"
|
||||
placeholder="Months"
|
||||
options={customRetainingOptions}
|
||||
@@ -472,6 +477,7 @@
|
||||
<div class="button-container u-main-end u-flex u-gap-8">
|
||||
<Button
|
||||
text
|
||||
size="xs"
|
||||
on:click={() => {
|
||||
policyInEdit = false;
|
||||
showCustomPolicy = false;
|
||||
@@ -487,6 +493,7 @@
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
size="xs"
|
||||
secondary
|
||||
on:click={() => {
|
||||
if (!backupPolicyName) {
|
||||
@@ -506,18 +513,19 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</Card.Base>
|
||||
{:else}
|
||||
<div class="custom-policy-wrapper u-padding-inline-4 u-width-full-line">
|
||||
<button
|
||||
type="button"
|
||||
class="custom-policy-text"
|
||||
<Button
|
||||
size="s"
|
||||
extraCompact
|
||||
class="u-underline"
|
||||
on:click={() => (showCustomPolicy = true)}
|
||||
>Create custom policy
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Layout.Stack>
|
||||
{/if}
|
||||
</FormList>
|
||||
</div>
|
||||
@@ -541,13 +549,14 @@
|
||||
border: solid 0.0625rem #d7d7da;
|
||||
}
|
||||
|
||||
.custom-policy-text {
|
||||
color: #19191c;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.custom-policy-card {
|
||||
background-color: #f9f9fa !important;
|
||||
/* taken from pink 2 */
|
||||
:global(.timing-label) {
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 140%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
:global(.theme-dark) .preset-label-card {
|
||||
@@ -558,39 +567,27 @@
|
||||
border: solid 0.0625rem #424248;
|
||||
}
|
||||
|
||||
:global(.theme-dark) .custom-policy-text {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
:global(.theme-dark) .custom-policy-card {
|
||||
background: #2c2c2f !important;
|
||||
}
|
||||
|
||||
:global(.height-fit-content) {
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
:global(.time-holder .input-time input[type='time']) {
|
||||
padding-block: 0.34rem !important;
|
||||
}
|
||||
|
||||
@media (max-width: 767.99px) {
|
||||
.time-holder {
|
||||
gap: 0;
|
||||
gap: 1rem;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
:global(.time-holder .input-time.hide > li label) {
|
||||
display: none;
|
||||
.input-time {
|
||||
margin-block-start: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
/** the modal, for some reason has issues with inner padding on smaller devices. */
|
||||
@media (max-width: 409px) {
|
||||
.modal {
|
||||
--p-modal-padding: 0.95rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 315px) {
|
||||
.modal {
|
||||
--p-modal-padding: 0.9rem;
|
||||
/* week days selector */
|
||||
:global(.tags-input) {
|
||||
padding-right: unset !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
+107
-136
@@ -1,7 +1,6 @@
|
||||
<script lang="ts">
|
||||
import Card from '$lib/components/card.svelte';
|
||||
import { DropList, DropListItem, Modal } from '$lib/components';
|
||||
import { Button, FormList, InputCheckbox } from '$lib/elements/forms/index';
|
||||
import { Button } from '$lib/elements/forms/index';
|
||||
|
||||
import { app } from '$lib/stores/app';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
@@ -16,15 +15,23 @@
|
||||
import type { BackupPolicy, BackupPolicyList } from '$lib/sdk/backups';
|
||||
import { backupFrequencies } from '$lib/helpers/backups';
|
||||
import { Click, trackEvent } from '$lib/actions/analytics';
|
||||
import { Icon, Tooltip } from '@appwrite.io/pink-svelte';
|
||||
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
|
||||
import {
|
||||
ActionMenu,
|
||||
Divider,
|
||||
Icon,
|
||||
Layout,
|
||||
Popover,
|
||||
Tooltip,
|
||||
Typography
|
||||
} from '@appwrite.io/pink-svelte';
|
||||
import { IconDotsHorizontal, IconPlus, IconTrash } from '@appwrite.io/pink-icons-svelte';
|
||||
import { Confirm } from '$lib/components/index.js';
|
||||
import Ellipse from './components/Ellipse.svelte';
|
||||
|
||||
let showDropdown = [];
|
||||
let showDelete = false;
|
||||
let selectedPolicy: BackupPolicy = null;
|
||||
|
||||
let showEveryPolicy = false;
|
||||
let confirmedDeletion = false;
|
||||
|
||||
export let showCreatePolicy = false;
|
||||
export let policies: BackupPolicyList;
|
||||
@@ -47,7 +54,6 @@
|
||||
} finally {
|
||||
showDelete = false;
|
||||
selectedPolicy = null;
|
||||
confirmedDeletion = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,11 +135,9 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="u-flex u-flex-vertical u-gap-16">
|
||||
<Card
|
||||
class="backups-policy-list-card u-margin-block-start-24"
|
||||
style="padding: 0; min-width: 21.5rem;">
|
||||
<div class="inner-card u-flex-vertical-mobile">
|
||||
<Layout.Stack gap="l">
|
||||
<Card class="backups-policy-list-card" style="padding: 0; min-width: 21.5rem;">
|
||||
<div class="inner-card u-flex-vertical-mobile" class:empty={policies.total === 0}>
|
||||
{#each policies.policies as policy, index (policy.$id)}
|
||||
{@const policyDescription = getPolicyDescription(policy.schedule)}
|
||||
{@const policyDescriptionShort = getTruncatedPolicyDescription(policyDescription)}
|
||||
@@ -146,81 +150,78 @@
|
||||
class:opacity-gradient-bottom={index === 2}
|
||||
class:u-padding-block-start-10={index !== 0}
|
||||
class:u-padding-block-end-10={index === 0 && policies.policies.length > 1}>
|
||||
<div class="u-flex-vertical u-gap-2">
|
||||
<div class="u-flex u-main-space-between">
|
||||
<h3 class="body-text-2 u-bold darker-neutral-color">{policy.name}</h3>
|
||||
<DropList
|
||||
noArrow
|
||||
bind:show={showDropdown[index]}
|
||||
placement="bottom-end">
|
||||
<button
|
||||
class="is-only-icon is-text"
|
||||
aria-label="More options"
|
||||
on:click|preventDefault={() => {
|
||||
showDropdown[index] = !showDropdown[index];
|
||||
}}>
|
||||
<span class="icon-dots-horizontal" aria-hidden="true" />
|
||||
</button>
|
||||
<Layout.Stack direction="column" gap="xxxs">
|
||||
<Layout.Stack direction="row" justifyContent="space-between">
|
||||
<Typography.Text variant="m-500">{policy.name}</Typography.Text>
|
||||
<Popover let:toggle padding="none" placement="bottom-end">
|
||||
<Button extraCompact on:click={toggle}>
|
||||
<Icon icon={IconDotsHorizontal} />
|
||||
</Button>
|
||||
|
||||
<svelte:fragment slot="list">
|
||||
<DropListItem
|
||||
on:click={() => {
|
||||
showDelete = true;
|
||||
selectedPolicy = policy;
|
||||
showDropdown[index] = false;
|
||||
trackEvent(Click.PolicyDeleteClick);
|
||||
}}>
|
||||
Delete
|
||||
</DropListItem>
|
||||
<svelte:fragment slot="tooltip" let:toggle>
|
||||
<ActionMenu.Root width="180px">
|
||||
<ActionMenu.Item.Button
|
||||
status="danger"
|
||||
trailingIcon={IconTrash}
|
||||
on:click={(e) => {
|
||||
toggle(e);
|
||||
showDelete = true;
|
||||
selectedPolicy = policy;
|
||||
trackEvent(Click.PolicyDeleteClick);
|
||||
}}>
|
||||
Delete
|
||||
</ActionMenu.Item.Button>
|
||||
</ActionMenu.Root>
|
||||
</svelte:fragment>
|
||||
</DropList>
|
||||
</div>
|
||||
</Popover>
|
||||
</Layout.Stack>
|
||||
|
||||
<div
|
||||
class="policy-item-subtitles u-flex u-gap-6"
|
||||
style="width: fit-content;">
|
||||
{#if shouldUseTooltip}
|
||||
<Tooltip>
|
||||
<span>
|
||||
<Typography.Caption variant="400">
|
||||
<Layout.Stack gap="xs" direction="row" alignItems="baseline">
|
||||
{#if shouldUseTooltip}
|
||||
<Tooltip>
|
||||
{policyDescriptionShort}
|
||||
</span>
|
||||
<span slot="tooltip">{policyDescription}</span>
|
||||
</Tooltip>
|
||||
{:else}
|
||||
{policyDescription}
|
||||
{/if}
|
||||
<span slot="tooltip">{policyDescription}</span>
|
||||
</Tooltip>
|
||||
{:else}
|
||||
{policyDescription}
|
||||
{/if}
|
||||
|
||||
<span class="small-ellipse">●</span>
|
||||
<Ellipse size="s" />
|
||||
|
||||
{formatRetentionMessage(policy.retention)}
|
||||
</div>
|
||||
</div>
|
||||
{formatRetentionMessage(policy.retention)}
|
||||
</Layout.Stack>
|
||||
</Typography.Caption>
|
||||
</Layout.Stack>
|
||||
|
||||
<div
|
||||
class="policy-cycles u-flex u-main-space-between u-padding-block-2 policy-item-subtitles">
|
||||
<!-- Prev / Next section -->
|
||||
<div class="policy-cycles u-flex u-gap-24 u-padding-block-2">
|
||||
<div style="width: 128px" class="u-flex-vertical policy-item-caption">
|
||||
<span style="color: #97979B">Previous</span>
|
||||
<div
|
||||
class="u-flex u-gap-4 u-cross-center policy-item-subtitles darker-neutral-color">
|
||||
<span
|
||||
class="medium-ellipse"
|
||||
class:success={!!lastBackupDates[policy.$id]}>●</span>
|
||||
<span class="policy-item-subtitles">
|
||||
<div class="u-flex u-gap-4 u-cross-center darker-neutral-color">
|
||||
<Ellipse
|
||||
color={lastBackupDates[policy.$id]
|
||||
? 'var(--bgcolor-success)'
|
||||
: undefined} />
|
||||
|
||||
<Typography.Caption variant="400">
|
||||
{#if lastBackupDates[policy.$id]}
|
||||
{toLocaleDateTime(lastBackupDates[policy.$id])}
|
||||
{:else}
|
||||
No backups yet
|
||||
{/if}
|
||||
</span>
|
||||
</Typography.Caption>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="u-border-vertical" />
|
||||
<div>
|
||||
<Divider vertical />
|
||||
</div>
|
||||
|
||||
<div style="width: 128px" class="u-flex-vertical policy-item-caption">
|
||||
<span style="color: #97979B">Next</span>
|
||||
<div
|
||||
class="u-flex u-gap-4 u-cross-center policy-item-subtitles darker-neutral-color">
|
||||
|
||||
<Typography.Caption variant="400">
|
||||
{toLocaleDateTime(
|
||||
parseExpression(policy.schedule, {
|
||||
utc: true
|
||||
@@ -228,10 +229,14 @@
|
||||
.next()
|
||||
.toString()
|
||||
)}
|
||||
</div>
|
||||
</Typography.Caption>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if index !== policies.total - 1}
|
||||
<Divider class="item-divider" />
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="u-padding-24 u-flex-vertical u-gap-16 u-cross-center">
|
||||
{#if $app.themeInUse === 'dark'}
|
||||
@@ -284,59 +289,28 @@
|
||||
</div>
|
||||
{/if}
|
||||
</Card>
|
||||
</div>
|
||||
</Layout.Stack>
|
||||
|
||||
<Modal title="Delete policy" bind:show={showDelete} onSubmit={deletePolicy}>
|
||||
<FormList>
|
||||
<div class="u-flex-vertical u-gap-16">
|
||||
<p class="text" data-private>
|
||||
Are you sure you want to delete the <b>{selectedPolicy.name}</b> policy?
|
||||
</p>
|
||||
<Confirm title="Delete policy" bind:open={showDelete} onSubmit={deletePolicy} confirmDeletion>
|
||||
<Layout.Stack gap="l">
|
||||
<Typography.Text variant="m-400">
|
||||
Are you sure you want to delete the <b>{selectedPolicy.name}</b> policy?
|
||||
</Typography.Text>
|
||||
|
||||
<p class="text" data-private>
|
||||
<b
|
||||
>This will also delete all backups associated with this policy. This action is
|
||||
irreversible.</b>
|
||||
</p>
|
||||
<Typography.Text variant="m-600">
|
||||
This will also delete all backups associated with this policy. This action is
|
||||
irreversible.
|
||||
</Typography.Text>
|
||||
</Layout.Stack>
|
||||
</Confirm>
|
||||
|
||||
<div class="input-check-box-friction">
|
||||
<InputCheckbox
|
||||
required
|
||||
id="delete_policy"
|
||||
bind:checked={confirmedDeletion}
|
||||
label="I understand and confirm" />
|
||||
</div>
|
||||
</div>
|
||||
</FormList>
|
||||
<svelte:fragment slot="footer">
|
||||
<Button text on:click={() => (showDelete = false)}>Cancel</Button>
|
||||
<Button secondary submit disabled={!confirmedDeletion}>Delete</Button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
<style lang="scss">
|
||||
.inner-card {
|
||||
margin: 0 -1px;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.u-border-vertical {
|
||||
width: 1px;
|
||||
height: 34px;
|
||||
background-color: hsl(var(--border));
|
||||
}
|
||||
|
||||
:global(.small-ellipse) {
|
||||
font-size: 0.25rem;
|
||||
}
|
||||
|
||||
:global(.medium-ellipse) {
|
||||
font-size: 0.5rem;
|
||||
color: hsl(var(--color-neutral-20));
|
||||
}
|
||||
|
||||
:global(.medium-ellipse.success) {
|
||||
color: hsl(var(--color-success-100));
|
||||
&.empty {
|
||||
block-size: 365px;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.u-gap-6) {
|
||||
@@ -350,10 +324,10 @@
|
||||
.policy-card-item-padding {
|
||||
padding: var(--space-3, 6px) var(--space-4, 8px);
|
||||
border-block-end: solid 0.0625rem hsl(var(--border));
|
||||
}
|
||||
|
||||
.policy-card-item-padding:last-child {
|
||||
border-block-end: none;
|
||||
&:last-child {
|
||||
border-block-end: none;
|
||||
}
|
||||
}
|
||||
|
||||
.u-padding-block-start-10 {
|
||||
@@ -364,22 +338,10 @@
|
||||
padding-block-end: 10px;
|
||||
}
|
||||
|
||||
.policy-item-subtitles {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 150%;
|
||||
font-style: normal;
|
||||
font-family: Inter;
|
||||
}
|
||||
|
||||
:global(.input-check-box-friction .choice-item-title) {
|
||||
margin-block-start: 1px;
|
||||
}
|
||||
|
||||
:global(.theme-light .policy-item-subtitles) {
|
||||
color: var(--fgcolor-neutral-secondary, #56565c);
|
||||
}
|
||||
|
||||
:global(.theme-light .policy-item-caption) {
|
||||
color: var(--color-neutral-50, #818186);
|
||||
}
|
||||
@@ -408,39 +370,48 @@
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.policy-cycles {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.policy-card-item-padding.opacity-gradient-bottom[data-show-every='false']
|
||||
+ :global(.item-divider) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.policy-card-item-padding[data-visible='true'] {
|
||||
display: block;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.policy-card-item-padding[data-visible='true']:nth-child(3) {
|
||||
.policy-card-item-padding[data-visible='true']:nth-child(4) {
|
||||
opacity: 0.25;
|
||||
border-block-end: none;
|
||||
}
|
||||
|
||||
.policy-card-item-padding[data-visible='true']:nth-child(3) .policy-cycles {
|
||||
.policy-card-item-padding[data-visible='true']:nth-child(4) .policy-cycles {
|
||||
height: 0;
|
||||
margin: unset;
|
||||
padding: unset;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.policy-card-item-padding[data-visible='false']:nth-child(n + 4) {
|
||||
.policy-card-item-padding[data-visible='false']:nth-child(n + 5) {
|
||||
opacity: 0;
|
||||
height: 0;
|
||||
padding: unset;
|
||||
border-block-end: none;
|
||||
}
|
||||
|
||||
.policy-card-item-padding[data-visible='true'][data-show-every='true']:nth-child(3):not(
|
||||
.policy-card-item-padding[data-visible='true'][data-show-every='true']:nth-child(4):not(
|
||||
:last-child
|
||||
) {
|
||||
border-block-end: solid 0.0625rem hsl(var(--border));
|
||||
}
|
||||
|
||||
.policy-card-item-padding[data-visible='true'][data-show-every='true']:nth-child(3)
|
||||
.policy-card-item-padding[data-visible='true'][data-show-every='true']:nth-child(4)
|
||||
.policy-cycles,
|
||||
.policy-card-item-padding[data-visible='true'][data-show-every='true']:nth-child(3),
|
||||
.policy-card-item-padding[data-visible='true'][data-show-every='true']:nth-child(4),
|
||||
.policy-cycles {
|
||||
opacity: 1;
|
||||
height: auto;
|
||||
|
||||
+42
-67
@@ -1,92 +1,67 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { Card, Icon, Input, Layout, Typography } from '@appwrite.io/pink-svelte';
|
||||
import { Click, trackEvent } from '$lib/actions/analytics';
|
||||
import { InnerModal } from '$lib/components';
|
||||
import TextCounter from '$lib/elements/forms/textCounter.svelte';
|
||||
import { IconX } from '@appwrite.io/pink-icons-svelte';
|
||||
import Button from '$lib/elements/forms/button.svelte';
|
||||
|
||||
export let id: string;
|
||||
export let show = false;
|
||||
export let name: string;
|
||||
export let autofocus = true;
|
||||
export let fullWidth = false;
|
||||
export let databaseId: string;
|
||||
|
||||
let icon = 'info';
|
||||
let element: HTMLInputElement;
|
||||
let error = null;
|
||||
const pattern = String.raw`^[a-zA-Z0-9][a-zA-Z0-9._\-]*$`;
|
||||
|
||||
onMount(() => {
|
||||
if (element && autofocus) {
|
||||
element.focus();
|
||||
}
|
||||
});
|
||||
|
||||
$: if (!show) {
|
||||
id = null;
|
||||
}
|
||||
|
||||
const handleInvalid = (event: Event) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (element.validity.patternMismatch) {
|
||||
icon = 'exclamation';
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
$: if (show) {
|
||||
trackEvent(Click.ShowCustomIdClick);
|
||||
}
|
||||
|
||||
$: if (id === databaseId) {
|
||||
icon = 'exclamation';
|
||||
element?.setCustomValidity('Database ID must be different from the one being restored.');
|
||||
error = 'Database ID must be different from the one being restored.';
|
||||
} else {
|
||||
icon = 'info';
|
||||
element?.setCustomValidity('');
|
||||
}
|
||||
|
||||
$: if (id?.length) {
|
||||
icon = 'info';
|
||||
} else {
|
||||
id = null;
|
||||
error = null;
|
||||
}
|
||||
</script>
|
||||
|
||||
<InnerModal bind:show {fullWidth}>
|
||||
<svelte:fragment slot="title">{name} ID</svelte:fragment>
|
||||
<svelte:fragment slot="subtitle">
|
||||
Enter a custom {name} ID. Leave blank for a randomly generated one.
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="content">
|
||||
<div class="form u-gap-8">
|
||||
<div class="input-text-wrapper">
|
||||
<input
|
||||
id="id"
|
||||
placeholder="Enter ID"
|
||||
maxlength={36}
|
||||
{pattern}
|
||||
autocomplete="off"
|
||||
type="text"
|
||||
class="input-text"
|
||||
bind:value={id}
|
||||
bind:this={element}
|
||||
on:invalid={handleInvalid} />
|
||||
<TextCounter count={id?.length ?? 0} max={36} />
|
||||
</div>
|
||||
<div
|
||||
class="u-flex u-gap-4 u-margin-block-start-8 u-small"
|
||||
class:u-color-text-warning={icon === 'exclamation'}>
|
||||
<span
|
||||
class:icon-info={icon === 'info'}
|
||||
class:icon-exclamation={icon === 'exclamation'}
|
||||
class="u-cross-center u-line-height-1 u-color-text-gray"
|
||||
aria-hidden="true" />
|
||||
<span class="text u-line-height-1-5">
|
||||
Allowed characters: alphanumeric, non-leading hyphen, underscore, period.
|
||||
Database ID must be different from the one being restored.
|
||||
</span>
|
||||
</div>
|
||||
<Card.Base
|
||||
variant="secondary"
|
||||
padding="s"
|
||||
--input-background-color="var(--bgcolor-neutral-primary)">
|
||||
<Layout.Stack gap="xl">
|
||||
<Layout.Stack gap="s">
|
||||
<Layout.Stack direction="row" justifyContent="space-between" alignContent="center">
|
||||
<Typography.Text variant="m-600">{name} ID</Typography.Text>
|
||||
<Button extraCompact on:click={() => (show = false)}>
|
||||
<Icon icon={IconX} size="s" />
|
||||
</Button>
|
||||
</Layout.Stack>
|
||||
<Typography.Text>
|
||||
Enter a custom {name} ID. Leave blank for a randomly generated one.
|
||||
</Typography.Text>
|
||||
</Layout.Stack>
|
||||
<Input.Text
|
||||
id="id"
|
||||
placeholder="Enter ID"
|
||||
maxlength={36}
|
||||
{pattern}
|
||||
{autofocus}
|
||||
helper={error}
|
||||
state={error ? 'warning' : 'default'}
|
||||
autocomplete="off"
|
||||
type="text"
|
||||
class="input-text"
|
||||
bind:value={id} />
|
||||
|
||||
<div class="u-flex u-gap-4 u-margin-block-start-8 u-small">
|
||||
<span class="text u-line-height-1-5">
|
||||
Allowed characters: alphanumeric, non-leading hyphen, underscore, period. Database
|
||||
ID must be different from the one being restored.
|
||||
</span>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</InnerModal>
|
||||
</Layout.Stack>
|
||||
</Card.Base>
|
||||
|
||||
+9
-1
@@ -1,7 +1,7 @@
|
||||
import { writable } from 'svelte/store';
|
||||
import type { Column } from '$lib/helpers/types';
|
||||
import type { UserBackupPolicy } from '$lib/helpers/backups';
|
||||
|
||||
export const policyPricing = 20; //TODO: get this from the backend
|
||||
export const showCreatePolicy = writable(false);
|
||||
export const showCreateBackup = writable(false);
|
||||
|
||||
@@ -27,3 +27,11 @@ export const presetPolicies = writable<UserBackupPolicy[]>([
|
||||
description: 'Runs every day and is retained for 7 days'
|
||||
}
|
||||
]);
|
||||
|
||||
export const columns = writable<Column[]>([
|
||||
{ id: 'backups', title: 'Backups', type: 'string', width: { min: 180 } },
|
||||
{ id: 'size', title: 'Size', type: 'integer', width: { min: 163 } },
|
||||
{ id: 'status', title: 'Status', type: 'enum', width: { min: 163 } },
|
||||
{ id: 'policy', title: 'Policy', type: 'string', width: { min: 163 } },
|
||||
{ id: 'actions', title: '', type: 'string', width: 48 }
|
||||
]);
|
||||
|
||||
+170
-207
@@ -1,34 +1,47 @@
|
||||
<script lang="ts">
|
||||
import { Card, DropList, DropListItem, Modal } from '$lib/components';
|
||||
import { Card, Modal } from '$lib/components';
|
||||
import { Button, FormList, InputCheckbox, InputText } from '$lib/elements/forms';
|
||||
import {
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableCellCheck,
|
||||
TableCellHead,
|
||||
TableCellHeadCheck,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
TableScroll
|
||||
} from '$lib/elements/table';
|
||||
import RestoreModal from './restoreModal.svelte';
|
||||
import type { PageData } from './$types';
|
||||
import { timeFromNow, toLocaleDateTime } from '$lib/helpers/date';
|
||||
import { Pill } from '$lib/elements';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { calculateSize } from '$lib/helpers/sizeConvertion';
|
||||
import { ID } from '@appwrite.io/console';
|
||||
import { columns } from './store';
|
||||
import { database } from '../store';
|
||||
import type { BackupArchive } 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 { Dependencies } from '$lib/constants';
|
||||
import { Badge, FloatingActionBar, Tooltip } from '@appwrite.io/pink-svelte';
|
||||
import DualTimeView from '$lib/components/dualTimeView.svelte';
|
||||
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import {
|
||||
ActionMenu,
|
||||
Badge,
|
||||
FloatingActionBar,
|
||||
Icon,
|
||||
Layout,
|
||||
Popover,
|
||||
Status,
|
||||
Table,
|
||||
Tag,
|
||||
Tooltip,
|
||||
Typography
|
||||
} from '@appwrite.io/pink-svelte';
|
||||
import {
|
||||
IconDotsHorizontal,
|
||||
IconDuplicate,
|
||||
IconPencil,
|
||||
IconRefresh,
|
||||
IconTrash
|
||||
} 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';
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
let showDelete = false;
|
||||
@@ -47,8 +60,7 @@
|
||||
{
|
||||
id: 'new',
|
||||
title: 'Restore in new database',
|
||||
description:
|
||||
'Duplicate the database from the selected backup version into a new database.'
|
||||
description: 'Duplicate the database from the selected backup version to a new.'
|
||||
},
|
||||
{
|
||||
id: 'same',
|
||||
@@ -133,121 +145,124 @@
|
||||
selectedRestoreOption = 'new';
|
||||
newDatabaseInfo = { name: null, id: null };
|
||||
}
|
||||
|
||||
$: disableButton =
|
||||
(selectedRestoreOption === 'new' &&
|
||||
(!newDatabaseInfo.name || $database.$id === newDatabaseInfo.id)) ||
|
||||
(selectedRestoreOption === 'same' && !confirmSameDbRestore);
|
||||
|
||||
function getBackupStatus(backup: BackupArchive) {
|
||||
switch (backup.status) {
|
||||
case 'pending':
|
||||
return 'pending';
|
||||
case 'completed':
|
||||
return 'complete';
|
||||
case 'uploading':
|
||||
case 'downloading':
|
||||
return 'processing';
|
||||
case 'failed':
|
||||
return 'failed';
|
||||
default:
|
||||
return 'waiting';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<TableScroll class="custom-height-table-column">
|
||||
<TableHeader>
|
||||
<TableCellHeadCheck
|
||||
bind:selected={selectedBackups}
|
||||
pageItemsIds={data.backups.archives.map((b) => b.$id)} />
|
||||
<TableCellHead width={192}>Backups</TableCellHead>
|
||||
<TableCellHead width={80}>Size</TableCellHead>
|
||||
<TableCellHead width={120}>Status</TableCellHead>
|
||||
<TableCellHead width={120}>Policy</TableCellHead>
|
||||
<TableCellHead width={48} />
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{#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 })}`}
|
||||
<TableRow>
|
||||
<TableCellCheck id={backup.$id} bind:selectedIds={selectedBackups} />
|
||||
<TableCell title={backup.$createdAt}>
|
||||
<DualTimeView time={backup.$createdAt}>
|
||||
{cleanBackupName(backup)}
|
||||
</DualTimeView>
|
||||
</TableCell>
|
||||
<TableCell title="Backup Size">
|
||||
{#if backup.status === 'completed'}
|
||||
{calculateSize(backup.size)}
|
||||
{:else}
|
||||
-
|
||||
{/if}
|
||||
</TableCell>
|
||||
<TableCell title="Backup Status">
|
||||
<div class="u-flex u-gap-8 u-cross-baseline">
|
||||
<Pill
|
||||
warning={backup.status === 'pending'}
|
||||
danger={backup.status === 'failed'}
|
||||
success={backup.status === 'completed'}>
|
||||
{backup.status.toLowerCase()}
|
||||
</Pill>
|
||||
<!--{#if backup.status === 'Failed'}-->
|
||||
<!-- <span class="u-underline">Get support</span>-->
|
||||
<!--{/if}-->
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell title="Backup Policy">
|
||||
<div class="u-flex u-main-space-between u-cross-baseline">
|
||||
<Tooltip>
|
||||
<span>
|
||||
{policy?.name || 'Manual'}
|
||||
</span>
|
||||
<span slot="tooltip"
|
||||
>{policy
|
||||
? `Retained until: ${formattedRetainedUntil}`
|
||||
: `Retained forever`}</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</TableCell>
|
||||
<Table.Root let:root allowSelection columns={$columns} bind:selectedRows={selectedBackups}>
|
||||
<svelte:fragment slot="header" let:root>
|
||||
{#each $columns as column}
|
||||
<Table.Header.Cell column={column.id} {root}>{column.title}</Table.Header.Cell>
|
||||
{/each}
|
||||
</svelte:fragment>
|
||||
|
||||
<TableCell class="last-dropdown-item">
|
||||
<DropList
|
||||
class="drop-list-menu"
|
||||
noArrow
|
||||
bind:show={showDropdown[index]}
|
||||
placement="bottom-end">
|
||||
<button
|
||||
class="button is-only-icon is-text"
|
||||
aria-label="More options"
|
||||
on:click|preventDefault={() => {
|
||||
showDropdown[index] = !showDropdown[index];
|
||||
}}>
|
||||
<span class="icon-dots-horizontal" aria-hidden="true" />
|
||||
</button>
|
||||
{#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-main-space-between 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="action" {root}>
|
||||
<Popover let:toggle padding="m" placement="bottom-end">
|
||||
<Button extraCompact on:click={toggle}>
|
||||
<Icon icon={IconDotsHorizontal} />
|
||||
</Button>
|
||||
|
||||
<svelte:fragment slot="list">
|
||||
<svelte:fragment slot="tooltip" let:toggle>
|
||||
<ActionMenu.Root width="180px">
|
||||
{#if backup.status === 'completed'}
|
||||
<DropListItem
|
||||
icon="refresh"
|
||||
on:click={() => {
|
||||
<ActionMenu.Item.Button
|
||||
trailingIcon={IconRefresh}
|
||||
on:click={(e) => {
|
||||
toggle(e);
|
||||
showRestore = true;
|
||||
selectedBackup = backup;
|
||||
showDropdown[index] = false;
|
||||
trackEvent(Click.BackupRestoreClick);
|
||||
}}>
|
||||
Restore
|
||||
</DropListItem>
|
||||
</ActionMenu.Item.Button>
|
||||
{/if}
|
||||
<DropListItem
|
||||
icon="trash"
|
||||
on:click={() => {
|
||||
<ActionMenu.Item.Button
|
||||
status="danger"
|
||||
trailingIcon={IconTrash}
|
||||
on:click={(e) => {
|
||||
toggle(e);
|
||||
showDelete = true;
|
||||
selectedBackup = backup;
|
||||
showDropdown[index] = false;
|
||||
trackEvent('click_backup_delete');
|
||||
}}>
|
||||
Delete
|
||||
</DropListItem>
|
||||
|
||||
<DropListItem
|
||||
icon="duplicate"
|
||||
on:click={() => {
|
||||
</ActionMenu.Item.Button>
|
||||
<ActionMenu.Item.Button
|
||||
trailingIcon={IconDuplicate}
|
||||
on:click={(e) => {
|
||||
toggle(e);
|
||||
copy(backup.$id);
|
||||
showDropdown[index] = false;
|
||||
}}>
|
||||
Copy ID
|
||||
</DropListItem>
|
||||
</svelte:fragment>
|
||||
</DropList>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{/each}
|
||||
</TableBody>
|
||||
</TableScroll>
|
||||
</ActionMenu.Item.Button>
|
||||
</ActionMenu.Root>
|
||||
</svelte:fragment>
|
||||
</Popover>
|
||||
</Table.Cell>
|
||||
</Table.Row.Base>
|
||||
{/each}
|
||||
</Table.Root>
|
||||
|
||||
{#if selectedBackups.length > 0}
|
||||
<FloatingActionBar>
|
||||
@@ -265,11 +280,11 @@
|
||||
</FloatingActionBar>
|
||||
{/if}
|
||||
|
||||
<Modal
|
||||
<Confirm
|
||||
title="Delete {selectedBackups.length ? 'backups' : 'backup'}"
|
||||
bind:show={showDelete}
|
||||
bind:open={showDelete}
|
||||
onSubmit={deleteBackups}>
|
||||
<p class="text" data-private>
|
||||
<Typography.Text>
|
||||
Are you sure you want to delete
|
||||
{#if selectedBackups.length}
|
||||
<b>{selectedBackups.length}</b> {selectedBackups.length > 1 ? 'backups' : 'backup'}?
|
||||
@@ -277,63 +292,53 @@
|
||||
the <b>{cleanBackupName(selectedBackup)}</b> backup?
|
||||
{/if}
|
||||
<br />This action is irreversible.
|
||||
</p>
|
||||
|
||||
<svelte:fragment slot="footer">
|
||||
<Button text on:click={() => (showDelete = false)}>Cancel</Button>
|
||||
<Button secondary submit>Delete</Button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
</Typography.Text>
|
||||
</Confirm>
|
||||
|
||||
<Modal title="Restore backup" bind:show={showRestore} onSubmit={restoreBackup}>
|
||||
<Card
|
||||
isTile
|
||||
class="restore-modal-inner-card u-width-full-line"
|
||||
style="border-radius: var(--border-radius-small, 8px); padding: 1rem;">
|
||||
<div class="u-flex u-flex-vertical u-gap-4">
|
||||
<span class="body-text-2 u-bold darker-neutral-color">
|
||||
<Card radius="m" padding="s">
|
||||
<Layout.Stack gap="xxs">
|
||||
<Typography.Text variant="m-500">
|
||||
{cleanBackupName(selectedBackup)}
|
||||
</span>
|
||||
<div class="u-flex u-cross-center u-gap-6 u-width-full-line">
|
||||
<span class="u-flex u-cross-center u-gap-4">
|
||||
<span class="u-color-text-success u-font-size-12">●</span> Completed
|
||||
</span>
|
||||
<span class="small-ellipse">●</span>
|
||||
{calculateSize(selectedBackup.size)}
|
||||
<span class="small-ellipse">●</span>
|
||||
</Typography.Text>
|
||||
|
||||
<!-- TODO: ellipsis-->
|
||||
{timeFromNow(selectedBackup.$createdAt)}
|
||||
</div>
|
||||
</div>
|
||||
<Typography.Caption variant="500">
|
||||
<Layout.Stack direction="row" gap="xs">
|
||||
<Ellipse color="var(--bgcolor-success)" /> Completed
|
||||
<Ellipse size="s" />
|
||||
{calculateSize(selectedBackup.size)}
|
||||
<Ellipse size="s" />
|
||||
{timeFromNow(selectedBackup.$createdAt)}
|
||||
</Layout.Stack>
|
||||
</Typography.Caption>
|
||||
</Layout.Stack>
|
||||
</Card>
|
||||
|
||||
<FormList>
|
||||
<div class="u-flex u-flex-vertical-mobile u-gap-16">
|
||||
<Layout.Stack direction="row" gap="l">
|
||||
{#each restoreOptions as restoreOption}
|
||||
<div class="u-width-full-line">
|
||||
<LabelCard
|
||||
padding="s"
|
||||
name="restore"
|
||||
value={restoreOption.id}
|
||||
bind:group={selectedRestoreOption}>
|
||||
<svelte:fragment slot="custom">
|
||||
<svelte:fragment slot="title">
|
||||
<div class="u-flex u-flex-vertical u-gap-4 u-width-full-line">
|
||||
<h4 class="body-text-2 u-bold">
|
||||
{restoreOption.title}
|
||||
</h4>
|
||||
<p class="u-color-text-offline u-small">
|
||||
{restoreOption.description}
|
||||
</p>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
<p class="u-color-text-offline u-small">
|
||||
{restoreOption.description}
|
||||
</p>
|
||||
</LabelCard>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</Layout.Stack>
|
||||
|
||||
{#if selectedRestoreOption === 'new'}
|
||||
<div class="u-flex-vertical u-gap-8">
|
||||
<Layout.Stack gap="s" alignItems="flex-start">
|
||||
<InputText
|
||||
id="name"
|
||||
label="Database name"
|
||||
@@ -342,75 +347,33 @@
|
||||
autofocus
|
||||
required />
|
||||
{#if !showCustomId}
|
||||
<div>
|
||||
<Pill button on:click={() => (showCustomId = !showCustomId)}
|
||||
><span class="icon-pencil" aria-hidden="true" /><span class="text">
|
||||
Database ID
|
||||
</span></Pill>
|
||||
</div>
|
||||
<Tag
|
||||
size="s"
|
||||
on:click={() => {
|
||||
showCustomId = true;
|
||||
}}><Icon icon={IconPencil} /> Database ID</Tag>
|
||||
{:else}
|
||||
<div class="u-flex u-flex-vertical u-gap-8">
|
||||
<RestoreModal
|
||||
autofocus={false}
|
||||
name="Database"
|
||||
bind:show={showCustomId}
|
||||
databaseId={$database.$id}
|
||||
bind:id={newDatabaseInfo.id} />
|
||||
</div>
|
||||
<RestoreModal
|
||||
autofocus={false}
|
||||
name="Database"
|
||||
bind:show={showCustomId}
|
||||
databaseId={$database.$id}
|
||||
bind:id={newDatabaseInfo.id} />
|
||||
{/if}
|
||||
</div>
|
||||
</Layout.Stack>
|
||||
{:else}
|
||||
<div class="input-check-box-friction">
|
||||
<InputCheckbox
|
||||
required
|
||||
size="s"
|
||||
id="delete_policy"
|
||||
bind:checked={confirmSameDbRestore}>
|
||||
<svelte:fragment slot="description">
|
||||
<span style="margin-block-start: 1px;">
|
||||
Overwrite <b>{$database.name}</b> with the selected backup version
|
||||
</span>
|
||||
</svelte:fragment>
|
||||
</InputCheckbox>
|
||||
</div>
|
||||
<InputCheckbox
|
||||
required
|
||||
size="s"
|
||||
id="delete_policy"
|
||||
bind:checked={confirmSameDbRestore}
|
||||
label="Overwrite '{$database.name}' with the selected backup version">
|
||||
</InputCheckbox>
|
||||
{/if}
|
||||
</FormList>
|
||||
|
||||
<svelte:fragment slot="footer">
|
||||
<Button text on:click={() => (showRestore = false)}>Cancel</Button>
|
||||
<Button submit>Restore</Button>
|
||||
<Button submit disabled={disableButton}>Restore</Button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
|
||||
<style lang="scss">
|
||||
:global(.custom-height-table-column .table-col) {
|
||||
height: 54px;
|
||||
padding: 0 1rem; /* removes vertical padding for constrained height */
|
||||
}
|
||||
|
||||
:global(.restore-modal-inner-card) {
|
||||
background: hsl(var(--color-neutral-5));
|
||||
border: 1px solid hsl(var(--color-neutral-10));
|
||||
}
|
||||
|
||||
:global(.theme-dark .restore-modal-inner-card) {
|
||||
background: hsl(var(--color-neutral-85));
|
||||
border: 1px solid hsl(var(--color-neutral-80));
|
||||
}
|
||||
|
||||
// centers item horizontally!
|
||||
:global(.last-dropdown-item div) {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.actions {
|
||||
.indicator {
|
||||
border-radius: 0.25rem;
|
||||
background: hsl(var(--color-information-100));
|
||||
color: hsl(var(--color-neutral-0));
|
||||
|
||||
padding: 0rem 0.375rem;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/stores';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { Id, Modal } from '$lib/components';
|
||||
import { Id } from '$lib/components';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { toLocaleDateTime } from '$lib/helpers/date';
|
||||
|
||||
@@ -47,7 +47,10 @@
|
||||
?.map((policy) => getPolicyDescription(policy.schedule))
|
||||
.join(', ')}
|
||||
|
||||
<Tooltip placement="bottom" disabled={!policies || !lastBackup}>
|
||||
<Tooltip
|
||||
placement="bottom"
|
||||
disabled={!policies || !lastBackup}
|
||||
maxWidth="fit-content">
|
||||
<span class="u-trim">
|
||||
{#if !policies}
|
||||
<span class="icon-exclamation" /> No backup policies
|
||||
@@ -55,7 +58,9 @@
|
||||
{description}
|
||||
{/if}
|
||||
</span>
|
||||
<span slot="tooltip">{`Last backup: ${lastBackup}`}</span>
|
||||
<span slot="tooltip">
|
||||
{`Last backup: ${lastBackup}`}
|
||||
</span>
|
||||
</Tooltip>
|
||||
{:else}
|
||||
{toLocaleDateTime(database[column.id])}
|
||||
|
||||
Reference in New Issue
Block a user