diff --git a/src/lib/components/empty.svelte b/src/lib/components/empty.svelte index 475dc4016..f4707bb17 100644 --- a/src/lib/components/empty.svelte +++ b/src/lib/components/empty.svelte @@ -7,6 +7,7 @@ import { trackEvent } from '$lib/actions/analytics'; export let single = false; + export let card = true; export let noMedia = false; export let target: string = null; export let href: string = null; @@ -22,7 +23,7 @@ {#if single} -
+
{#if !noMedia} @@ -31,11 +32,13 @@ on:click|preventDefault on:click={track} aria-label="create {target}"> - {#if $app.themeInUse === 'dark'} - - {:else} - - {/if} + + {#if $app.themeInUse === 'dark'} + + {:else} + + {/if} + {/if} @@ -54,7 +57,7 @@ text event="empty_documentation" ariaLabel="create {target}">Documentation -
@@ -67,7 +70,8 @@ on:click={track} aria-label="create" type="button" - class="card u-grid u-cross-center u-width-full-line dashed" + class:card + class="u-grid u-cross-center u-width-full-line dashed" class:common-section={marginTop}>
diff --git a/src/lib/components/gridItem1.svelte b/src/lib/components/gridItem1.svelte index ac1bb8ea0..03c10ef87 100644 --- a/src/lib/components/gridItem1.svelte +++ b/src/lib/components/gridItem1.svelte @@ -8,7 +8,9 @@
+
+
diff --git a/src/routes/(console)/project-[project]/databases/+page.ts b/src/routes/(console)/project-[project]/databases/+page.ts index 290c5b2d6..c572f7237 100644 --- a/src/routes/(console)/project-[project]/databases/+page.ts +++ b/src/routes/(console)/project-[project]/databases/+page.ts @@ -1,24 +1,87 @@ import { CARD_LIMIT, Dependencies } from '$lib/constants'; import { getLimit, getPage, getView, pageToOffset, View } from '$lib/helpers/load'; import { sdk } from '$lib/stores/sdk'; -import { Query } from '@appwrite.io/console'; +import { type Models, Query } from '@appwrite.io/console'; +import { timeFromNow } from '$lib/helpers/date'; import type { PageLoad } from './$types'; export const load: PageLoad = async ({ url, route, depends }) => { depends(Dependencies.DATABASES); + const page = getPage(url); const limit = getLimit(url, route, CARD_LIMIT); const view = getView(url, route, View.Grid); const offset = pageToOffset(page, limit); + const { databases, policies, lastBackups } = await fetchDatabasesAndBackups(limit, offset); + return { offset, limit, view, - databases: await sdk.forProject.databases.list([ - Query.limit(limit), - Query.offset(offset), - Query.orderDesc('') - ]) + policies, + databases, + lastBackups }; }; + +// TODO: @itznotabug we should improve this! +async function fetchDatabasesAndBackups(limit: number, offset: number) { + const databases = await sdk.forProject.databases.list([ + Query.limit(limit), + Query.offset(offset), + Query.orderDesc('') + ]); + + const policies = await fetchPolicies(databases); + const lastBackups = await fetchLastBackups(databases); + + return { databases, policies, lastBackups }; +} + +async function fetchPolicies(databases: Models.DatabaseList) { + const databasePolicies: Record = {}; + + await Promise.all( + databases.databases.map(async (database) => { + try { + const { policies } = await sdk.forProject.backups.listPolicies([ + Query.limit(1), + Query.equal('resourceId', database.$id) + ]); + + if (policies.length > 0) { + databasePolicies[database.$id] = true; + } + } catch (e) { + // ignore + } + }) + ); + + return databasePolicies; +} + +async function fetchLastBackups(databases: Models.DatabaseList) { + const lastBackups: Record = {}; + + await Promise.all( + databases.databases.map(async (database) => { + try { + const { archives } = await sdk.forProject.backups.listArchives([ + Query.limit(1), + Query.orderDesc('$createdAt'), + Query.equal('resourceId', database.$id) + ]); + + if (archives.length > 0) { + lastBackups[database.$id] = timeFromNow(archives[0].$createdAt); + } + } catch (e) { + // ignore + } + }) + ); + + return lastBackups; +} diff --git a/src/routes/(console)/project-[project]/databases/create.svelte b/src/routes/(console)/project-[project]/databases/create.svelte index 8e08fa2a2..908af3f75 100644 --- a/src/routes/(console)/project-[project]/databases/create.svelte +++ b/src/routes/(console)/project-[project]/databases/create.svelte @@ -1,12 +1,16 @@ @@ -59,6 +68,20 @@ {:else} {/if} + + {#if isCloud && $organization?.billingPlan === BillingPlan.FREE} + {#if showPlanUpgradeAlert} + (showPlanUpgradeAlert = false)}> + This database won't be backed up + Upgrade your plan to manage database backup policies + + + + + {/if} + {:else} + Backup Policies Tabs here + {/if} diff --git a/src/routes/(console)/project-[project]/databases/database-[database]/backups/+page.svelte b/src/routes/(console)/project-[project]/databases/database-[database]/backups/+page.svelte new file mode 100644 index 000000000..4d770fd23 --- /dev/null +++ b/src/routes/(console)/project-[project]/databases/database-[database]/backups/+page.svelte @@ -0,0 +1,159 @@ + + + + +
+ {#if !isDisabled} +
+ (showCreatePolicy = true)} /> + + +
+ +
+ (showCreateManualBackup = true)} /> + + {#if data.backups.total} +
+ + + {#if data.backups.total > 6} + + {/if} + + {:else} + (showCreateManualBackup = true)}> +
+
+ Create a backup to get started. +
+

+ Need a hand? Learn more in our documentation. +

+
+ +
+ + + +
+
+ {/if} + + {:else} +
+ + + + + +
+ {/if} + + + + + + +

+ Manual backups are retained forever unless manually deleted. Use them when making significant + changes to your data structure or as a safeguard for future rollbacks. +

+ + + Depending on the size of your data, this may take a while. + + + + + + +
diff --git a/src/routes/(console)/project-[project]/databases/database-[database]/backups/+page.ts b/src/routes/(console)/project-[project]/databases/database-[database]/backups/+page.ts new file mode 100644 index 000000000..42f05494e --- /dev/null +++ b/src/routes/(console)/project-[project]/databases/database-[database]/backups/+page.ts @@ -0,0 +1,47 @@ +import { getLimit, getPage, getView, pageToOffset, View } from '$lib/helpers/load'; +import type { PageLoad } from './$types'; +import { CARD_LIMIT, Dependencies } from '$lib/constants'; +import { sdk } from '$lib/stores/sdk'; +import { type Models, Query } from '@appwrite.io/console'; + +export const load: PageLoad = async ({ params, url, route, depends }) => { + depends(Dependencies.BACKUPS); + const page = getPage(url); + const limit = getLimit(url, route, CARD_LIMIT); + const view = getView(url, route, View.Grid); + const offset = pageToOffset(page, limit); + + let backups: Models.BackupArchiveList = { total: 0, archives: [] }; + let policies: Models.BackupPolicyList = { total: 0, policies: [] }; + + try { + [backups, policies] = await Promise.all([ + await sdk.forProject.backups.listArchives([ + Query.limit(limit), + Query.offset(offset), + Query.orderDesc(''), + Query.equal('resourceType', 'database'), + Query.equal('resourceId', params.database) + ]), + + await sdk.forProject.backups.listPolicies([ + Query.limit(limit), + Query.offset(offset), + Query.orderDesc('') + // TODO: add queries once supported. + // Query.equal('resourceType', 'database'), + // Query.equal('resourceId', params.database), + ]) + ]); + } catch (e) { + // ignore + } + + return { + offset, + limit, + view, + backups, + policies + }; +}; diff --git a/src/routes/(console)/project-[project]/databases/database-[database]/backups/containerHeader.svelte b/src/routes/(console)/project-[project]/databases/database-[database]/backups/containerHeader.svelte new file mode 100644 index 000000000..2062f87ce --- /dev/null +++ b/src/routes/(console)/project-[project]/databases/database-[database]/backups/containerHeader.svelte @@ -0,0 +1,44 @@ + + +
+
+
{title}
+
+ + +
+ + diff --git a/src/routes/(console)/project-[project]/databases/database-[database]/backups/createPolicy.svelte b/src/routes/(console)/project-[project]/databases/database-[database]/backups/createPolicy.svelte new file mode 100644 index 000000000..7f6278ccc --- /dev/null +++ b/src/routes/(console)/project-[project]/databases/database-[database]/backups/createPolicy.svelte @@ -0,0 +1,254 @@ + + + + +
+ +
+ {#each ['hourly', 'daily', 'weekly', 'monthly'] as freq} + + {/each} +
+
+ +
+
+ {#if policyFrequency === 'monthly'} + + {:else if policyFrequency === 'weekly'} +
+ + ({ + ...option, + checked: daysSelectionArray.includes(option.value) + }))} /> +
+ {/if} + + {#if policyFrequency !== 'hourly'} +
+ +
+ {/if} +
+ {backupScheduleText()} + +
+ +
+ + + {#if policyRetention === 365 * 100} + Every backup created under this policy will be retained forever. + {:else} + Every backup created under this policy will be retained for {backupRetainingOptions.find((option) => option.value === policyRetention) + ?.label ?? '3 months'} before being automatically deleted. + {/if} + +
+ + +
+ + + + + +
+ + diff --git a/src/routes/(console)/project-[project]/databases/database-[database]/backups/policy.svelte b/src/routes/(console)/project-[project]/databases/database-[database]/backups/policy.svelte new file mode 100644 index 000000000..ff7e997aa --- /dev/null +++ b/src/routes/(console)/project-[project]/databases/database-[database]/backups/policy.svelte @@ -0,0 +1,237 @@ + + + +
+ {#each policies.policies as policy, index} + +
+
+
+

{policy.name}

+ + + + + { + showDelete = true; + selectedPolicy = policy; + showDropdown[index] = false; + }}> + Delete + + + +
+ +
+ + ● + {formatRetentionMessage(policy.retention)} +
+
+ + +
+ + + + + + + + + + + + + + + + + + +
+ Next +
+ + ● + + + {toLocaleDateTime( + parseExpression(policy.schedule, { utc: false }).next().toString() + )} +
+
+
+
+ {:else} + (showCreatePolicy = true)}> +
+ {#if $app.themeInUse === 'dark'} + + {:else} + + {/if} +
+ +
+
+ Ensure your data stays safe +
+

+ Create a backup policy to automate regular and secure data protection. +

+
+
+ +
+
+ {/each} +
+
+ + +

+ Are you sure you want to delete {selectedPolicy.name} policy? +
This action is irreversible. +

+ + + + +
+ + diff --git a/src/routes/(console)/project-[project]/databases/database-[database]/backups/table.svelte b/src/routes/(console)/project-[project]/databases/database-[database]/backups/table.svelte new file mode 100644 index 000000000..e891ecaf8 --- /dev/null +++ b/src/routes/(console)/project-[project]/databases/database-[database]/backups/table.svelte @@ -0,0 +1,240 @@ + + + + + Backups + Size + Size + Policy + + + {#each data.backups.archives as backup, index} + + + {cleanBackupName(backup)} + + + {#if backup.status === 'completed'} + {calculateSize(backup.size)} + {:else} + - + {/if} + + +
+ + {backup.status.toLowerCase()} + + + + +
+
+ +
+ {backup.policyId ? backup.policyId : 'Manual'} + + + + + { + showRestore = true; + selectedBackup = backup; + showDropdown[index] = false; + }}> + Restore + + { + showDelete = true; + selectedBackup = backup; + showDropdown[index] = false; + }}> + Delete + + + +
+
+
+ {/each} +
+
+ + +

+ Are you sure you want to delete {cleanBackupName(selectedBackup)}? +
This action is irreversible. +

+ + + + +
+ + +

+ Restoring this backup will duplicate the database from the selected backup version. This + action may take a while. +

+ + +
+ + {cleanBackupName(selectedBackup)} + +
+ + ● Completed + + ● + {calculateSize(selectedBackup.size)} + ● + Created {timeFromNow(selectedBackup.$createdAt)} +
+
+
+ + + + + {#if !showCustomId} +
+ (showCustomId = !showCustomId)} + > +
+ {:else} +
+ + + {#if error} + {error} + {/if} +
+ {/if} +
+ + + + + +
+ + diff --git a/src/routes/(console)/project-[project]/databases/database-[database]/store.ts b/src/routes/(console)/project-[project]/databases/database-[database]/store.ts index 35f8a9c59..4da4ebf91 100644 --- a/src/routes/(console)/project-[project]/databases/database-[database]/store.ts +++ b/src/routes/(console)/project-[project]/databases/database-[database]/store.ts @@ -12,3 +12,13 @@ export const columns = writable([ { id: '$createdAt', title: 'Created', type: 'datetime', show: true, width: 120 }, { id: '$updatedAt', title: 'Updated', type: 'datetime', show: true, width: 120 } ]); + +export const backupRetainingOptions = [ + { label: '3 Days', value: 3 }, + { label: '1 Week', value: 7 }, + { label: '2 Weeks', value: 14 }, + { label: '1 Month', value: 30 }, + { label: '3 Months', value: 90 }, + { label: '1 Year', value: 365 }, + { label: 'Forever', value: 365 * 100 } +]; diff --git a/src/routes/(console)/project-[project]/databases/grid.svelte b/src/routes/(console)/project-[project]/databases/grid.svelte index d7a4a471e..1e1a5062e 100644 --- a/src/routes/(console)/project-[project]/databases/grid.svelte +++ b/src/routes/(console)/project-[project]/databases/grid.svelte @@ -16,6 +16,15 @@ {#each data.databases.databases as database} {database.name} + + {#if data.lastBackups && data.lastBackups[database.$id]} + Last backup: {data.lastBackups[database.$id]} + {:else if !data.policies || !data.policies[database.$id]} + No backup policies + {:else} + Last backup: No backups yet + {/if} + {database.$id} {/each} @@ -23,3 +32,9 @@

Create a database

+ +