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/package.json b/package.json index b4fc20d0f..2db84e49e 100644 --- a/package.json +++ b/package.json @@ -22,11 +22,11 @@ }, "dependencies": { "@ai-sdk/svelte": "^1.1.24", - "@appwrite.io/console": "https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@fe3277e", + "@appwrite.io/console": "https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@2752", "@appwrite.io/pink-icons": "0.25.0", - "@appwrite.io/pink-icons-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@46f65c7", + "@appwrite.io/pink-icons-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@4472521", "@appwrite.io/pink-legacy": "^1.0.3", - "@appwrite.io/pink-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@46f65c7", + "@appwrite.io/pink-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@4472521", "@faker-js/faker": "^9.9.0", "@popperjs/core": "^2.11.8", "@sentry/sveltekit": "^8.38.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 82b9a6afc..a05369a8a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,20 +12,20 @@ importers: specifier: ^1.1.24 version: 1.1.24(svelte@5.25.3)(zod@3.24.3) '@appwrite.io/console': - specifier: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@fe3277e - version: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@fe3277e + specifier: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@2752 + version: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@2752 '@appwrite.io/pink-icons': specifier: 0.25.0 version: 0.25.0 '@appwrite.io/pink-icons-svelte': - specifier: https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@46f65c7 - version: https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@46f65c7(svelte@5.25.3) + specifier: https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@4472521 + version: https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@4472521(svelte@5.25.3) '@appwrite.io/pink-legacy': specifier: ^1.0.3 version: 1.0.3 '@appwrite.io/pink-svelte': - specifier: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@46f65c7 - version: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@46f65c7(svelte@5.25.3) + specifier: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@4472521 + version: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@4472521(svelte@5.25.3) '@faker-js/faker': specifier: ^9.9.0 version: 9.9.0 @@ -260,8 +260,8 @@ packages: '@analytics/type-utils@0.6.2': resolution: {integrity: sha512-TD+xbmsBLyYy/IxFimW/YL/9L2IEnM7/EoV9Aeh56U64Ify8o27HJcKjo38XY9Tcn0uOq1AX3thkKgvtWvwFQg==} - '@appwrite.io/console@https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@fe3277e': - resolution: {tarball: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@fe3277e} + '@appwrite.io/console@https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@2752': + resolution: {tarball: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@2752} version: 1.10.0 '@appwrite.io/pink-icons-svelte@2.0.0-RC.1': @@ -269,8 +269,8 @@ packages: peerDependencies: svelte: ^4.0.0 - '@appwrite.io/pink-icons-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@46f65c7': - resolution: {tarball: https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@46f65c7} + '@appwrite.io/pink-icons-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@4472521': + resolution: {tarball: https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@4472521} version: 2.0.0-RC.1 peerDependencies: svelte: ^4.0.0 @@ -284,8 +284,8 @@ packages: '@appwrite.io/pink-legacy@1.0.3': resolution: {integrity: sha512-GGde5fmPhs+s6/3aFeMPc/kKADG/gTFkYQSy6oBN8pK0y0XNCLrZZgBv+EBbdhwdtqVEWXa0X85Mv9w7jcIlwQ==} - '@appwrite.io/pink-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@46f65c7': - resolution: {tarball: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@46f65c7} + '@appwrite.io/pink-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@4472521': + resolution: {tarball: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@4472521} version: 2.0.0-RC.2 peerDependencies: svelte: ^4.0.0 @@ -3703,13 +3703,13 @@ snapshots: '@analytics/type-utils@0.6.2': {} - '@appwrite.io/console@https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@fe3277e': {} + '@appwrite.io/console@https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@2752': {} '@appwrite.io/pink-icons-svelte@2.0.0-RC.1(svelte@5.25.3)': dependencies: svelte: 5.25.3 - '@appwrite.io/pink-icons-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@46f65c7(svelte@5.25.3)': + '@appwrite.io/pink-icons-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@4472521(svelte@5.25.3)': dependencies: svelte: 5.25.3 @@ -3722,7 +3722,7 @@ snapshots: '@appwrite.io/pink-icons': 1.0.0 the-new-css-reset: 1.11.3 - '@appwrite.io/pink-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@46f65c7(svelte@5.25.3)': + '@appwrite.io/pink-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@4472521(svelte@5.25.3)': dependencies: '@appwrite.io/pink-icons-svelte': 2.0.0-RC.1(svelte@5.25.3) '@floating-ui/dom': 1.6.13 diff --git a/src/lib/commandCenter/searchers/projects.ts b/src/lib/commandCenter/searchers/projects.ts index 1df30a3b7..d9c5c8da0 100644 --- a/src/lib/commandCenter/searchers/projects.ts +++ b/src/lib/commandCenter/searchers/projects.ts @@ -4,22 +4,64 @@ import { sdk } from '$lib/stores/sdk'; import { Query } from '@appwrite.io/console'; import { get } from 'svelte/store'; import type { Searcher } from '../commands'; +import { project } from '$routes/(console)/project-[region]-[project]/store'; import { base } from '$app/paths'; export const projectsSearcher = (async (query: string) => { + const q = query.toLowerCase().trim(); + const keywords = [ + 'endpoint', + 'api key', + 'api-key', + 'apikey', + 'project id', + 'project-id', + 'api end' + ]; + + const wantsCredentials = keywords.some((k) => q.includes(k)); + + if (wantsCredentials) { + const curr = get(project); + if (curr?.$id) { + return [ + { + label: 'Go to Settings', + callback: () => { + goto(`${base}/project-${curr.region}-${curr.$id}/settings`); + }, + group: 'navigation' + } + ]; + } + return []; + } + const { projects } = await sdk.forConsole.projects.list({ queries: [Query.equal('teamId', get(organization).$id), Query.orderDesc('')] }); return projects - .filter((project) => project.name.toLowerCase().includes(query.toLowerCase())) + .filter((project) => { + const searchable = [project.name, project.$id, project.region] + .filter(Boolean) + .join(' ') + .toLowerCase(); + + const words = q.split(/\s+/).filter(Boolean); + return words.every((w) => searchable.includes(w)); + }) .map((project) => { + const href = `${base}/project-${project.region}-${project.$id}`; + + const label = project.name; + return { - label: project.name, + label, callback: () => { - goto(`${base}/project-${project.region}-${project.$id}`); + goto(href); }, group: 'projects' - } as const; + }; }); }) satisfies Searcher; 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}> { + 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); diff --git a/src/lib/components/id.svelte b/src/lib/components/id.svelte index 314e8fac2..201f651c1 100644 --- a/src/lib/components/id.svelte +++ b/src/lib/components/id.svelte @@ -95,10 +95,11 @@ export let tooltipPortal = false; export let tooltipDelay: number = 0; export let tooltipPlacement: TooltipPlacement = undefined; + export let copyText: string | undefined = undefined; {#key value} - + { - return realtime - .forProject(page.params.region, page.params.project) - .subscribe(['console'], async (response) => { - if (!response.channels.includes(`projects.${getProjectId()}`)) return; - if (response.events.includes('migrations.*')) { - if (response.payload.source === 'Backup') return; - migration = response.payload; - } - }); + return realtime.forProject(page.params.region, ['console'], async (response) => { + if (!response.channels.includes(`projects.${getProjectId()}`)) return; + if (response.events.includes('migrations.*')) { + const payload = response.payload as Models.Migration; + if (payload.source === 'Backup') return; + migration = payload; + } + }); }); diff --git a/src/lib/components/permissions/permissions.svelte b/src/lib/components/permissions/permissions.svelte index 46b6a9bb3..d76000cd2 100644 --- a/src/lib/components/permissions/permissions.svelte +++ b/src/lib/components/permissions/permissions.svelte @@ -20,6 +20,7 @@ import { IconPlus, IconX } from '@appwrite.io/pink-icons-svelte'; import type { PinkColumn } from '$lib/helpers/types'; import { Card } from '$lib/components'; + import { TableScroll } from '$lib/elements/table'; export let withCreate = false; export let permissions: string[] = []; @@ -124,7 +125,7 @@ {#if [...$groups]?.length} -
+ Role @@ -139,7 +140,7 @@ {#each [...$groups] as [role, permission] (role)} - + deleteRole(role)} /> {/each} -
+
{/if} - - diff --git a/src/lib/components/permissions/row.svelte b/src/lib/components/permissions/row.svelte index a8c6b5080..c70fd5f66 100644 --- a/src/lib/components/permissions/row.svelte +++ b/src/lib/components/permissions/row.svelte @@ -1,5 +1,5 @@ {#if region} - - - - {#if flagSrc} - {region?.name} - {/if} - - - {region?.name} - - + + + + API endpoint + {#if flagSrc} + {region?.name} + {/if} {/if} + + diff --git a/src/lib/elements/forms/inputSelect.svelte b/src/lib/elements/forms/inputSelect.svelte index 5f56249ff..57791bdd7 100644 --- a/src/lib/elements/forms/inputSelect.svelte +++ b/src/lib/elements/forms/inputSelect.svelte @@ -21,16 +21,17 @@ }[]; export let leadingIcon: ComponentType | undefined = undefined; - let element: HTMLSelectElement; let error: string; const handleInvalid = (event: Event) => { event.preventDefault(); + const element = event.target as HTMLInputElement; if (element.validity.valueMissing) { error = 'This field is required'; return; } + error = element.validationMessage; }; @@ -38,12 +39,6 @@ return typeof value === 'boolean' ? true : !!value; }; - $: if (required && !isNotEmpty(value)) { - element?.setCustomValidity('This field is required'); - } else { - element?.setCustomValidity(''); - } - $: if (isNotEmpty(value)) { error = null; } diff --git a/src/lib/helpers/buildTimeout.ts b/src/lib/helpers/buildTimeout.ts new file mode 100644 index 000000000..3915811c2 --- /dev/null +++ b/src/lib/helpers/buildTimeout.ts @@ -0,0 +1,45 @@ +import type { Models } from '@appwrite.io/console'; + +/** + * Checks if a build has exceeded the maximum build timeout duration + */ +function isBuildTimedOut(createdAt: string, status: string, timeoutSeconds: number): boolean { + if (!['waiting', 'processing', 'building'].includes(status)) { + return false; + } + + if (!timeoutSeconds || timeoutSeconds <= 0) { + return false; + } + + const created = new Date(createdAt); + const elapsedSeconds = Math.floor((Date.now() - created.getTime()) / 1000); + + return elapsedSeconds > timeoutSeconds; +} + +/** + * Gets the effective status for a build, considering timeout + */ +export function getEffectiveBuildStatus( + originalStatus: string, + createdAt: string, + consoleVariables: Models.ConsoleVariables | undefined +): string { + const timeoutSeconds = getBuildTimeoutSeconds(consoleVariables); + if (isBuildTimedOut(createdAt, originalStatus, timeoutSeconds)) { + return 'failed'; + } + return originalStatus; +} + +/** + * Helper to get timeout value from console variables + */ +function getBuildTimeoutSeconds(consoleVariables: Models.ConsoleVariables | undefined): number { + if (!consoleVariables?._APP_COMPUTE_BUILD_TIMEOUT) { + return 0; + } + const timeout = parseInt(String(consoleVariables._APP_COMPUTE_BUILD_TIMEOUT), 10); + return isNaN(timeout) ? 0 : timeout; +} diff --git a/src/lib/images/promos/db-operators-dark.png b/src/lib/images/promos/db-operators-dark.png new file mode 100644 index 000000000..559c6fce1 Binary files /dev/null and b/src/lib/images/promos/db-operators-dark.png differ diff --git a/src/lib/images/promos/db-operators-light.png b/src/lib/images/promos/db-operators-light.png new file mode 100644 index 000000000..a3dd30e70 Binary files /dev/null and b/src/lib/images/promos/db-operators-light.png differ diff --git a/src/lib/images/promos/transactions-api-dark.png b/src/lib/images/promos/transactions-api-dark.png deleted file mode 100644 index 4fa749489..000000000 Binary files a/src/lib/images/promos/transactions-api-dark.png and /dev/null differ diff --git a/src/lib/images/promos/transactions-api-light.png b/src/lib/images/promos/transactions-api-light.png deleted file mode 100644 index f9796c5e0..000000000 Binary files a/src/lib/images/promos/transactions-api-light.png and /dev/null differ diff --git a/src/lib/layout/footer.svelte b/src/lib/layout/footer.svelte index 54e8ec053..ede26618b 100644 --- a/src/lib/layout/footer.svelte +++ b/src/lib/layout/footer.svelte @@ -76,7 +76,7 @@ {/if} - {#if $version} + {#if $version && !isCloud} {/if} {#if $isSmallViewport} - {#if $version} + {#if $version && !isCloud} @@ -151,15 +151,18 @@ style="white-space: nowrap;"> Version {$version} - {#if isCloud} - - - {/if} + {/if} + {#if isCloud} + + + + + {/if} {/if} diff --git a/src/lib/stores/sdk.ts b/src/lib/stores/sdk.ts index 8fd1b477b..daeb7ffba 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 ''; @@ -112,7 +111,6 @@ if (!building) { scopedConsoleClient.setProject('console'); clientConsole.setEndpoint(endpoint).setProject('console'); - clientRealtime.setEndpoint(endpoint).setProject('console'); clientProject.setEndpoint(endpoint).setMode('admin'); clientRealtime.setEndpoint(endpoint).setProject('console'); } @@ -141,12 +139,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 +194,8 @@ export const sdk = { }; export enum RuleType { - DEPLOYMENT = 'deployment', API = 'api', + DEPLOYMENT = 'deployment', REDIRECT = 'redirect' } @@ -191,11 +209,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)/bottomAlerts.ts b/src/routes/(console)/bottomAlerts.ts index 864daa6d3..b0316aeac 100644 --- a/src/routes/(console)/bottomAlerts.ts +++ b/src/routes/(console)/bottomAlerts.ts @@ -1,32 +1,32 @@ import { isCloud } from '$lib/system'; import { isSameDay } from '$lib/helpers/date'; import { type BottomModalAlertItem, showBottomModalAlert } from '$lib/stores/bottom-alerts'; -import TransactionsApiDark from '$lib/images/promos/transactions-api-dark.png'; -import TransactionsApiLight from '$lib/images/promos/transactions-api-light.png'; +import DbOperatorsDark from '$lib/images/promos/db-operators-dark.png'; +import DbOperatorsLight from '$lib/images/promos/db-operators-light.png'; const listOfPromotions: BottomModalAlertItem[] = []; if (isCloud) { - const transactionsApiPromo: BottomModalAlertItem = { - id: 'modal:transactions_api_announcement', + const dbOperatorsPromo: BottomModalAlertItem = { + id: 'modal:db_operators_announcement', src: { - dark: TransactionsApiDark, - light: TransactionsApiLight + dark: DbOperatorsDark, + light: DbOperatorsLight }, - title: 'Announcing Transactions API', - message: 'Ensure data consistency across tables with atomic, all-or-nothing commits.', + title: 'Announcing DB operators', + message: 'Update multiple fields without fetching the entire row.', plan: 'free', importance: 8, scope: 'project', cta: { text: 'Read announcement', - link: () => 'https://appwrite.io/blog/post/announcing-transactions-api', + link: () => 'https://appwrite.io/blog/post/announcing-db-operators', external: true, hideOnClick: true }, show: true }; - listOfPromotions.push(transactionsApiPromo); + listOfPromotions.push(dbOperatorsPromo); } export function addBottomModalAlerts() { diff --git a/src/routes/(console)/onboarding/create-project/+page.svelte b/src/routes/(console)/onboarding/create-project/+page.svelte index 12dcb1a76..fdc92f8cf 100644 --- a/src/routes/(console)/onboarding/create-project/+page.svelte +++ b/src/routes/(console)/onboarding/create-project/+page.svelte @@ -1,5 +1,6 @@ diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(suggestions)/empty.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(suggestions)/empty.svelte index f7c0b189d..7a5602eb7 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(suggestions)/empty.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(suggestions)/empty.svelte @@ -635,7 +635,7 @@ try { if (VARS.MOCK_AI_SUGGESTIONS) { /* animation */ - await sleep(NOTIFICATION_AND_MOCK_DELAY); + await sleep(NOTIFICATION_AND_MOCK_DELAY * 100); suggestedColumns = mockSuggestions; } else { suggestedColumns = (await sdk 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 b9c1c57c8..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 @@ -161,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]/table-[table]/+layout.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/+layout.svelte index e3a3de05d..bc4f7f186 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 @@ @@ -122,11 +127,11 @@ - {#if deployment.status === 'failed'} + {#if effectiveStatus === 'failed'} {@render titleSnippet('Status')} - + {:else} diff --git a/src/routes/(console)/project-[region]-[project]/functions/function-[function]/(components)/downloadActionMenuItem.svelte b/src/routes/(console)/project-[region]-[project]/functions/function-[function]/(components)/downloadActionMenuItem.svelte index b91ee6d6f..85d45bb27 100644 --- a/src/routes/(console)/project-[region]-[project]/functions/function-[function]/(components)/downloadActionMenuItem.svelte +++ b/src/routes/(console)/project-[region]-[project]/functions/function-[function]/(components)/downloadActionMenuItem.svelte @@ -2,7 +2,7 @@ import { page } from '$app/state'; import { SubMenu } from '$lib/components/menu'; import { type Models } from '@appwrite.io/console'; - import { IconDownload } from '@appwrite.io/pink-icons-svelte'; + import { IconDownload, IconChevronRight } from '@appwrite.io/pink-icons-svelte'; import { ActionMenu } from '@appwrite.io/pink-svelte'; import { getOutputDownload, getSourceDownload } from '../store'; @@ -13,7 +13,8 @@ {#if deployment?.status === 'ready' || deployment?.status === 'failed' || deployment?.status === 'building'} - Download + Download diff --git a/src/routes/(console)/project-[region]-[project]/functions/function-[function]/(modals)/createCli.svelte b/src/routes/(console)/project-[region]-[project]/functions/function-[function]/(modals)/createCli.svelte index ae5cdc4c3..70124e38e 100644 --- a/src/routes/(console)/project-[region]-[project]/functions/function-[function]/(modals)/createCli.svelte +++ b/src/routes/(console)/project-[region]-[project]/functions/function-[function]/(modals)/createCli.svelte @@ -54,26 +54,26 @@ function setCodeSnippets() { return { Unix: { - code: `appwrite client --projectId="${page.params.project}" && \\ -appwrite functions createDeployment \\ - --functionId=${functionId} \\ + code: `appwrite client --project-id="${page.params.project}" && \\ +appwrite functions create-deployment \\ + --function-id=${functionId} \\ --code="." \\ --activate=true`, language: 'bash' }, CMD: { - code: `appwrite client --projectId="${page.params.project}" && ^ -appwrite functions createDeployment ^ - --functionId=${functionId} ^ + code: `appwrite client --project-id="${page.params.project}" && ^ +appwrite functions create-deployment ^ + --function-id=${functionId} ^ --code="." ^ --activate`, language: 'CMD' }, PowerShell: { - code: `appwrite client --projectId="${page.params.project}" && , -appwrite functions createDeployment , - --functionId=${functionId} , + code: `appwrite client --project-id="${page.params.project}" && , +appwrite functions create-deployment , + --function-id=${functionId} , --code="." , --activate`, language: 'PowerShell' diff --git a/src/routes/(console)/project-[region]-[project]/functions/function-[function]/(modals)/deleteModal.svelte b/src/routes/(console)/project-[region]-[project]/functions/function-[function]/(modals)/deleteModal.svelte index a8a5d819e..e913d705b 100644 --- a/src/routes/(console)/project-[region]-[project]/functions/function-[function]/(modals)/deleteModal.svelte +++ b/src/routes/(console)/project-[region]-[project]/functions/function-[function]/(modals)/deleteModal.svelte @@ -1,5 +1,6 @@ - - - Are you sure you want to delete this deployment? - 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..9a9b47b2e 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]/table.svelte b/src/routes/(console)/project-[region]-[project]/functions/function-[function]/table.svelte index c4c1d1743..7b46acc09 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 @@ -28,6 +28,8 @@ import Delete from './(modals)/deleteModal.svelte'; import { capitalize } from '$lib/helpers/string'; import { deploymentStatusConverter } from '$lib/stores/git'; + import { getEffectiveBuildStatus } from '$lib/helpers/buildTimeout'; + import { regionalConsoleVariables } from '$routes/(console)/project-[region]-[project]/store'; import DownloadActionMenuItem from './(components)/downloadActionMenuItem.svelte'; import { Menu } from '$lib/components/menu'; import { sdk } from '$lib/stores/sdk'; @@ -82,9 +84,13 @@ {/each} {/snippet} - {#snippet children(root)} {#each data.deploymentList.deployments as deployment (deployment.$id)} + {@const effectiveStatus = getEffectiveBuildStatus( + deployment.status, + deployment.$createdAt, + $regionalConsoleVariables + )} {deployment.$id} {/key} {:else if column.id === 'status'} - {@const status = deployment.status} - {#if data?.activeDeployment?.$id === deployment?.$id} {:else} + status={deploymentStatusConverter(effectiveStatus)} + label={capitalize(effectiveStatus)} /> {/if} {:else if column.id === 'type'} {:else if column.id === '$updatedAt'} {:else if column.id === 'buildDuration'} - {#if ['waiting'].includes(deployment.status)} + {#if ['waiting'].includes(effectiveStatus)} - - {:else if ['processing', 'building'].includes(deployment.status)} + {:else if ['processing', 'building'].includes(effectiveStatus)} {:else} {formatTimeDetailed(deployment.buildDuration)} @@ -139,7 +143,7 @@ placement={'bottom'}>
{ selectedDeployment = deployment; @@ -155,7 +159,7 @@ {#if deployment.status === 'ready' && deployment.$id !== $func.deploymentId} { selectedDeployment = deployment; showActivate = true; @@ -167,7 +171,7 @@ - {#if deployment.status === 'processing' || deployment.status === 'building' || deployment.status === 'waiting'} + {#if effectiveStatus === 'processing' || effectiveStatus === 'building' || effectiveStatus === 'waiting'} { @@ -180,9 +184,9 @@ Cancel {/if} - {#if deployment.status !== 'building' && deployment.status !== 'processing' && deployment.status !== 'waiting'} + {#if effectiveStatus !== 'building' && effectiveStatus !== 'processing' && effectiveStatus !== 'waiting'} { selectedDeployment = deployment; 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/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 ff672f611..7c1ae8ee6 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'; @@ -72,8 +72,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({ @@ -90,20 +92,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 f04ed60e3..65f7b732c 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'; @@ -138,8 +138,10 @@ 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({ @@ -156,20 +158,17 @@ } 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 cd5f35eab..f8c8f17c0 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'; @@ -99,8 +99,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({ @@ -117,20 +119,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 681abc97b..e4a98a6a9 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'; @@ -158,7 +158,9 @@ ${prefix}APPWRITE_ENDPOINT = "${sdk.forProject(page.params.region, page.params.p ); async function createWebPlatform() { - hostnameError = hostname !== '' ? !new RegExp(extendedHostnameRegex).test(hostname) : null; + const hostnameRegex = new RegExp(extendedHostnameRegex); + const finalHostname = hostname?.trim() || 'localhost'; + hostnameError = !hostnameRegex.test(finalHostname); if (hostnameError) { return; @@ -171,7 +173,7 @@ ${prefix}APPWRITE_ENDPOINT = "${sdk.forProject(page.params.region, page.params.p type: PlatformType.Web, name: `${selectedFramework.label} app`, key: key, - hostname: hostname === '' ? undefined : hostname + hostname: finalHostname }); isPlatformCreated = true; @@ -184,8 +186,10 @@ ${prefix}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({ @@ -202,21 +206,18 @@ ${prefix}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 () => { + unsubscribe(); resetPlatformStore(); - subscription?.close(); }; }); @@ -271,7 +272,8 @@ ${prefix}APPWRITE_ENDPOINT = "${sdk.forProject(page.params.region, page.params.p protocol or port number required. - + + 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/(components)/deploymentActionMenu.svelte b/src/routes/(console)/project-[region]-[project]/sites/(components)/deploymentActionMenu.svelte index b3d988be7..231611a16 100644 --- a/src/routes/(console)/project-[region]-[project]/sites/(components)/deploymentActionMenu.svelte +++ b/src/routes/(console)/project-[region]-[project]/sites/(components)/deploymentActionMenu.svelte @@ -15,6 +15,8 @@ IconXCircle } from '@appwrite.io/pink-icons-svelte'; import { ActionMenu, Icon, Tooltip } from '@appwrite.io/pink-svelte'; + import { getEffectiveBuildStatus } from '$lib/helpers/buildTimeout'; + import { regionalConsoleVariables } from '$routes/(console)/project-[region]-[project]/store'; export let selectedDeployment: Models.Deployment; export let deployment: Models.Deployment; @@ -51,6 +53,11 @@ + {@const effectiveStatus = getEffectiveBuildStatus( + deployment.status, + deployment.$createdAt, + $regionalConsoleVariables + )} {#if !inCard}
@@ -70,7 +77,7 @@
Source is empty
{/if} - {#if deployment?.status === 'ready' && deployment?.$id !== activeDeployment} + {#if effectiveStatus === 'ready' && deployment?.$id !== activeDeployment} { @@ -82,7 +89,7 @@ Activate {/if} - {#if deployment?.status === 'ready' || deployment?.status === 'failed' || deployment?.status === 'building'} + {#if effectiveStatus === 'ready' || effectiveStatus === 'failed' || effectiveStatus === 'building'} @@ -112,7 +119,7 @@ {/if} - {#if deployment?.status === 'processing' || deployment?.status === 'building' || deployment.status === 'waiting'} + {#if effectiveStatus === 'processing' || effectiveStatus === 'building' || effectiveStatus === 'waiting'} {/if} - {#if deployment.status !== 'building' && deployment.status !== 'processing' && deployment?.status !== 'waiting'} + {#if ['ready', 'failed'].includes(deployment.status)} import { capitalize } from '$lib/helpers/string'; import { app } from '$lib/stores/app'; + import { getEffectiveBuildStatus } from '$lib/helpers/buildTimeout'; + import { regionalConsoleVariables } from '$routes/(console)/project-[region]-[project]/store'; import type { Models } from '@appwrite.io/console'; import { Badge, Card, Layout, Logs, Spinner, Typography } from '@appwrite.io/pink-svelte'; import LogsTimer from './logsTimer.svelte'; @@ -38,15 +40,19 @@ emptyCopy?: string; } = $props(); + let effectiveStatus = $derived( + getEffectiveBuildStatus(deployment.status, deployment.$createdAt, $regionalConsoleVariables) + ); + function setCopy() { - if (deployment.status === 'failed') { + if (effectiveStatus === 'failed') { return 'Your deployment has failed.'; - } else if (deployment.status === 'building') { + } else if (effectiveStatus === 'building') { //Do not remove empty space before the string it's an invisible character return 'Preparing for build ... \n'; - } else if (deployment.status === 'waiting') { + } else if (effectiveStatus === 'waiting') { return 'Preparing for build ... \n'; - } else if (deployment.status === 'processing') { + } else if (effectiveStatus === 'processing') { return 'Preparing for build ... \n'; } else { return emptyCopy; @@ -62,16 +68,16 @@ Deployment logs + type={badgeTypeDeployment(effectiveStatus)} /> - + {/if} - {#if ['waiting', 'processing'].includes(deployment.status) || (deployment.status === 'building' && !deployment?.buildLogs?.length)} + {#if ['waiting', 'processing'].includes(effectiveStatus) || (effectiveStatus === 'building' && !deployment?.buildLogs?.length)} Waiting for build to start... diff --git a/src/routes/(console)/project-[region]-[project]/sites/(components)/logsTimer.svelte b/src/routes/(console)/project-[region]-[project]/sites/(components)/logsTimer.svelte index 204ce91ec..47bc28da3 100644 --- a/src/routes/(console)/project-[region]-[project]/sites/(components)/logsTimer.svelte +++ b/src/routes/(console)/project-[region]-[project]/sites/(components)/logsTimer.svelte @@ -1,16 +1,21 @@ - {#if ['processing', 'building'].includes(status)} + {#if ['processing', 'building'].includes(effectiveStatus)}

diff --git a/src/routes/(console)/project-[region]-[project]/sites/(components)/siteCard.svelte b/src/routes/(console)/project-[region]-[project]/sites/(components)/siteCard.svelte index 6bf425f7d..78ad644f4 100644 --- a/src/routes/(console)/project-[region]-[project]/sites/(components)/siteCard.svelte +++ b/src/routes/(console)/project-[region]-[project]/sites/(components)/siteCard.svelte @@ -23,6 +23,8 @@ import { isCloud } from '$lib/system'; import { sdk } from '$lib/stores/sdk'; import { capitalize } from '$lib/helpers/string'; + import { getEffectiveBuildStatus } from '$lib/helpers/buildTimeout'; + import { regionalConsoleVariables } from '$routes/(console)/project-[region]-[project]/store'; import { regionalProtocol } from '$routes/(console)/project-[region]-[project]/store'; import type { Snippet } from 'svelte'; @@ -40,6 +42,9 @@ footer?: Snippet; } = $props(); + let effectiveStatus = $derived( + getEffectiveBuildStatus(deployment.status, deployment.$createdAt, $regionalConsoleVariables) + ); let show = $state(false); const totalSize = $derived(humanFileSize(deployment?.totalSize ?? 0)); @@ -113,15 +118,15 @@
- {#if deployment.status === 'failed'} + {#if effectiveStatus === 'failed'} Status + status={effectiveStatus} + label={capitalize(effectiveStatus)} /> {:else} 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/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 @@ {#snippet footer()} - {#if deployment?.status === 'ready' && data.proxyRuleList?.total} + {#if effectiveStatus === 'ready' && data.proxyRuleList?.total} @@ -269,7 +293,11 @@ bind:value={buildCommand} placeholder={frameworkAdapterData?.buildCommand || 'Enter build command'} /> -
@@ -280,7 +308,12 @@ bind:value={outputDirectory} placeholder={frameworkAdapterData?.outputDirectory || 'Enter output directory'} /> -
@@ -305,7 +338,7 @@ - + diff --git a/src/routes/(console)/supportWizard.svelte b/src/routes/(console)/supportWizard.svelte index 6c6aa7045..ac39f7e89 100644 --- a/src/routes/(console)/supportWizard.svelte +++ b/src/routes/(console)/supportWizard.svelte @@ -125,11 +125,11 @@