diff --git a/package.json b/package.json index b804dae27..37a8b8b4b 100644 --- a/package.json +++ b/package.json @@ -22,12 +22,13 @@ }, "dependencies": { "@ai-sdk/svelte": "^1.1.24", - "@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@d7e354b", + "@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@9c55755", "@appwrite.io/pink-icons": "0.25.0", "@appwrite.io/pink-icons-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@865e2fc", "@appwrite.io/pink-legacy": "^1.0.3", "@appwrite.io/pink-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@865e2fc", "@faker-js/faker": "^9.9.0", + "@plausible-analytics/tracker": "^0.4.4", "@popperjs/core": "^2.11.8", "@sentry/sveltekit": "^8.38.0", "@stripe/stripe-js": "^3.5.0", @@ -42,7 +43,6 @@ "ignore": "^6.0.2", "nanoid": "^5.1.5", "nanotar": "^0.1.1", - "plausible-tracker": "^0.3.9", "pretty-bytes": "^6.1.1", "prismjs": "^1.30.0", "remarkable": "^2.0.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8aee84871..c112c59e6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,8 +12,8 @@ importers: specifier: ^1.1.24 version: 1.1.24(svelte@5.25.3)(zod@3.24.3) '@appwrite.io/console': - specifier: https://pkg.vc/-/@appwrite/@appwrite.io/console@d7e354b - version: https://pkg.vc/-/@appwrite/@appwrite.io/console@d7e354b + specifier: https://pkg.vc/-/@appwrite/@appwrite.io/console@9c55755 + version: https://pkg.vc/-/@appwrite/@appwrite.io/console@9c55755 '@appwrite.io/pink-icons': specifier: 0.25.0 version: 0.25.0 @@ -29,6 +29,9 @@ importers: '@faker-js/faker': specifier: ^9.9.0 version: 9.9.0 + '@plausible-analytics/tracker': + specifier: ^0.4.4 + version: 0.4.4 '@popperjs/core': specifier: ^2.11.8 version: 2.11.8 @@ -71,9 +74,6 @@ importers: nanotar: specifier: ^0.1.1 version: 0.1.1 - plausible-tracker: - specifier: ^0.3.9 - version: 0.3.9 pretty-bytes: specifier: ^6.1.1 version: 6.1.1 @@ -272,9 +272,9 @@ packages: '@analytics/type-utils@0.6.2': resolution: {integrity: sha512-TD+xbmsBLyYy/IxFimW/YL/9L2IEnM7/EoV9Aeh56U64Ify8o27HJcKjo38XY9Tcn0uOq1AX3thkKgvtWvwFQg==} - '@appwrite.io/console@https://pkg.vc/-/@appwrite/@appwrite.io/console@d7e354b': - resolution: {tarball: https://pkg.vc/-/@appwrite/@appwrite.io/console@d7e354b} - version: 1.10.0 + '@appwrite.io/console@https://pkg.vc/-/@appwrite/@appwrite.io/console@9c55755': + resolution: {tarball: https://pkg.vc/-/@appwrite/@appwrite.io/console@9c55755} + version: 2.1.0 '@appwrite.io/pink-icons-svelte@2.0.0-RC.1': resolution: {integrity: sha512-iLFlV55hj8mGuAbmxJGenxN5RaZMmVT4GJb9dv/MP1xBAtYibFq7JvBcxm18qV2KU8c31Rntf+Ub4GL7HwqTYg==} @@ -1008,6 +1008,9 @@ packages: resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==} engines: {node: '>= 10.0.0'} + '@plausible-analytics/tracker@0.4.4': + resolution: {integrity: sha512-fz0NOYUEYXtg1TBaPEEvtcBq3FfmLFuTe1VZw4M8sTWX129br5dguu3M15+plOQnc181ShYe67RfwhKgK89VnA==} + '@playwright/test@1.56.1': resolution: {integrity: sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==} engines: {node: '>=18'} @@ -2900,10 +2903,6 @@ packages: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} - plausible-tracker@0.3.9: - resolution: {integrity: sha512-hMhneYm3GCPyQon88SZrVJx+LlqhM1kZFQbuAgXPoh/Az2YvO1B6bitT9qlhpiTdJlsT5lsr3gPmzoVjb5CDXA==} - engines: {node: '>=10'} - playwright-core@1.56.1: resolution: {integrity: sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==} engines: {node: '>=18'} @@ -3823,7 +3822,7 @@ snapshots: '@analytics/type-utils@0.6.2': {} - '@appwrite.io/console@https://pkg.vc/-/@appwrite/@appwrite.io/console@d7e354b': {} + '@appwrite.io/console@https://pkg.vc/-/@appwrite/@appwrite.io/console@9c55755': {} '@appwrite.io/pink-icons-svelte@2.0.0-RC.1(svelte@5.25.3)': dependencies: @@ -4575,6 +4574,8 @@ snapshots: '@parcel/watcher-win32-x64': 2.5.1 optional: true + '@plausible-analytics/tracker@0.4.4': {} + '@playwright/test@1.56.1': dependencies: playwright: 1.56.1 @@ -6675,8 +6676,6 @@ snapshots: picomatch@4.0.3: {} - plausible-tracker@0.3.9: {} - playwright-core@1.56.1: {} playwright@1.56.1: diff --git a/src/lib/actions/analytics.ts b/src/lib/actions/analytics.ts index 6880f1c2f..cfd42ae8f 100644 --- a/src/lib/actions/analytics.ts +++ b/src/lib/actions/analytics.ts @@ -1,5 +1,5 @@ import Analytics, { type AnalyticsPlugin } from 'analytics'; -import Plausible from 'plausible-tracker'; +import { init, track } from '@plausible-analytics/tracker'; import { get } from 'svelte/store'; import { page } from '$app/state'; import { user } from '$lib/stores/user'; @@ -11,30 +11,30 @@ import { getReferrerAndUtmSource, getTrackedQueryParams } from '$lib/helpers/utm function plausible(domain: string): AnalyticsPlugin { if (!browser) return { name: 'analytics-plugin-plausible' }; - const instance = Plausible({ - domain + init({ + domain, + autoCapturePageviews: false }); return { name: 'analytics-plugin-plausible', page: ({ payload }) => { - instance.trackPageview({ + track('pageview', { url: payload.properties.path, - referrer: payload.properties.referrer, - deviceWidth: payload.properties.width + props: { + referrer: payload.properties.referrer, + deviceWidth: String(payload.properties.width) + } }); }, track: ({ payload }) => { - instance.trackEvent( - payload.event, - { - props: payload.properties - }, - { - url: payload.properties.path, - deviceWidth: payload.properties.width + track(payload.event, { + url: payload.properties.path, + props: { + ...payload.properties, + deviceWidth: String(payload.properties.width) } - ); + }); }, loaded: () => true }; @@ -247,6 +247,7 @@ export enum Submit { ProjectDelete = 'submit_project_delete', ProjectUpdateName = 'submit_project_update_name', ProjectUpdateTeam = 'submit_project_update_team', + ProjectUpdateLabels = 'submit_project_update_labels', ProjectService = 'submit_project_service', ProjectUpdateSMTP = 'submit_project_update_smtp', MemberCreate = 'submit_member_create', @@ -268,7 +269,7 @@ export enum Submit { AuthSessionAlertsUpdate = 'submit_auth_session_alerts_update', AuthMembershipPrivacyUpdate = 'submit_auth_membership_privacy_update', AuthMockNumbersUpdate = 'submit_auth_mock_numbers_update', - AuthInvalidateSesssion = 'submit_auth_invalidate_session', + AuthInvalidateSession = 'submit_auth_invalidate_session', SessionsLengthUpdate = 'submit_sessions_length_update', SessionsLimitUpdate = 'submit_sessions_limit_update', SessionDelete = 'submit_session_delete', @@ -277,6 +278,7 @@ export enum Submit { DatabaseDelete = 'submit_database_delete', DatabaseUpdateName = 'submit_database_update_name', DatabaseImportCsv = 'submit_database_import_csv', + DatabaseBackupDelete = 'submit_database_backup_delete', ColumnCreate = 'submit_column_create', ColumnUpdate = 'submit_column_update', diff --git a/src/lib/components/archiveProject.svelte b/src/lib/components/archiveProject.svelte index a8b736284..8a324b395 100644 --- a/src/lib/components/archiveProject.svelte +++ b/src/lib/components/archiveProject.svelte @@ -1,4 +1,5 @@ @@ -194,7 +205,7 @@
+ badge={`${archivedTotalOverall}`}> {#if isPlanBelowPro} These projects are archived and require a plan upgrade to restore access. @@ -204,7 +215,7 @@
- + {#each projectsToArchive as project} {@const platforms = filterPlatforms( project.platforms.map((platform) => getPlatformInfo(platform.type)) @@ -264,7 +275,7 @@ {/each} - {#if platforms.length > 3} + {#if platforms.length > 2} {/each} + +
@@ -353,4 +373,7 @@ align-items: center; gap: 8px; } + :global(.pagination-container) { + margin-top: 16px; + } diff --git a/src/lib/components/backupRestoreBox.svelte b/src/lib/components/backupRestoreBox.svelte index f1de9d3ba..05cb96e8f 100644 --- a/src/lib/components/backupRestoreBox.svelte +++ b/src/lib/components/backupRestoreBox.svelte @@ -5,7 +5,6 @@ import { isCloud, isSelfHosted } from '$lib/system'; import { organization } from '$lib/stores/organization'; import { BillingPlan, Dependencies } from '$lib/constants'; - import type { BackupArchive, BackupRestoration } from '$lib/sdk/backups'; import { goto, invalidate } from '$app/navigation'; import { page } from '$app/state'; import { addNotification } from '$lib/stores/notifications'; @@ -13,10 +12,11 @@ import { getProjectId } from '$lib/helpers/project'; import { toLocaleDate } from '$lib/helpers/date'; import { Typography } from '@appwrite.io/pink-svelte'; + import { type Models } from '@appwrite.io/console'; const backupRestoreItems: { - archives: Map; - restorations: Map; + archives: Map; + restorations: Map; } = { archives: new Map(), restorations: new Map() @@ -117,7 +117,7 @@ if (which === 'restorations') lastDatabaseRestorationId = null; } - function backupName(item: BackupArchive | BackupRestoration, key: string) { + function backupName(item: Models.BackupArchive | Models.BackupRestoration, key: string) { const column = key === 'archives' ? '$createdAt' : 'startedAt'; return toLocaleDate(item[column]); diff --git a/src/lib/components/billing/alerts/paymentMandate.svelte b/src/lib/components/billing/alerts/paymentMandate.svelte index 0126f2f9a..e69de29bb 100644 --- a/src/lib/components/billing/alerts/paymentMandate.svelte +++ b/src/lib/components/billing/alerts/paymentMandate.svelte @@ -1,25 +0,0 @@ - - -{#if $paymentMissingMandate && $paymentMissingMandate?.country?.toLowerCase() === 'in' && $paymentMissingMandate.mandateId === null && !hideBillingHeaderRoutes.includes(page.url.pathname)} - - The payment method for {$organization.name} needs to be verified. - - - - -{/if} diff --git a/src/lib/components/domains/cnameTable.svelte b/src/lib/components/domains/cnameTable.svelte index b198524da..9569ce89f 100644 --- a/src/lib/components/domains/cnameTable.svelte +++ b/src/lib/components/domains/cnameTable.svelte @@ -35,16 +35,22 @@ - + - Type - Name - Value + Type + Name + Value - CNAME - {subdomain} - + CNAME + {subdomain} + - + - Type - Name - Value + Type + Name + Value - {variant.toUpperCase()} - {subdomain || '@'} - + {variant.toUpperCase()} + {subdomain || '@'} + {#if $regionalConsoleVariables._APP_DOMAIN_TARGET_CAA} - + CAA - @ - + @ + diff --git a/src/lib/components/limit.svelte b/src/lib/components/limit.svelte index c8e72a401..4221f7693 100644 --- a/src/lib/components/limit.svelte +++ b/src/lib/components/limit.svelte @@ -8,6 +8,8 @@ export let sum: number; export let limit: number; export let name: string; + export let pageParam: string = 'page'; + export let removeOnFirstPage: boolean = false; const options = [ { label: '6', value: 6 }, @@ -23,10 +25,17 @@ url.searchParams.set('limit', limit.toString()); await preferences.setLimit(limit); - if (url.searchParams.has('page')) { - const page = Number(url.searchParams.get('page')); - const newPage = Math.floor(((page - 1) * previousLimit) / limit); - url.searchParams.set('page', newPage.toString()); + if (url.searchParams.has(pageParam)) { + const page = Number(url.searchParams.get(pageParam)); + const prev = + Number.isFinite(previousLimit) && previousLimit > 0 ? previousLimit : limit; + const newPage = Math.floor(((page - 1) * prev) / limit) + 1; + const safePage = Math.max(1, Number.isFinite(newPage) ? newPage : 1); + if (removeOnFirstPage && safePage === 1) { + url.searchParams.delete(pageParam); + } else { + url.searchParams.set(pageParam, safePage.toString()); + } } await goto(url.toString()); diff --git a/src/lib/components/multiSelectTable.svelte b/src/lib/components/multiSelectTable.svelte index cd35b38e6..ce0196e34 100644 --- a/src/lib/components/multiSelectTable.svelte +++ b/src/lib/components/multiSelectTable.svelte @@ -1,5 +1,13 @@ {#key computeKey} @@ -104,13 +186,7 @@ if (confirmDeletion) { showConfirmDeletion = true; } else { - const state = await onDelete?.(selectedRows); - if (state instanceof Error) { - // user should handle error on their own! - } else { - notifySuccess(); - selectedRows = []; - } + await consumeDeleteOperation(); } }}>Delete @@ -129,16 +205,13 @@ disableModal = true; onDeleteError = null; - const state = await onDelete?.(selectedRows); - if (state instanceof Error) { - disableModal = false; - onDeleteError = state.message || `Failed to delete ${resource}s`; - } else { - notifySuccess(); - selectedRows = []; - disableModal = false; + const allDeleted = await consumeDeleteOperation(); + + if (allDeleted) { showConfirmDeletion = false; } + + disableModal = false; }}> {@const selectionCount = selectedRows.length} diff --git a/src/lib/components/pagination.svelte b/src/lib/components/pagination.svelte index f272f7f66..975e4cf6e 100644 --- a/src/lib/components/pagination.svelte +++ b/src/lib/components/pagination.svelte @@ -6,15 +6,21 @@ export let limit: number; export let offset: number; export let useCreateLink = true; + export let pageParam: string = 'page'; + export let removeOnFirstPage: boolean = false; $: currentPage = Math.floor(offset / limit + 1); function getLink(page: number): string { const url = new URL(pageStore.url); if (page === 1) { - url.searchParams.delete('page'); + if (removeOnFirstPage) { + url.searchParams.delete(pageParam); + } else { + url.searchParams.set(pageParam, '1'); + } } else { - url.searchParams.set('page', page.toString()); + url.searchParams.set(pageParam, page.toString()); } return url.toString(); diff --git a/src/lib/components/paginationWithLimit.svelte b/src/lib/components/paginationWithLimit.svelte index 7cf78f6b5..b48fbc0f6 100644 --- a/src/lib/components/paginationWithLimit.svelte +++ b/src/lib/components/paginationWithLimit.svelte @@ -8,13 +8,19 @@ offset, total, name, - useCreateLink = true + useCreateLink = true, + pageParam = 'page', + removeOnFirstPage = false, + ...restProps }: { limit: number; offset: number; total: number; name: string; useCreateLink?: boolean; + pageParam?: string; + removeOnFirstPage?: boolean; + [key: string]: unknown; } = $props(); const showLimit = $derived(!!useCreateLink); @@ -22,10 +28,17 @@ const alignItems = $derived(showLimit ? 'center' : 'flex-end'); - + {#if showLimit} - + {/if} - + diff --git a/src/lib/components/paginator.svelte b/src/lib/components/paginator.svelte index ae8aaf9c4..053e2ea14 100644 --- a/src/lib/components/paginator.svelte +++ b/src/lib/components/paginator.svelte @@ -13,7 +13,8 @@ name = 'items', gap = 's', offset = $bindable(0), - children + children, + ...restProps }: { items: T[]; limit?: number; @@ -26,6 +27,7 @@ | undefined; offset?: number; children: Snippet<[T[], number]>; + [key: string]: unknown; } = $props(); let total = $derived(items.length); @@ -33,7 +35,7 @@ let paginatedItems = $derived(items.slice(offset, offset + limit)); - + {@render children(paginatedItems, limit)} {#if !hideFooter} diff --git a/src/lib/components/searchQuery.svelte b/src/lib/components/searchQuery.svelte index d34c9b4d6..a0b89a212 100644 --- a/src/lib/components/searchQuery.svelte +++ b/src/lib/components/searchQuery.svelte @@ -1,6 +1,6 @@ diff --git a/src/lib/helpers/buildTimeout.ts b/src/lib/helpers/buildTimeout.ts index 3915811c2..df10478ee 100644 --- a/src/lib/helpers/buildTimeout.ts +++ b/src/lib/helpers/buildTimeout.ts @@ -22,14 +22,24 @@ function isBuildTimedOut(createdAt: string, status: string, timeoutSeconds: numb * Gets the effective status for a build, considering timeout */ export function getEffectiveBuildStatus( - originalStatus: string, - createdAt: string, + deployment: Models.Deployment, consoleVariables: Models.ConsoleVariables | undefined ): string { + const originalStatus = deployment.status; + const createdAt = deployment.$createdAt; + const timeoutSeconds = getBuildTimeoutSeconds(consoleVariables); if (isBuildTimedOut(createdAt, originalStatus, timeoutSeconds)) { return 'failed'; } + + const isReady = originalStatus === 'ready'; + const hasScreenshot = deployment.screenshotLight && deployment.screenshotDark; + + if (isReady && !hasScreenshot) { + return 'finalizing'; + } + return originalStatus; } diff --git a/src/lib/helpers/date.ts b/src/lib/helpers/date.ts index 2d56c1cb6..29878256a 100644 --- a/src/lib/helpers/date.ts +++ b/src/lib/helpers/date.ts @@ -206,6 +206,18 @@ export function timeFromNow(datetime: string): string { return dayjs().to(dayjs(datetime)); } +export function timeFromNowShort(datetime: string): string { + if (!datetime || !isValidDate(datetime)) { + return 'n/a'; + } + + const timeStr = dayjs().to(dayjs(datetime)); + return timeStr + .replace('second', 'sec') // seconds > secs + .replace('minute', 'min') // minutes > mins + .replace('hour', 'hr'); // hours > hrs +} + export function hoursToDays(hours: number) { if (hours > 24) { return `${Math.floor(hours / 24)} days`; diff --git a/src/lib/layout/activity.svelte b/src/lib/layout/activity.svelte index dce36d66f..c77fa3488 100644 --- a/src/lib/layout/activity.svelte +++ b/src/lib/layout/activity.svelte @@ -1,9 +1,9 @@ @@ -29,55 +49,57 @@ expanded={databasesScreen && !insideSideSheet} slotSpacing={databasesScreen && !insideSideSheet}> {#if logs.total} -
- - - User - Event - Location - IP - Date - - {#each logs.logs as log} - - - - {#if log.userEmail} - {#if log.userName} - - {log.userName} - {:else} - - {log.userEmail} - {/if} + + + User + Event + Location + IP + Date + + {#each logs.logs as log} + + + + {#if log.userEmail} + {#if log.userName} + + {log.userName} {:else} -
- -
- {log.userName ?? 'Anonymous'} + + {log.userEmail} {/if} -
-
- - {log.event} - - - {#if log.countryCode !== '--'} - {log.countryName} {:else} - Unknown +
+ +
+ {log.userName ?? 'Anonymous'} {/if} -
- - {log.ip} - - - {toLocaleDateTime(log.time)} - -
- {/each} -
-
+
+
+ + + {log.event} + + + + {#if log.countryCode !== '--'} + {log.countryName} + {:else} + Unknown + {/if} + + + + + + + + + +
+ {/each} +
{ - if (typeof services === 'undefined') { - throw new AppwriteException('Missing required parameter: "services"'); - } - const apiPath = '/backups/archives'; - const payload: Payload = {}; - if (typeof services !== 'undefined') { - payload['services'] = services; - } - if (typeof resourceId !== 'undefined') { - payload['resourceId'] = resourceId; - } - const uri = new URL(this.client.config.endpoint + apiPath); - - const apiHeaders: { [header: string]: string } = { - 'content-type': 'application/json' - }; - - return await this.client.call('post', uri, apiHeaders, payload); - } - - async deleteArchive(archiveId: string): Promise { - if (typeof archiveId === 'undefined') { - throw new AppwriteException('Missing required parameter: "archiveId"'); - } - const apiPath = '/backups/archives/{archiveId}'.replace('{archiveId}', archiveId); - const payload: Payload = {}; - const uri = new URL(this.client.config.endpoint + apiPath); - - const apiHeaders: { [header: string]: string } = { - 'content-type': 'application/json' - }; - - return await this.client.call('delete', uri, apiHeaders, payload); - } - - async listArchives(queries?: string[]): Promise { - const apiPath = '/backups/archives'; - const payload: Payload = {}; - if (typeof queries !== 'undefined') { - payload['queries'] = queries; - } - const uri = new URL(this.client.config.endpoint + apiPath); - - const apiHeaders: { [header: string]: string } = { - 'content-type': 'application/json' - }; - - return await this.client.call('get', uri, apiHeaders, payload); - } - - async getArchive(archiveId: string): Promise { - if (typeof archiveId === 'undefined') { - throw new AppwriteException('Missing required parameter: "archiveId"'); - } - const apiPath = '/backups/archives/{archiveId}'.replace('{archiveId}', archiveId); - const payload: Payload = {}; - const uri = new URL(this.client.config.endpoint + apiPath); - - const apiHeaders: { [header: string]: string } = { - 'content-type': 'application/json' - }; - - return await this.client.call('get', uri, apiHeaders, payload); - } - - async listPolicies(queries?: string[]): Promise { - const apiPath = '/backups/policies'; - const payload: Payload = {}; - if (typeof queries !== 'undefined') { - payload['queries'] = queries; - } - const uri = new URL(this.client.config.endpoint + apiPath); - - const apiHeaders: { [header: string]: string } = { - 'content-type': 'application/json' - }; - - return await this.client.call('get', uri, apiHeaders, payload); - } - - async createPolicy( - policyId: string, - services: string[], - retention: number, - schedule: string, - name?: string, - resourceId?: string, - enabled?: boolean - ): Promise { - if (typeof policyId === 'undefined') { - throw new AppwriteException('Missing required parameter: "policyId"'); - } - if (typeof services === 'undefined') { - throw new AppwriteException('Missing required parameter: "services"'); - } - if (typeof retention === 'undefined') { - throw new AppwriteException('Missing required parameter: "retention"'); - } - if (typeof schedule === 'undefined') { - throw new AppwriteException('Missing required parameter: "schedule"'); - } - const apiPath = '/backups/policies'; - const payload: Payload = {}; - if (typeof policyId !== 'undefined') { - payload['policyId'] = policyId; - } - if (typeof name !== 'undefined') { - payload['name'] = name; - } - if (typeof services !== 'undefined') { - payload['services'] = services; - } - if (typeof resourceId !== 'undefined') { - payload['resourceId'] = resourceId; - } - if (typeof enabled !== 'undefined') { - payload['enabled'] = enabled; - } - if (typeof retention !== 'undefined') { - payload['retention'] = retention; - } - if (typeof schedule !== 'undefined') { - payload['schedule'] = schedule; - } - const uri = new URL(this.client.config.endpoint + apiPath); - - const apiHeaders: { [header: string]: string } = { - 'content-type': 'application/json' - }; - - return await this.client.call('post', uri, apiHeaders, payload); - } - - async getPolicy(policyId: string): Promise { - if (typeof policyId === 'undefined') { - throw new AppwriteException('Missing required parameter: "policyId"'); - } - const apiPath = '/backups/policies/{policyId}'.replace('{policyId}', policyId); - const payload: Payload = {}; - const uri = new URL(this.client.config.endpoint + apiPath); - - const apiHeaders: { [header: string]: string } = { - 'content-type': 'application/json' - }; - - return await this.client.call('get', uri, apiHeaders, payload); - } - - async updatePolicy( - policyId: string, - name?: string, - retention?: number, - schedule?: string, - enabled?: boolean - ): Promise { - if (typeof policyId === 'undefined') { - throw new AppwriteException('Missing required parameter: "policyId"'); - } - const apiPath = '/backups/policies/{policyId}'.replace('{policyId}', policyId); - const payload: Payload = {}; - if (typeof name !== 'undefined') { - payload['name'] = name; - } - if (typeof retention !== 'undefined') { - payload['retention'] = retention; - } - if (typeof schedule !== 'undefined') { - payload['schedule'] = schedule; - } - if (typeof enabled !== 'undefined') { - payload['enabled'] = enabled; - } - const uri = new URL(this.client.config.endpoint + apiPath); - - const apiHeaders: { [header: string]: string } = { - 'content-type': 'application/json' - }; - - return await this.client.call('patch', uri, apiHeaders, payload); - } - - async deletePolicy(policyId: string): Promise { - if (typeof policyId === 'undefined') { - throw new AppwriteException('Missing required parameter: "policyId"'); - } - const apiPath = '/backups/policies/{policyId}'.replace('{policyId}', policyId); - const payload: Payload = {}; - const uri = new URL(this.client.config.endpoint + apiPath); - - const apiHeaders: { [header: string]: string } = { - 'content-type': 'application/json' - }; - - return await this.client.call('delete', uri, apiHeaders, payload); - } - - async createRestoration( - archiveId: string, - services: string[], - newResourceId?: string, - newResourceName?: string - ): Promise { - if (typeof archiveId === 'undefined') { - throw new AppwriteException('Missing required parameter: "archiveId"'); - } - if (typeof services === 'undefined') { - throw new AppwriteException('Missing required parameter: "services"'); - } - const apiPath = '/backups/restoration'; - const payload: Payload = {}; - if (typeof archiveId !== 'undefined') { - payload['archiveId'] = archiveId; - } - if (typeof services !== 'undefined') { - payload['services'] = services; - } - if (typeof newResourceId !== 'undefined') { - payload['newResourceId'] = newResourceId; - } - if (typeof newResourceName !== 'undefined') { - payload['newResourceName'] = newResourceName; - } - const uri = new URL(this.client.config.endpoint + apiPath); - - const apiHeaders: { [header: string]: string } = { - 'content-type': 'application/json' - }; - - return await this.client.call('post', uri, apiHeaders, payload); - } - - async listRestorations(queries?: string[]): Promise { - const apiPath = '/backups/restorations'; - const payload: Payload = {}; - if (typeof queries !== 'undefined') { - payload['queries'] = queries; - } - const uri = new URL(this.client.config.endpoint + apiPath); - - const apiHeaders: { [header: string]: string } = { - 'content-type': 'application/json' - }; - - return await this.client.call('get', uri, apiHeaders, payload); - } - - async getRestoration(restorationId: string): Promise { - if (typeof restorationId === 'undefined') { - throw new AppwriteException('Missing required parameter: "restorationId"'); - } - const apiPath = '/backups/restorations/{restorationId}'.replace( - '{restorationId}', - restorationId - ); - const payload: Payload = {}; - const uri = new URL(this.client.config.endpoint + apiPath); - - const apiHeaders: { [header: string]: string } = { - 'content-type': 'application/json' - }; - - return await this.client.call('get', uri, apiHeaders, payload); - } -} diff --git a/src/lib/stores/billing.ts b/src/lib/stores/billing.ts index 6db2db697..61608aa3c 100644 --- a/src/lib/stores/billing.ts +++ b/src/lib/stores/billing.ts @@ -8,7 +8,7 @@ import MarkedForDeletion from '$lib/components/billing/alerts/markedForDeletion. import MissingPaymentMethod from '$lib/components/billing/alerts/missingPaymentMethod.svelte'; import newDevUpgradePro from '$lib/components/billing/alerts/newDevUpgradePro.svelte'; import PaymentAuthRequired from '$lib/components/billing/alerts/paymentAuthRequired.svelte'; -import PaymentMandate from '$lib/components/billing/alerts/paymentMandate.svelte'; + import { BillingPlan, NEW_DEV_PRO_UPGRADE_COUPON } from '$lib/constants'; import { cachedStore } from '$lib/helpers/cache'; import type { BillingPlansMap } from '$lib/sdk/billing'; @@ -529,28 +529,6 @@ export function checkForMarkedForDeletion(org: Models.Organization) { } } -export const paymentMissingMandate = writable(null); - -export async function checkForMandate(org: Models.Organization) { - const paymentId = org.paymentMethodId ?? org.backupPaymentMethodId; - if (!paymentId) return; - - const paymentMethod = await sdk.forConsole.account.getPaymentMethod({ - paymentMethodId: paymentId - }); - - if (paymentMethod?.mandateId === null && paymentMethod?.country.toLowerCase() === 'in') { - headerAlert.add({ - id: 'paymentMandate', - component: PaymentMandate, - show: true, - importance: 8 - }); - activeHeaderAlert.set(headerAlert.get()); - paymentMissingMandate.set(paymentMethod); - } -} - export async function checkForMissingPaymentMethod() { const orgs = await sdk.forConsole.organizations.list({ queries: [ diff --git a/src/lib/stores/database.ts b/src/lib/stores/database.ts index 4c93859ce..648708bd0 100644 --- a/src/lib/stores/database.ts +++ b/src/lib/stores/database.ts @@ -27,9 +27,9 @@ export async function checkForDatabaseBackupPolicies( if (isCloud && backupsEnabled) { try { - const policies = await sdk - .forProject(region, projectId) - .backups.listPolicies([Query.limit(1), Query.equal('resourceId', database.$id)]); + const policies = await sdk.forProject(region, projectId).backups.listPolicies({ + queries: [Query.limit(1), Query.equal('resourceId', database.$id)] + }); total = policies.total; } catch (e) { diff --git a/src/lib/stores/oauth-providers.ts b/src/lib/stores/oauth-providers.ts index 44a1777f4..22df4b17b 100644 --- a/src/lib/stores/oauth-providers.ts +++ b/src/lib/stores/oauth-providers.ts @@ -27,7 +27,7 @@ export const oAuthProviders: Record = { apple: { name: 'Apple', icon: 'apple', - docs: 'https://developer.apple.com/', + docs: 'https://developer.apple.com/sign-in-with-apple/', component: Apple }, auth0: { diff --git a/src/lib/stores/sdk.ts b/src/lib/stores/sdk.ts index 38cf83b7f..efea17959 100644 --- a/src/lib/stores/sdk.ts +++ b/src/lib/stores/sdk.ts @@ -3,6 +3,7 @@ import { Account, Assistant, Avatars, + Backups, Client, Console, Functions, @@ -25,7 +26,6 @@ import { Realtime, Organizations } from '@appwrite.io/console'; -import { Backups } from '../sdk/backups'; import { Sources } from '$lib/sdk/sources'; import { REGION_FRA, diff --git a/src/lib/stores/uploader.ts b/src/lib/stores/uploader.ts index ef848232c..8a04eae8a 100644 --- a/src/lib/stores/uploader.ts +++ b/src/lib/stores/uploader.ts @@ -1,4 +1,4 @@ -import { Client, ID, type Models, Sites, Storage } from '@appwrite.io/console'; +import { Client, Functions, ID, type Models, Sites, Storage } from '@appwrite.io/console'; import { writable } from 'svelte/store'; import { getApiEndpoint } from '$lib/stores/sdk'; import { page } from '$app/state'; @@ -32,6 +32,13 @@ const temporarySites = (region: string, projectId: string) => { return new Sites(clientProject); }; +const temporaryFunctions = (region: string, projectId: string) => { + const clientProject = new Client().setMode('admin'); + const endpoint = getApiEndpoint(region); + clientProject.setEndpoint(endpoint).setProject(projectId); + return new Functions(clientProject); +}; + const createUploader = () => { const { subscribe, set, update } = writable({ isOpen: false, @@ -110,7 +117,19 @@ const createUploader = () => { newFile.status = 'success'; updateFile(newFile.$id, newFile); }, - uploadSiteDeployment: async (siteId: string, code: File) => { + uploadSiteDeployment: async ({ + siteId, + code, + buildCommand, + installCommand, + outputDirectory + }: { + siteId: string; + code: File; + buildCommand?: string; + installCommand?: string; + outputDirectory?: string; + }) => { const newDeployment: UploaderFile = { $id: '', resourceId: siteId, @@ -132,6 +151,49 @@ const createUploader = () => { siteId, code, activate: true, + buildCommand, + installCommand, + outputDirectory, + onProgress: (progress) => { + newDeployment.$id = progress.$id; + newDeployment.progress = progress.progress; + newDeployment.status = progress.progress === 100 ? 'success' : 'pending'; + updateFile(progress.$id, newDeployment); + } + }); + newDeployment.$id = uploadedFile.$id; + newDeployment.progress = 100; + newDeployment.status = 'success'; + updateFile(newDeployment.$id, newDeployment); + }, + uploadFunctionDeployment: async ({ + functionId, + code + }: { + functionId: string; + code: File; + }) => { + const newDeployment: UploaderFile = { + $id: '', + resourceId: functionId, + name: code.name, + size: code.size, + progress: 0, + status: 'pending' + }; + update((n) => { + n.isOpen = true; + n.isCollapsed = false; + n.files.unshift(newDeployment); + return n; + }); + const uploadedFile = await temporaryFunctions( + page.params.region, + page.params.project + ).createDeployment({ + functionId, + code, + activate: true, onProgress: (progress) => { newDeployment.$id = progress.$id; newDeployment.progress = progress.progress; diff --git a/src/routes/(console)/+layout.svelte b/src/routes/(console)/+layout.svelte index 196d56b8a..61feb71e8 100644 --- a/src/routes/(console)/+layout.svelte +++ b/src/routes/(console)/+layout.svelte @@ -15,7 +15,6 @@ import { calculateTrialDay, checkForEnterpriseTrial, - checkForMandate, checkForMarkedForDeletion, checkForMissingPaymentMethod, checkForNewDevUpgradePro, @@ -308,7 +307,6 @@ if (org?.billingPlan !== BillingPlan.FREE) { await paymentExpired(org); await checkPaymentAuthorizationRequired(org); - await checkForMandate(org); if (org?.billingTrialDays) { calculateTrialDay(org); @@ -334,7 +332,6 @@ { }) : null, fetch(`${endpoint}/health/version`, { - headers: { 'X-Appwrite-Project': project } + headers: { 'X-Appwrite-Project': project as string } }).then((response) => response.json() as { version?: string }), sdk.forConsole.console.variables() ]); diff --git a/src/routes/(console)/account/identities.svelte b/src/routes/(console)/account/identities.svelte index 924d22621..c326fdc1a 100644 --- a/src/routes/(console)/account/identities.svelte +++ b/src/routes/(console)/account/identities.svelte @@ -5,7 +5,6 @@ import { addNotification } from '$lib/stores/notifications'; import { sdk } from '$lib/stores/sdk'; import { identities } from './store'; - import { toLocaleDateTime } from '$lib/helpers/date'; import { invalidate } from '$app/navigation'; import { Dependencies } from '$lib/constants'; import { oAuthProviders } from '$lib/stores/oauth-providers'; @@ -82,7 +81,13 @@ - {toLocaleDateTime(identity.providerAccessTokenExpiry)} + {#if identity.providerAccessTokenExpiry} + + {:else} + - + {/if} diff --git a/src/routes/(console)/organization-[organization]/+layout.ts b/src/routes/(console)/organization-[organization]/+layout.ts index 7b63473f1..63f15ce7e 100644 --- a/src/routes/(console)/organization-[organization]/+layout.ts +++ b/src/routes/(console)/organization-[organization]/+layout.ts @@ -22,7 +22,7 @@ export const load: LayoutLoad = async ({ params, depends, parent }) => { depends(Dependencies.MEMBERS); depends(Dependencies.PAYMENT_METHODS); - const requestedOrg = await checkPlatformAndRedirect(params, organizations, prefs); + await checkPlatformAndRedirect(params, organizations, prefs); let roles = isCloud ? [] : defaultRoles; let scopes = isCloud ? [] : defaultScopes; @@ -49,15 +49,10 @@ export const load: LayoutLoad = async ({ params, depends, parent }) => { sdk.forConsole.account.updatePrefs({ prefs: newPrefs }); } - // fetch org only if we haven't already fetched it for platform check - const orgPromise: Promise = requestedOrg - ? Promise.resolve(requestedOrg) - : (sdk.forConsole.teams.get({ - teamId: params.organization - }) as Promise); - const [org, members, countryList, locale] = await Promise.all([ - orgPromise, + sdk.forConsole.teams.get({ + teamId: params.organization + }) as Promise, sdk.forConsole.teams.listMemberships({ teamId: params.organization }), sdk.forConsole.locale.listCountries(), sdk.forConsole.locale.get(), @@ -101,7 +96,7 @@ async function checkPlatformAndRedirect( params: { organization: string }, organizations: { teams: Array<{ $id: string; platform?: string }> }, prefs: Record -): Promise { +) { // check if preloaded let requestedOrg = organizations.teams.find((team) => team.$id === params.organization) as | Models.Organization @@ -159,8 +154,4 @@ async function checkPlatformAndRedirect( redirect(303, resolve('/(console)')); } } - - // send the org, - // if already in the full list so we don't have to make another API request. - return requestedOrg; } diff --git a/src/routes/(console)/organization-[organization]/+page.svelte b/src/routes/(console)/organization-[organization]/+page.svelte index 4794d8659..37ef03204 100644 --- a/src/routes/(console)/organization-[organization]/+page.svelte +++ b/src/routes/(console)/organization-[organization]/+page.svelte @@ -71,12 +71,6 @@ return getServiceLimit('projects', null, data.currentPlan); }); - const projectsToArchive = $derived.by(() => { - return isCloud - ? data.projects.projects.filter((project) => project.status === 'archived') - : []; - }); - function filterPlatforms(platforms: { name: string; icon: string }[]) { return platforms.filter( (value, index, self) => index === self.findIndex((t) => t.name === value.name) @@ -136,6 +130,19 @@ return project.status === 'archived'; } + const projectsToArchive = $derived( + (data.archivedProjectsPage ?? data.projects.projects).filter( + (project) => project.status === 'archived' + ) + ); + + const activeTotalOverall = $derived( + data?.activeTotalOverall ?? + data?.organization?.projects?.length ?? + data?.projects?.total ?? + 0 + ); + function clearSearch() { searchQuery?.clearInput(); } @@ -163,7 +170,7 @@ - + {#if $canWriteProjects} {#if projectCreationDisabled && reachedProjectLimit} @@ -238,7 +245,7 @@ {#if data.projects.total > 0} {#each data.projects.projects as project} @@ -323,13 +330,16 @@ name="Projects" limit={data.limit} offset={data.offset} - total={data.projects.total} /> + total={activeTotalOverall} /> + currentPlan={$currentPlan} + archivedTotalOverall={data.archivedTotalOverall} + archivedOffset={data.archivedOffset} + limit={data.limit} /> diff --git a/src/routes/(console)/organization-[organization]/+page.ts b/src/routes/(console)/organization-[organization]/+page.ts index dfd1d75fa..fc4256b03 100644 --- a/src/routes/(console)/organization-[organization]/+page.ts +++ b/src/routes/(console)/organization-[organization]/+page.ts @@ -1,11 +1,11 @@ import { Query } from '@appwrite.io/console'; +import { isCloud } from '$lib/system'; import { sdk } from '$lib/stores/sdk'; import { getLimit, getPage, getSearch, pageToOffset } from '$lib/helpers/load'; import { CARD_LIMIT, Dependencies } from '$lib/constants'; import type { PageLoad } from './$types'; import { redirect } from '@sveltejs/kit'; import { base } from '$app/paths'; -import { isCloud } from '$lib/system'; export const load: PageLoad = async ({ params, url, route, depends, parent }) => { const { scopes } = await parent(); @@ -20,26 +20,76 @@ export const load: PageLoad = async ({ params, url, route, depends, parent }) => const offset = pageToOffset(page, limit); const search = getSearch(url); - const projects = await sdk.forConsole.projects.list({ - queries: [ - Query.offset(offset), - Query.equal('teamId', params.organization), - Query.limit(limit), - Query.orderDesc(''), - Query.select(['$id', 'name', 'platforms', 'region', ...(isCloud ? ['status'] : [])]) - ], - search: search || undefined - }); + const archivedPageRaw = parseInt(url.searchParams.get('archivedPage') || '1', 10); + const archivedPage = + Number.isFinite(archivedPageRaw) && archivedPageRaw > 0 ? archivedPageRaw : 1; + const archivedOffset = pageToOffset(archivedPage, limit); + + const searchQueries = search + ? [Query.or([Query.search('search', search), Query.contains('labels', search)])] + : []; + const commonQueries = [Query.equal('teamId', params.organization)]; + const activeQueries = isCloud + ? [Query.or([Query.equal('status', 'active'), Query.isNull('status')])] + : []; + + const [activeProjects, archivedProjects, activeTotal, archivedTotal] = await Promise.all([ + sdk.forConsole.projects.list({ + queries: [ + Query.offset(offset), + Query.limit(limit), + Query.orderDesc(''), + ...commonQueries, + ...searchQueries, + ...activeQueries + ] + }), + isCloud + ? sdk.forConsole.projects.list({ + queries: [ + Query.offset(archivedOffset), + Query.limit(limit), + Query.orderDesc(''), + ...commonQueries, + ...searchQueries, + Query.equal('status', 'archived') + ] + }) + : Promise.resolve({ projects: [], total: 0 }), + sdk.forConsole.projects.list({ + queries: [...commonQueries, ...activeQueries, ...searchQueries] + }), + isCloud + ? sdk.forConsole.projects.list({ + queries: [...commonQueries, ...searchQueries, Query.equal('status', 'archived')] + }) + : Promise.resolve({ projects: [], total: 0 }) + ]); // set `default` if no region! - for (const project of projects.projects) { + for (const project of activeProjects.projects) { project.region ??= 'default'; } + if (isCloud) { + for (const project of archivedProjects.projects) { + project.region ??= 'default'; + } + } return { offset, limit, - projects, + projects: { + ...activeProjects, + projects: activeProjects.projects, + total: activeTotal.total + }, + activeProjectsPage: activeProjects.projects, + archivedProjectsPage: archivedProjects.projects, + activeTotalOverall: activeTotal.total, + archivedTotalOverall: archivedTotal.total, + archivedOffset, + archivedPage, search }; }; diff --git a/src/routes/(console)/organization-[organization]/billing/+page.svelte b/src/routes/(console)/organization-[organization]/billing/+page.svelte index 797584f7e..fdb29040c 100644 --- a/src/routes/(console)/organization-[organization]/billing/+page.svelte +++ b/src/routes/(console)/organization-[organization]/billing/+page.svelte @@ -25,22 +25,15 @@ import { Alert } from '@appwrite.io/pink-svelte'; import { goto, invalidate } from '$app/navigation'; import { Dependencies } from '$lib/constants'; - import { base } from '$app/paths'; import type { PageData } from './$types'; + import { resolve } from '$app/paths'; export let data: PageData; - // Reactive statement to update organization when data changes $: organization = data.organization; - - // why are these reactive? - $: defaultPaymentMethod = data?.paymentMethods?.paymentMethods?.find( - (method) => method.$id === organization?.paymentMethodId - ); - - $: backupPaymentMethod = data?.paymentMethods?.paymentMethods?.find( - (method) => method.$id === organization?.backupPaymentMethodId - ); + $: baseUrl = resolve('/(console)/organization-[organization]/billing', { + organization: organization.$id + }); onMount(async () => { if (page.url.searchParams.has('type')) { @@ -62,7 +55,7 @@ organization.$id, invoice.clientSecret, organization.paymentMethodId, - `${base}/organization-${organization.$id}/billing?type=validate-invoice&invoice=${invoice.$id}` + `${baseUrl}?type=validate-invoice&invoice=${invoice.$id}` ); } @@ -122,7 +115,7 @@ {/if} {/if} - {#if defaultPaymentMethod?.failed && !backupPaymentMethod} + {#if data.primaryPaymentMethod?.failed && !data.backupPaymentMethod} @@ -156,7 +149,13 @@ currentInvoice={data?.billingInvoice} /> {/if} - + + + { depends(Dependencies.CREDIT); depends(Dependencies.INVOICES); depends(Dependencies.ADDRESS); - //aggregation reloads on page param changes + // aggregation reloads on page param changes depends(Dependencies.BILLING_AGGREGATION); const billingAddressId = (organization as Models.Organization)?.billingAddressId; @@ -93,6 +93,7 @@ export const load: PageLoad = async ({ parent, depends, url, route }) => { // make number const credits = availableCredit ? availableCredit.available : null; + const { backup, primary } = getOrganizationPaymentMethods(organization, paymentMethods); return { paymentMethods, @@ -109,6 +110,32 @@ export const load: PageLoad = async ({ parent, depends, url, route }) => { offset: pageToOffset( getPage(url) || 1, getLimit(url, route, DEFAULT_BILLING_PROJECTS_LIMIT) - ) + ), + + backupPaymentMethod: backup, + primaryPaymentMethod: primary }; }; + +function getOrganizationPaymentMethods( + organization: Models.Organization, + paymentMethods: Models.PaymentMethodList +): { + backup: Models.PaymentMethod | null; + primary: Models.PaymentMethod | null; +} { + let backup: Models.PaymentMethod | null = null; + let primary: Models.PaymentMethod | null = null; + + for (const paymentMethod of paymentMethods.paymentMethods) { + if (paymentMethod.$id === organization.paymentMethodId) { + primary = paymentMethod; + } else if (paymentMethod.$id === organization.backupPaymentMethodId) { + backup = paymentMethod; + } + + if (primary && backup) break; + } + + return { primary, backup }; +} diff --git a/src/routes/(console)/organization-[organization]/billing/deleteOrgPayment.svelte b/src/routes/(console)/organization-[organization]/billing/deleteOrgPayment.svelte index 014cf9fc5..58164982e 100644 --- a/src/routes/(console)/organization-[organization]/billing/deleteOrgPayment.svelte +++ b/src/routes/(console)/organization-[organization]/billing/deleteOrgPayment.svelte @@ -27,7 +27,7 @@ message: `The payment method has been removed from ${$organization.name}` }); trackEvent(Submit.OrganizationPaymentDelete); - invalidate(Dependencies.ORGANIZATION); + await invalidate(Dependencies.PAYMENT_METHODS); showDelete = false; } catch (e) { error = e.message; @@ -50,7 +50,7 @@ message: `The payment method has been removed from ${$organization.name}` }); trackEvent(Submit.OrganizationBackupPaymentDelete); - await invalidate(Dependencies.ORGANIZATION); + await invalidate(Dependencies.PAYMENT_METHODS); showDelete = false; } catch (e) { error = e.message; @@ -70,10 +70,10 @@ {:else} + title="Remove payment method" + onSubmit={isBackup ? removeBackupMethod : removeDefaultMethod}> Are you sure you want to remove the payment method from {$organization?.name}? diff --git a/src/routes/(console)/organization-[organization]/billing/paymentMethods.svelte b/src/routes/(console)/organization-[organization]/billing/paymentMethods.svelte index 82f9df979..fb1eba319 100644 --- a/src/routes/(console)/organization-[organization]/billing/paymentMethods.svelte +++ b/src/routes/(console)/organization-[organization]/billing/paymentMethods.svelte @@ -35,14 +35,14 @@ export let organization: Models.Organization; export let methods: Models.PaymentMethodList; + export let backupMethod: Models.PaymentMethod; + export let primaryMethod: Models.PaymentMethod; let showEdit = false; let showDelete = false; let showPayment = false; let showReplace = false; let isSelectedBackup = false; - let backupPaymentMethod: Models.PaymentMethod; - let defaultPaymentMethod: Models.PaymentMethod; async function addPaymentMethod(paymentMethodId: string) { try { @@ -55,7 +55,7 @@ message: `A new payment method has been added to ${organization.name}` }); trackEvent(Submit.OrganizationPaymentUpdate); - invalidate(Dependencies.ORGANIZATION); + await invalidate(Dependencies.PAYMENT_METHODS); } catch (error) { addNotification({ type: 'error', @@ -75,7 +75,7 @@ type: 'success', message: `A new payment method has been added to ${organization.name}` }); - invalidate(Dependencies.ORGANIZATION); + await invalidate(Dependencies.PAYMENT_METHODS); } catch (error) { addNotification({ type: 'error', @@ -85,33 +85,15 @@ } } - $: if (organization?.backupPaymentMethodId) { - sdk.forConsole.organizations - .getPaymentMethod({ - organizationId: organization.$id, - paymentMethodId: organization.backupPaymentMethodId - }) - .then((res) => (backupPaymentMethod = res)); - } - - $: if (organization?.paymentMethodId) { - sdk.forConsole.organizations - .getPaymentMethod({ - organizationId: organization.$id, - paymentMethodId: organization.paymentMethodId - }) - .then((res) => (defaultPaymentMethod = res)); - } - $: if (!showReplace) { isSelectedBackup = false; } $: hasPaymentError = - defaultPaymentMethod?.lastError || - defaultPaymentMethod?.expired || - backupPaymentMethod?.lastError || - backupPaymentMethod?.expired; + primaryMethod?.lastError || + primaryMethod?.expired || + backupMethod?.lastError || + backupMethod?.expired; @@ -137,14 +119,14 @@ - + - {#if defaultPaymentMethod?.userId === $user?.$id} + {#if primaryMethod?.userId === $user?.$id} { @@ -176,14 +158,14 @@ {#if organization?.backupPaymentMethodId} - + - {#if backupPaymentMethod?.userId === $user?.$id} + {#if backupMethod?.userId === $user?.$id} { @@ -323,8 +305,8 @@ {/if} {#if showEdit && isCloud && hasStripePublicKey} + bind:show={showEdit} + selectedPaymentMethod={isSelectedBackup ? backupMethod : primaryMethod} /> {/if} {#if isCloud && hasStripePublicKey} diff --git a/src/routes/(console)/organization-[organization]/billing/replaceCard.svelte b/src/routes/(console)/organization-[organization]/billing/replaceCard.svelte index edb71a016..847ebd016 100644 --- a/src/routes/(console)/organization-[organization]/billing/replaceCard.svelte +++ b/src/routes/(console)/organization-[organization]/billing/replaceCard.svelte @@ -12,17 +12,29 @@ import type { PaymentMethod as StripePaymentMethod } from '@stripe/stripe-js'; import type { Models } from '@appwrite.io/console'; - export let show = false; - export let isBackup = false; - export let organization: Models.Organization; - export let methods: Models.PaymentMethodList; + let { + show = $bindable(false), + isBackup = false, + methods, + organization + }: { + show?: boolean; + isBackup?: boolean; + methods: Models.PaymentMethodList; + organization: Models.Organization; + } = $props(); - let name: string; - let error: string = null; - let selectedPaymentMethodId: string; - let showState: boolean = false; - let state: string = ''; - let paymentMethod: StripePaymentMethod | null = null; + let name: string | null = $state(null); + let error: string | null = $state(null); + let showState: boolean = $state(false); + let countryState: string | null = $state(null); + let paymentMethod: StripePaymentMethod | null = $state(null); + let selectedPaymentMethodId: string | null = $state(null); + + const filteredMethods = $derived(methods?.paymentMethods.filter((method) => !!method?.last4)); + const submitEvent = $derived( + isBackup ? Submit.OrganizationBackupPaymentUpdate : Submit.OrganizationPaymentUpdate + ); onMount(async () => { if (!organization.paymentMethodId && !organization.backupPaymentMethodId) { @@ -44,12 +56,12 @@ async function handleSubmit() { try { if (selectedPaymentMethodId === '$new') { - if (showState && !state) { + if (showState && !countryState) { throw Error('Please select a state'); } let method: Models.PaymentMethod; if (showState) { - method = await setPaymentMethod(paymentMethod.id, name, state); + method = await setPaymentMethod(paymentMethod.id, name, countryState); } else { const card = await submitStripeCard(name, organization.$id); if (card && Object.hasOwn(card, 'id')) { @@ -68,21 +80,17 @@ ? await addBackupPaymentMethod(selectedPaymentMethodId) : await addPaymentMethod(selectedPaymentMethodId); + await invalidate(Dependencies.PAYMENT_METHODS); + addNotification({ type: 'success', message: `Your ${isBackup ? 'backup' : 'default'} payment method has been updated` }); - invalidate(Dependencies.ORGANIZATION); - trackEvent( - isBackup ? Submit.OrganizationBackupPaymentDelete : Submit.OrganizationPaymentDelete - ); + trackEvent(submitEvent); show = false; - } catch (e) { - error = e.message; - trackError( - e, - isBackup ? Submit.OrganizationBackupPaymentDelete : Submit.OrganizationPaymentDelete - ); + } catch (err) { + error = err.message; + trackError(err, submitEvent); } } @@ -92,8 +100,8 @@ organizationId: organization.$id, paymentMethodId }); - } catch (e) { - error = e.message; + } catch (err) { + error = err.message; } } @@ -103,23 +111,21 @@ organizationId: organization.$id, paymentMethodId }); - } catch (e) { - error = e.message; + } catch (err) { + error = err.message; } } - - $: filteredMethods = methods?.paymentMethods.filter((method) => !!method?.last4);

Replace the existing payment method for your organization.

.layout-level-progress-bars { gap: 1rem; - z-index: 2; + z-index: 100; display: flex; flex-direction: column; diff --git a/src/routes/(console)/project-[region]-[project]/auth/+page.svelte b/src/routes/(console)/project-[region]-[project]/auth/+page.svelte index 391274650..e8b9c7a29 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/+page.svelte @@ -9,6 +9,7 @@ import { AvatarInitials, Copy, + type DeleteOperation, type DeleteOperationState, Empty, EmptySearch, @@ -60,20 +61,22 @@ ); } - async function handleDelete(selectedRows: string[]): Promise { - const promises = selectedRows.map((userId) => { - return sdk.forProject(page.params.region, page.params.project).users.delete({ userId }); - }); + async function handleDelete(batchDelete: DeleteOperation): Promise { + const result = await batchDelete((userId) => + sdk.forProject(page.params.region, page.params.project).users.delete({ userId }) + ); try { - await Promise.all(promises); - trackEvent(Submit.UserDelete, { total: selectedRows.length }); - } catch (error) { - trackError(error, Submit.UserDelete); - return error; + if (result.error) { + trackError(result.error, Submit.UserDelete); + } else { + trackEvent(Submit.UserDelete, { total: result.deleted.length }); + } } finally { await invalidate(Dependencies.USERS); } + + return result; } diff --git a/src/routes/(console)/project-[region]-[project]/auth/security/+page.svelte b/src/routes/(console)/project-[region]-[project]/auth/security/+page.svelte index 48718cba3..385cfe1f5 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/security/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/security/+page.svelte @@ -1,26 +1,23 @@ - - - - - + + diff --git a/src/routes/(console)/project-[region]-[project]/auth/security/passwordPolicies.svelte b/src/routes/(console)/project-[region]-[project]/auth/security/passwordPolicies.svelte new file mode 100644 index 000000000..22898b060 --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/auth/security/passwordPolicies.svelte @@ -0,0 +1,159 @@ + + +
+ + Password policies + + + + + + Enabling this option prevents users from reusing recent passwords by + comparing the new password with their password history. + + {#if passwordHistoryEnabled} + + {/if} + + + + + + + + Enabling this option prevents users from setting insecure passwords by + comparing the user's password with the 10k most commonly used passwords. + + + + + + + + Do not allow passwords that contain any part of the user's personal data. + This includes the user's name, email, or phone. + + + + + + + + + +
diff --git a/src/routes/(console)/project-[region]-[project]/auth/security/sessionSecurity.svelte b/src/routes/(console)/project-[region]-[project]/auth/security/sessionSecurity.svelte new file mode 100644 index 000000000..6babbbecc --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/auth/security/sessionSecurity.svelte @@ -0,0 +1,92 @@ + + +
+ + Session security + + + + + Enabling this option will send an email to the users when a new session is + created. + + + + + + + + Enabling this option will clear all existing sessions when the user changes + their password. + + + + + + + + + +
diff --git a/src/routes/(console)/project-[region]-[project]/auth/security/updatePasswordDictionary.svelte b/src/routes/(console)/project-[region]-[project]/auth/security/updatePasswordDictionary.svelte deleted file mode 100644 index 0e921ab04..000000000 --- a/src/routes/(console)/project-[region]-[project]/auth/security/updatePasswordDictionary.svelte +++ /dev/null @@ -1,61 +0,0 @@ - - -
- - Password dictionary - - - - Enabling this option prevent users from setting insecure passwords by comparing the - user's password with the 10k most commonly used passwords. - - - - - - - -
diff --git a/src/routes/(console)/project-[region]-[project]/auth/security/updatePasswordHistory.svelte b/src/routes/(console)/project-[region]-[project]/auth/security/updatePasswordHistory.svelte deleted file mode 100644 index 0b4218487..000000000 --- a/src/routes/(console)/project-[region]-[project]/auth/security/updatePasswordHistory.svelte +++ /dev/null @@ -1,80 +0,0 @@ - - -
- - Password history - Set the maximum number of passwords saved per user. - - - - Enabling this option prevents users from reusing recent passwords by comparing the - new password with their password history. - - - - - - - - -
diff --git a/src/routes/(console)/project-[region]-[project]/auth/security/updatePersonalDataCheck.svelte b/src/routes/(console)/project-[region]-[project]/auth/security/updatePersonalDataCheck.svelte deleted file mode 100644 index 71f87c24d..000000000 --- a/src/routes/(console)/project-[region]-[project]/auth/security/updatePersonalDataCheck.svelte +++ /dev/null @@ -1,56 +0,0 @@ - - -
- - Personal data - - - - Do not allow passwords that contain any part of the user's personal data. This - includes the user's name, email, or phone. - - - - - - -
diff --git a/src/routes/(console)/project-[region]-[project]/auth/security/updateSessionAlerts.svelte b/src/routes/(console)/project-[region]-[project]/auth/security/updateSessionAlerts.svelte deleted file mode 100644 index ba8af9ec9..000000000 --- a/src/routes/(console)/project-[region]-[project]/auth/security/updateSessionAlerts.svelte +++ /dev/null @@ -1,54 +0,0 @@ - - -
- - Session alerts - - - - Enabling this option will send an email to the users when a new session is created. - - - - - - -
diff --git a/src/routes/(console)/project-[region]-[project]/auth/security/updateSessionInvalidation.svelte b/src/routes/(console)/project-[region]-[project]/auth/security/updateSessionInvalidation.svelte deleted file mode 100644 index c2c449e3e..000000000 --- a/src/routes/(console)/project-[region]-[project]/auth/security/updateSessionInvalidation.svelte +++ /dev/null @@ -1,56 +0,0 @@ - - -
- - Invalidate sessions - - - - Enabling this option will clear all existing sessions when the user changes their - password. - - - - - - - -
diff --git a/src/routes/(console)/project-[region]-[project]/auth/teams/+page.svelte b/src/routes/(console)/project-[region]-[project]/auth/teams/+page.svelte index 18b2eb2d9..8a58aec9a 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/teams/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/teams/+page.svelte @@ -12,6 +12,7 @@ SearchQuery, PaginationWithLimit, type DeleteOperationState, + type DeleteOperation, MultiSelectionTable } from '$lib/components'; import Create from '../createTeam.svelte'; @@ -44,20 +45,22 @@ ); }; - async function handleDelete(selectedRows: string[]): Promise { - const promises = selectedRows.map((teamId) => { - return sdk.forProject(page.params.region, page.params.project).teams.delete({ teamId }); - }); + async function handleDelete(batchDelete: DeleteOperation): Promise { + const result = await batchDelete((teamId) => + sdk.forProject(page.params.region, page.params.project).teams.delete({ teamId }) + ); try { - await Promise.all(promises); - trackEvent(Submit.TeamDelete, { total: selectedRows.length }); - } catch (error) { - trackError(error, Submit.TeamDelete); - return error; + if (result.error) { + trackError(result.error, Submit.TeamDelete); + } else { + trackEvent(Submit.TeamDelete, { total: result.deleted.length }); + } } finally { await invalidate(Dependencies.TEAMS); } + + return result; } diff --git a/src/routes/(console)/project-[region]-[project]/auth/teams/team-[team]/members/+page.svelte b/src/routes/(console)/project-[region]-[project]/auth/teams/team-[team]/members/+page.svelte index 98999a305..d9368560f 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/teams/team-[team]/members/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/teams/team-[team]/members/+page.svelte @@ -6,6 +6,7 @@ AvatarInitials, PaginationWithLimit, MultiSelectionTable, + type DeleteOperation, type DeleteOperationState } from '$lib/components'; import { Button } from '$lib/elements/forms'; @@ -29,23 +30,25 @@ let showDelete = $state(false); let selectedMembership: Models.Membership | null = $state(null); - async function handleBulkDelete(selectedRows: string[]): Promise { - const promises = selectedRows.map((membershipId) => { - return sdk.forProject(page.params.region, page.params.project).teams.deleteMembership({ + async function handleBulkDelete(batchDelete: DeleteOperation): Promise { + const result = await batchDelete((membershipId) => + sdk.forProject(page.params.region, page.params.project).teams.deleteMembership({ teamId: page.params.team, membershipId - }); - }); + }) + ); try { - await Promise.all(promises); - trackEvent(Submit.MembershipUpdate, { total: selectedRows.length }); - } catch (error) { - trackError(error, Submit.MembershipUpdate); - return error; + if (result.error) { + trackError(result.error, Submit.MembershipUpdate); + } else { + trackEvent(Submit.MembershipUpdate, { total: result.deleted.length }); + } } finally { await invalidate(Dependencies.MEMBERSHIPS); } + + return result; } diff --git a/src/routes/(console)/project-[region]-[project]/auth/user-[user]/identities/table.svelte b/src/routes/(console)/project-[region]-[project]/auth/user-[user]/identities/table.svelte index 5d9e956af..e1f053b95 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/user-[user]/identities/table.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/user-[user]/identities/table.svelte @@ -1,5 +1,10 @@ diff --git a/src/routes/(console)/project-[region]-[project]/auth/user-[user]/memberships/+page.svelte b/src/routes/(console)/project-[region]-[project]/auth/user-[user]/memberships/+page.svelte index 0c0b5525b..6f2b29355 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/user-[user]/memberships/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/user-[user]/memberships/+page.svelte @@ -4,6 +4,7 @@ import { AvatarInitials, type DeleteOperationState, + type DeleteOperation, MultiSelectionTable } from '$lib/components'; import { Button } from '$lib/elements/forms'; @@ -23,14 +24,14 @@ let showDelete = $state(false); let selectedMembership: Models.Membership | null = $state(null); - async function handleBulkDelete(selectedRows: string[]): Promise { + async function handleBulkDelete(batchDelete: DeleteOperation): Promise { // Precompute a lookup map from membershipId to teamId for efficient access const membershipIdToTeamId: Record = {}; for (const membership of data.memberships.memberships) { membershipIdToTeamId[membership.$id] = membership.teamId; } - const promises = selectedRows.map((membershipId) => + const result = await batchDelete((membershipId) => sdk.forProject(page.params.region, page.params.project).teams.deleteMembership({ teamId: membershipIdToTeamId[membershipId] || '', membershipId @@ -38,14 +39,16 @@ ); try { - await Promise.all(promises); - trackEvent(Submit.MembershipUpdate, { total: selectedRows.length }); - } catch (error) { - trackError(error, Submit.MembershipUpdate); - return error; + if (result.error) { + trackError(result.error, Submit.MembershipUpdate); + } else { + trackEvent(Submit.MembershipUpdate, { total: result.deleted.length }); + } } finally { await invalidate(Dependencies.MEMBERSHIPS); } + + return result; } diff --git a/src/routes/(console)/project-[region]-[project]/auth/user-[user]/sessions/+page.svelte b/src/routes/(console)/project-[region]-[project]/auth/user-[user]/sessions/+page.svelte index b7def9138..77feda0fb 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/user-[user]/sessions/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/user-[user]/sessions/+page.svelte @@ -1,6 +1,13 @@ diff --git a/src/routes/(console)/project-[region]-[project]/databases/+page.svelte b/src/routes/(console)/project-[region]-[project]/databases/+page.svelte index e95c01cca..adba24b05 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/+page.svelte @@ -14,9 +14,11 @@ import Table from './table.svelte'; import { registerCommands } from '$lib/commandCenter'; import { canWriteDatabases } from '$lib/stores/roles'; - import { Icon } from '@appwrite.io/pink-svelte'; + import { Icon, Tooltip } from '@appwrite.io/pink-svelte'; import { IconPlus } from '@appwrite.io/pink-icons-svelte'; import EmptySearch from '$lib/components/emptySearch.svelte'; + import { isServiceLimited } from '$lib/stores/billing'; + import { organization } from '$lib/stores/organization'; export let data: PageData; @@ -30,6 +32,8 @@ ); } + $: isLimited = isServiceLimited('databases', $organization?.billingPlan, data.databases.total); + $: $registerCommands([ { label: 'Create database', @@ -37,7 +41,7 @@ showCreate = true; }, keys: ['c'], - disabled: showCreate || !$canWriteDatabases, + disabled: showCreate || !$canWriteDatabases || isLimited, icon: IconPlus, group: 'databases', rank: 10 @@ -52,10 +56,20 @@ bind:view={data.view} searchPlaceholder="Search by name or ID"> {#if $canWriteDatabases} - + +
+ +
+ + You have reached the maximum number of databases for your plan. + +
{/if} @@ -63,7 +77,11 @@ {#if data.view === 'grid'} {:else} - +
{/if} , policies: Record; + let lastBackups: Record = {}; + let policies: Record = {}; if (isCloud && backupsEnabled) { [policies, lastBackups] = await Promise.all([ @@ -85,19 +85,21 @@ async function fetchDatabasesAndBackups( async function fetchPolicies(databases: Models.DatabaseList, params: RouteParams) { if (isSelfHosted) return {}; - const databasePolicies: Record = {}; + const databasePolicies: Record = {}; await Promise.all( databases.databases.map(async (database) => { try { const { policies } = await sdk .forProject(params.region, params.project) - .backups.listPolicies([ - // TODO: are all needed!? - // Query.limit(3), - Query.equal('resourceType', 'database'), - Query.equal('resourceId', database.$id) - ]); + .backups.listPolicies({ + queries: [ + // TODO: are all needed!? + // Query.limit(3), + Query.equal('resourceType', 'database'), + Query.equal('resourceId', database.$id) + ] + }); if (policies.length > 0) { databasePolicies[database.$id] = policies; @@ -121,12 +123,14 @@ async function fetchLastBackups(databases: Models.DatabaseList, params: RoutePar try { const { archives } = await sdk .forProject(params.region, params.project) - .backups.listArchives([ - Query.limit(1), - Query.orderDesc('$createdAt'), - Query.equal('resourceType', 'database'), - Query.equal('resourceId', database.$id) - ]); + .backups.listArchives({ + queries: [ + Query.limit(1), + Query.orderDesc('$createdAt'), + Query.equal('resourceType', 'database'), + Query.equal('resourceId', database.$id) + ] + }); if (archives.length > 0) { lastBackups[database.$id] = timeFromNow(archives[0].$createdAt); diff --git a/src/routes/(console)/project-[region]-[project]/databases/create.svelte b/src/routes/(console)/project-[region]-[project]/databases/create.svelte index f00b68e59..3fc1736dd 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/create.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/create.svelte @@ -61,16 +61,14 @@ const totalPoliciesPromise = totalPolicies.map((policy) => { cronExpression(policy); - return sdk - .forProject(page.params.region, page.params.project) - .backups.createPolicy( - ID.unique(), - ['databases'], - policy.retained, - policy.schedule, - policy.label, - resourceId - ); + return sdk.forProject(page.params.region, page.params.project).backups.createPolicy({ + policyId: ID.unique(), + services: ['databases'], + retention: policy.retained, + schedule: policy.schedule, + name: policy.label, + resourceId + }); }); await Promise.all(totalPoliciesPromise); diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/backups/+page.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/backups/+page.svelte index 99521078d..e64490541 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/backups/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/backups/+page.svelte @@ -66,9 +66,10 @@ const createManualBackup = async () => { try { - await sdk - .forProject(page.params.region, page.params.project) - .backups.createArchive(['databases'], data.database.$id); + await sdk.forProject(page.params.region, page.params.project).backups.createArchive({ + services: ['databases'], + resourceId: data.database.$id + }); await invalidate(Dependencies.BACKUPS); addNotification({ type: 'success', @@ -119,16 +120,14 @@ const totalPoliciesPromise = totalPolicies.map((policy) => { cronExpression(policy); - return sdk - .forProject(page.params.region, page.params.project) - .backups.createPolicy( - ID.unique(), - ['databases'], - policy.retained, - policy.schedule, - policy.label, - data.database.$id - ); + return sdk.forProject(page.params.region, page.params.project).backups.createPolicy({ + policyId: ID.unique(), + services: ['databases'], + retention: policy.retained, + schedule: policy.schedule, + name: policy.label, + resourceId: data.database.$id + }); }); try { diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/backups/+page.ts b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/backups/+page.ts index 52d9e65c9..60d9bfb21 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/backups/+page.ts +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/backups/+page.ts @@ -2,8 +2,8 @@ import { getLimit, getPage, getView, pageToOffset, View } from '$lib/helpers/loa 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'; +import { type Models } from '@appwrite.io/console'; export const load = async ({ params, url, route, depends, parent }) => { depends(Dependencies.BACKUPS); @@ -13,8 +13,8 @@ export const load = async ({ params, url, route, depends, parent }) => { const view = getView(url, route, View.Grid); const offset = pageToOffset(page, limit); - let backups: BackupArchiveList = { total: 0, archives: [] }; - let policies: BackupPolicyList = { total: 0, policies: [] }; + let backups: Models.BackupArchiveList = { total: 0, archives: [] }; + let policies: Models.BackupPolicyList = { total: 0, policies: [] }; // already loaded by parent. const { currentPlan } = await parent(); @@ -23,23 +23,23 @@ export const load = async ({ params, url, route, depends, parent }) => { if (isCloud && backupsEnabled) { try { [backups, policies] = await Promise.all([ - sdk - .forProject(params.region, params.project) - .backups.listArchives([ + sdk.forProject(params.region, params.project).backups.listArchives({ + queries: [ Query.limit(limit), Query.offset(offset), Query.orderDesc('$createdAt'), Query.equal('resourceType', 'database'), Query.equal('resourceId', params.database) - ]), + ] + }), - sdk - .forProject(params.region, params.project) - .backups.listPolicies([ + sdk.forProject(params.region, params.project).backups.listPolicies({ + queries: [ Query.orderDesc('$createdAt'), Query.equal('resourceType', 'database'), Query.equal('resourceId', params.database) - ]) + ] + }) ]); } catch (e) { // ignore @@ -59,17 +59,17 @@ export const load = async ({ params, url, route, depends, parent }) => { }; }; -const groupArchivesByPolicy = (archives: BackupArchive[]) => { +const groupArchivesByPolicy = (archives: Models.BackupArchive[]) => { return archives.reduce((acc, archive) => { if (!acc.has(archive.policyId)) { acc.set(archive.policyId, []); } acc.get(archive.policyId)!.push(archive); return acc; - }, new Map()); + }, new Map()); }; -const getLatestBackupForPolicies = (policyIdMap: Map) => { +const getLatestBackupForPolicies = (policyIdMap: Map) => { const latestBackups = new Map(); for (const [policyId, archives] of policyIdMap) { const latestBackup = archives.sort( diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/backups/policy.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/backups/policy.svelte index e4ccd8b91..f73fc68a9 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/backups/policy.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/backups/policy.svelte @@ -11,7 +11,6 @@ import { toLocaleDateTime } from '$lib/helpers/date'; import EmptyDark from '$lib/images/backups/backups-dark.png'; import EmptyLight from '$lib/images/backups/backups-light.png'; - import type { BackupPolicy, BackupPolicyList } from '$lib/sdk/backups'; import { backupFrequencies } from '$lib/helpers/backups'; import { Click, trackEvent } from '$lib/actions/analytics'; import { @@ -27,21 +26,22 @@ import { Confirm } from '$lib/components/index.js'; import Ellipse from './components/Ellipse.svelte'; import { page } from '$app/state'; + import { type Models } from '@appwrite.io/console'; let showDelete = false; - let selectedPolicy: BackupPolicy = null; + let selectedPolicy: Models.BackupPolicy = null; let showEveryPolicy = false; export let showCreatePolicy = false; - export let policies: BackupPolicyList; + export let policies: Models.BackupPolicyList; export let lastBackupDates: Record; async function deletePolicy() { try { - await sdk - .forProject(page.params.region, page.params.project) - .backups.deletePolicy(selectedPolicy.$id); + await sdk.forProject(page.params.region, page.params.project).backups.deletePolicy({ + policyId: selectedPolicy.$id + }); addNotification({ type: 'success', message: 'Backup policy has been deleted' diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/backups/table.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/backups/table.svelte index d33593160..5cd807592 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/backups/table.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/backups/table.svelte @@ -3,6 +3,7 @@ Card, Confirm, type DeleteOperationState, + type DeleteOperation, Modal, MultiSelectionTable } from '$lib/components'; @@ -14,11 +15,10 @@ import { addNotification } from '$lib/stores/notifications'; import { invalidate } from '$app/navigation'; import { calculateSize } from '$lib/helpers/sizeConvertion'; - import { ID } from '@appwrite.io/console'; + import { ID, type Models } from '@appwrite.io/console'; import { columns } from './store'; import { database } from '../store'; - import type { BackupArchive, BackupPolicy } from '$lib/sdk/backups'; - import { Click, trackEvent } from '$lib/actions/analytics'; + import { Click, Submit, trackError, trackEvent } from '$lib/actions/analytics'; import { copy } from '$lib/helpers/copy'; import { LabelCard } from '$lib/components/index.js'; import DualTimeView from '$lib/components/dualTimeView.svelte'; @@ -52,7 +52,7 @@ } = $props(); let showDelete = $state(false); - let selectedBackup: BackupArchive | null = $state(null); + let selectedBackup: Models.BackupArchive | null = $state(null); let showDropdown = []; @@ -86,15 +86,15 @@ ); }); - function getPolicyDetails(policyId: string | null): BackupPolicy | null { + function getPolicyDetails(policyId: string | null): Models.BackupPolicy | null { return data.policies.policies.find((policy) => policy.$id === policyId); } - function getCleanBackupName(backup: BackupArchive): string { + function getCleanBackupName(backup: Models.BackupArchive): string { return toLocaleDateTime(backup.$createdAt).replaceAll(',', ''); } - function getBackupStatus(backup: BackupArchive) { + function getBackupStatus(backup: Models.BackupArchive) { switch (backup.status) { case 'pending': return 'pending'; @@ -110,39 +110,46 @@ } } - async function deleteBackups(selectedRows: string[]): Promise { - const promises = selectedRows.map((archiveId) => { - return sdk + async function deleteSingleBackup(archiveId: string) { + try { + await sdk .forProject(page.params.region, page.params.project) - .backups.deleteArchive(archiveId); - }); + .backups.deleteArchive({ archiveId }); + + addNotification({ + type: 'success', + message: 'Backup deleted' + }); + + showDelete = false; + selectedBackup = null; + await invalidate(Dependencies.BACKUPS); + } catch (error) { + addNotification({ + type: 'error', + message: error.message + }); + } + } + + async function deleteBackups(batchDelete: DeleteOperation): Promise { + const result = await batchDelete((archiveId) => + sdk + .forProject(page.params.region, page.params.project) + .backups.deleteArchive({ archiveId }) + ); try { - await Promise.all(promises); - - if (selectedBackup) { - addNotification({ - type: 'success', - message: '1 backup deleted' - }); - } - } catch (error) { - if (selectedBackup) { - addNotification({ - type: 'error', - message: error.message - }); + if (result.error) { + trackError(result.error, Submit.DatabaseBackupDelete); } else { - return error; + trackEvent(Submit.DatabaseBackupDelete); } } finally { - if (selectedBackup) { - showDelete = false; - selectedBackup = null; - } - await invalidate(Dependencies.BACKUPS); } + + return result; } async function restoreBackup() { @@ -154,12 +161,12 @@ try { await sdk .forProject(page.params.region, page.params.project) - .backups.createRestoration( - selectedBackup.$id, - ['databases'], - newDatabaseInfo.id ?? ID.unique(), - newDatabaseInfo.name - ); + .backups.createRestoration({ + archiveId: selectedBackup.$id, + services: ['databases'], + newResourceId: newDatabaseInfo.id ?? ID.unique(), + newResourceName: newDatabaseInfo.name + }); await invalidate(Dependencies.BACKUPS); addNotification({ @@ -299,7 +306,7 @@ bind:open={showDelete} onSubmit={async () => { if (!selectedBackup) return; - await deleteBackups([selectedBackup.$id]); + await deleteSingleBackup(selectedBackup.$id); }}> Are you sure you want to delete the {getCleanBackupName(selectedBackup)} backup? diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/columns/float.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/columns/float.svelte index bf86bf0c3..3badf6d94 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/columns/float.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/columns/float.svelte @@ -86,7 +86,7 @@ label="Min" placeholder="Enter size" bind:value={data.min} - step={0.1} + step="any" {disabled} required={editing} /> @@ -95,7 +95,7 @@ label="Max" placeholder="Enter size" bind:value={data.max} - step={0.1} + step="any" {disabled} required={editing} /> @@ -109,7 +109,7 @@ bind:value={data.default} disabled={data.required || data.array || disabled} nullable={!data.required && !data.array} - step={0.1} /> + step="any" /> diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/rows/columns/types/relationship.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/rows/columns/types/relationship.svelte index f790579c5..fccc94df4 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/rows/columns/types/relationship.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/rows/columns/types/relationship.svelte @@ -297,7 +297,7 @@ { + const success = await updateRowContents(row); + if (success) { + // database update succeeded! + paginatedRows.update(index, row); + } + return success; + }} noInlineEdit={isRelatedToMany && hasItems} onChange={(row) => paginatedRows.update(index, row)} onRevert={(row) => paginatedRows.update(index, row)} diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/store.ts b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/store.ts index 6c6a28489..913bba1e3 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/store.ts +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/store.ts @@ -193,7 +193,9 @@ export const expandTabs = writable(null); export const spreadsheetRenderKey = writable('initial'); export const paginatedRowsLoading = writable(false); -export const paginatedRows = createSparsePagedDataStore(SPREADSHEET_PAGE_LIMIT); +export const paginatedRows = createSparsePagedDataStore( + SPREADSHEET_PAGE_LIMIT +); export const PROHIBITED_ROW_KEYS = [ '$id', diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table.svelte index fa19cde37..6a5c73352 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table.svelte @@ -3,7 +3,12 @@ import { resolve } from '$app/paths'; import { page } from '$app/state'; import { Submit, trackError, trackEvent } from '$lib/actions/analytics'; - import { Id, MultiSelectionTable, type DeleteOperationState } from '$lib/components'; + import { + Id, + MultiSelectionTable, + type DeleteOperation, + type DeleteOperationState + } from '$lib/components'; import { Dependencies } from '$lib/constants'; import DualTimeView from '$lib/components/dualTimeView.svelte'; import { canWriteTables } from '$lib/stores/roles'; @@ -20,23 +25,26 @@ data: PageData; } = $props(); - async function onDelete(selectedTables: string[]): Promise { - const promises = selectedTables.map((tableId) => + async function onDelete(batchDelete: DeleteOperation): Promise { + const result = await batchDelete((tableId) => sdk.forProject(page.params.region, page.params.project).tablesDB.deleteTable({ databaseId: page.params.database, tableId }) ); + try { - await Promise.all(promises); - trackEvent(Submit.TableDelete); - } catch (error) { - trackError(error, Submit.TableDelete); - return error; + if (result.error) { + trackError(result.error, Submit.TableDelete); + } else { + trackEvent(Submit.TableDelete, { total: result.deleted.length }); + } } finally { await invalidate(Dependencies.TABLES); subNavigation.update(); } + + return result; } function getTableHref(table: Models.Table) { diff --git a/src/routes/(console)/project-[region]-[project]/databases/table.svelte b/src/routes/(console)/project-[region]-[project]/databases/table.svelte index 10ec6f307..0764bf9ea 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/table.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/table.svelte @@ -2,14 +2,23 @@ import { base } from '$app/paths'; import { page } from '$app/state'; import { Id } from '$lib/components'; - import { toLocaleDateTime } from '$lib/helpers/date'; + import DualTimeView from '$lib/components/dualTimeView.svelte'; import { columns } from './store'; import { IconExclamation } from '@appwrite.io/pink-icons-svelte'; import { Layout, Tooltip, Table, Icon } from '@appwrite.io/pink-svelte'; - import type { BackupPolicy } from '$lib/sdk/backups'; + import { type Models } from '@appwrite.io/console'; - export let data; - const tables = data.tables; + let { + tables, + policies, + databases, + lastBackups + }: { + tables: Record; + databases: Models.DatabaseList; + lastBackups: Record; + policies: Record; + } = $props(); function getPolicyDescription(cron: string): string { const [minute, hour, dayOfMonth, , dayOfWeek] = cron.split(' '); @@ -19,6 +28,10 @@ if (minute !== '*' && hour === '*') return 'Hourly'; if (hour !== '*') return 'Daily'; } + + function getPoliciesDescription(policies: Models.BackupPolicy[] | null): string { + return policies?.map((policy) => getPolicyDescription(policy.schedule)).join(', ') ?? ''; + } @@ -27,7 +40,7 @@ {title} {/each} - {#each data.databases.databases as database (database.$id)} + {#each databases.databases as database (database.$id)} {@const tableId = tables[database?.$id] ?? null} {@const tableHref = tableId ? `/table-${tableId}` : ''} @@ -45,18 +58,16 @@ {:else if column.id === 'name'} {database.name} {:else if column.id === 'backup'} - {@const policies = data.policies?.[database.$id] ?? null} - {@const lastBackup = data.lastBackups?.[database.$id] ?? null} - {@const description = policies - ?.map((policy: BackupPolicy) => getPolicyDescription(policy.schedule)) - .join(', ')} + {@const backupPolicies = policies?.[database.$id] ?? null} + {@const lastBackup = lastBackups?.[database.$id] ?? null} + {@const description = getPoliciesDescription(backupPolicies)} - {#if !policies} + {#if !backupPolicies} + {:else if column.type === 'datetime'} + {:else} - {toLocaleDateTime(database[column.id])} + {database[column.id]} {/if} {/each} diff --git a/src/routes/(console)/project-[region]-[project]/functions/create-function/manual/+page.svelte b/src/routes/(console)/project-[region]-[project]/functions/create-function/manual/+page.svelte index e53837412..1a2b1ab93 100644 --- a/src/routes/(console)/project-[region]-[project]/functions/create-function/manual/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/functions/create-function/manual/+page.svelte @@ -1,6 +1,6 @@ diff --git a/src/routes/(console)/project-[region]-[project]/functions/function-[function]/(modals)/createManual.svelte b/src/routes/(console)/project-[region]-[project]/functions/function-[function]/(modals)/createManual.svelte index 03e7f1dcf..acb5ece7a 100644 --- a/src/routes/(console)/project-[region]-[project]/functions/function-[function]/(modals)/createManual.svelte +++ b/src/routes/(console)/project-[region]-[project]/functions/function-[function]/(modals)/createManual.svelte @@ -6,15 +6,14 @@ import { Button } from '$lib/elements/forms'; import { InvalidFileType, removeFile } from '$lib/helpers/files'; import { addNotification } from '$lib/stores/notifications'; - import { sdk } from '$lib/stores/sdk'; import { IconInfo } from '@appwrite.io/pink-icons-svelte'; import { Icon, Layout, Tooltip, Typography, Upload } from '@appwrite.io/pink-svelte'; import { func } from '../store'; - import { page } from '$app/state'; import { consoleVariables } from '$routes/(console)/store'; import { currentPlan } from '$lib/stores/organization'; import { isCloud } from '$lib/system'; import { humanFileSize } from '$lib/helpers/sizeConvertion'; + import { uploader } from '$lib/stores/uploader'; export let show = false; @@ -30,17 +29,16 @@ async function create() { try { - await sdk - .forProject(page.params.region, page.params.project) - .functions.createDeployment({ - functionId: $func.$id, - code: files[0], - activate: true - }); - await invalidate(Dependencies.DEPLOYMENTS); - files = undefined; + uploader.uploadFunctionDeployment({ + functionId: $func.$id, + code: files[0] + }); show = false; - trackEvent(Submit.DeploymentCreate); + files = undefined; + await invalidate(Dependencies.DEPLOYMENTS); + trackEvent(Submit.DeploymentCreate, { + type: 'function' + }); addNotification({ type: 'success', message: 'Deployment created successfully' diff --git a/src/routes/(console)/project-[region]-[project]/functions/function-[function]/deployment-[deployment]/+page.svelte b/src/routes/(console)/project-[region]-[project]/functions/function-[function]/deployment-[deployment]/+page.svelte index 9a9b47b2e..140524354 100644 --- a/src/routes/(console)/project-[region]-[project]/functions/function-[function]/deployment-[deployment]/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/functions/function-[function]/deployment-[deployment]/+page.svelte @@ -41,11 +41,7 @@ let { data } = $props(); let effectiveStatus = $derived( - getEffectiveBuildStatus( - data.deployment.status, - data.deployment.$createdAt, - $regionalConsoleVariables - ) + getEffectiveBuildStatus(data.deployment, $regionalConsoleVariables) ); let showDelete = $state(false); let showCancel = $state(false); diff --git a/src/routes/(console)/project-[region]-[project]/functions/function-[function]/domains/+page.ts b/src/routes/(console)/project-[region]-[project]/functions/function-[function]/domains/+page.ts index 4145bf828..5fdc16ceb 100644 --- a/src/routes/(console)/project-[region]-[project]/functions/function-[function]/domains/+page.ts +++ b/src/routes/(console)/project-[region]-[project]/functions/function-[function]/domains/+page.ts @@ -29,6 +29,7 @@ export const load: PageLoad = async ({ depends, params, url, route, parent }) => Query.limit(limit), Query.offset(offset), Query.orderDesc(''), + Query.orderDesc('$updatedAt'), ...parsedQueries.values() ], search: search || undefined diff --git a/src/routes/(console)/project-[region]-[project]/functions/function-[function]/domains/store.ts b/src/routes/(console)/project-[region]-[project]/functions/function-[function]/domains/store.ts index f741c03ee..5f36c6bc1 100644 --- a/src/routes/(console)/project-[region]-[project]/functions/function-[function]/domains/store.ts +++ b/src/routes/(console)/project-[region]-[project]/functions/function-[function]/domains/store.ts @@ -15,5 +15,12 @@ export const columns = writable([ title: 'Target', type: 'string', width: { min: 120, max: 400 } + }, + + { + id: 'updated', + title: '', + type: 'string', + width: { min: 160, max: 180 } } ]); diff --git a/src/routes/(console)/project-[region]-[project]/functions/function-[function]/domains/table.svelte b/src/routes/(console)/project-[region]-[project]/functions/function-[function]/domains/table.svelte index 98e29e5ba..83daf1ffe 100644 --- a/src/routes/(console)/project-[region]-[project]/functions/function-[function]/domains/table.svelte +++ b/src/routes/(console)/project-[region]-[project]/functions/function-[function]/domains/table.svelte @@ -25,6 +25,7 @@ import { regionalProtocol } from '$routes/(console)/project-[region]-[project]/store'; import DnsRecordsAction from '$lib/components/domains/dnsRecordsAction.svelte'; import ViewLogsModal from '$lib/components/domains/viewLogsModal.svelte'; + import { timeFromNowShort } from '$lib/helpers/date'; let { proxyRules, @@ -46,6 +47,28 @@ ? 'Deployed from ' + proxy.deploymentVcsProviderBranch : 'Active deployment'; }; + + function updatedLabel(proxyRule: Models.ProxyRule): string { + if (proxyRule.status === 'verified') { + return ''; + } + + const timeStr = timeFromNowShort(proxyRule.$updatedAt); + if (timeStr === 'n/a') { + return ''; + } + + const prefix = + proxyRule.status === 'created' + ? 'Checked' + : proxyRule.status === 'verifying' + ? 'Updated' + : proxyRule.status === 'unverified' + ? 'Failed' + : ''; + + return prefix + ' ' + timeStr; + } @@ -117,6 +140,15 @@ {:else if column.id === 'target'} {proxyTarget(proxyRule)} + {:else if column.id === 'updated' && proxyRule.status !== 'verified'} + + + {updatedLabel(proxyRule)} + + {/if} {/each} diff --git a/src/routes/(console)/project-[region]-[project]/functions/function-[function]/executions/table.svelte b/src/routes/(console)/project-[region]-[project]/functions/function-[function]/executions/table.svelte index f681c04ef..1e6915c38 100644 --- a/src/routes/(console)/project-[region]-[project]/functions/function-[function]/executions/table.svelte +++ b/src/routes/(console)/project-[region]-[project]/functions/function-[function]/executions/table.svelte @@ -1,5 +1,10 @@ diff --git a/src/routes/(console)/project-[region]-[project]/functions/function-[function]/table.svelte b/src/routes/(console)/project-[region]-[project]/functions/function-[function]/table.svelte index 7b46acc09..a70bf52d5 100644 --- a/src/routes/(console)/project-[region]-[project]/functions/function-[function]/table.svelte +++ b/src/routes/(console)/project-[region]-[project]/functions/function-[function]/table.svelte @@ -1,5 +1,10 @@ @@ -87,8 +95,7 @@ {#snippet children(root)} {#each data.deploymentList.deployments as deployment (deployment.$id)} {@const effectiveStatus = getEffectiveBuildStatus( - deployment.status, - deployment.$createdAt, + deployment, $regionalConsoleVariables )} { - const promises = selectedRows.map((id) => + async function handleDelete(batchDelete: DeleteOperation): Promise { + const result = await batchDelete((id) => sdk .forProject(page.params.region, page.params.project) .messaging.delete({ messageId: id }) ); try { - await Promise.all(promises); - trackEvent(Submit.MessagingMessageDelete, { total: selectedRows.length }); - } catch (error) { - trackError(error, Submit.MessagingMessageDelete); - return error; + if (result.error) { + trackError(result.error, Submit.MessagingMessageDelete); + } else { + trackEvent(Submit.MessagingMessageDelete, { total: result.deleted.length }); + } } finally { await invalidate(Dependencies.MESSAGING_MESSAGES); } + + return result; } onMount(() => { diff --git a/src/routes/(console)/project-[region]-[project]/messaging/providers/table.svelte b/src/routes/(console)/project-[region]-[project]/messaging/providers/table.svelte index a54178b3d..f7ed22281 100644 --- a/src/routes/(console)/project-[region]-[project]/messaging/providers/table.svelte +++ b/src/routes/(console)/project-[region]-[project]/messaging/providers/table.svelte @@ -1,7 +1,12 @@ diff --git a/src/routes/(console)/project-[region]-[project]/messaging/topics/table.svelte b/src/routes/(console)/project-[region]-[project]/messaging/topics/table.svelte index f5525754e..709529b0f 100644 --- a/src/routes/(console)/project-[region]-[project]/messaging/topics/table.svelte +++ b/src/routes/(console)/project-[region]-[project]/messaging/topics/table.svelte @@ -1,7 +1,12 @@ diff --git a/src/routes/(console)/project-[region]-[project]/messaging/topics/topic-[topic]/table.svelte b/src/routes/(console)/project-[region]-[project]/messaging/topics/topic-[topic]/table.svelte index c9a64e9af..113d9d76e 100644 --- a/src/routes/(console)/project-[region]-[project]/messaging/topics/topic-[topic]/table.svelte +++ b/src/routes/(console)/project-[region]-[project]/messaging/topics/topic-[topic]/table.svelte @@ -2,7 +2,12 @@ import { invalidate } from '$app/navigation'; import { base } from '$app/paths'; import { Submit, trackError, trackEvent } from '$lib/actions/analytics'; - import { type DeleteOperationState, Id, MultiSelectionTable } from '$lib/components'; + import { + type DeleteOperationState, + type DeleteOperation, + Id, + MultiSelectionTable + } from '$lib/components'; import { Dependencies } from '$lib/constants'; import type { PageData } from './$types'; import ProviderType from '../../providerType.svelte'; @@ -32,8 +37,8 @@ return record; }); - async function handleDelete(selectedRows: string[]): Promise { - async function deleteSubscriber(subscriberId: string) { + async function handleDelete(batchDelete: DeleteOperation): Promise { + const result = await batchDelete(async (subscriberId) => { await sdk .forProject(page.params.region, page.params.project) .messaging.deleteSubscriber({ @@ -44,19 +49,19 @@ const { target } = subscribers[subscriberId]; const { [target.$id]: _, ...rest } = $targetsById; $targetsById = rest; - } - - const promises = selectedRows.map((id) => deleteSubscriber(id)); + }); try { - await Promise.all(promises); - trackEvent(Submit.MessagingTopicSubscriberDelete, { total: selectedRows.length }); - } catch (error) { - trackError(error, Submit.MessagingTopicSubscriberDelete); - return error; + if (result.error) { + trackError(result.error, Submit.MessagingTopicSubscriberDelete); + } else { + trackEvent(Submit.MessagingTopicSubscriberDelete, { total: result.deleted.length }); + } } finally { await invalidate(Dependencies.MESSAGING_TOPIC_SUBSCRIBERS); } + + return result; } diff --git a/src/routes/(console)/project-[region]-[project]/overview/(components)/table.svelte b/src/routes/(console)/project-[region]-[project]/overview/(components)/table.svelte index 9cfb56a83..535be9bd6 100644 --- a/src/routes/(console)/project-[region]-[project]/overview/(components)/table.svelte +++ b/src/routes/(console)/project-[region]-[project]/overview/(components)/table.svelte @@ -67,7 +67,7 @@ allowSelection={$canWriteKeys} showSuccessNotification={false} resource={`${capitalize(label)} key`} - onDelete={(selectedRows) => { + onDelete={(_, selectedRows) => { showDeleteModal = true; selectedKeys = selectedRows; }}> diff --git a/src/routes/(console)/project-[region]-[project]/overview/platforms/+page.svelte b/src/routes/(console)/project-[region]-[project]/overview/platforms/+page.svelte index 22d0bce77..de3cbcb2f 100644 --- a/src/routes/(console)/project-[region]-[project]/overview/platforms/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/overview/platforms/+page.svelte @@ -81,7 +81,11 @@ import { page } from '$app/state'; import type { PageProps } from './$types'; import type { Models } from '@appwrite.io/console'; - import { type DeleteOperationState, MultiSelectionTable } from '$lib/components'; + import { + type DeleteOperationState, + type DeleteOperation, + MultiSelectionTable + } from '$lib/components'; import { sdk } from '$lib/stores/sdk'; import { Submit, trackError } from '$lib/actions/analytics'; import { invalidate } from '$app/navigation'; @@ -129,24 +133,26 @@ } async function handlePlatformDelete( - selectedPlatforms: string[] + batchDelete: DeleteOperation ): Promise { - const promises = selectedPlatforms.map((platformId) => { - return sdk.forConsole.projects.deletePlatform({ + const result = await batchDelete((platformId) => + sdk.forConsole.projects.deletePlatform({ projectId: page.params.project, platformId - }); - }); + }) + ); try { - await Promise.all(promises); - trackEvent(Submit.PlatformDelete, { total: selectedPlatforms.length }); - } catch (error) { - trackError(error, Submit.PlatformDelete); - return error; + if (result.error) { + trackError(result.error, Submit.PlatformDelete); + } else { + trackEvent(Submit.PlatformDelete, { total: result.deleted.length }); + } } finally { await invalidate(Dependencies.PROJECT); } + + return result; } setOverviewAction(Action); diff --git a/src/routes/(console)/project-[region]-[project]/settings/+page.svelte b/src/routes/(console)/project-[region]-[project]/settings/+page.svelte index edf09b92f..8c427cc23 100644 --- a/src/routes/(console)/project-[region]-[project]/settings/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/settings/+page.svelte @@ -15,6 +15,7 @@ import ChangeOrganization from './changeOrganization.svelte'; import UpdateVariables from '../updateVariables.svelte'; import { page } from '$app/state'; + import UpdateLabels from './updateLabels.svelte'; export let data; @@ -84,8 +85,9 @@ {#if $project} - {#if $canWriteProjects} + + const { organization } = await parent(); const rules = await sdk.forProject(params.region, params.project).proxy.listRules({ - queries: [Query.equal('type', RuleType.API), Query.equal('trigger', RuleTrigger.MANUAL)], + queries: [ + Query.equal('type', RuleType.API), + Query.equal('trigger', RuleTrigger.MANUAL), + Query.orderDesc('$updatedAt') + ], search: search || undefined }); diff --git a/src/routes/(console)/project-[region]-[project]/settings/domains/table.svelte b/src/routes/(console)/project-[region]-[project]/settings/domains/table.svelte index 594988a7f..051a20d3a 100644 --- a/src/routes/(console)/project-[region]-[project]/settings/domains/table.svelte +++ b/src/routes/(console)/project-[region]-[project]/settings/domains/table.svelte @@ -24,6 +24,7 @@ import { regionalProtocol } from '../../store'; import DnsRecordsAction from '$lib/components/domains/dnsRecordsAction.svelte'; import ViewLogsModal from '$lib/components/domains/viewLogsModal.svelte'; + import { timeFromNowShort } from '$lib/helpers/date'; let { domains, @@ -45,8 +46,36 @@ type: 'string', format: 'string', width: { min: 300, max: 550 } + }, + { + id: 'updated', + title: '', + type: 'string', + width: { min: 160, max: 180 } } ]; + + function updatedLabel(proxyRule: Models.ProxyRule): string { + if (proxyRule.status === 'verified') { + return ''; + } + + const timeStr = timeFromNowShort(proxyRule.$updatedAt); + if (timeStr === 'n/a') { + return ''; + } + + const prefix = + proxyRule.status === 'created' + ? 'Checked' + : proxyRule.status === 'verifying' + ? 'Updated' + : proxyRule.status === 'unverified' + ? 'Failed' + : ''; + + return prefix + ' ' + timeStr; + } @@ -116,6 +145,15 @@ {/if} + {:else if column.id === 'updated' && proxyRule.status !== 'verified'} + + + {updatedLabel(proxyRule)} + + {/if} {/each} diff --git a/src/routes/(console)/project-[region]-[project]/settings/updateLabels.svelte b/src/routes/(console)/project-[region]-[project]/settings/updateLabels.svelte new file mode 100644 index 000000000..21d324ec4 --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/settings/updateLabels.svelte @@ -0,0 +1,102 @@ + + +
+ + Labels + Categorize and manage your projects for easy searching based on specific criteria by assigning + them customizable labels. + + + {#key labels.length} + + {/key} + + {#each suggestedLabels as suggestedLabel} + { + if (!labels.includes(suggestedLabel)) { + labels = [...labels, suggestedLabel]; + } else { + labels = labels.filter((e) => e !== suggestedLabel); + } + }}> + + {suggestedLabel} + + {/each} + + {error ? error : 'Only alphanumeric characters are allowed'} + + + + + + + + diff --git a/src/routes/(console)/project-[region]-[project]/settings/webhooks/+page.svelte b/src/routes/(console)/project-[region]-[project]/settings/webhooks/+page.svelte index 4ee496dec..e7ec5e8d1 100644 --- a/src/routes/(console)/project-[region]-[project]/settings/webhooks/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/settings/webhooks/+page.svelte @@ -2,7 +2,7 @@ import { base } from '$app/paths'; import { page } from '$app/state'; import { Empty, Id } from '$lib/components'; - import { toLocaleDateTime } from '$lib/helpers/date'; + import DualTimeView from '$lib/components/dualTimeView.svelte'; import { Container } from '$lib/layout'; import { Button } from '$lib/elements/forms'; import type { PageData } from './$types'; @@ -58,7 +58,11 @@ {:else if column.id === 'events'} {webhook.events.length} {:else if column.type === 'datetime'} - {webhook[column.id] ? toLocaleDateTime(webhook[column.id]) : '-'} + {#if webhook[column.id]} + + {:else} + - + {/if} {:else if column.id === 'enabled'} {@const effectiveStatus = getEffectiveBuildStatus( - deployment.status, - deployment.$createdAt, + deployment, $regionalConsoleVariables )} {#if !inCard} diff --git a/src/routes/(console)/project-[region]-[project]/sites/(components)/logs.svelte b/src/routes/(console)/project-[region]-[project]/sites/(components)/logs.svelte index 7fae6d0ab..51c407652 100644 --- a/src/routes/(console)/project-[region]-[project]/sites/(components)/logs.svelte +++ b/src/routes/(console)/project-[region]-[project]/sites/(components)/logs.svelte @@ -6,6 +6,7 @@ case 'ready': return 'success'; case 'building': + case 'finalizing': return 'warning'; case 'processing': return undefined; @@ -40,9 +41,7 @@ emptyCopy?: string; } = $props(); - let effectiveStatus = $derived( - getEffectiveBuildStatus(deployment.status, deployment.$createdAt, $regionalConsoleVariables) - ); + let effectiveStatus = $derived(getEffectiveBuildStatus(deployment, $regionalConsoleVariables)); function setCopy() { if (effectiveStatus === 'failed') { @@ -73,7 +72,7 @@ variant="secondary" type={badgeTypeDeployment(effectiveStatus)} /> - + {/if} diff --git a/src/routes/(console)/project-[region]-[project]/sites/(components)/logsTimer.svelte b/src/routes/(console)/project-[region]-[project]/sites/(components)/logsTimer.svelte index 47bc28da3..79a00f541 100644 --- a/src/routes/(console)/project-[region]-[project]/sites/(components)/logsTimer.svelte +++ b/src/routes/(console)/project-[region]-[project]/sites/(components)/logsTimer.svelte @@ -7,15 +7,13 @@ import { Layout, Spinner, Typography } from '@appwrite.io/pink-svelte'; - let { status, deployment }: { status: string; deployment: Models.Deployment } = $props(); + let { deployment }: { deployment: Models.Deployment } = $props(); - let effectiveStatus = $derived( - getEffectiveBuildStatus(status, deployment.$createdAt, $regionalConsoleVariables) - ); + let effectiveStatus = $derived(getEffectiveBuildStatus(deployment, $regionalConsoleVariables)); - {#if ['processing', 'building'].includes(effectiveStatus)} + {#if ['processing', 'building', 'finalizing'].includes(effectiveStatus)}

diff --git a/src/routes/(console)/project-[region]-[project]/sites/(components)/siteCard.svelte b/src/routes/(console)/project-[region]-[project]/sites/(components)/siteCard.svelte index 78ad644f4..eb392282a 100644 --- a/src/routes/(console)/project-[region]-[project]/sites/(components)/siteCard.svelte +++ b/src/routes/(console)/project-[region]-[project]/sites/(components)/siteCard.svelte @@ -42,9 +42,7 @@ footer?: Snippet; } = $props(); - let effectiveStatus = $derived( - getEffectiveBuildStatus(deployment.status, deployment.$createdAt, $regionalConsoleVariables) - ); + let effectiveStatus = $derived(getEffectiveBuildStatus(deployment, $regionalConsoleVariables)); let show = $state(false); const totalSize = $derived(humanFileSize(deployment?.totalSize ?? 0)); diff --git a/src/routes/(console)/project-[region]-[project]/sites/create-site/deploying/+page.svelte b/src/routes/(console)/project-[region]-[project]/sites/create-site/deploying/+page.svelte index 3cb100543..b41104e09 100644 --- a/src/routes/(console)/project-[region]-[project]/sites/create-site/deploying/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/sites/create-site/deploying/+page.svelte @@ -12,32 +12,76 @@ import { onMount } from 'svelte'; import { getFrameworkIcon } from '$lib/stores/sites'; import type { Models } from '@appwrite.io/console'; + import { getEffectiveBuildStatus } from '$lib/helpers/buildTimeout'; + import { regionalConsoleVariables } from '$routes/(console)/project-[region]-[project]/store'; let { data } = $props(); let deployment = $state(data.deployment); + let skipScreenshotTimeout: ReturnType | null = $state(null); + + let effectiveStatus = $derived(getEffectiveBuildStatus(deployment, $regionalConsoleVariables)); onMount(() => { - return realtime.forConsole(page.params.region, 'console', async (response) => { - if ( - response.events.includes( - `sites.${data.site.$id}.deployments.${data.deployment.$id}.update` - ) - ) { - deployment = response.payload as Models.Deployment; - if (deployment.status === 'ready') { - const resolvedUrl = resolve( - '/(console)/project-[region]-[project]/sites/create-site/finish', - { - region: page.params.region, - project: page.params.project + const timeoutCleanup = () => { + if (skipScreenshotTimeout) { + clearTimeout(skipScreenshotTimeout); + skipScreenshotTimeout = null; + } + }; + + const realtimeUnsubscribe = realtime.forConsole( + page.params.region, + 'console', + async (response) => { + if ( + response.events.includes( + `sites.${data.site.$id}.deployments.${data.deployment.$id}.update` + ) + ) { + deployment = response.payload as Models.Deployment; + + const isReady = deployment.status === 'ready'; + + const isFinished = + isReady && deployment.screenshotLight && deployment.screenshotDark; + + // Fallback mechanism + // If ready but not finished for over 30 seconds, go anyway + if (isReady && !skipScreenshotTimeout) { + skipScreenshotTimeout = setTimeout(async () => { + goToFinishScreen(); + }, 30000); + } + + if (isReady && isFinished) { + if (skipScreenshotTimeout) { + clearTimeout(skipScreenshotTimeout); + skipScreenshotTimeout = null; } - ); - await goto(`${resolvedUrl}?site=${data.site.$id}`); + + goToFinishScreen(); + } } } - }); + ); + + return () => { + realtimeUnsubscribe(); + timeoutCleanup(); + }; }); + + async function goToFinishScreen() { + const resolvedUrl = resolve( + '/(console)/project-[region]-[project]/sites/create-site/finish', + { + region: page.params.region, + project: page.params.project + } + ); + await goto(`${resolvedUrl}?site=${data.site.$id}`); + } - {#if ['processing', 'building'].includes(data.deployment.status)} + {#if ['processing', 'building', 'finalizing'].includes(effectiveStatus)} Deployment will continue in the background diff --git a/src/routes/(console)/project-[region]-[project]/sites/create-site/manual/+page.svelte b/src/routes/(console)/project-[region]-[project]/sites/create-site/manual/+page.svelte index 8cecb4acc..5a4536527 100644 --- a/src/routes/(console)/project-[region]-[project]/sites/create-site/manual/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/sites/create-site/manual/+page.svelte @@ -1,6 +1,6 @@ @@ -74,8 +82,7 @@ {#snippet children(root)} {#each data.deploymentList.deployments as deployment (deployment.$id)} {@const effectiveStatus = getEffectiveBuildStatus( - deployment.status, - deployment.$createdAt, + deployment, $regionalConsoleVariables )} {deployment.$id} - {@const status = deployment.status} {@const effectiveStatus = getEffectiveBuildStatus( - status, - deployment.$createdAt, + deployment, $regionalConsoleVariables )} {#if activeDeployment?.$id === deployment?.$id} diff --git a/src/routes/(console)/project-[region]-[project]/sites/site-[site]/domains/+page.ts b/src/routes/(console)/project-[region]-[project]/sites/site-[site]/domains/+page.ts index a942cc3c0..25c021aa6 100644 --- a/src/routes/(console)/project-[region]-[project]/sites/site-[site]/domains/+page.ts +++ b/src/routes/(console)/project-[region]-[project]/sites/site-[site]/domains/+page.ts @@ -28,6 +28,7 @@ export const load = async ({ params, depends, url, route, parent }) => { Query.limit(limit), Query.offset(offset), Query.orderDesc(''), + Query.orderDesc('$updatedAt'), ...parsedQueries.values() ], search: search || undefined diff --git a/src/routes/(console)/project-[region]-[project]/sites/site-[site]/domains/store.ts b/src/routes/(console)/project-[region]-[project]/sites/site-[site]/domains/store.ts index f741c03ee..5f36c6bc1 100644 --- a/src/routes/(console)/project-[region]-[project]/sites/site-[site]/domains/store.ts +++ b/src/routes/(console)/project-[region]-[project]/sites/site-[site]/domains/store.ts @@ -15,5 +15,12 @@ export const columns = writable([ title: 'Target', type: 'string', width: { min: 120, max: 400 } + }, + + { + id: 'updated', + title: '', + type: 'string', + width: { min: 160, max: 180 } } ]); diff --git a/src/routes/(console)/project-[region]-[project]/sites/site-[site]/domains/table.svelte b/src/routes/(console)/project-[region]-[project]/sites/site-[site]/domains/table.svelte index b89528517..a3921aeaa 100644 --- a/src/routes/(console)/project-[region]-[project]/sites/site-[site]/domains/table.svelte +++ b/src/routes/(console)/project-[region]-[project]/sites/site-[site]/domains/table.svelte @@ -25,6 +25,7 @@ import { regionalProtocol } from '$routes/(console)/project-[region]-[project]/store'; import DnsRecordsAction from '$lib/components/domains/dnsRecordsAction.svelte'; import ViewLogsModal from '$lib/components/domains/viewLogsModal.svelte'; + import { timeFromNowShort } from '$lib/helpers/date'; let { proxyRules, @@ -46,6 +47,28 @@ ? 'Deployed from ' + proxy.deploymentVcsProviderBranch : 'Active deployment'; }; + + function updatedLabel(proxyRule: Models.ProxyRule): string { + if (proxyRule.status === 'verified') { + return ''; + } + + const timeStr = timeFromNowShort(proxyRule.$updatedAt); + if (timeStr === 'n/a') { + return ''; + } + + const prefix = + proxyRule.status === 'created' + ? 'Checked' + : proxyRule.status === 'verifying' + ? 'Updated' + : proxyRule.status === 'unverified' + ? 'Failed' + : ''; + + return prefix + ' ' + timeStr; + } @@ -117,6 +140,15 @@ {:else if column.id === 'target'} {proxyTarget(proxyRule)} + {:else if column.id === 'updated' && proxyRule.status !== 'verified'} + + + {updatedLabel(proxyRule)} + + {/if} {/each} diff --git a/src/routes/(console)/project-[region]-[project]/sites/site-[site]/logs/table.svelte b/src/routes/(console)/project-[region]-[project]/sites/site-[site]/logs/table.svelte index da4890035..de2504715 100644 --- a/src/routes/(console)/project-[region]-[project]/sites/site-[site]/logs/table.svelte +++ b/src/routes/(console)/project-[region]-[project]/sites/site-[site]/logs/table.svelte @@ -1,5 +1,10 @@ diff --git a/src/routes/(console)/project-[region]-[project]/sites/table.svelte b/src/routes/(console)/project-[region]-[project]/sites/table.svelte index 680c82d38..3f15ba97d 100644 --- a/src/routes/(console)/project-[region]-[project]/sites/table.svelte +++ b/src/routes/(console)/project-[region]-[project]/sites/table.svelte @@ -1,7 +1,7 @@