diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml new file mode 100644 index 000000000..6926b0c50 --- /dev/null +++ b/.github/workflows/copilot-setup-steps.yml @@ -0,0 +1,28 @@ +name: Copilot Setup Steps + +# Automatically run the setup steps when they are changed to allow for easy validation, and +# allow manual testing through the repository's "Actions" tab +on: + workflow_dispatch: + push: + paths: + - .github/workflows/copilot-setup-steps.yml + pull_request: + paths: + - .github/workflows/copilot-setup-steps.yml + +jobs: + copilot-setup-steps: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v5 + - name: Use Node.js + uses: actions/setup-node@v6 + with: + node-version: 20 + - name: Install pnpm + uses: pnpm/action-setup@v4 + - name: Install dependencies + run: pnpm install --frozen-lockfile diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..82bcb13c7 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,104 @@ +# Appwrite Console - Copilot Instructions + +## Repository Overview + +Appwrite Console is the web-based GUI for the Appwrite backend-as-a-service platform. Single-page application built with **Svelte 5 + SvelteKit 2**, **TypeScript** (not strict mode), **Vite 7**, tested with **Vitest + Playwright**. Package manager: **pnpm 10.15.1**, Node 20+. ~1500 files with extensive component-based architecture. + +## Critical Build & Test Commands + +### Setup (REQUIRED before any commands) + +1. **Install pnpm**: `npm install -g corepack && corepack enable && corepack prepare pnpm@10.15.1 --activate` +2. **Create .env**: `cp .env.example .env` (configure `PUBLIC_APPWRITE_ENDPOINT` and `PUBLIC_CONSOLE_MODE`) +3. **Configure network access** (if using GitHub Actions or restricted environments): + - Ensure firewall/proxy allows access to: `pkg.pr.new`, `pkg.vc`, `registry.npmjs.org` + - These domains are required for dependencies: `@appwrite.io/console`, `@appwrite.io/pink-icons-svelte`, `@appwrite.io/pink-svelte` + - In GitHub Actions: Use `pnpm/action-setup@v4` which handles registry configuration + - If network errors persist, check proxy settings: `npm config get proxy` and `npm config get https-proxy` +4. **Install dependencies**: `pnpm install --frozen-lockfile` (if pkg.pr.new/pkg.vc fail due to network restrictions, installation may still succeed with cached versions) + +### Development Commands + +**Standard workflow**: `check` → `lint` → `test` → `build` (before committing) + +- `pnpm run check` - TypeScript/Svelte validation (~30-60s) +- `pnpm run lint` - ESLint check (~10-20s) +- `pnpm run format` - Auto-fix Prettier formatting +- `pnpm run test` - Vitest unit tests with TZ=EST (~10-30s) +- `pnpm run build` - Production build via build.js (~60-120s) +- `pnpm dev` - Dev server on port 3000 +- `pnpm run preview` - Preview build on port 4173 +- `pnpm run e2e` - Playwright tests (needs `pnpm exec playwright install --with-deps chromium` first, ~120s+) + +**CI Pipeline** (`.github/workflows/tests.yml`): audit → install → check → lint → test → build + +## Project Structure + +``` +src/ +├── lib/ # Reusable logic ($lib alias) +│ ├── components/ # Feature components (billing, domains, permissions, etc.) +│ ├── elements/ # Basic UI elements +│ ├── helpers/ # Utility functions (array, date, string, etc.) +│ ├── stores/ # Svelte stores for state +│ ├── sdk/ # Appwrite SDK wrappers +│ └── constants.ts, flags.ts, system.ts +├── routes/ +│ ├── (console)/ # Auth-required routes +│ │ ├── organization-[organization]/ +│ │ └── project-[region]-[project]/ # databases, functions, messaging, storage +│ └── (public)/ # Public routes (login, register, auth callbacks) +├── themes/ # Theme definitions ($themes alias) +└── app.html, hooks.{client,server}.ts, service-worker.ts +``` + +**SvelteKit conventions**: `+page.svelte` (component), `+page.ts` (data loader), `+layout.svelte` (wrapper), `+error.svelte` (errors). Groups like `(console)` organize routes without affecting URLs. Dynamic params: `[param]`. + +## Key Configuration + +**svelte.config.js**: Adapter = static SPA (fallback: index.html), base path `/console`, aliases: `$lib`, `$routes`, `$themes` +**vite.config.ts**: Dev port 3000, Vitest (client=jsdom, server=node), test files: `src/**/*.{test,spec}.{js,ts}` +**tsconfig.json**: Extends `.svelte-kit/tsconfig.json`, **NOT strict mode** (`strict: false`) +**eslint.config.js**: Flat config (ESLint 9+), many rules disabled (see TODOs) +**.prettierrc**: 4 spaces, single quotes, 100 char width, no trailing commas + +## Testing + +**Unit (Vitest)**: Tests in `src/lib/helpers/*.test.ts`, run with `TZ=EST` (timezone matters). Setup mocks SvelteKit (`$app/*`) in `vitest-setup-client.ts`. +**E2E (Playwright)**: Tests in `e2e/journeys/*.spec.ts`, needs build+preview on port 4173, retries 3x, timeout 120s, Chromium only. + +## Common Pitfalls + +1. **Blank page in dev**: Disable ad blockers if seeing "Failed to fetch dynamically imported module" (known SvelteKit issue) +2. **Network errors on install**: + - pkg.pr.new/pkg.vc deps may fail due to firewall/proxy restrictions + - Check access: `curl -I https://pkg.pr.new` and `curl -I https://pkg.vc` + - Configure proxy if needed: `npm config set proxy http://proxy:port` and `npm config set https-proxy http://proxy:port` + - GitHub Actions: Ensure runner has internet access; use `pnpm/action-setup@v4` action + - Local dev: Often safe to continue with cached versions if network fails +3. **OOM on build**: Set `NODE_OPTIONS=--max_old_space_size=8192` (like Dockerfile does) +4. **Test failures**: Always use `pnpm run test` (sets TZ=EST), not `vitest` directly +5. **TS errors not showing**: Run `pnpm run check` explicitly (dev server doesn't always surface them) +6. **Format vs lint conflicts**: Run `pnpm run format` before `pnpm run lint` +7. **E2E timeouts**: Wait 120s for preview server startup, tests auto-retry 3x +8. **Stale build**: Clear `.svelte-kit` if changes not reflected: `rm -rf .svelte-kit && pnpm run build` + +## Code Conventions + +- Imports: Use `$lib`, `$routes`, `$themes` aliases +- Components: PascalCase, in `src/lib/components/[feature]/` +- Helpers: Pure functions in `src/lib/helpers/` +- Types: Inline or `.d.ts`, not `.types.ts` files +- Comments: Minimal, use for TODOs or complex logic +- TypeScript: Not strict mode, `any` tolerated + +## Workflow + +1. Run Appwrite backend locally (see [docs](https://appwrite.io/docs/advanced/self-hosting)) +2. Configure `.env` with backend endpoint +3. `pnpm install --frozen-lockfile` +4. `pnpm dev` (hot reload on port 3000) +5. Before commit: `pnpm run check && pnpm run format && pnpm run lint && pnpm run test && pnpm run build` +6. **Take screenshots**: For any UI changes, capture screenshots and include them in the PR description or comments before finalizing + +**Trust these instructions** - only search if incomplete/incorrect. See CONTRIBUTING.md for PR conventions. Use `--frozen-lockfile` always. Docker builds: multi-stage, final image is nginx serving static files from `/console` path. diff --git a/src/lib/components/apiEndpoint.svelte b/src/lib/components/apiEndpoint.svelte new file mode 100644 index 000000000..33e70341d --- /dev/null +++ b/src/lib/components/apiEndpoint.svelte @@ -0,0 +1,15 @@ + + + + + + + API endpoint + + + diff --git a/src/lib/components/backupRestoreBox.svelte b/src/lib/components/backupRestoreBox.svelte index 2f660985e..f1de9d3ba 100644 --- a/src/lib/components/backupRestoreBox.svelte +++ b/src/lib/components/backupRestoreBox.svelte @@ -125,20 +125,18 @@ onMount(() => { // fast path: don't subscribe if org is on a free plan or is self-hosted. - if (isSelfHosted || (isCloud && $organization.billingPlan === BillingPlan.FREE)) return; + if (isSelfHosted || (isCloud && $organization?.billingPlan === BillingPlan.FREE)) return; - return realtime - .forProject(page.params.region, page.params.project) - .subscribe('console', (response) => { - if (!response.channels.includes(`projects.${getProjectId()}`)) return; + return realtime.forProject(page.params.region, 'console', (response) => { + if (!response.channels.includes(`projects.${getProjectId()}`)) return; - if ( - response.events.includes('archives.*') || - response.events.includes('restorations.*') - ) { - updateOrAddItem(response.payload); - } - }); + if ( + response.events.includes('archives.*') || + response.events.includes('restorations.*') + ) { + updateOrAddItem(response.payload); + } + }); }); diff --git a/src/lib/components/copy.svelte b/src/lib/components/copy.svelte index c178913c7..d9a619e01 100644 --- a/src/lib/components/copy.svelte +++ b/src/lib/components/copy.svelte @@ -57,6 +57,7 @@ disabled={tooltipDisabled} portal={tooltipPortal} delay={tooltipDelay} + maxWidth="500px" placement={tooltipPlacement}> ; @@ -28,7 +35,7 @@ * The structure is as follows - * `{ migrationId: { status: status, table: table } }` */ - const importItems: Writable = writable(new Map()); + let importItems = $state(new Map()); async function showCompletionNotification(database: string, table: string, payload: Payload) { const isSuccess = payload.status === 'completed'; @@ -37,13 +44,9 @@ if (!isSuccess && !isError) return; let errorMessage = 'Import failed. Check your CSV for correct fields and required values.'; - if (isError && Array.isArray(payload.errors)) { - try { - // the `errors` is a list of json encoded string. - errorMessage = JSON.parse(payload.errors[0]).message; - } catch { - // do nothing, fallback to default message. - } + const errors = getErrors(payload); + if (errors) { + errorMessage = extractErrorMessage(errors); } const type = isSuccess ? 'success' : 'error'; @@ -73,7 +76,7 @@ const resourceId = importData.resourceId ?? ''; const [databaseId, tableId] = resourceId.split(':') ?? []; - const current = $importItems.get(importData.$id); + const current = importItems.get(importData.$id); let tableName = current?.table ?? null; if (!tableName && tableId) { @@ -91,30 +94,27 @@ } if (tableId && tableName === null) { - importItems.update((items) => { - const next = new Map(items); - next.delete(importData.$id); - return next; - }); + const next = new Map(importItems); + next.delete(importData.$id); + importItems = next; return; } - importItems.update((items) => { - const existing = items.get(importData.$id); + const existing = importItems.get(importData.$id); - const isDone = (s: string) => s === 'completed' || s === 'failed'; - const isInProgress = (s: string) => ['pending', 'processing', 'uploading'].includes(s); + const isDone = (s: string) => s === 'completed' || s === 'failed'; + const isInProgress = (s: string) => ['pending', 'processing', 'uploading'].includes(s); - const shouldSkip = - (existing && isDone(existing.status) && isInProgress(status)) || - existing?.status === status; + const shouldSkip = + (existing && isDone(existing.status) && isInProgress(status)) || + existing?.status === status; - if (shouldSkip) return items; - - const next = new Map(items); - next.set(importData.$id, { status, table: tableName ?? undefined }); - return next; - }); + if (!shouldSkip) { + const next = new Map(importItems); + const errors = getErrors(importData); + next.set(importData.$id, { status, table: tableName ?? undefined, errors }); + importItems = next; + } if (status === 'completed' || status === 'failed') { await showCompletionNotification(databaseId, tableId, importData); @@ -122,10 +122,27 @@ } function clear() { - importItems.update((items) => { - items.clear(); - return items; - }); + importItems = new Map(); + } + + function getErrors(importData: Payload | Models.Migration): string[] | undefined { + return Array.isArray(importData.errors) ? importData.errors : undefined; + } + + function parseError(error: string): string | CsvImportError { + try { + return JSON.parse(error) as CsvImportError; + } catch { + return error; + } + } + + function extractErrorMessage(errors: string[]): string { + try { + return JSON.parse(errors[0]).message; + } catch { + return 'Import failed. Check your CSV for correct fields and required values.'; + } } function graphSize(status: string): number { @@ -148,8 +165,9 @@ const name = collectionName ? `${collectionName}` : ''; switch (status) { case 'completed': + return `CSV import completed${name ? ` to ${name}` : ''}`; case 'failed': - return `Import to ${name} ${status}`; + return `CSV import failed${name ? ` to ${name}` : ''}`; case 'processing': return `Importing CSV file${name ? ` to ${name}` : ''}`; default: @@ -169,7 +187,7 @@ migrations.migrations.forEach(updateOrAddItem); }); - return sdk.forConsoleIn(page.params.region).realtime.subscribe('console', (response) => { + return realtime.forConsole(page.params.region, 'console', (response) => { if (!response.channels.includes(`projects.${getProjectId()}`)) return; if (response.events.includes('migrations.*')) { updateOrAddItem(response.payload as Payload); @@ -177,8 +195,18 @@ }); }); - $: isOpen = true; - $: showCsvImportBox = $importItems.size > 0; + let isOpen = $state(true); + let showCsvImportBox = $derived(importItems.size > 0); + + let showDetails = $state(false); + let selectedErrors = $state([]); + let parsedErrors = $state>([]); + + function openDetails(errors: string[] | undefined) { + selectedErrors = errors ?? []; + parsedErrors = selectedErrors.map(parseError); + showDetails = true; + } {#if showCsvImportBox} @@ -187,26 +215,23 @@

- Importing rows ({$importItems.size}) + Importing rows ({importItems.size})

-
- {#each [...$importItems.entries()] as [key, value] (key)} + {#each [...importItems.entries()] as [key, value] (key)}
  • @@ -222,6 +247,25 @@ class:is-danger={value.status === 'failed'} style="--graph-size:{graphSize(value.status)}%">
+ {#if value.status === 'failed'} + + + + There was an import issue. + openDetails(value.errors)} + >View details + + + {/if} @@ -232,6 +276,18 @@ {/if} + + + + + + + + diff --git a/src/lib/components/sidebar.svelte b/src/lib/components/sidebar.svelte index 6a694d6e9..4ed4b7920 100644 --- a/src/lib/components/sidebar.svelte +++ b/src/lib/components/sidebar.svelte @@ -12,8 +12,7 @@ Button, Layout, Avatar, - Typography, - Badge + Typography } from '@appwrite.io/pink-svelte'; import { @@ -80,13 +79,7 @@ { name: 'Functions', icon: IconLightningBolt, slug: 'functions', category: 'build' }, { name: 'Messaging', icon: IconChatBubble, slug: 'messaging', category: 'build' }, { name: 'Storage', icon: IconFolder, slug: 'storage', category: 'build' }, - { - name: 'Sites', - icon: IconGlobeAlt, - slug: 'sites', - category: 'deploy', - badge: 'Early access' - } + { name: 'Sites', icon: IconGlobeAlt, slug: 'sites', category: 'deploy' } ]; const isSelected = (service: string): boolean => { @@ -239,12 +232,6 @@ class:has-text={state === 'open'} class="link-text"> {projectOption.name} - {#if projectOption?.badge} - - {/if} {projectOption.name} diff --git a/src/lib/elements/link.svelte b/src/lib/elements/link.svelte index 6e75e98a5..444a94d18 100644 --- a/src/lib/elements/link.svelte +++ b/src/lib/elements/link.svelte @@ -29,6 +29,7 @@ {#if href} {:else} - + {/if} diff --git a/src/lib/stores/billing.ts b/src/lib/stores/billing.ts index d23fa3346..46134dde3 100644 --- a/src/lib/stores/billing.ts +++ b/src/lib/stores/billing.ts @@ -488,6 +488,9 @@ export async function paymentExpired(org: Organization) { const nots = get(notifications); const expiredNotification = nots.some((n) => n.message === expiredMessage); const expiringNotification = nots.some((n) => n.message === expiringMessage); + const cardExpiry = new Date(payment.expiryYear, payment.expiryMonth, 1); + const nextMonth = new Date(year, month + 1, 1); + const isExpiringNextMonth = cardExpiry.getTime() === nextMonth.getTime(); if (payment.expired && !expiredNotification) { addNotification({ type: 'error', @@ -503,7 +506,7 @@ export async function paymentExpired(org: Organization) { } ] }); - } else if (!expiringNotification && payment.expiryYear <= year && payment.expiryMonth < month) { + } else if (!expiringNotification && !payment.expired && isExpiringNextMonth) { addNotification({ type: 'warning', isHtml: true, diff --git a/src/lib/stores/sdk.ts b/src/lib/stores/sdk.ts index 8fd1b477b..92c5a53cf 100644 --- a/src/lib/stores/sdk.ts +++ b/src/lib/stores/sdk.ts @@ -42,7 +42,6 @@ import { SUBDOMAIN_TOR } from '$lib/constants'; import { building } from '$app/environment'; -import { getProjectId } from '$lib/helpers/project'; export function getApiEndpoint(region?: string): string { if (building) return ''; @@ -141,12 +140,32 @@ const sdkForProject = { }; export const realtime = { - forProject(region: string, _projectId: string) { + forProject( + region: string, + channels: string | string[], + callback: AppwriteRealtimeResponseEvent + ) { const endpoint = getApiEndpoint(region); if (endpoint !== clientRealtime.config.endpoint) { clientRealtime.setEndpoint(endpoint); } - return clientRealtime; + + // because uses a different client! + const realtime = new Realtime(clientRealtime); + + return createRealtimeSubscription(realtime, channels, callback); + }, + + forConsole( + region: string, + channels: string | string[], + callback: AppwriteRealtimeResponseEvent + ): () => void { + const realtimeInstance = region + ? sdk.forConsoleIn(region).realtime + : sdk.forConsole.realtime; + + return createRealtimeSubscription(realtimeInstance, channels, callback); } }; @@ -176,8 +195,8 @@ export const sdk = { }; export enum RuleType { - DEPLOYMENT = 'deployment', API = 'api', + DEPLOYMENT = 'deployment', REDIRECT = 'redirect' } @@ -191,11 +210,24 @@ export enum RuleTrigger { MANUAL = 'manual' } -/** - * Some type imports are broken on the SDK, this works correctly for the time being! - */ -export type AppwriteRealtimeSubscription = Awaited>; - -export const createAdminClient = () => { - return new Client().setEndpoint(getApiEndpoint()).setMode('admin').setProject(getProjectId()); +export type RealtimeResponse = { + events: string[]; + channels: string[]; + timestamp: string; + payload: unknown; }; + +export type AppwriteRealtimeResponseEvent = (response: RealtimeResponse) => void; + +function createRealtimeSubscription( + realtimeInstance: Realtime, + channels: string | string[], + callback: AppwriteRealtimeResponseEvent +): () => void { + const channelsArray = Array.isArray(channels) ? channels : [channels]; + const subscriptionPromise = realtimeInstance.subscribe(channelsArray, callback); + + return () => { + subscriptionPromise.then((sub) => sub.close()); + }; +} diff --git a/src/routes/(console)/project-[region]-[project]/+layout.svelte b/src/routes/(console)/project-[region]-[project]/+layout.svelte index f233956c0..426821259 100644 --- a/src/routes/(console)/project-[region]-[project]/+layout.svelte +++ b/src/routes/(console)/project-[region]-[project]/+layout.svelte @@ -27,15 +27,13 @@ import CsvImportBox from '$lib/components/csvImportBox.svelte'; onMount(() => { - return realtime - .forProject(page.params.region, page.params.project) - .subscribe(['project', 'console'], (response) => { - if (response.events.includes('stats.connections')) { - for (const [projectId, value] of Object.entries(response.payload)) { - stats.add(projectId, [new Date(response.timestamp).toISOString(), value]); - } + return realtime.forProject(page.params.region, ['project', 'console'], (response) => { + if (response.events.includes('stats.connections')) { + for (const [projectId, value] of Object.entries(response.payload)) { + stats.add(projectId, [new Date(response.timestamp).toISOString(), value]); } - }); + } + }); }); $: $registerCommands([ diff --git a/src/routes/(console)/project-[region]-[project]/auth/+page.svelte b/src/routes/(console)/project-[region]-[project]/auth/+page.svelte index 80f6ed5cc..391274650 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/+page.svelte @@ -1,4 +1,4 @@ - @@ -9,8 +9,10 @@ import { AvatarInitials, Copy, + type DeleteOperationState, Empty, EmptySearch, + MultiSelectionTable, PaginationWithLimit, SearchQuery } from '$lib/components'; @@ -20,14 +22,7 @@ import type { Models } from '@appwrite.io/console'; import { writable } from 'svelte/store'; import Create from './createUser.svelte'; - import { - Badge, - Icon, - Table, - Layout, - Typography, - FloatingActionBar - } from '@appwrite.io/pink-svelte'; + import { Badge, Icon, Table, Layout, Typography } from '@appwrite.io/pink-svelte'; import { Tag } from '@appwrite.io/pink-svelte'; import { IconDuplicate, IconPlus } from '@appwrite.io/pink-icons-svelte'; import { canWriteUsers } from '$lib/stores/roles'; @@ -37,11 +32,11 @@ import { sdk } from '$lib/stores/sdk'; import { Submit, trackError, trackEvent } from '$lib/actions/analytics'; import { Dependencies } from '$lib/constants'; - import { addNotification } from '$lib/stores/notifications'; import { invalidate } from '$app/navigation'; - import Confirm from '$lib/components/confirm.svelte'; - export let data; + import type { PageProps } from './$types'; + + let { data }: PageProps = $props(); const columns = writable([ { id: '$id', title: 'User ID', type: 'string', width: 200 }, @@ -59,44 +54,25 @@ } ]); - let selectedUsers: string[] = []; - let showDelete = false; - let deleting = false; - async function userCreated(event: CustomEvent>>) { await goto( `${base}/project-${page.params.region}-${page.params.project}/auth/user-${event.detail.$id}` ); } - async function handleDelete() { - showDelete = false; - deleting = true; - - const promises = selectedUsers.map((userId) => - sdk.forProject(page.params.region, page.params.project).users.delete(userId) - ); + async function handleDelete(selectedRows: string[]): Promise { + const promises = selectedRows.map((userId) => { + return sdk.forProject(page.params.region, page.params.project).users.delete({ userId }); + }); try { await Promise.all(promises); - trackEvent(Submit.UserDelete, { - total: selectedUsers.length - }); - addNotification({ - type: 'success', - message: `${selectedUsers.length} user${selectedUsers.length > 1 ? 's' : ''} deleted` - }); - invalidate(Dependencies.USERS); + trackEvent(Submit.UserDelete, { total: selectedRows.length }); } catch (error) { - addNotification({ - type: 'error', - message: error.message - }); trackError(error, Submit.UserDelete); + return error; } finally { - selectedUsers = []; - showDelete = false; - deleting = false; + await invalidate(Dependencies.USERS); } } @@ -116,101 +92,111 @@ {#if data.users.total} - - + onDelete={handleDelete} + allowSelection={$canWriteUsers}> + {#snippet header(root)} {#each $columns as { id, title } (id)} {title} {/each} - - {#each data.users.users as user} - - {#each $columns as { id } (id)} - - {#if id === '$id'} - - - - {user.$id} - - - {:else if id === 'name'} - - {#if user.email || user.phone} - {#if user.name} - + {/snippet} + + {#snippet children(root)} + {#each data.users.users as user} + + {#each $columns as { id } (id)} + + {#if id === '$id'} + + + + {user.$id} + + + {:else if id === 'name'} + + {#if user.email || user.phone} + {#if user.name} + + + {user.name} + + {:else} +
+ +
+ {/if} + {:else} +
+ +
{user.name} - {:else} -
- -
{/if} +
+ {:else if id === 'identifiers'} + + {user.email && user.phone + ? [user.email, user.phone].join(',') + : user.email || user.phone} + + {:else if id === 'status'} + {#if user.status} + {@const success = + user.emailVerification || user.phoneVerification} + {:else} -
- -
- - {user.name} - + + {/if} + {:else if id === 'labels'} + + {user.labels.join(', ')} + + {:else if id === 'joined'} + + {:else if id === 'lastActivity'} + {#if user.accessedAt} + + {:else} + never {/if} -
- {:else if id === 'identifiers'} - - {user.email && user.phone - ? [user.email, user.phone].join(',') - : user.email || user.phone} - - {:else if id === 'status'} - {#if user.status} - {@const success = - user.emailVerification || user.phoneVerification} - {:else} - + {user[id]} {/if} - {:else if id === 'labels'} - - {user.labels.join(', ')} - - {:else if id === 'joined'} - - {:else if id === 'lastActivity'} - {#if user.accessedAt} - - {:else} - never - {/if} - {:else} - {user[id]} - {/if} -
- {/each} -
- {/each} -
+ + {/each} + + {/each} + {/snippet} + + {#snippet deleteContentNotice()} + This action is irreversible and will permanently remove the selected users and all + their data. + {/snippet} + + secondary + href={`${base}/project-${page.params.region}-${page.params.project}/auth`} + >Clear Search {:else} showCreateUser.set(true)} /> {/if} - - {#if selectedUsers.length > 0} - - - - - {selectedUsers.length > 1 ? 'users' : 'user'} - selected - - - - - - - - {/if} - - - - Are you sure you want to delete {selectedUsers.length} - {selectedUsers.length > 1 ? 'users' : 'user'}? - - - This action is irreversible and will permanently remove the selected users and all their - data. - - 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 3a64c2500..18b2eb2d9 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/teams/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/teams/+page.svelte @@ -1,4 +1,4 @@ - @@ -10,7 +10,9 @@ EmptySearch, AvatarInitials, SearchQuery, - PaginationWithLimit + PaginationWithLimit, + type DeleteOperationState, + MultiSelectionTable } from '$lib/components'; import Create from '../createTeam.svelte'; import { goto } from '$app/navigation'; @@ -19,31 +21,16 @@ import type { Models } from '@appwrite.io/console'; import { writable } from 'svelte/store'; import { canWriteTeams } from '$lib/stores/roles'; - import { - Icon, - Layout, - Table, - FloatingActionBar, - Badge, - Typography - } from '@appwrite.io/pink-svelte'; + import { Icon, Layout, Table } from '@appwrite.io/pink-svelte'; import { IconPlus } from '@appwrite.io/pink-icons-svelte'; import DualTimeView from '$lib/components/dualTimeView.svelte'; import { sdk } from '$lib/stores/sdk'; import { Submit, trackError, trackEvent } from '$lib/actions/analytics'; import { Dependencies } from '$lib/constants'; - import { addNotification } from '$lib/stores/notifications'; import { invalidate } from '$app/navigation'; - import Confirm from '$lib/components/confirm.svelte'; + import type { PageProps } from './$types'; - export let data; - - const region = page.params.region; - const project = page.params.project; - - let selectedTeams: string[] = []; - let showDelete = false; - let deleting = false; + let { data }: PageProps = $props(); const columns = writable([ { id: 'name', title: 'Name', type: 'string', width: { min: 200, max: 300 } }, @@ -52,37 +39,24 @@ ]); const teamCreated = async (event: CustomEvent>>) => { - await goto(`${base}/project-${region}-${project}/auth/teams/team-${event.detail.$id}`); + await goto( + `${base}/project-${page.params.region}-${page.params.project}/auth/teams/team-${event.detail.$id}` + ); }; - async function handleDelete() { - showDelete = false; - deleting = true; - - const promises = selectedTeams.map((teamId) => - sdk.forProject(page.params.region, page.params.project).teams.delete(teamId) - ); + async function handleDelete(selectedRows: string[]): Promise { + const promises = selectedRows.map((teamId) => { + return sdk.forProject(page.params.region, page.params.project).teams.delete({ teamId }); + }); try { await Promise.all(promises); - trackEvent(Submit.TeamDelete, { - total: selectedTeams.length - }); - addNotification({ - type: 'success', - message: `${selectedTeams.length} team${selectedTeams.length > 1 ? 's' : ''} deleted` - }); - invalidate(Dependencies.TEAMS); + trackEvent(Submit.TeamDelete, { total: selectedRows.length }); } catch (error) { - addNotification({ - type: 'error', - message: error.message - }); trackError(error, Submit.TeamDelete); + return error; } finally { - selectedTeams = []; - showDelete = false; - deleting = false; + await invalidate(Dependencies.TEAMS); } } @@ -101,54 +75,47 @@ {#if data.teams.total} - - + onDelete={handleDelete} + allowSelection={$canWriteTeams}> + {#snippet header(root)} {#each $columns as { id, title }} {title} {/each} - - {#each data.teams.teams as team (team.$id)} - - {#each $columns as column} - - {#if column.id === 'name'} - - - {team.name} - - {:else if column.id === 'members'} - {team.total} members - {:else if column.id === 'created'} - - {/if} - - {/each} - - {/each} - + {/snippet} - {#if selectedTeams.length > 0} - - - - - {selectedTeams.length > 1 ? 'teams' : 'team'} - selected - - - - - - - - {/if} + {#snippet children(root)} + {@const TableRowComponent = $canWriteTeams ? Table.Row.Link : Table.Row.Base} + {#each data.teams.teams as team (team.$id)} + {@const href = $canWriteTeams + ? `${base}/project-${page.params.region}-${page.params.project}/auth/teams/team-${team.$id}` + : undefined} + + {#each $columns as column} + + {#if column.id === 'name'} + + + {team.name} + + {:else if column.id === 'members'} + {team.total} members + {:else if column.id === 'created'} + + {/if} + + {/each} + + {/each} + {/snippet} + + {#snippet deleteContentNotice()} + This action is irreversible and will permanently remove the selected teams and all + their memberships. + {/snippet} + {:else if data.search} - @@ -172,14 +142,3 @@ - - - - Are you sure you want to delete {selectedTeams.length} - {selectedTeams.length > 1 ? 'teams' : 'team'}? - - - This action is irreversible and will permanently remove the selected teams and all their - memberships. - - 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 629c5bdda..98999a305 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 @@ -1,77 +1,50 @@ @@ -85,71 +58,62 @@ {#if data.memberships.total} - - + {#snippet header(root)} Name Roles Joined - - {#each data.memberships.memberships as membership (membership.$id)} - {@const username = membership.userName ? membership.userName : '-'} - - - - - {username} - - - - {membership.roles} - - - - - - - - - {/each} - + {/snippet} - {#if selectedMemberships.length > 0} - - - - - {selectedMemberships.length > 1 ? 'memberships' : 'membership'} - selected - - - - - - - - {/if} + {#snippet children(root)} + {#each data.memberships.memberships as membership (membership.$id)} + {@const username = membership.userName ? membership.userName : '-'} + + + + + {username} + + + + {membership.roles} + + + + + + + + + {/each} + {/snippet} + + {#snippet deleteContentNotice()} + This action is irreversible and will remove the selected members from this team. + {/snippet} + - - invalidate(Dependencies.MEMBERSHIPS)} /> + invalidate(Dependencies.MEMBERSHIPS)} /> - - - Are you sure you want to delete {selectedMemberships.length} - {selectedMemberships.length > 1 ? 'memberships' : 'membership'}? - - - This action is irreversible and will remove the selected members from this team. - - + invalidate(Dependencies.MEMBERSHIPS)} /> 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 13e55efa8..5d9e956af 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,122 +1,88 @@ - - + + {#snippet header(root)} {#each columns as { id, title }} {title} {/each} - - {#each data.identities.identities as identity (identity.$id)} - - {#each columns as column} - - {#if column.id === '$id'} - {#key columns} - - {identity[column.id]} - - {/key} - {:else if column.id === 'provider'} - {@const provider = oAuthProviders[identity[column.id]]} -
-
- {provider.name} + {/snippet} + + {#snippet children(root)} + {#each data.identities.identities as identity (identity.$id)} + + {#each columns as column} + + {#if column.id === '$id'} + {#key columns} + + {identity[column.id]} + + {/key} + {:else if column.id === 'provider'} + {@const provider = oAuthProviders[identity[column.id]]} +
+
+ {provider.name} +
+ {provider.name}
- {provider.name} -
- {:else if column.type === 'datetime'} - {#if !identity[column.id]} - - + {:else if column.type === 'datetime'} + {#if !identity[column.id]} + - + {:else} + + {/if} {:else} - + {identity[column.id]} {/if} - {:else} - {identity[column.id]} - {/if} - - {/each} - - {/each} - - -{#if selectedIds.length > 0} - - - - - {selectedIds.length > 1 ? 'identities' : 'identity'} - selected - - - - - - - -{/if} - - - - Are you sure you want to delete {selectedIds.length} - {selectedIds.length > 1 ? 'identities' : 'identity'}? - - + + {/each} + + {/each} + {/snippet} + 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 0deaed8da..0c0b5525b 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 @@ -1,50 +1,36 @@ {#if data.memberships.total} - - + {#snippet header(root)} Name Roles Joined - - {#each data.memberships.memberships as membership} - - - - - {membership.teamName ? membership.teamName : 'n/a'} - - - - {membership.roles} - - - - - - - - - {/each} - + {/snippet} + + {#snippet children(root)} + {#each data.memberships.memberships as membership} + + + + + {membership.teamName ? membership.teamName : 'n/a'} + + + + {membership.roles} + + + + + + + + + {/each} + {/snippet} + + {#snippet deleteContentNotice()} + This action is irreversible and will remove the user from the selected teams. + {/snippet} + {:else} {/if} - - {#if selectedMemberships.length > 0} - - - - - {selectedMemberships.length > 1 ? 'memberships' : 'membership'} - selected - - - - - - - - {/if} - - - - Are you sure you want to delete {selectedMemberships.length} - {selectedMemberships.length > 1 ? 'memberships' : 'membership'}? - - - This action is irreversible and will remove the user from the selected teams. - - diff --git a/src/routes/(console)/project-[region]-[project]/auth/user-[user]/targets/table.svelte b/src/routes/(console)/project-[region]-[project]/auth/user-[user]/targets/table.svelte index 09aa63619..e71859a43 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/user-[user]/targets/table.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/user-[user]/targets/table.svelte @@ -1,6 +1,5 @@ - - + + {#snippet header(root)} {#each $columns as { id, title }} {title} {/each} - - {#each data.targets.targets as target (target.$id)} - {@const provider = data.providersById[target.providerId]} - - {#each $columns as column} - - {#if column.id === '$id'} - {#key $columns} - - {target[column.id]} - - {/key} - {:else if column.id === 'target'} - {#if target.providerType === MessagingProviderType.Push} - {target.name} + {/snippet} + + {#snippet children(root)} + {#each data.targets.targets as target (target.$id)} + {@const provider = data.providersById[target.providerId]} + + {#each $columns as column} + + {#if column.id === '$id'} + {#key $columns} + + {target[column.id]} + + {/key} + {:else if column.id === 'target'} + {#if target.providerType === MessagingProviderType.Push} + {target.name} + {:else} + {target.identifier} + {/if} + {:else if column.id === 'providerType'} + + {:else if column.id === 'provider'} + {#if provider} + + {/if} + {:else if column.id === '$createdAt'} + {:else} - {target.identifier} + {target[column.id]} {/if} - {:else if column.id === 'providerType'} - - {:else if column.id === 'provider'} - {#if provider} - - {/if} - {:else if column.id === '$createdAt'} - - {:else} - {target[column.id]} - {/if} - - {/each} - - {/each} - - -{#if selectedIds.length > 0} - - - - - {selectedIds.length > 1 ? 'targets' : 'target'} - selected - - - - - - - -{/if} - - - - Are you sure you want to delete {selectedIds.length} - {selectedIds.length > 1 ? 'targets' : 'target'}? - - + + {/each} + + {/each} + {/snippet} + 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 6c6a8de18..99521078d 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 @@ -69,11 +69,11 @@ await sdk .forProject(page.params.region, page.params.project) .backups.createArchive(['databases'], data.database.$id); + await invalidate(Dependencies.BACKUPS); addNotification({ type: 'success', message: 'Database backup has started' }); - invalidate(Dependencies.BACKUPS); trackEvent('click_manual_submit'); showFeedbackNotification(); } catch (error) { @@ -86,7 +86,7 @@ } }; - const trackEvents = (policies) => { + const trackEvents = (policies: UserBackupPolicy[]) => { policies.forEach((policy) => { let actualDay = null; const monthlyBackupFrequency = policy.monthlyBackupFrequency; @@ -139,7 +139,6 @@ ? `Backup policies have been created` : `${totalPolicies[0].label} policy has been created`; - // TODO: html isn't yet supported on Toast. addNotification({ isHtml: true, type: 'success', @@ -148,7 +147,7 @@ trackEvents(totalPolicies); - invalidate(Dependencies.BACKUPS); + await invalidate(Dependencies.BACKUPS); showFeedbackNotification(); } catch (err) { addNotification({ @@ -162,19 +161,14 @@ }; onMount(() => { - return realtime - .forProject(page.params.region, page.params.project) - .subscribe(['project', 'console'], (response) => { - // fast path return. - if (!response.channels.includes(`projects.${getProjectId()}`)) return; + return realtime.forProject(page.params.region, ['project', 'console'], (response) => { + // fast path return. + if (!response.channels.includes(`projects.${getProjectId()}`)) return; - if ( - response.events.includes('archives.*') || - response.events.includes('policies.*') - ) { - invalidate(Dependencies.BACKUPS); - } - }); + if (response.events.includes('archives.*') || response.events.includes('policies.*')) { + invalidate(Dependencies.BACKUPS); + } + }); }); 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 689afe74f..d33593160 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 @@ -1,5 +1,11 @@ - - + + {#snippet header(root)} {#each $columns as column} {column.title} {/each} - + {/snippet} - {#each data.backups.archives as backup, index} - {@const policy = policyDetails(backup.policyId)} - {@const retainedUntil = new Date( - new Date(policy?.$createdAt).getTime() + policy?.retention * 24 * 60 * 60 * 1000 - )} - {@const formattedRetainedUntil = `${retainedUntil.getDate()} ${retainedUntil.toLocaleString('en-US', { month: 'short' })}, ${retainedUntil.getFullYear()} ${retainedUntil.toLocaleTimeString('en-US', { hour12: false })}`} - - - - {cleanBackupName(backup)} - - - - {#if backup.status === 'completed'} - {calculateSize(backup.size)} - {:else} - - - {/if} - - - {@const backupStatus = getBackupStatus(backup)} - - - - - - -
- - - {policy?.name || 'Manual'} - - {policy - ? `Retained until: ${formattedRetainedUntil}` - : `Retained forever`} - -
-
- -
- - - - - {#if backup.status === 'completed'} + {#snippet children(root)} + {#each data.backups.archives as backup, index} + {@const policy = getPolicyDetails(backup.policyId)} + {@const retainedUntil = new Date( + new Date(policy?.$createdAt).getTime() + policy?.retention * 24 * 60 * 60 * 1000 + )} + {@const formattedRetainedUntil = `${retainedUntil.getDate()} ${retainedUntil.toLocaleString('en-US', { month: 'short' })}, ${retainedUntil.getFullYear()} ${retainedUntil.toLocaleTimeString('en-US', { hour12: false })}`} + + + + {getCleanBackupName(backup)} + + + + {#if backup.status === 'completed'} + {calculateSize(backup.size)} + {:else} + - + {/if} + + + {@const backupStatus = getBackupStatus(backup)} + + + + + + +
+ + + {policy?.name || 'Manual'} + + {policy + ? `Retained until: ${formattedRetainedUntil}` + : `Retained forever`} + +
+
+ +
+ + + + + {#if backup.status === 'completed'} + { + toggle(e); + showRestore = true; + selectedBackup = backup; + showDropdown[index] = false; + trackEvent(Click.BackupRestoreClick); + }}> + Restore + + {/if} { toggle(e); - showRestore = true; + copy(backup.$id); + showDropdown[index] = false; + trackEvent(Click.BackupCopyIdClick); + }}> + Copy ID + + { + toggle(e); + showDelete = true; selectedBackup = backup; showDropdown[index] = false; - trackEvent(Click.BackupRestoreClick); + trackEvent(Click.BackupDeleteClick); }}> - Restore + Delete - {/if} - { - toggle(e); - copy(backup.$id); - showDropdown[index] = false; - trackEvent(Click.BackupCopyIdClick); - }}> - Copy ID - - { - toggle(e); - showDelete = true; - selectedBackup = backup; - showDropdown[index] = false; - trackEvent(Click.BackupDeleteClick); - }}> - Delete - - - - -
-
-
- {/each} - - -{#if selectedBackups.length > 0} - - - - - {selectedBackups.length > 1 ? 'backups' : 'backup'} - selected - - - - - - - -{/if} +
+
+
+
+
+
+ {/each} + {/snippet} + + + onSubmit={async () => { + if (!selectedBackup) return; + await deleteBackups([selectedBackup.$id]); + }}> - Are you sure you want to delete - {#if selectedBackups.length} - {selectedBackups.length} {selectedBackups.length > 1 ? 'backups' : 'backup'}? - {:else} - the {cleanBackupName(selectedBackup)} backup? - {/if} -
This action is irreversible. + Are you sure you want to delete the {getCleanBackupName(selectedBackup)} backup?
+ + This action is irreversible.
- {cleanBackupName(selectedBackup)} + {getCleanBackupName(selectedBackup)} @@ -374,6 +382,6 @@ - + diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/+layout.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/+layout.svelte index 93034a2d5..f5908ae50 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/+layout.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/+layout.svelte @@ -20,7 +20,7 @@ @@ -456,7 +455,6 @@ - + {#key currentRowId} + + {/key} { if (row) { work = initWork(); - focusFirstInput(); + requestAnimationFrame(() => focusFirstInput()); } else { work = null; } diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/sheetOptions.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/sheetOptions.svelte index 761178f79..3fdf50d14 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/sheetOptions.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/sheetOptions.svelte @@ -117,15 +117,20 @@ if (type === 'header') { if (isSequence) { - return ['sort-asc', 'sort-desc'].includes(item.action ?? ''); + return ['sort-asc', 'sort-desc'].includes(item?.action); } - if (['delete', 'update', 'duplicate-header'].includes(item.action) && isSystemColumn) { + if (['delete', 'update', 'duplicate-header'].includes(item?.action) && isSystemColumn) { + return false; + } + + // hide column-left and create-index for $id (first column, already indexed) + if (columnId === '$id' && ['column-left', 'create-index'].includes(item?.action)) { return false; } // hide sort options for relationship columns - if (isRelationship(column) && ['sort-asc', 'sort-desc'].includes(item.action ?? '')) { + if (isRelationship(column) && ['sort-asc', 'sort-desc'].includes(item?.action)) { return false; } } 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 324a1e087..ecf308d49 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 @@ -53,6 +53,8 @@ Button, Layout, Link, + Popover, + Keyboard, Badge, FloatingActionBar, Icon, @@ -78,7 +80,8 @@ IconSwitchHorizontal, IconText, IconToggle, - IconViewList + IconViewList, + IconArrowExpand } from '@appwrite.io/pink-icons-svelte'; import type { HeaderCellAction, RowCellAction } from './sheetOptions.svelte'; import SheetOptions from './sheetOptions.svelte'; @@ -120,7 +123,9 @@ : 24; let selectedRows = []; + let showExpandIconForId: number | null = null; let spreadsheetContainer: SpreadsheetContainer; + let previouslyFocusedElement: Element | null = null; let currentPage = 1; let jumpToPageReactive = 0; @@ -170,8 +175,8 @@ { id: '$id', title: '$id', - width: getColumnWidth('$id', 225), - minimumWidth: 225, + width: getColumnWidth('$id', 250), + minimumWidth: 250, draggable: false, type: 'string', icon: IconFingerPrint, @@ -537,9 +542,14 @@ } } else if (type === 'row') { if (action === 'update') { - $databaseRowSheetOptions.show = true; - $databaseRowSheetOptions.row = row; - $databaseRowSheetOptions.title = 'Update row'; + databaseRowSheetOptions.update((opts) => { + return { + ...opts, + row, + show: true, + title: 'Update row' + }; + }); } if (action === 'duplicate-row') { @@ -747,6 +757,13 @@ $: totalPages = Math.ceil($rows.total / SPREADSHEET_PAGE_LIMIT) || 1; $: rowSelection = !$spreadsheetLoading && !$paginatedRowsLoading ? true : ('disabled' as const); + + $: if (!$databaseRowSheetOptions.show && previouslyFocusedElement) { + requestAnimationFrame(() => { + (previouslyFocusedElement as HTMLElement)?.focus(); + previouslyFocusedElement = null; + }); + } @@ -776,6 +793,14 @@ bottomActionTooltip={{ text: 'Create row', placement: 'top-end' + }} + expandKbdShortcut="Cmd+Enter" + on:expandKbdShortcut={({ detail }) => { + const focusedRowId = detail.rowId; + const focusedRow = $rows.rows.find((row) => row.$id === focusedRowId); + + previouslyFocusedElement = document.activeElement; + onSelectSheetOption('update', null, 'row', focusedRow); }}> {#each $tableColumns as column (column.id)} @@ -867,56 +892,133 @@ valueWithoutHover={row.$sequence}> {#each $tableColumns as { id: columnId, isEditable } (columnId)} {@const rowColumn = $columns.find((col) => col.key === columnId)} - - {#if columnId === '$id'} - {row.$id} - {:else if columnId === '$createdAt' || columnId === '$updatedAt'} - - {:else if columnId === 'actions'} - - onSelectSheetOption(option, null, 'row', row)} - onVisibilityChanged={(visible) => { - canShowDatetimePopover = !visible; - }}> - {#snippet children(toggle)} - - - - {/snippet} - - {:else if isRelationship(rowColumn)} - {@const args = getDisplayNamesForTable(row[columnId])} - {#if !isRelationshipToMany(rowColumn)} - {#if row[columnId]} - {@const displayValue = args - .map((arg) => row[columnId]?.[arg]) - .filter(Boolean) - .join(' | ')} + {#if columnId === '$id'} + + + + + Expand row + + + + + + + + + + + + {:else} + + {#if columnId === '$createdAt' || columnId === '$updatedAt'} + + {:else if columnId === 'actions'} + + onSelectSheetOption(option, null, 'row', row)} + onVisibilityChanged={(visible) => { + canShowDatetimePopover = !visible; + }}> + {#snippet children(toggle)} + + + + {/snippet} + + {:else if isRelationship(rowColumn)} + {@const args = getDisplayNamesForTable(row[columnId])} + {#if !isRelationshipToMany(rowColumn)} + {#if row[columnId]} + {@const displayValue = args + .map((arg) => row[columnId]?.[arg]) + .filter(Boolean) + .join(' | ')} + + {#if displayValue} + { + $databaseRelatedRowSheetOptions.tableId = + row[columnId]?.['$tableId']; + $databaseRelatedRowSheetOptions.rows = + row[columnId]?.['$id']; + $databaseRelatedRowSheetOptions.show = true; + }}> + {displayValue} + + {:else} + + {/if} {:else} {/if} {:else} - + {@const itemsNum = row[columnId]?.length} + Items {/if} + {:else if isSpatialType(rowColumn) && row[columnId] !== null} + + {JSON.stringify(row[columnId])} + {:else} - {@const itemsNum = row[columnId]?.length} - Items - {/if} - {:else if isSpatialType(rowColumn) && row[columnId] !== null} - - {JSON.stringify(row[columnId])} - - {:else} - {@const value = row[columnId]} - {@const formatted = formatColumn(row[columnId])} - {@const isEmptyArray = formatted === 'Empty'} - {@const isDatetimeAttribute = rowColumn.type === 'datetime'} - {@const isEncryptedAttribute = - isString(rowColumn) && rowColumn.encrypt} - {#if isDatetimeAttribute} - - Timestamp - {toLocaleDateTime(value, true)} - - {:else if isEncryptedAttribute} - - {:else if formatted.length > 20} - + {@const value = row[columnId]} + {@const formatted = formatColumn(row[columnId])} + {@const isEmptyArray = formatted === 'Empty'} + {@const isDatetimeAttribute = rowColumn.type === 'datetime'} + {@const isEncryptedAttribute = + isString(rowColumn) && rowColumn.encrypt} + {#if isDatetimeAttribute} + + Timestamp + {toLocaleDateTime(value, true)} + + {:else if isEncryptedAttribute} + + {:else if formatted.length > 20} + + + {formatted} + + + {formatted} + + + {:else if formatted === 'null'} + + {:else if isEmptyArray} + + {:else} {formatted} - - {formatted} - - - {:else if formatted === 'null'} - - {:else if isEmptyArray} - - {:else} - - {formatted} - + {/if} {/if} - {/if} - - {@const isRelatedToMany = isRelationshipToMany(rowColumn)} - {@const hasItems = isRelatedToMany - ? row[columnId]?.length - : false} + + {@const isRelatedToMany = isRelationshipToMany(rowColumn)} + {@const hasItems = isRelatedToMany + ? row[columnId]?.length + : false} - paginatedRows.update(index, row)} - onRevert={(row) => paginatedRows.update(index, row)} - openSideSheet={() => { - close(); /* closes the editor */ + paginatedRows.update(index, row)} + onRevert={(row) => paginatedRows.update(index, row)} + openSideSheet={() => { + close(); /* closes the editor */ - if (isRelationshipToMany(rowColumn)) { - openSideSheetForRelationsToMany( - row[columnId], - rowColumn - ); - } else { - onSelectSheetOption('update', null, 'row', row); - } - }} /> - - + if (isRelationshipToMany(rowColumn)) { + openSideSheetForRelationsToMany( + row[columnId], + rowColumn + ); + } else { + onSelectSheetOption('update', null, 'row', row); + } + }} /> + + + {/if} {/each} {/if} 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 28e5c2362..fa19cde37 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 @@ -1,110 +1,85 @@ - - + {onDelete}> + {#snippet header(root)} {#each $tableViewColumns as { id, title }} {title} {/each} - - {#each data.tables.tables as table (table.$id)} - - {#each $tableViewColumns as column} - - {#if column.id === '$id'} - {#key $tableViewColumns} - {table.$id} - {/key} - {:else if column.id === 'name'} - {table.name} - {:else} - - {/if} - - {/each} - - {/each} - + {/snippet} -{#if selectedTables.length > 0} - - - - - {selectedTables.length > 1 ? 'tables' : 'table'} - selected - - - - - - - -{/if} - - - - Are you sure you want to delete {selectedTables.length} - {selectedTables.length > 1 ? 'tables' : 'table'}? - - + {#snippet children(root)} + {#each data.tables.tables as table (table.$id)} + + {#each $tableViewColumns as column} + + {#if column.id === '$id'} + {#key $tableViewColumns} + {table.$id} + {/key} + {:else if column.id === 'name'} + {table.name} + {:else} + + {/if} + + {/each} + + {/each} + {/snippet} + 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 40c38c541..f84db9f7e 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 @@ -185,27 +185,24 @@ on:invalid={handleInvalid}> - - + + Drag and drop file here or click to upload - - - + justifyContent="center"> + + + Only .tar.gz files allowed + - Only .tar.gz files allowed - + {#if maxSize > 0} - Max file size: {readableMaxSize.value + readableMaxSize.unit} {/if} diff --git a/src/routes/(console)/project-[region]-[project]/functions/create-function/template-[template]/configuration.svelte b/src/routes/(console)/project-[region]-[project]/functions/create-function/template-[template]/configuration.svelte index 34f815e6e..cd21e5838 100644 --- a/src/routes/(console)/project-[region]-[project]/functions/create-function/template-[template]/configuration.svelte +++ b/src/routes/(console)/project-[region]-[project]/functions/create-function/template-[template]/configuration.svelte @@ -41,8 +41,8 @@ variables.map((variable) => { if (variable.value === '{apiEndpoint}') { - variable.value = getApiEndpoint(); - variable.placeholder = getApiEndpoint(); + variable.value = getApiEndpoint(page.params.region); + variable.placeholder = getApiEndpoint(page.params.region); } else if (variable.value === '{projectId}') { variable.value = page.params.project; variable.placeholder = page.params.project; diff --git a/src/routes/(console)/project-[region]-[project]/functions/function-[function]/+layout.svelte b/src/routes/(console)/project-[region]-[project]/functions/function-[function]/+layout.svelte index 4f8ad5851..aca7e790e 100644 --- a/src/routes/(console)/project-[region]-[project]/functions/function-[function]/+layout.svelte +++ b/src/routes/(console)/project-[region]-[project]/functions/function-[function]/+layout.svelte @@ -21,30 +21,29 @@ onMount(() => { let previousStatus = null; - return realtime - .forProject(page.params.region, page.params.project) - .subscribe('console', (message) => { - if ( - message.payload.status !== 'ready' && - previousStatus === message.payload.status - ) { - return; - } - previousStatus = message.payload.status; - if (message.events.includes('functions.*.deployments.*.create')) { - invalidate(Dependencies.DEPLOYMENTS); - return; - } - if (message.events.includes('functions.*.deployments.*.update')) { - invalidate(Dependencies.DEPLOYMENTS); - invalidate(Dependencies.FUNCTION); - return; - } - if (message.events.includes('functions.*.deployments.*.delete')) { - invalidate(Dependencies.DEPLOYMENTS); - return; - } - }); + return realtime.forProject(page.params.region, 'console', (response) => { + const payload = response.payload as Models.Deployment; + if (payload.status !== 'ready' && previousStatus === payload.status) { + return; + } + + previousStatus = payload.status; + if (response.events.includes('functions.*.deployments.*.create')) { + invalidate(Dependencies.DEPLOYMENTS); + return; + } + + if (response.events.includes('functions.*.deployments.*.update')) { + invalidate(Dependencies.DEPLOYMENTS); + invalidate(Dependencies.FUNCTION); + return; + } + + if (response.events.includes('functions.*.deployments.*.delete')) { + invalidate(Dependencies.DEPLOYMENTS); + return; + } + }); }); $: $registerCommands([ 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 83247cdb2..52adc05e9 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 @@ -1,7 +1,7 @@ diff --git a/src/routes/(console)/project-[region]-[project]/functions/function-[function]/executions/+page.svelte b/src/routes/(console)/project-[region]-[project]/functions/function-[function]/executions/+page.svelte index 7c4280fc1..55a275d5b 100644 --- a/src/routes/(console)/project-[region]-[project]/functions/function-[function]/executions/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/functions/function-[function]/executions/+page.svelte @@ -4,7 +4,7 @@ import { Dependencies } from '$lib/constants'; import { Button } from '$lib/elements/forms'; import { Container, ResponsiveContainerHeader } from '$lib/layout'; - import { sdk } from '$lib/stores/sdk'; + import { realtime } from '$lib/stores/sdk'; import { onMount } from 'svelte'; import { project } from '$routes/(console)/project-[region]-[project]/store'; import { base } from '$app/paths'; @@ -12,11 +12,13 @@ import { IconPlus } from '@appwrite.io/pink-icons-svelte'; import Table from './table.svelte'; import { columns } from './store'; + import type { PageProps } from './$types'; + import { page } from '$app/state'; - export let data; + let { data }: PageProps = $props(); onMount(() => { - return sdk.forConsole.realtime.subscribe('console', (response) => { + return realtime.forConsole(page.params.region, 'console', (response) => { if (response.events.includes('functions.*.executions.*')) { invalidate(Dependencies.EXECUTIONS); } 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 8f0785ed7..f681c04ef 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,17 +1,10 @@ - - + + {#snippet header(root)} {#each columns as { id, title }} {title} {/each} - - {#each executions.executions as log (log.$id)} - { - e.stopPropagation(); - open = true; - selectedLogId = log.$id; - }}> - {#each columns as column} - - {#if column.id === '$id'} - {#key column.id} - {log.$id} - {/key} - {:else if column.id === 'deploymentId'} - {log.deploymentId} - {:else if column.id === '$createdAt'} - - {:else if column.id === 'requestPath'} - - {log.requestPath} - - {:else if column.id === 'responseStatusCode'} - - {:else if column.id === 'requestMethod'} - - {log.requestMethod} - - {:else if column.id === 'trigger'} - {capitalize(log.trigger)} - {:else if column.id === 'status'} - {@const status = log.status} - -
- - -
- - {`Scheduled to execute on ${toLocaleDateTime(log.scheduledAt)}`} - -
- {:else if column.id === 'duration'} - {#if ['processing', 'waiting'].includes(log.status)} - - {:else} - {calculateTime(log.duration)} + {/snippet} + + {#snippet children(root)} + {#each executions.executions as log (log.$id)} + { + e.stopPropagation(); + open = true; + selectedLogId = log.$id; + }}> + {#each columns as column} + + {#if column.id === '$id'} + {#key column.id} + {log.$id} + {/key} + {:else if column.id === 'deploymentId'} + {log.deploymentId} + {:else if column.id === '$createdAt'} + + {:else if column.id === 'requestPath'} + + {log.requestPath} + + {:else if column.id === 'responseStatusCode'} + + {:else if column.id === 'requestMethod'} + + {log.requestMethod} + + {:else if column.id === 'trigger'} + {capitalize(log.trigger)} + {:else if column.id === 'status'} + {@const status = log.status} + +
+ + +
+ + {`Scheduled to execute on ${toLocaleDateTime(log.scheduledAt)}`} + +
+ {:else if column.id === 'duration'} + {#if ['processing', 'waiting'].includes(log.status)} + + {:else} + {calculateTime(log.duration)} + {/if} {/if} - {/if} -
- {/each} -
- {/each} -
+ + {/each} + + {/each} + {/snippet} + - -{#if selectedRows.length > 0} - - - - - {selectedRows.length > 1 ? 'executions' : 'execution'} - selected - - - - - - - -{/if} - - -

- Are you sure you want to delete {selectedRows.length} - {selectedRows.length > 1 ? 'executions' : 'execution'}? -

- -

This action is irreversible.

-
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 eecafbd5a..c4c1d1743 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,5 @@ - - + {#snippet header(root)} {#each columns as { id, title }} {title} {/each} - - {#each data.deploymentList.deployments as deployment (deployment.$id)} - - {#each columns as column} - - {#if column.id === '$id'} - {#key column.id} - {deployment.$id} - {/key} - {:else if column.id === 'status'} - {@const status = deployment.status} + {/snippet} - {#if data?.activeDeployment?.$id === deployment?.$id} - - {:else} - - {/if} - {:else if column.id === 'type'} - - {:else if column.id === '$updatedAt'} - - {:else if column.id === 'buildDuration'} - {#if ['waiting'].includes(deployment.status)} - - - {:else if ['processing', 'building'].includes(deployment.status)} - - {:else} - {formatTimeDetailed(deployment.buildDuration)} - {/if} - {:else if column.id === 'totalSize'} - {calculateSize(deployment.totalSize)} - {:else if column.id === 'sourceSize'} - {calculateSize(deployment.sourceSize)} - {:else if column.id === 'buildSize'} - {calculateSize(deployment.buildSize)} - {/if} - - {/each} - - - + {#snippet children(root)} + {#each data.deploymentList.deployments as deployment (deployment.$id)} + + {#each columns as column} + + {#if column.id === '$id'} + {#key column.id} + {deployment.$id} + {/key} + {:else if column.id === 'status'} + {@const status = deployment.status} - - - -
+ {#if data?.activeDeployment?.$id === deployment?.$id} + + {:else} + + {/if} + {:else if column.id === 'type'} + + {:else if column.id === '$updatedAt'} + + {:else if column.id === 'buildDuration'} + {#if ['waiting'].includes(deployment.status)} + - + {:else if ['processing', 'building'].includes(deployment.status)} + + {:else} + {formatTimeDetailed(deployment.buildDuration)} + {/if} + {:else if column.id === 'totalSize'} + {calculateSize(deployment.totalSize)} + {:else if column.id === 'sourceSize'} + {calculateSize(deployment.sourceSize)} + {:else if column.id === 'buildSize'} + {calculateSize(deployment.buildSize)} + {/if} + + {/each} + + + + + + + +
+ { + selectedDeployment = deployment; + showRedeploy = true; + toggle(); + trackEvent(Click.FunctionsRedeployClick); + }} + style="width: 100%"> + Redeploy + +
+
Source is empty
+
+ {#if deployment.status === 'ready' && deployment.$id !== $func.deploymentId} { selectedDeployment = deployment; - showRedeploy = true; + showActivate = true; toggle(); - trackEvent(Click.FunctionsRedeployClick); - }} - style="width: 100%"> - Redeploy + }}> + Activate -
-
Source is empty
-
- {#if deployment.status === 'ready' && deployment.$id !== $func.deploymentId} - { - selectedDeployment = deployment; - showActivate = true; - toggle(); - }}> - Activate - - {/if} + {/if} - + - {#if deployment.status === 'processing' || deployment.status === 'building' || deployment.status === 'waiting'} - { - selectedDeployment = deployment; - toggle(); + {#if deployment.status === 'processing' || deployment.status === 'building' || deployment.status === 'waiting'} + { + selectedDeployment = deployment; + toggle(); - showCancel = true; - trackEvent(Click.FunctionsDeploymentCancelClick); - }}> - Cancel - - {/if} - {#if deployment.status !== 'building' && deployment.status !== 'processing' && deployment.status !== 'waiting'} - { - selectedDeployment = deployment; - toggle(); + showCancel = true; + trackEvent(Click.FunctionsDeploymentCancelClick); + }}> + Cancel + + {/if} + {#if deployment.status !== 'building' && deployment.status !== 'processing' && deployment.status !== 'waiting'} + { + selectedDeployment = deployment; + toggle(); - showDelete = true; - trackEvent(Click.FunctionsDeploymentDeleteClick); - }}> - Delete - - {/if} -
-
-
-
-
- {/each} -
+ showDelete = true; + trackEvent(Click.FunctionsDeploymentDeleteClick); + }}> + Delete + + {/if} + + + + + + {/each} + {/snippet} + + {#snippet deleteContent(count)} +

+ Are you sure you want to delete {count} + {count > 1 ? 'deployments' : 'deployment'} from your function - + {page.data.function.name}? +

+ {/snippet} + {#if selectedDeployment} - + {/if} - -{#if selectedRows.length > 0} - - - - - {selectedRows.length > 1 ? 'deployments' : 'deployment'} - selected - - - - - - - -{/if} - - -

- Are you sure you want to delete {selectedRows.length} - {selectedRows.length > 1 ? 'deployments' : 'deployment'} from your function - - {$func.name}? -

- -

This action is irreversible.

-
diff --git a/src/routes/(console)/project-[region]-[project]/messaging/+page.svelte b/src/routes/(console)/project-[region]-[project]/messaging/+page.svelte index 6f009719b..b096f2561 100644 --- a/src/routes/(console)/project-[region]-[project]/messaging/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/messaging/+page.svelte @@ -1,7 +1,15 @@ - - + onDelete={handleDelete} + allowSelection={$canWriteProviders}> + {#snippet header(root)} {#each $columns as { id, title }} {title} {/each} - - {#each data.providers.providers as provider (provider.$id)} - - {#each $columns as column} - - {#if column.id === '$id'} - {#key $columns} - {provider.$id} - {/key} - {:else if column.id === 'provider'} - - {:else if column.id === 'type'} - - {:else if column.id === 'enabled'} - - - {#if provider.enabled} - - {/if} - - - {:else} - {provider[column.id]} - {/if} - - {/each} - - {/each} - - -{#if selectedIds.length > 0} - - - - - {selectedIds.length > 1 ? 'providers' : 'provider'} - selected - - - - - - - -{/if} - - - - Are you sure you want to delete {selectedIds.length} - {selectedIds.length > 1 ? 'providers' : 'provider'}? - - + + {#each $columns as column} + + {#if column.id === '$id'} + {#key $columns} + {provider.$id} + {/key} + {:else if column.id === 'provider'} + + {:else if column.id === 'type'} + + {:else if column.id === 'enabled'} + + + {#if provider.enabled} + + {/if} + + + {:else} + {provider[column.id]} + {/if} + + {/each} + + {/each} + {/snippet} + 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 b4d653169..f5525754e 100644 --- a/src/routes/(console)/project-[region]-[project]/messaging/topics/table.svelte +++ b/src/routes/(console)/project-[region]-[project]/messaging/topics/table.svelte @@ -1,9 +1,7 @@ - - + + {#snippet header(root)} {#each columns as { id, title }} {title} {/each} - - {#each data.topics.topics as topic (topic.$id)} - - {#each columns as column (column.id)} - - {#if column.id === '$id'} - {#key column.id} - {topic.$id} - {/key} - {:else if column.type === 'datetime'} - {#if topic[column.id]} - - {:else}-{/if} - {:else if column.id === 'total'} - {topic.emailTotal + topic.smsTotal + topic.pushTotal} - {:else} - {topic[column.id]} - {/if} - - {/each} - - {/each} - + {/snippet} -{#if selectedIds.length > 0} - - - - - {selectedIds.length > 1 ? 'topics' : 'topic'} - selected - - - - - - - -{/if} - - - - Are you sure you want to delete {selectedIds.length} - {selectedIds.length > 1 ? 'topics' : 'topic'}? - - + {#snippet children(root)} + {#each data.topics.topics as topic (topic.$id)} + + {#each columns as column (column.id)} + + {#if column.id === '$id'} + {#key column.id} + {topic.$id} + {/key} + {:else if column.type === 'datetime'} + {#if topic[column.id]} + + {:else}-{/if} + {:else if column.id === 'total'} + {topic.emailTotal + topic.smsTotal + topic.pushTotal} + {:else} + {topic[column.id]} + {/if} + + {/each} + + {/each} + {/snippet} + 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 a42fcfd07..c9a64e9af 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,10 +2,8 @@ import { invalidate } from '$app/navigation'; import { base } from '$app/paths'; import { Submit, trackError, trackEvent } from '$lib/actions/analytics'; - import { Id } from '$lib/components'; + import { type DeleteOperationState, Id, MultiSelectionTable } from '$lib/components'; import { Dependencies } from '$lib/constants'; - import { Button } from '$lib/elements/forms'; - import { addNotification } from '$lib/stores/notifications'; import type { PageData } from './$types'; import ProviderType from '../../providerType.svelte'; import DualTimeView from '$lib/components/dualTimeView.svelte'; @@ -15,21 +13,26 @@ import { targetsById } from '../../store'; import { MessagingProviderType, type Models } from '@appwrite.io/console'; import type { Column } from '$lib/helpers/types'; - import { Badge, FloatingActionBar, Table, Typography } from '@appwrite.io/pink-svelte'; - import Confirm from '$lib/components/confirm.svelte'; + import { Table } from '@appwrite.io/pink-svelte'; - export let columns: Column[]; - export let data: PageData; + let { + data, + columns + }: { + data: PageData; + columns: Column[]; + } = $props(); - let subscribers: Record = {}; - let selectedIds: string[] = []; - let selected: Record = {}; - let showDelete = false; - let deleting = false; + const subscribers = $derived.by(() => { + const record: Record = {}; + for (const subscriber of data.subscribers.subscribers) { + record[subscriber.$id] = subscriber; + } - async function handleDelete() { - showDelete = false; + return record; + }); + async function handleDelete(selectedRows: string[]): Promise { async function deleteSubscriber(subscriberId: string) { await sdk .forProject(page.params.region, page.params.project) @@ -37,111 +40,68 @@ topicId: page.params.topic, subscriberId }); + const { target } = subscribers[subscriberId]; const { [target.$id]: _, ...rest } = $targetsById; $targetsById = rest; } - const promises = selectedIds.map((id) => deleteSubscriber(id)); + const promises = selectedRows.map((id) => deleteSubscriber(id)); try { await Promise.all(promises); - trackEvent(Submit.MessagingTopicSubscriberDelete, { - total: selectedIds.length - }); - addNotification({ - type: 'success', - message: `${selectedIds.length} subscriber${ - selectedIds.length > 1 ? 's' : '' - } deleted` - }); - invalidate(Dependencies.MESSAGING_TOPIC_SUBSCRIBERS); + trackEvent(Submit.MessagingTopicSubscriberDelete, { total: selectedRows.length }); } catch (error) { - addNotification({ - type: 'error', - message: error.message - }); trackError(error, Submit.MessagingTopicSubscriberDelete); + return error; } finally { - selectedIds = []; - showDelete = false; + await invalidate(Dependencies.MESSAGING_TOPIC_SUBSCRIBERS); } } - - $: data.subscribers.subscribers.forEach((s) => { - subscribers[s.$id] = s; - }); - $: selectedIds.forEach((id) => { - selected[id] = subscribers[id]; - }); - - + + {#snippet header(root)} {#each columns as { id, title }} {title} {/each} - - {#each data.subscribers.subscribers as subscriber (subscriber.$id)} - {@const target = subscriber.target} - - {#each columns as column} - - {#if column.id === '$id'} - {#key column.id} - - {subscriber.$id} + {/snippet} + + {#snippet children(root)} + {#each data.subscribers.subscribers as subscriber (subscriber.$id)} + {@const target = subscriber.target} + + {#each columns as column} + + {#if column.id === '$id'} + {#key column.id} + + {subscriber.$id} + + {/key} + {:else if column.id === 'targetId'} + + {subscriber[column.id]} - {/key} - {:else if column.id === 'targetId'} - - {subscriber[column.id]} - - {:else if column.id === 'target'} - {#if target.providerType === MessagingProviderType.Push} - {target.name} + {:else if column.id === 'target'} + {#if target.providerType === MessagingProviderType.Push} + {target.name} + {:else} + {target.identifier} + {/if} + {:else if column.id === 'type'} + + {:else if column.id === '$createdAt'} + {:else} - {target.identifier} + {subscriber[column.id]} {/if} - {:else if column.id === 'type'} - - {:else if column.id === '$createdAt'} - - {:else} - {subscriber[column.id]} - {/if} - - {/each} - - {/each} - - -{#if selectedIds.length > 0} - - - - - {selectedIds.length > 1 ? 'subscribers' : 'subscriber'} - selected - - - - - - - -{/if} - - - - Are you sure you want to delete {selectedIds.length} - {selectedIds.length > 1 ? 'subscribers' : 'subscriber'}? - - + + {/each} + + {/each} + {/snippet} + diff --git a/src/routes/(console)/project-[region]-[project]/overview/(components)/create.svelte b/src/routes/(console)/project-[region]-[project]/overview/(components)/create.svelte index 0df14759c..84e6a7809 100644 --- a/src/routes/(console)/project-[region]-[project]/overview/(components)/create.svelte +++ b/src/routes/(console)/project-[region]-[project]/overview/(components)/create.svelte @@ -15,6 +15,7 @@ import { writable } from 'svelte/store'; import Scopes from '../api-keys/scopes.svelte'; import { page } from '$app/state'; + import { copy } from '$lib/helpers/copy'; const projectId = page.params.project; @@ -28,7 +29,7 @@ async function create() { try { - const { $id } = await sdk.forConsole.projects.createKey({ + const { $id, secret } = await sdk.forConsole.projects.createKey({ projectId, name, scopes, @@ -45,7 +46,21 @@ ); addNotification({ message: `API key has been created`, - type: 'success' + type: 'success', + buttons: [ + { + name: 'Copy API key', + method: async () => { + await copy(secret); + } + }, + { + name: 'Copy endpoint', + method: async () => { + await copy(sdk.forConsole.client.config.endpoint); + } + } + ] }); } catch (error) { addNotification({ diff --git a/src/routes/(console)/project-[region]-[project]/overview/(components)/deleteBatch.svelte b/src/routes/(console)/project-[region]-[project]/overview/(components)/deleteBatch.svelte index ff694c3cd..3a742d94c 100644 --- a/src/routes/(console)/project-[region]-[project]/overview/(components)/deleteBatch.svelte +++ b/src/routes/(console)/project-[region]-[project]/overview/(components)/deleteBatch.svelte @@ -59,7 +59,12 @@ } - + 1 ? 's' : ''}`}>

Are you sure you want to delete {keyIds.length} {label} key{keyIds.length > 1 ? 's' : ''}? 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 28bf1a389..9cfb56a83 100644 --- a/src/routes/(console)/project-[region]-[project]/overview/(components)/table.svelte +++ b/src/routes/(console)/project-[region]-[project]/overview/(components)/table.svelte @@ -2,27 +2,32 @@ import { base } from '$app/paths'; import { page } from '$app/state'; import { goto } from '$app/navigation'; - import { Empty } from '$lib/components'; - import { Button } from '$lib/elements/forms'; + import { Empty, MultiSelectionTable } from '$lib/components'; import { canWriteKeys } from '$lib/stores/roles'; import type { Models } from '@appwrite.io/console'; import { diffDays } from '$lib/helpers/date'; import DualTimeView from '$lib/components/dualTimeView.svelte'; import { devKeyColumns, keyColumns, showDevKeysCreateModal } from '../store'; - import { Badge, FloatingActionBar, Layout, Table } from '@appwrite.io/pink-svelte'; + import { Badge, Layout, Table } from '@appwrite.io/pink-svelte'; import DeleteBatch from './deleteBatch.svelte'; import { capitalize } from '$lib/helpers/string'; import { getEffectiveScopes } from '../api-keys/scopes.svelte'; - export let keyType: 'api' | 'dev' = 'api'; - export let keys: Models.KeyList | Models.DevKeyList; + let { + keyType = 'api', + keys + }: { + keyType?: 'api' | 'dev'; + keys: Models.KeyList | Models.DevKeyList; + } = $props(); - let showDeleteModal = false; - let selectedRows: string[] = []; + let selectedKeys = $state([]); + let showDeleteModal = $state(false); const isApiKey = keyType === 'api'; const label = isApiKey ? 'API' : 'dev'; const slug = isApiKey ? 'api-keys' : 'dev-keys'; + const columns = isApiKey ? $keyColumns : $devKeyColumns; function getApiKeyScopeCount(key: Models.Key | Models.DevKey) { const apiKey = key as Models.Key; @@ -53,55 +58,65 @@ else return 'Dev keys allow bypassing rate limits and CORS errors in your development environment.'; } - - const columns = isApiKey ? $keyColumns : $devKeyColumns; {#if keys.total} - - + { + showDeleteModal = true; + selectedKeys = selectedRows; + }}> + {#snippet header(root)} {#each columns as column} {column.title} {/each} - - {#each getKeys() as key (key.$id)} - - - {key.name} - - - {#if key.accessedAt} - - {:else} - never - {/if} - - - {@const expiration = getExpiryDetails(key)} - - {#if key.expire} - + {/snippet} + + {#snippet children(root)} + {#each getKeys() as key (key.$id)} + + + {key.name} + + + {#if key.accessedAt} + {:else} never {/if} - - {#if expiration.status} - - {/if} - - - {#if isApiKey} - - {getApiKeyScopeCount(key)} Scopes - {/if} - - {/each} - + + {@const expiration = getExpiryDetails(key)} + + {#if key.expire} + + {:else} + never + {/if} + + {#if expiration.status} + + {/if} + + + {#if isApiKey} + + {getApiKeyScopeCount(key)} Scopes + + {/if} + + {/each} + {/snippet} + {:else} { + on:click={async () => { if (isApiKey) { - goto( + await goto( `${base}/project-${page.params.region}-${page.params.project}/overview/${slug}/create` ); } else { @@ -120,21 +135,4 @@ }} /> {/if} -{#if selectedRows.length > 0} - - - - - {capitalize(label)} - {selectedRows.length > 1 ? 'keys' : 'key'} - selected - - - - - - - -{/if} - - + diff --git a/src/routes/(console)/project-[region]-[project]/overview/api-keys/[key]/header.svelte b/src/routes/(console)/project-[region]-[project]/overview/api-keys/[key]/header.svelte index 0b4b527d6..984700097 100644 --- a/src/routes/(console)/project-[region]-[project]/overview/api-keys/[key]/header.svelte +++ b/src/routes/(console)/project-[region]-[project]/overview/api-keys/[key]/header.svelte @@ -3,6 +3,10 @@ import { page } from '$app/state'; import { Cover, CoverTitle } from '$lib/layout'; import { key } from './store'; + import { RegionEndpoint, Copy } from '$lib/components'; + import { Layout, Tag, Icon } from '@appwrite.io/pink-svelte'; + import { IconDuplicate } from '@appwrite.io/pink-icons-svelte'; + import { projectRegion } from '../../../store'; const projectId = page.params.project; @@ -12,5 +16,26 @@ {$key?.name} + + {#if $key?.secret} + + + + API secret + + + {/if} + {#if $projectRegion} + + {/if} + + + diff --git a/src/routes/(console)/project-[region]-[project]/overview/assets/platform-sdk-dark.png b/src/routes/(console)/project-[region]-[project]/overview/assets/platform-sdk-dark.png new file mode 100644 index 000000000..e778c55b9 Binary files /dev/null and b/src/routes/(console)/project-[region]-[project]/overview/assets/platform-sdk-dark.png differ diff --git a/src/routes/(console)/project-[region]-[project]/overview/assets/platform-sdk.jpg b/src/routes/(console)/project-[region]-[project]/overview/assets/platform-sdk.jpg new file mode 100644 index 000000000..b891d6a12 Binary files /dev/null and b/src/routes/(console)/project-[region]-[project]/overview/assets/platform-sdk.jpg differ diff --git a/src/routes/(console)/project-[region]-[project]/overview/header.svelte b/src/routes/(console)/project-[region]-[project]/overview/header.svelte index 8a412281c..ff2107397 100644 --- a/src/routes/(console)/project-[region]-[project]/overview/header.svelte +++ b/src/routes/(console)/project-[region]-[project]/overview/header.svelte @@ -2,19 +2,24 @@ import { page } from '$app/state'; import { Id, RegionEndpoint } from '$lib/components'; import { Cover } from '$lib/layout'; - import { project, projectRegion } from '../store'; + import { projectRegion } from '../store'; import { hasOnboardingDismissed, setHasOnboardingDismissed } from '$lib/helpers/onboarding'; import { goto } from '$app/navigation'; - import { base } from '$app/paths'; + import { resolve } from '$app/paths'; import { Layout, Button, Typography } from '@appwrite.io/pink-svelte'; import { user } from '$lib/stores/user'; import { isSmallViewport } from '$lib/stores/viewport'; import { trackEvent } from '$lib/actions/analytics'; function dismissOnboarding() { - setHasOnboardingDismissed($project.$id, $user); + setHasOnboardingDismissed(page.params.project, $user); trackEvent('onboarding_hub_platform_dismiss'); - goto(`${base}/project-${$project.region}-${$project.$id}/overview/platforms`); + goto( + resolve('/(console)/project-[region]-[project]/overview/platforms', { + region: page.params.region, + project: page.params.project + }) + ); } @@ -24,12 +29,15 @@ - {$project?.name} + {page.data.project?.name} - {$project.$id} - + {page.params.project} + {#if $projectRegion} + + {/if} @@ -53,7 +61,7 @@ >Follow a few quick steps to get started with Appwrite

- {#if !hasOnboardingDismissed($project.$id, $user)} + {#if !hasOnboardingDismissed(page.params.project, $user)} Dismiss this page diff --git a/src/routes/(console)/project-[region]-[project]/overview/onboard.svelte b/src/routes/(console)/project-[region]-[project]/overview/onboard.svelte index 5b98b45d1..78757226d 100644 --- a/src/routes/(console)/project-[region]-[project]/overview/onboard.svelte +++ b/src/routes/(console)/project-[region]-[project]/overview/onboard.svelte @@ -14,12 +14,7 @@ import { app } from '$lib/stores/app'; import AuthPreview from './assets/auth-preview.svg'; import AuthPreviewDark from './assets/auth-preview-dark.svg'; - import { - IconArrowRight, - IconNodeJs, - IconPhp, - IconPython - } from '@appwrite.io/pink-icons-svelte'; + import { IconArrowRight } from '@appwrite.io/pink-icons-svelte'; import DatabaseImgSource from './assets/database.png'; import DatabaseImgSourceDark from './assets/database-dark.png'; import DiscordImgSource from './assets/discord.png'; @@ -31,9 +26,10 @@ import PlatformAndroidImgSourceDark from './assets/platform-android-dark.svg'; import PlatformFlutterImgSource from './assets/platform-flutter.svg'; import PlatformFlutterImgSourceDark from './assets/platform-flutter-dark.svg'; + import PlatformSdkImgSource from './assets/platform-sdk.jpg'; + import PlatformSdkImgSourceDark from './assets/platform-sdk-dark.png'; import { base } from '$app/paths'; import { isSmallViewport } from '$lib/stores/viewport'; - import { AvatarGroup } from '$lib/components'; import type { Models } from '@appwrite.io/console'; import { getPlatformInfo } from '$lib/helpers/platform'; import { Click, trackEvent } from '$lib/actions/analytics'; @@ -103,7 +99,9 @@
- + { openPlatformWizard(0, platformMap.get('Web')); @@ -341,17 +339,34 @@ - - Or connect - server side -
- -
-
+ or + + + +
+ + + Create API key + Connect your server or backend to Appwrite + +
+ +
+
+
+
+
{ - let subscription: AppwriteRealtimeSubscription; - sdk.forConsole.realtime - .subscribe('console', (response) => { - if (response.events.includes(`projects.${projectId}.ping`)) { - connectionSuccessful = true; - invalidate(Dependencies.ORGANIZATION); - invalidate(Dependencies.PROJECT); - subscription?.close(); - } - }) - .then((realtime) => (subscription = realtime)); + const unsubscribe = realtime.forConsole(page.params.region, 'console', (response) => { + if (response.events.includes(`projects.${projectId}.ping`)) { + connectionSuccessful = true; + invalidate(Dependencies.ORGANIZATION); + invalidate(Dependencies.PROJECT); + unsubscribe(); + } + }); return () => { - subscription?.close(); + unsubscribe(); resetPlatformStore(); }; }); diff --git a/src/routes/(console)/project-[region]-[project]/overview/platforms/createApple.svelte b/src/routes/(console)/project-[region]-[project]/overview/platforms/createApple.svelte index 0f428a78e..a8fea18ca 100644 --- a/src/routes/(console)/project-[region]-[project]/overview/platforms/createApple.svelte +++ b/src/routes/(console)/project-[region]-[project]/overview/platforms/createApple.svelte @@ -18,7 +18,7 @@ import { Card } from '$lib/components'; import { page } from '$app/state'; import { onMount } from 'svelte'; - import { type AppwriteRealtimeSubscription, sdk } from '$lib/stores/sdk'; + import { realtime, sdk } from '$lib/stores/sdk'; import { Submit, trackError, trackEvent } from '$lib/actions/analytics'; import { addNotification } from '$lib/stores/notifications'; import { fade } from 'svelte/transition'; @@ -97,8 +97,10 @@ APPWRITE_PUBLIC_ENDPOINT: "${sdk.forProject(page.params.region, page.params.proj message: 'Platform created.' }); - invalidate(Dependencies.PROJECT); - invalidate(Dependencies.PLATFORMS); + await Promise.all([ + invalidate(Dependencies.PROJECT), + invalidate(Dependencies.PLATFORMS) + ]); } catch (error) { trackError(error, Submit.PlatformCreate); addNotification({ @@ -115,20 +117,17 @@ APPWRITE_PUBLIC_ENDPOINT: "${sdk.forProject(page.params.region, page.params.proj } onMount(() => { - let subscription: AppwriteRealtimeSubscription; - sdk.forConsole.realtime - .subscribe('console', (response) => { - if (response.events.includes(`projects.${projectId}.ping`)) { - connectionSuccessful = true; - invalidate(Dependencies.ORGANIZATION); - invalidate(Dependencies.PROJECT); - subscription?.close(); - } - }) - .then((realtime) => (subscription = realtime)); + const unsubscribe = realtime.forConsole(page.params.region, 'console', (response) => { + if (response.events.includes(`projects.${projectId}.ping`)) { + connectionSuccessful = true; + invalidate(Dependencies.ORGANIZATION); + invalidate(Dependencies.PROJECT); + unsubscribe(); + } + }); return () => { - subscription?.close(); + unsubscribe(); resetPlatformStore(); }; }); diff --git a/src/routes/(console)/project-[region]-[project]/overview/platforms/createFlutter.svelte b/src/routes/(console)/project-[region]-[project]/overview/platforms/createFlutter.svelte index e0e662602..9f567ff28 100644 --- a/src/routes/(console)/project-[region]-[project]/overview/platforms/createFlutter.svelte +++ b/src/routes/(console)/project-[region]-[project]/overview/platforms/createFlutter.svelte @@ -18,7 +18,7 @@ import { Card } from '$lib/components'; import { page } from '$app/state'; import { onMount } from 'svelte'; - import { type AppwriteRealtimeSubscription, sdk } from '$lib/stores/sdk'; + import { realtime, sdk } from '$lib/stores/sdk'; import { Submit, trackError, trackEvent } from '$lib/actions/analytics'; import { addNotification } from '$lib/stores/notifications'; import { fade } from 'svelte/transition'; @@ -161,8 +161,10 @@ client.ping(); message: 'Platform created.' }); - invalidate(Dependencies.PROJECT); - invalidate(Dependencies.PLATFORMS); + await Promise.all([ + invalidate(Dependencies.PROJECT), + invalidate(Dependencies.PLATFORMS) + ]); } catch (error) { trackError(error, Submit.PlatformCreate); addNotification({ @@ -179,20 +181,17 @@ client.ping(); } onMount(() => { - let subscription: AppwriteRealtimeSubscription; - sdk.forConsole.realtime - .subscribe('console', (response) => { - if (response.events.includes(`projects.${projectId}.ping`)) { - connectionSuccessful = true; - invalidate(Dependencies.ORGANIZATION); - invalidate(Dependencies.PROJECT); - subscription?.close(); - } - }) - .then((realtime) => (subscription = realtime)); + const unsubscribe = realtime.forConsole(page.params.region, 'console', (response) => { + if (response.events.includes(`projects.${projectId}.ping`)) { + connectionSuccessful = true; + invalidate(Dependencies.ORGANIZATION); + invalidate(Dependencies.PROJECT); + unsubscribe(); + } + }); return () => { - subscription?.close(); + unsubscribe(); resetPlatformStore(); }; }); diff --git a/src/routes/(console)/project-[region]-[project]/overview/platforms/createReactNative.svelte b/src/routes/(console)/project-[region]-[project]/overview/platforms/createReactNative.svelte index af345372e..0a55a52a9 100644 --- a/src/routes/(console)/project-[region]-[project]/overview/platforms/createReactNative.svelte +++ b/src/routes/(console)/project-[region]-[project]/overview/platforms/createReactNative.svelte @@ -18,7 +18,7 @@ import { Card } from '$lib/components'; import { page } from '$app/state'; import { onMount } from 'svelte'; - import { type AppwriteRealtimeSubscription, sdk } from '$lib/stores/sdk'; + import { realtime, sdk } from '$lib/stores/sdk'; import { Submit, trackError, trackEvent } from '$lib/actions/analytics'; import { addNotification } from '$lib/stores/notifications'; import { fade } from 'svelte/transition'; @@ -128,8 +128,10 @@ EXPO_PUBLIC_APPWRITE_ENDPOINT=${sdk.forProject(page.params.region, page.params.p message: 'Platform created.' }); - invalidate(Dependencies.PROJECT); - invalidate(Dependencies.PLATFORMS); + await Promise.all([ + invalidate(Dependencies.PROJECT), + invalidate(Dependencies.PLATFORMS) + ]); } catch (error) { trackError(error, Submit.PlatformCreate); addNotification({ @@ -146,20 +148,17 @@ EXPO_PUBLIC_APPWRITE_ENDPOINT=${sdk.forProject(page.params.region, page.params.p } onMount(() => { - let subscription: AppwriteRealtimeSubscription; - sdk.forConsole.realtime - .subscribe('console', (response) => { - if (response.events.includes(`projects.${projectId}.ping`)) { - connectionSuccessful = true; - invalidate(Dependencies.ORGANIZATION); - invalidate(Dependencies.PROJECT); - subscription?.close(); - } - }) - .then((realtime) => (subscription = realtime)); + const unsubscribe = realtime.forConsole(page.params.region, 'console', (response) => { + if (response.events.includes(`projects.${projectId}.ping`)) { + connectionSuccessful = true; + invalidate(Dependencies.ORGANIZATION); + invalidate(Dependencies.PROJECT); + unsubscribe(); + } + }); return () => { - subscription?.close(); + unsubscribe(); resetPlatformStore(); }; }); diff --git a/src/routes/(console)/project-[region]-[project]/overview/platforms/createWeb.svelte b/src/routes/(console)/project-[region]-[project]/overview/platforms/createWeb.svelte index debaff0fc..8516bac0c 100644 --- a/src/routes/(console)/project-[region]-[project]/overview/platforms/createWeb.svelte +++ b/src/routes/(console)/project-[region]-[project]/overview/platforms/createWeb.svelte @@ -28,7 +28,7 @@ } from '@appwrite.io/pink-icons-svelte'; import { page } from '$app/state'; import { onMount } from 'svelte'; - import { type AppwriteRealtimeSubscription, sdk } from '$lib/stores/sdk'; + import { realtime, sdk } from '$lib/stores/sdk'; import { Submit, trackError, trackEvent } from '$lib/actions/analytics'; import { addNotification } from '$lib/stores/notifications'; import { fade } from 'svelte/transition'; @@ -241,8 +241,10 @@ APPWRITE_ENDPOINT = "${sdk.forProject(page.params.region, page.params.project).c message: 'Platform created.' }); - invalidate(Dependencies.PROJECT); - invalidate(Dependencies.PLATFORMS); + await Promise.all([ + invalidate(Dependencies.PROJECT), + invalidate(Dependencies.PLATFORMS) + ]); } catch (error) { trackError(error, Submit.PlatformCreate); addNotification({ @@ -259,21 +261,18 @@ APPWRITE_ENDPOINT = "${sdk.forProject(page.params.region, page.params.project).c } onMount(() => { - let subscription: AppwriteRealtimeSubscription; - sdk.forConsole.realtime - .subscribe('console', (response) => { - if (response.events.includes(`projects.${projectId}.ping`)) { - connectionSuccessful = true; - invalidate(Dependencies.ORGANIZATION); - invalidate(Dependencies.PROJECT); - subscription?.close(); - } - }) - .then((realtime) => (subscription = realtime)); + const unsubscribe = realtime.forConsole(page.params.region, 'console', (response) => { + if (response.events.includes(`projects.${projectId}.ping`)) { + connectionSuccessful = true; + invalidate(Dependencies.ORGANIZATION); + invalidate(Dependencies.PROJECT); + unsubscribe(); + } + }); return () => { + unsubscribe(); resetPlatformStore(); - subscription?.close(); }; }); diff --git a/src/routes/(console)/project-[region]-[project]/settings/migrations/+page.svelte b/src/routes/(console)/project-[region]-[project]/settings/migrations/+page.svelte index b6f8083bc..cc82cb18c 100644 --- a/src/routes/(console)/project-[region]-[project]/settings/migrations/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/settings/migrations/+page.svelte @@ -38,13 +38,11 @@ let migration: Models.Migration = null; onMount(() => { - return realtime - .forProject(page.params.region, page.params.project) - .subscribe(['project', 'console'], (response) => { - if (response.events.includes('migrations.*')) { - invalidate(Dependencies.MIGRATIONS); - } - }); + return realtime.forProject(page.params.region, ['project', 'console'], (response) => { + if (response.events.includes('migrations.*')) { + invalidate(Dependencies.MIGRATIONS); + } + }); }); $: $registerCommands([ diff --git a/src/routes/(console)/project-[region]-[project]/sites/+page.svelte b/src/routes/(console)/project-[region]-[project]/sites/+page.svelte index e1eb6bb66..3c4ee175a 100644 --- a/src/routes/(console)/project-[region]-[project]/sites/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/sites/+page.svelte @@ -25,7 +25,8 @@ import { onMount } from 'svelte'; import { invalidate } from '$app/navigation'; import { Dependencies } from '$lib/constants'; - import { sdk } from '$lib/stores/sdk'; + import { realtime } from '$lib/stores/sdk'; + import { page } from '$app/state'; export let data; @@ -49,7 +50,7 @@ $updateCommandGroupRanks({ sites: 1000 }); onMount(() => { - return sdk.forConsole.realtime.subscribe('console', (response) => { + return realtime.forConsole(page.params.region, 'console', (response) => { if (response.events.includes('sites.*')) { invalidate(Dependencies.SITES); } 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 bdb1741ac..3cb100543 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 @@ -7,42 +7,36 @@ import Aside from '../aside.svelte'; import Logs from '../../(components)/logs.svelte'; import { Copy, SvgIcon } from '$lib/components'; - import { type AppwriteRealtimeSubscription, sdk } from '$lib/stores/sdk'; + import { realtime } from '$lib/stores/sdk'; import { goto } from '$app/navigation'; import { onMount } from 'svelte'; import { getFrameworkIcon } from '$lib/stores/sites'; + import type { Models } from '@appwrite.io/console'; let { data } = $props(); let deployment = $state(data.deployment); onMount(() => { - let subscription: AppwriteRealtimeSubscription; - sdk.forConsoleIn(page.params.region) - .realtime.subscribe('console', async (response) => { - if ( - response.events.includes( - `sites.${data.site.$id}.deployments.${data.deployment.$id}.update` - ) - ) { - deployment = response.payload; - if (response.payload.status === 'ready') { - 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}`); - } + 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 + } + ); + await goto(`${resolvedUrl}?site=${data.site.$id}`); } - }) - .then((realtime) => (subscription = realtime)); - - return () => { - subscription?.close(); - }; + } + }); }); 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 281b68314..8cecb4acc 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 @@ -174,24 +174,21 @@ on:invalid={handleInvalid}> - - - Drag and drop file here or click to upload - - + + + Drag and drop file here or click to upload; - + justifyContent="center"> + + + Only .tar.gz files allowed + - Only .tar.gz files allowed - + {#if maxSize > 0} { if (variable.value === '{apiEndpoint}') { - variable.value = getApiEndpoint(); - variable.placeholder = getApiEndpoint(); + variable.value = getApiEndpoint(page.params.region); + variable.placeholder = getApiEndpoint(page.params.region); } else if (variable.value === '{projectId}') { variable.value = page.params.project; variable.placeholder = page.params.project; diff --git a/src/routes/(console)/project-[region]-[project]/sites/site-[site]/+page.svelte b/src/routes/(console)/project-[region]-[project]/sites/site-[site]/+page.svelte index adbf16091..9a3c98817 100644 --- a/src/routes/(console)/project-[region]-[project]/sites/site-[site]/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/sites/site-[site]/+page.svelte @@ -7,19 +7,21 @@ import { Button } from '$lib/elements/forms'; import InstantRollbackDomain from './instantRollbackModal.svelte'; import { app } from '$lib/stores/app'; - import { sdk } from '$lib/stores/sdk'; + import { realtime } from '$lib/stores/sdk'; import { invalidate } from '$app/navigation'; import { Dependencies } from '$lib/constants'; import { onMount } from 'svelte'; import { page } from '$app/state'; import { base } from '$app/paths'; + import type { PageProps } from './$types'; import { regionalProtocol } from '$routes/(console)/project-[region]-[project]/store'; - export let data; - let showRollback = false; + let { data }: PageProps = $props(); + + let showRollback = $state(false); onMount(() => { - return sdk.forConsole.realtime.subscribe('console', (response) => { + return realtime.forConsole(page.params.region, 'console', (response) => { if (response.events.includes(`sites.${page.params.site}.deployments.*`)) { invalidate(Dependencies.SITE); } diff --git a/src/routes/(console)/project-[region]-[project]/sites/site-[site]/deployments/+layout.svelte b/src/routes/(console)/project-[region]-[project]/sites/site-[site]/deployments/+layout.svelte index 8ff3fe4e8..64a7454fa 100644 --- a/src/routes/(console)/project-[region]-[project]/sites/site-[site]/deployments/+layout.svelte +++ b/src/routes/(console)/project-[region]-[project]/sites/site-[site]/deployments/+layout.svelte @@ -1,7 +1,7 @@ diff --git a/src/routes/(console)/project-[region]-[project]/sites/site-[site]/deployments/table.svelte b/src/routes/(console)/project-[region]-[project]/sites/site-[site]/deployments/table.svelte index 9bb700a5a..8f75512aa 100644 --- a/src/routes/(console)/project-[region]-[project]/sites/site-[site]/deployments/table.svelte +++ b/src/routes/(console)/project-[region]-[project]/sites/site-[site]/deployments/table.svelte @@ -1,5 +1,5 @@ - - + {#snippet header(root)} {#each $columns as { id, title }} {title} {/each} - - {#each data.deploymentList.deployments as deployment (deployment.$id)} - - {#each $columns as column} - - {#if column.id === '$id'} - {#key column.id} - {deployment.$id} - {/key} - {:else if column.id === 'status'} - {@const status = deployment.status} - {#if data?.activeDeployment?.$id === deployment?.$id} - - {:else} - + {/snippet} + + {#snippet children(root)} + {#each data.deploymentList.deployments as deployment (deployment.$id)} + + {#each $columns as column} + + {#if column.id === '$id'} + {#key column.id} + {deployment.$id} + {/key} + {:else if column.id === 'status'} + {@const status = deployment.status} + {#if data?.activeDeployment?.$id === deployment?.$id} + + {:else} + + {/if} + {:else if column.id === 'type'} + + {:else if column.id === '$updatedAt'} + + {:else if column.id === 'buildDuration'} + {#if ['waiting'].includes(deployment.status)} + - + {:else if ['processing', 'building'].includes(deployment.status)} + + {:else} + {formatTimeDetailed(deployment.buildDuration)} + {/if} + {:else if column.id === 'totalSize'} + {calculateSize(deployment?.totalSize ?? 0)} + {:else if column.id === 'sourceSize'} + {calculateSize(deployment.sourceSize)} + {:else if column.id === 'buildSize'} + {calculateSize(deployment.buildSize)} {/if} - {:else if column.id === 'type'} - - {:else if column.id === '$updatedAt'} - - {:else if column.id === 'buildDuration'} - {#if ['waiting'].includes(deployment.status)} - - - {:else if ['processing', 'building'].includes(deployment.status)} - - {:else} - {formatTimeDetailed(deployment.buildDuration)} - {/if} - {:else if column.id === 'totalSize'} - {calculateSize(deployment?.totalSize ?? 0)} - {:else if column.id === 'sourceSize'} - {calculateSize(deployment.sourceSize)} - {:else if column.id === 'buildSize'} - {calculateSize(deployment.buildSize)} - {/if} + + {/each} + + + + - {/each} - - - - - - - {/each} - + + {/each} + {/snippet} + + {#snippet deleteContent(count)} + Are you sure you want to delete {count} + {count > 1 ? 'deployments' : 'deployment'} from your site - + {page.data.site.name}? + {/snippet} + {#if selectedDeployment} @@ -154,33 +151,3 @@ selectedDeploymentId={selectedDeployment.$id} bind:show={showActivate} /> {/if} - -{#if selectedRows.length > 0} - - - - - {selectedRows.length > 1 ? 'deployments' : 'deployment'} - selected - - - - - - - -{/if} - - -

- Are you sure you want to delete {selectedRows.length} - {selectedRows.length > 1 ? 'deployments' : 'deployment'} from your site - - {$site.name}? -

- -

This action is irreversible.

-
diff --git a/src/routes/(console)/project-[region]-[project]/sites/site-[site]/logs/+page.svelte b/src/routes/(console)/project-[region]-[project]/sites/site-[site]/logs/+page.svelte index 7d710cb40..3ad6b4645 100644 --- a/src/routes/(console)/project-[region]-[project]/sites/site-[site]/logs/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/sites/site-[site]/logs/+page.svelte @@ -4,16 +4,17 @@ import { Dependencies } from '$lib/constants'; import { Button } from '$lib/elements/forms'; import { Container, ResponsiveContainerHeader } from '$lib/layout'; - import { sdk } from '$lib/stores/sdk'; + import { realtime } from '$lib/stores/sdk'; import { onMount } from 'svelte'; import Table from './table.svelte'; import { Card, Empty } from '@appwrite.io/pink-svelte'; import { columns } from './store'; + import { page } from '$app/state'; export let data; onMount(() => { - return sdk.forConsole.realtime.subscribe('console', (response) => { + return realtime.forConsole(page.params.region, 'console', (response) => { if (response.events.includes('sites.*.executions.*')) { invalidate(Dependencies.EXECUTIONS); } 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 9d74a095d..da4890035 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,137 +1,105 @@ - - + + {#snippet header(root)} {#each filteredColumns as { id, title }} {title} {/each} - - {#each logs.executions as log (log.$id)} - { - e.stopPropagation(); - openSheet = true; - selectedLogId = log.$id; - }}> - {#each filteredColumns as column} - - {#if column.id === '$id'} - {#key column.id} - {log.$id} - {/key} - {:else if column.id === 'deploymentId'} - {log.deploymentId} - {:else if column.id === 'requestMethod'} - - {log.requestMethod} - - {:else if column.id === 'duration'} - {#if ['processing', 'waiting'].includes(log.status)} - - {:else} - {calculateTime(log.duration)} + {/snippet} + + {#snippet children(root)} + {#each logs.executions as log (log.$id)} + { + e.stopPropagation(); + openSheet = true; + selectedLogId = log.$id; + }}> + {#each filteredColumns as column} + + {#if column.id === '$id'} + {#key column.id} + {log.$id} + {/key} + {:else if column.id === 'deploymentId'} + {log.deploymentId} + {:else if column.id === 'requestMethod'} + + {log.requestMethod} + + {:else if column.id === 'duration'} + {#if ['processing', 'waiting'].includes(log.status)} + + {:else} + {calculateTime(log.duration)} + {/if} + {:else if column.id === 'responseStatusCode'} +
+ +
+ {:else if column.id === 'requestPath'} + + {log.requestPath} + + {:else if column.id === '$createdAt'} + {/if} - {:else if column.id === 'responseStatusCode'} -
- -
- {:else if column.id === 'requestPath'} - - {log.requestPath} - - {:else if column.id === '$createdAt'} - - {/if} -
- {/each} -
- {/each} -
+ + {/each} + + {/each} + {/snippet} + - -{#if selectedRows.length > 0} - - - - - {selectedRows.length > 1 ? 'logs' : 'log'} - selected - - - - - - - -{/if} - - -

- Are you sure you want to delete {selectedRows.length} - {selectedRows.length > 1 ? 'logs' : 'log'}? -

- -

This action is irreversible.

-
diff --git a/src/routes/(console)/project-[region]-[project]/storage/bucket-[bucket]/+page.svelte b/src/routes/(console)/project-[region]-[project]/storage/bucket-[bucket]/+page.svelte index a4ae7b082..8caf3c05a 100644 --- a/src/routes/(console)/project-[region]-[project]/storage/bucket-[bucket]/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/storage/bucket-[bucket]/+page.svelte @@ -3,28 +3,25 @@ import { base } from '$app/paths'; import { page } from '$app/state'; import { Submit, trackError, trackEvent } from '$lib/actions/analytics'; - import { Avatar, Empty, EmptySearch, PaginationWithLimit, SearchQuery } from '$lib/components'; + import { + Avatar, + type DeleteOperationState, + Empty, + EmptySearch, + MultiSelectionTable, + PaginationWithLimit, + SearchQuery + } from '$lib/components'; import { Dependencies } from '$lib/constants'; import { Badge } from '@appwrite.io/pink-svelte'; import { Button } from '$lib/elements/forms'; import { calculateSize } from '$lib/helpers/sizeConvertion'; import { Container } from '$lib/layout'; import { ImageFormat, type Models } from '@appwrite.io/console'; - import { addNotification } from '$lib/stores/notifications'; import { uploader } from '$lib/stores/uploader'; import { sdk } from '$lib/stores/sdk.js'; import DeleteFile from './deleteFile.svelte'; - import { bucket } from './store'; - import Confirm from '$lib/components/confirm.svelte'; - import { - Layout, - Table, - Icon, - Popover, - ActionMenu, - FloatingActionBar, - Typography - } from '@appwrite.io/pink-svelte'; + import { Layout, Table, Icon, Popover, ActionMenu } from '@appwrite.io/pink-svelte'; import { onMount } from 'svelte'; import DualTimeView from '$lib/components/dualTimeView.svelte'; import { @@ -34,23 +31,20 @@ IconTrash } from '@appwrite.io/pink-icons-svelte'; import { isSmallViewport } from '$lib/stores/viewport'; + import type { PageProps } from './$types'; - export let data; + const { data }: PageProps = $props(); - let showDelete = false; - let selectedFile: Models.File = null; - let selectedFiles: string[] = []; - let showBulkDelete = false; - let deleting = false; - - const bucketId = page.params.bucket; + let showDelete = $state(false); + let isUploading = $state(false); + let selectedFile: Models.File | null = $state(null); function getPreview(fileId: string) { return ( sdk .forProject(page.params.region, page.params.project) .storage.getFilePreview({ - bucketId, + bucketId: page.params.bucket, fileId, height: 128, width: 128, @@ -71,42 +65,25 @@ showDelete = true; } - async function handleBulkDelete() { - showBulkDelete = false; - deleting = true; - - const promises = selectedFiles.map((fileId) => - sdk.forProject(page.params.region, page.params.project).storage.deleteFile({ - bucketId, + async function handleBulkDelete(selectedRows: string[]): Promise { + const promises = selectedRows.map((fileId) => { + return sdk.forProject(page.params.region, page.params.project).storage.deleteFile({ + bucketId: page.params.bucket, fileId - }) - ); + }); + }); try { await Promise.all(promises); - trackEvent(Submit.FileDelete, { - total: selectedFiles.length - }); - addNotification({ - type: 'success', - message: `${selectedFiles.length} file${selectedFiles.length > 1 ? 's' : ''} deleted` - }); - invalidate(Dependencies.FILES); + trackEvent(Submit.FileDelete, { total: selectedRows.length }); } catch (error) { - addNotification({ - type: 'error', - message: error.message - }); trackError(error, Submit.FileDelete); + return error; } finally { - selectedFiles = []; - showBulkDelete = false; - deleting = false; + await invalidate(Dependencies.FILES); } } - let isUploading = false; - const beforeunload = (event: BeforeUnloadEvent) => { // legacy browser **may** support showing a custom message. const message = 'An upload is in progress. Are you sure you want to leave?'; @@ -147,10 +124,10 @@
{#if data.files.total} - - + {#snippet header(root)} Filename Type Size Created - - {#each data.files.files as file} - {#if file.chunksTotal / file.chunksUploaded !== 1} - - - - - {file.name} -
- + {/snippet} + + {#snippet children(root)} + {#each data.files.files as file} + {#if file.chunksTotal / file.chunksUploaded !== 1} + + + + + {file.name} +
+ +
+
+
+ {file.mimeType} + + {calculateSize(file.sizeOriginal)} + + + + + +
+
- -
- {file.mimeType} - - {calculateSize(file.sizeOriginal)} - - - - - -
- -
-
-
- {:else} - {@const href = `${base}/project-${page.params.region}-${page.params.project}/storage/bucket-${bucketId}/file-${file.$id}`} - - -
- - {file.name} -
-
- {file.mimeType} - - {calculateSize(file.sizeOriginal)} - - - - - - - - - - Update - - + + {:else} + {@const href = `${base}/project-${page.params.region}-${page.params.project}/storage/bucket-${page.params.bucket}/file-${file.$id}`} + + +
+ + {file.name} +
+
+ {file.mimeType} + + {calculateSize(file.sizeOriginal)} + + + + + + + + + + Update + + { + e.stopPropagation(); + e.preventDefault(); + selectedFile = file; + showDelete = true; + }}> + Delete + + + + +
+ {/if} + {/each} + {/snippet} + + {#snippet deleteContentNotice()} + This action is irreversible and will permanently remove the selected files. + {/snippet} + + href={`${base}/project-${page.params.region}-${page.params.project}/storage/bucket-${page.params.bucket}`}> Clear Search @@ -268,39 +258,9 @@ allowCreate on:click={() => goto( - `${base}/project-${page.params.region}-${page.params.project}/storage/bucket-${bucketId}/create` + `${base}/project-${page.params.region}-${page.params.project}/storage/bucket-${page.params.bucket}/create` )} /> {/if} - - {#if selectedFiles.length > 0} - - - - - {selectedFiles.length > 1 ? 'files' : 'file'} - selected - - - - - - - - {/if} - - - - Are you sure you want to delete {selectedFiles.length} - {selectedFiles.length > 1 ? 'files' : 'file'} from {$bucket?.name}? - - - This action is irreversible and will permanently remove the selected files. - - diff --git a/static/icons/dark/color/resend.svg b/static/icons/dark/color/resend.svg index 4371f60b3..0ded97051 100644 --- a/static/icons/dark/color/resend.svg +++ b/static/icons/dark/color/resend.svg @@ -1,3 +1,3 @@ - + diff --git a/static/icons/light/color/resend.svg b/static/icons/light/color/resend.svg index 67af6bf6d..ad9911369 100644 --- a/static/icons/light/color/resend.svg +++ b/static/icons/light/color/resend.svg @@ -1,3 +1,3 @@ - +