Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Backups to pink2 #1740

Merged
merged 20 commits into from
Mar 19, 2025
Merged
Changes from 19 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 21 additions & 14 deletions src/lib/components/backupRestoreBox.svelte
Original file line number Diff line number Diff line change
@@ -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>
10 changes: 9 additions & 1 deletion src/lib/components/billing/paymentModal.svelte
Original file line number Diff line number Diff line change
@@ -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>
2 changes: 1 addition & 1 deletion src/lib/components/billing/selectPaymentMethod.svelte
Original file line number Diff line number Diff line change
@@ -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';
5 changes: 5 additions & 0 deletions src/lib/components/confirm.svelte
Original file line number Diff line number Diff line change
@@ -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}>
10 changes: 9 additions & 1 deletion src/lib/components/modal.svelte
Original file line number Diff line number Diff line change
@@ -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>
53 changes: 39 additions & 14 deletions src/lib/elements/forms/inputSelectCheckbox.svelte
Original file line number Diff line number Diff line change
@@ -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>
4 changes: 3 additions & 1 deletion src/lib/elements/forms/inputSwitch.svelte
Original file line number Diff line number Diff line change
@@ -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>
2 changes: 1 addition & 1 deletion src/lib/helpers/notifications.ts
Original file line number Diff line number Diff line change
@@ -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 {};
}
8 changes: 7 additions & 1 deletion src/lib/layout/headerAlert.svelte
Original file line number Diff line number Diff line change
@@ -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"
2 changes: 1 addition & 1 deletion src/routes/(console)/account/organizations/+page.svelte
Original file line number Diff line number Diff line change
@@ -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}
2 changes: 1 addition & 1 deletion src/routes/(console)/bottomAlerts.ts
Original file line number Diff line number Diff line change
@@ -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'
}
});
}
1 change: 1 addition & 0 deletions src/routes/(console)/project-[project]/+layout.svelte
Original file line number Diff line number Diff line change
@@ -122,6 +122,7 @@
<style>
.layout-level-progress-bars {
gap: 1rem;
z-index: 1;
display: flex;
flex-direction: column;

47 changes: 22 additions & 25 deletions src/routes/(console)/project-[project]/databases/create.svelte
Original file line number Diff line number Diff line change
@@ -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>
Original file line number Diff line number Diff line change
@@ -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>
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
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';
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);

Original file line number Diff line number Diff line change
@@ -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>
Original file line number Diff line number Diff line change
@@ -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>

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -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,92 +150,93 @@
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>

<svelte:fragment slot="list">
<DropListItem
on:click={() => {
showDelete = true;
selectedPolicy = policy;
showDropdown[index] = false;
trackEvent(Click.PolicyDeleteClick);
}}>
Delete
</DropListItem>
<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="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
})
.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>

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

<p class="text" data-private>
<b
>This will also delete all backups associated with this policy. This action is
irreversible.</b>
</p>

<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>
</Layout.Stack>

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

<Typography.Text variant="m-600">
This will also delete all backups associated with this policy. This action is
irreversible.
</Typography.Text>
</Layout.Stack>
</Confirm>

<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;
Original file line number Diff line number Diff line change
@@ -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.');
} else {
icon = 'info';
element?.setCustomValidity('');
}
$: if (id?.length) {
icon = 'info';
error = 'Database ID must be different from the one being restored.';
} 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>
Original file line number Diff line number Diff line change
@@ -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 }
]);

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -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';
9 changes: 7 additions & 2 deletions src/routes/(console)/project-[project]/databases/table.svelte
Original file line number Diff line number Diff line change
@@ -47,15 +47,20 @@
?.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
{:else}
{description}
{/if}
</span>
<span slot="tooltip">{`Last backup: ${lastBackup}`}</span>
<span slot="tooltip">
{`Last backup: ${lastBackup}`}
</span>
</Tooltip>
{:else}
{toLocaleDateTime(database[column.id])}