diff --git a/src/lib/actions/analytics.ts b/src/lib/actions/analytics.ts index 6880f1c2f..dea8860b5 100644 --- a/src/lib/actions/analytics.ts +++ b/src/lib/actions/analytics.ts @@ -268,7 +268,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 +277,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 8a9797ca0..8eb379591 100644 --- a/src/lib/components/archiveProject.svelte +++ b/src/lib/components/archiveProject.svelte @@ -1,4 +1,5 @@ @@ -196,7 +207,7 @@
+ badge={`${archivedTotalOverall}`}> {#if isPlanBelowPro} These projects are archived and require a plan upgrade to restore access. @@ -206,7 +217,7 @@
- + {#each projectsToArchive as project} {@const platforms = filterPlatforms( project.platforms.map((platform) => getPlatformInfo(platform.type)) @@ -266,7 +277,7 @@ {/each} - {#if platforms.length > 3} + {#if platforms.length > 2} {/each} + +
@@ -355,4 +375,7 @@ align-items: center; gap: 8px; } + :global(.pagination-container) { + margin-top: 16px; + } diff --git a/src/lib/components/billing/alerts/paymentMandate.svelte b/src/lib/components/billing/alerts/paymentMandate.svelte deleted file mode 100644 index 08d9207d7..000000000 --- a/src/lib/components/billing/alerts/paymentMandate.svelte +++ /dev/null @@ -1,26 +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/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} +
{ - const path = `/account/payment-methods/${paymentMethodId}/setup`; - const params = { - organizationId, - paymentMethodId - }; - const uri = new URL(this.client.config.endpoint + path); - return await this.client.call( - 'patch', - uri, - { - 'content-type': 'application/json' - }, - params - ); - } - async listAddresses(queries: string[] = []): Promise { const path = `/account/billing-addresses`; const params = { diff --git a/src/lib/stores/billing.ts b/src/lib/stores/billing.ts index 695ef874c..df2d29178 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 Size, sizeToBytes } from '$lib/helpers/sizeConvertion'; @@ -18,7 +18,6 @@ import type { Invoice, InvoiceList, PaymentList, - PaymentMethodData, Plan, PlansMap } from '$lib/sdk/billing'; @@ -535,24 +534,6 @@ export function checkForMarkedForDeletion(org: Organization) { } } -export const paymentMissingMandate = writable(null); - -export async function checkForMandate(org: Organization) { - const paymentId = org.paymentMethodId ?? org.backupPaymentMethodId; - if (!paymentId) return; - const paymentMethod = await sdk.forConsole.billing.getPaymentMethod(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.billing.listOrganization([ Query.notEqual('billingPlan', BillingPlan.FREE), diff --git a/src/routes/(console)/+layout.svelte b/src/routes/(console)/+layout.svelte index 109bdabb3..ce28dd7c2 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 ($plansInfo.get(org.billingPlan)?.trialDays) { calculateTrialDay(org); @@ -334,7 +332,6 @@ - {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 ef77274e8..aa1b03b8d 100644 --- a/src/routes/(console)/organization-[organization]/+layout.ts +++ b/src/routes/(console)/organization-[organization]/+layout.ts @@ -24,7 +24,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; @@ -46,13 +46,8 @@ 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(), @@ -96,7 +91,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 | Organization @@ -154,8 +149,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..83c40623c 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(); } @@ -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..b236341d9 100644 --- a/src/routes/(console)/organization-[organization]/+page.ts +++ b/src/routes/(console)/organization-[organization]/+page.ts @@ -5,7 +5,6 @@ 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 +19,69 @@ 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 [activeProjects, archivedProjects, activeTotal, archivedTotal] = await Promise.all([ + sdk.forConsole.projects.list({ + queries: [ + Query.offset(offset), + Query.equal('teamId', params.organization), + Query.or([Query.equal('status', 'active'), Query.isNull('status')]), + Query.limit(limit), + Query.orderDesc('') + ], + search: search || undefined + }), + sdk.forConsole.projects.list({ + queries: [ + Query.offset(archivedOffset), + Query.equal('teamId', params.organization), + Query.equal('status', 'archived'), + Query.limit(limit), + Query.orderDesc('') + ], + search: search || undefined + }), + sdk.forConsole.projects.list({ + queries: [ + Query.equal('teamId', params.organization), + Query.or([Query.equal('status', 'active'), Query.isNull('status')]) + ], + search: search || undefined + }), + sdk.forConsole.projects.list({ + queries: [ + Query.equal('teamId', params.organization), + Query.equal('status', 'archived') + ], + search: search || undefined + }) + ]); // set `default` if no region! - for (const project of projects.projects) { + for (const project of activeProjects.projects) { + project.region ??= 'default'; + } + 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 99729b8e9..51d7fe353 100644 --- a/src/routes/(console)/organization-[organization]/billing/+page.svelte +++ b/src/routes/(console)/organization-[organization]/billing/+page.svelte @@ -9,7 +9,6 @@ import PaymentHistory from './paymentHistory.svelte'; import TaxId from './taxId.svelte'; import { failedInvoice, tierToPlan, upgradeURL, useNewPricingModal } from '$lib/stores/billing'; - import type { PaymentMethodData } from '$lib/sdk/billing'; import { onMount } from 'svelte'; import { page } from '$app/state'; import { confirmPayment } from '$lib/stores/stripe'; @@ -21,22 +20,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: PaymentMethodData) => method.$id === organization?.paymentMethodId - ); - - $: backupPaymentMethod = data?.paymentMethods?.paymentMethods?.find( - (method: PaymentMethodData) => method.$id === organization?.backupPaymentMethodId - ); + $: baseUrl = resolve('/(console)/organization-[organization]/billing', { + organization: organization.$id + }); onMount(async () => { if (page.url.searchParams.has('type')) { @@ -58,7 +50,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}` ); } @@ -115,7 +107,7 @@ {/if} {/if} - {#if defaultPaymentMethod?.failed && !backupPaymentMethod} + {#if data.primaryPaymentMethod?.failed && !data.backupPaymentMethod} @@ -146,7 +138,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 Organization)?.billingAddressId; @@ -82,6 +83,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, @@ -98,6 +100,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: Organization, + paymentMethods: PaymentList +): { + backup: PaymentMethodData | null; + primary: PaymentMethodData | null; +} { + let backup: PaymentMethodData | null = null; + let primary: PaymentMethodData | 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 8f6625179..1de90ac4a 100644 --- a/src/routes/(console)/organization-[organization]/billing/deleteOrgPayment.svelte +++ b/src/routes/(console)/organization-[organization]/billing/deleteOrgPayment.svelte @@ -25,7 +25,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; @@ -34,7 +34,7 @@ showDelete = false; } } - async function removeBackuptMethod() { + async function removeBackupMethod() { if ($organization?.billingPlan !== BillingPlan.FREE && !hasOtherMethod) return; showDelete = false; @@ -45,7 +45,7 @@ message: `The payment method has been removed from ${$organization.name}` }); trackEvent(Submit.OrganizationBackupPaymentDelete); - invalidate(Dependencies.ORGANIZATION); + await invalidate(Dependencies.PAYMENT_METHODS); showDelete = false; } catch (e) { error = e.message; @@ -65,10 +65,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 124518f1e..445c65d10 100644 --- a/src/routes/(console)/organization-[organization]/billing/paymentMethods.svelte +++ b/src/routes/(console)/organization-[organization]/billing/paymentMethods.svelte @@ -34,16 +34,17 @@ IconTrash } from '@appwrite.io/pink-icons-svelte'; - export let organization: Organization; export let methods: PaymentList; + export let organization: Organization; + + export let backupMethod: PaymentMethodData; + export let primaryMethod: PaymentMethodData; let showPayment = false; let showEdit = false; let showDelete = false; let showReplace = false; let isSelectedBackup = false; - let backupPaymentMethod: PaymentMethodData; - let defaultPaymentMethod: PaymentMethodData; async function addPaymentMethod(paymentMethodId: string) { try { @@ -56,7 +57,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', @@ -76,7 +77,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', @@ -86,27 +87,15 @@ } } - $: if (organization?.backupPaymentMethodId) { - sdk.forConsole.billing - .getOrganizationPaymentMethod(organization.$id, organization.backupPaymentMethodId) - .then((res) => (backupPaymentMethod = res)); - } - - $: if (organization?.paymentMethodId) { - sdk.forConsole.billing - .getOrganizationPaymentMethod(organization.$id, 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; @@ -132,14 +121,14 @@ - + - {#if defaultPaymentMethod?.userId === $user?.$id} + {#if primaryMethod?.userId === $user?.$id} { @@ -171,14 +160,14 @@ {#if organization?.backupPaymentMethodId} - + - {#if backupPaymentMethod?.userId === $user?.$id} + {#if backupMethod?.userId === $user?.$id} { @@ -318,8 +307,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 ea4cc7d26..920f7b474 100644 --- a/src/routes/(console)/organization-[organization]/billing/replaceCard.svelte +++ b/src/routes/(console)/organization-[organization]/billing/replaceCard.svelte @@ -13,17 +13,29 @@ import { PaymentBoxes } from '$lib/components/billing'; import type { PaymentMethod } from '@stripe/stripe-js'; - export let organization: Organization; - export let show = false; - export let isBackup = false; - export let methods: PaymentList; + let { + show = $bindable(false), + isBackup = false, + methods, + organization + }: { + show?: boolean; + isBackup?: boolean; + methods: PaymentList; + organization: Organization; + } = $props(); - let name: string; - let error: string = null; - let selectedPaymentMethodId: string; - let showState: boolean = false; - let state: string = ''; - let paymentMethod: PaymentMethod | 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: PaymentMethod | 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) { @@ -45,12 +57,12 @@ async function handleSubmit() { try { if (selectedPaymentMethodId === '$new') { - if (showState && !state) { + if (showState && !countryState) { throw Error('Please select a state'); } let method: PaymentMethodData; 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')) { @@ -69,21 +81,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); } } @@ -93,8 +101,8 @@ organization.$id, paymentMethodId ); - } catch (e) { - error = e.message; + } catch (err) { + error = err.message; } } @@ -104,23 +112,21 @@ 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.

{ - 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..e413399e8 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} 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..7cee5aacc 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'; @@ -18,7 +19,7 @@ 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'; @@ -110,39 +111,44 @@ } } - 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); - }); + + 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() { @@ -299,7 +305,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]/spreadsheet.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/spreadsheet.svelte index d826c2f86..dabc3955c 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/spreadsheet.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/spreadsheet.svelte @@ -630,7 +630,6 @@ permissions: row.$permissions }); - invalidate(Dependencies.ROW); trackEvent(Submit.RowUpdate); addNotification({ message: 'Row has been updated', @@ -1105,7 +1104,14 @@ { + 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..0f88682d9 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/table.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/table.svelte @@ -2,7 +2,7 @@ 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'; @@ -72,8 +72,10 @@ {`Last backup: ${lastBackup}`} + {: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/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..94672a8d6 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 @@ diff --git a/src/routes/(console)/project-[region]-[project]/messaging/+page.svelte b/src/routes/(console)/project-[region]-[project]/messaging/+page.svelte index b096f2561..464237e79 100644 --- a/src/routes/(console)/project-[region]-[project]/messaging/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/messaging/+page.svelte @@ -3,6 +3,7 @@ import { page } from '$app/state'; import { type DeleteOperationState, + type DeleteOperation, Empty, EmptyFilter, EmptySearch, @@ -99,22 +100,24 @@ } ]); - async function handleDelete(selectedRows: string[]): Promise { - 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/domains/+page.ts b/src/routes/(console)/project-[region]-[project]/settings/domains/+page.ts index 39495486c..bf7570e23 100644 --- a/src/routes/(console)/project-[region]-[project]/settings/domains/+page.ts +++ b/src/routes/(console)/project-[region]-[project]/settings/domains/+page.ts @@ -21,7 +21,11 @@ export const load: PageLoad = async ({ depends, url, route, params, parent }) => 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/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'} - import { type DeleteOperationState, Id, MultiSelectionTable } from '$lib/components'; + import { + type DeleteOperationState, + type DeleteOperation, + Id, + MultiSelectionTable + } from '$lib/components'; import type { PageData } from './$types'; import { type Models } from '@appwrite.io/console'; import { formatTimeDetailed } from '$lib/helpers/timeConversion'; @@ -37,25 +42,28 @@ let selectedDeployment: Models.Deployment | null = $state(null); - async function deleteDeployments(selectedRows: string[]): Promise { - const promises = selectedRows.map((deploymentId) => + async function deleteDeployments(batchDelete: DeleteOperation): Promise { + const result = await batchDelete((deploymentId) => sdk.forProject(page.params.region, page.params.project).sites.deleteDeployment({ siteId: page.params.site, deploymentId }) ); + try { - await Promise.all(promises); - trackEvent(Submit.DeploymentDelete); - } catch (error) { - trackError(error, Submit.DeploymentDelete); - return error; + if (result.error) { + trackError(result.error, Submit.DeploymentDelete); + } else { + trackEvent(Submit.DeploymentDelete, { total: result.deleted.length }); + } } finally { await Promise.all([ invalidate(Dependencies.DEPLOYMENTS), invalidate(Dependencies.SITE) ]); } + + return result; } 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 @@