diff --git a/DEDICATED_DB_GAPS.md b/DEDICATED_DB_GAPS.md index 883be6c03..f8723daae 100644 --- a/DEDICATED_DB_GAPS.md +++ b/DEDICATED_DB_GAPS.md @@ -4,6 +4,7 @@ Tracking file for all missing dedicated DB features in console vs cloud/edge/ddb **Status: ALL ITEMS COMPLETE** ## Legend + - [x] Done --- @@ -99,6 +100,7 @@ Tracking file for all missing dedicated DB features in console vs cloud/edge/ddb ## Files Changed Summary ### New Files (28) + - `settings/updateName.svelte` - `settings/updateTier.svelte` - `settings/updateStorage.svelte` @@ -123,6 +125,7 @@ Tracking file for all missing dedicated DB features in console vs cloud/edge/ddb - `monitoring/+page.ts` ### Modified Files (5) + - `src/lib/sdk/dedicatedDatabases.ts` — Complete rewrite with all types, enums, and 45 SDK methods - `dedicatedOverview.svelte` — Added 5 new CardGrid sections + IP/storageClass fields - `settings/+page.svelte` — Rewritten with dedicated type branch + 19 sub-component imports diff --git a/bugs.md b/bugs.md new file mode 100644 index 000000000..a82518628 --- /dev/null +++ b/bugs.md @@ -0,0 +1,56 @@ +# VectorDB Console Bugs + +> VectorsDB shares the `collection-[collection]` route with documentsdb -- no separate pages needed. +> The `sdk.ts` abstraction (`useDatabaseSdk`) already handles all three types. +> The `.bind()` ternary in spreadsheet.svelte is intentional -- sdk.ts returns `Record` types but the spreadsheet expects raw `Models.Document`. Refactoring would require touching the entire document pipeline. + +## ~~Critical -- Will break vectorsdb in production~~ FIXED + +### ~~1. `collection-[collection]/+page.ts` -- listDocuments hardcoded to `documentsDB`~~ FIXED + +### ~~2. `collection-[collection]/+page.svelte` -- EmptySheet type hardcoded to `"documentsdb"`~~ FIXED + +### ~~3. `empty.svelte` -- vectorsdb gets tablesdb layout~~ FIXED + +### ~~4. `empty.svelte` CSS -- no vectorsdb selector~~ FIXED + +### ~~5. `empty.svelte` -- index-only check excludes vectorsdb~~ FIXED + +--- + +## ~~Major -- UX bugs~~ FIXED + +### ~~6. Keyboard hint hardcodes `Cmd` (macOS only)~~ FIXED + +### 7. `Mod-g` conflicts with browser "Find next" + +**File:** `editor/view.svelte:1137` +`Cmd+G` / `Ctrl+G` is the standard "Find next" shortcut after `Cmd+F`. +**Status:** Deprioritized -- CodeMirror captures keys when focused, minor edge case. + +--- + +## Minor -- Cleanup / consistency + +### 9. `getCollectionService()` type signature too broad + +**File:** `(entity)/helpers/sdk.ts:118` +Accepts all `DatabaseType` but only handles `documentsdb` and `vectorsdb`. Calling with `legacy` or `tablesdb` throws at runtime. +**Fix:** Narrow parameter type to `Extract`. + +### 10. `create.svelte` -- `dimension` not reset on modal close + +**File:** `(entity)/views/create.svelte` +`id` and `name` are reset in `updateAndCleanup()` but `dimension` is not. Reopening the modal after creating a collection with dimension 1536 will still show 1536. +**Fix:** Add `dimension = 768;` to the reset paths. + +### 11. `create.svelte` -- No max bound on dimension input + +**File:** `(entity)/views/create.svelte` +The `InputNumber` has `min={1}` but no `max`. A user could enter an arbitrarily large value. +**Fix:** Add a sensible `max` (e.g., 16384). + +### 12. Magic number `768` repeated + +**Files:** `create.svelte:39`, `+layout.svelte:277` +**Fix:** Extract to a constant like `DEFAULT_VECTOR_DIMENSION`. diff --git a/bun.lock b/bun.lock index 9b299690a..e1627b35c 100644 --- a/bun.lock +++ b/bun.lock @@ -1,16 +1,16 @@ { "lockfileVersion": 1, - "configVersion": 0, + "configVersion": 1, "workspaces": { "": { "name": "@appwrite/console", "dependencies": { "@ai-sdk/svelte": "^1.1.24", - "@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@9fa70f4", + "@appwrite.io/console": "github:appwrite/sdk-for-console#fc5ed94", "@appwrite.io/pink-icons": "0.25.0", - "@appwrite.io/pink-icons-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@b92a389", + "@appwrite.io/pink-icons-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@bfe7ce3", "@appwrite.io/pink-legacy": "^1.0.3", - "@appwrite.io/pink-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@b92a389", + "@appwrite.io/pink-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@bfe7ce3", "@codemirror/autocomplete": "^6.19.0", "@codemirror/commands": "^6.9.0", "@codemirror/lang-javascript": "^6.2.4", @@ -26,10 +26,10 @@ "@popperjs/core": "^2.11.8", "@sentry/sveltekit": "^8.55.1", "@stripe/stripe-js": "^3.5.0", - "@threlte/core": "^8.3.1", - "@threlte/extras": "^9.7.1", - "ai": "^6.0.67", - "analytics": "^0.8.16", + "@threlte/core": "^8.5.2", + "@threlte/extras": "^9.13.0", + "ai": "^6.0.138", + "analytics": "^0.8.19", "codemirror-json5": "^1.0.3", "cron-parser": "^4.9.0", "dayjs": "^1.11.20", @@ -38,7 +38,7 @@ "flatted": "^3.4.2", "ignore": "^6.0.2", "json5": "^2.2.3", - "nanoid": "^5.1.5", + "nanoid": "^5.1.7", "nanotar": "^0.1.1", "pretty-bytes": "^6.1.1", "remarkable": "^2.0.1", @@ -47,8 +47,8 @@ "tippy.js": "^6.3.7", }, "devDependencies": { - "@eslint/compat": "^1.3.1", - "@eslint/js": "^9.31.0", + "@eslint/compat": "^1.4.1", + "@eslint/js": "^9.39.4", "@lezer/common": "^1.5.0", "@melt-ui/pp": "^0.3.2", "@melt-ui/svelte": "^0.86.6", @@ -93,13 +93,13 @@ "flatted": "^3.4.2", "immutable": "^5.1.5", "minimatch": "10.2.3", - "picomatch": "^2.3.2", + "picomatch": "^4.0.4", "vite": "npm:rolldown-vite@latest", }, "packages": { "@adobe/css-tools": ["@adobe/css-tools@4.4.4", "", {}, "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg=="], - "@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.80", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-uM7kpZB5l977lW7+2X1+klBUxIZQ78+1a9jHlaHFEzcOcmmslTl3sdP0QqfuuBcO0YBM2gwOiqVdp8i4TRQYcw=="], + "@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.83", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-LvlWujbSdEkTBXBLFtF7GS6riXdHhH0O+DpDrCaNQvXeHmSF2jKsOg7JWXiCgygAHM5cWFAO3JYmZp83DjiuBQ=="], "@ai-sdk/provider": ["@ai-sdk/provider@3.0.8", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ=="], @@ -123,15 +123,15 @@ "@analytics/type-utils": ["@analytics/type-utils@0.6.4", "", {}, "sha512-Ou1gQxFakOWLcPnbFVsrPb8g1wLLUZYYJXDPjHkG07+5mustGs5yqACx42UAu4A6NszNN6Z5gGxhyH45zPWRxw=="], - "@appwrite.io/console": ["@appwrite.io/console@https://pkg.vc/-/@appwrite/@appwrite.io/console@9fa70f4", { "dependencies": { "json-bigint": "1.0.0" } }], + "@appwrite.io/console": ["@appwrite.io/console@github:appwrite/sdk-for-console#fc5ed94", { "dependencies": { "json-bigint": "1.0.0" } }, "appwrite-sdk-for-console-fc5ed94"], "@appwrite.io/pink-icons": ["@appwrite.io/pink-icons@0.25.0", "", {}, "sha512-0O3i2oEuh5mWvjO80i+X6rbzrWLJ1m5wmv2/M3a1p2PyBJsFxN8xQMTEmTn3Wl/D26SsM7SpzbdW6gmfgoVU9Q=="], - "@appwrite.io/pink-icons-svelte": ["@appwrite.io/pink-icons-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@b92a389", { "peerDependencies": { "svelte": "^4.0.0" } }], + "@appwrite.io/pink-icons-svelte": ["@appwrite.io/pink-icons-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@bfe7ce3", { "peerDependencies": { "svelte": "^4.0.0" } }], "@appwrite.io/pink-legacy": ["@appwrite.io/pink-legacy@1.0.3", "", { "dependencies": { "@appwrite.io/pink-icons": "1.0.0", "the-new-css-reset": "^1.11.2" } }, "sha512-GGde5fmPhs+s6/3aFeMPc/kKADG/gTFkYQSy6oBN8pK0y0XNCLrZZgBv+EBbdhwdtqVEWXa0X85Mv9w7jcIlwQ=="], - "@appwrite.io/pink-svelte": ["@appwrite.io/pink-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@b92a389", { "dependencies": { "@appwrite.io/pink-icons-svelte": "2.0.0-RC.1", "@floating-ui/dom": "^1.6.13", "@melt-ui/pp": "^0.3.2", "@melt-ui/svelte": "^0.86.6", "@tanstack/svelte-virtual": "^3.13.10", "ansicolor": "^2.0.3", "d3": "^7.9.0", "fuse.js": "^7.1.0", "pretty-bytes": "^6.1.1", "shiki": "^1.18.0", "svelte-motion": "^0.12.2", "svelte-sonner": "^0.3.28" }, "peerDependencies": { "svelte": "^4.0.0" } }], + "@appwrite.io/pink-svelte": ["@appwrite.io/pink-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@bfe7ce3", { "dependencies": { "@appwrite.io/pink-icons-svelte": "2.0.0-RC.1", "@floating-ui/dom": "^1.6.13", "@melt-ui/pp": "^0.3.2", "@melt-ui/svelte": "^0.86.6", "@tanstack/svelte-virtual": "^3.13.10", "ansicolor": "^2.0.3", "d3": "^7.9.0", "fuse.js": "^7.1.0", "pretty-bytes": "^6.1.1", "shiki": "^1.18.0", "svelte-motion": "^0.12.2", "svelte-sonner": "^0.3.28" }, "peerDependencies": { "svelte": "^4.0.0" } }], "@asamuzakjp/css-color": ["@asamuzakjp/css-color@3.2.0", "", { "dependencies": { "@csstools/css-calc": "^2.1.3", "@csstools/css-color-parser": "^3.0.9", "@csstools/css-parser-algorithms": "^3.0.4", "@csstools/css-tokenizer": "^3.0.3", "lru-cache": "^10.4.3" } }, "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw=="], @@ -499,9 +499,9 @@ "@threejs-kit/instanced-sprite-mesh": ["@threejs-kit/instanced-sprite-mesh@2.5.1", "", { "dependencies": { "diet-sprite": "^0.0.1", "earcut": "^2.2.4", "maath": "^0.10.7", "three-instanced-uniforms-mesh": "^0.52.4", "troika-three-utils": "^0.52.4" }, "peerDependencies": { "three": ">=0.170.0" } }, "sha512-pmt1ALRhbHhCJQTj2FuthH6PeLIeaM4hOuS2JO3kWSwlnvx/9xuUkjFR3JOi/myMqsH7pSsLIROSaBxDfttjeA=="], - "@threlte/core": ["@threlte/core@8.5.2", "", { "dependencies": { "mitt": "^3.0.1" }, "peerDependencies": { "svelte": ">=5", "three": ">=0.160" } }, "sha512-noxIsYlEYRFBo0U3T8Z4PWkfe23VCDxaHIlSzSWlOlBgd+mhKrhyM8lFmeznmZQS78z4obkWUJeYxx/jauD+rw=="], + "@threlte/core": ["@threlte/core@8.5.4", "", { "dependencies": { "mitt": "^3.0.1" }, "peerDependencies": { "svelte": ">=5", "three": ">=0.160" } }, "sha512-hqFkD0/CHVUFh/FavLKCI5snYqwGL4InO9hpYgbf+XirKyx/atqiDoiCv/gVhPjQAojojyur8Frj+lNu8u/J5Q=="], - "@threlte/extras": ["@threlte/extras@9.13.0", "", { "dependencies": { "@threejs-kit/instanced-sprite-mesh": "^2.5.1", "camera-controls": "^3.1.2", "three-mesh-bvh": "^0.9.1", "three-perf": "^1.0.11", "three-viewport-gizmo": "^2.2.0", "troika-three-text": "^0.52.4" }, "peerDependencies": { "svelte": ">=5", "three": ">=0.160" } }, "sha512-fHt5VcOoXyBT+wuytRHlKa1SUxNEI15L/kpudVWE9Z0G+U5TWIf01mt0BdQBGJOqwVJYxwFzRJQxfS2/kLAl9Q=="], + "@threlte/extras": ["@threlte/extras@9.13.3", "", { "dependencies": { "@threejs-kit/instanced-sprite-mesh": "^2.5.1", "camera-controls": "^3.1.2", "three-mesh-bvh": "^0.9.1", "three-perf": "^1.0.11", "three-viewport-gizmo": "^2.2.0", "troika-three-text": "^0.52.4" }, "peerDependencies": { "svelte": ">=5", "three": ">=0.160" } }, "sha512-ElXna1kGuS9b9pCG5swChpwpbrDxoMVxchCG8mXjjcXk9HW8qZDicf2mq0SBll2fLP0Nb8iGlkskSJ04qqWCGg=="], "@tweenjs/tween.js": ["@tweenjs/tween.js@23.1.3", "", {}, "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA=="], @@ -607,7 +607,7 @@ "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], - "ai": ["ai@6.0.138", "", { "dependencies": { "@ai-sdk/gateway": "3.0.80", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-49OfPe0f5uxJ6jUdA5BBXjIinP6+ZdYfAtpF2aEH64GA5wPcxH2rf/TBUQQ0bbamBz/D+TLMV18xilZqOC+zaA=="], + "ai": ["ai@6.0.141", "", { "dependencies": { "@ai-sdk/gateway": "3.0.83", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-+GomGQWaId3xN0wcugUW/H7xMMaFkID2PiS7K/Wugj45G3efv0BXhQ3psRZoQVoRbOpdNoUqcK/KTB+FR4h6qg=="], "ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="], @@ -1191,7 +1191,7 @@ "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], - "picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], + "picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], "playwright": ["playwright@1.58.2", "", { "dependencies": { "playwright-core": "1.58.2" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A=="], diff --git a/e2e/journeys/dedicated-databases.spec.ts b/e2e/journeys/dedicated-databases.spec.ts index 530f6a5b3..178d7885f 100644 --- a/e2e/journeys/dedicated-databases.spec.ts +++ b/e2e/journeys/dedicated-databases.spec.ts @@ -201,7 +201,9 @@ test.describe('Dedicated databases', () => { }); test('URL params pre-populate engine, tier, and name', async ({ page }) => { - await page.goto(`${CREATE_URL}?type=dedicated&engine=mysql&tier=s-1vcpu-1gb&name=TestDB`); + await page.goto( + `${CREATE_URL}?type=dedicated&engine=mysql&tier=s-1vcpu-1gb&name=TestDB` + ); await waitForCreatePage(page, 'Configuration'); // Name should be pre-filled @@ -528,9 +530,7 @@ test.describe('Dedicated databases', () => { await page.waitForLoadState('networkidle'); // The list page should have a create button - await expect( - page.getByRole('button', { name: /Create database/ }) - ).toBeVisible(); + await expect(page.getByRole('button', { name: /Create database/ })).toBeVisible(); // At least one database link should be visible const databaseLinks = page.locator('a[href*="/databases/database-"]'); @@ -578,11 +578,21 @@ test.describe('Dedicated databases', () => { await page.waitForLoadState('networkidle'); // If it is a dedicated overview, check status-related elements - if (await page.getByText('Status', { exact: true }).isVisible().catch(() => false)) { + if ( + await page + .getByText('Status', { exact: true }) + .isVisible() + .catch(() => false) + ) { // Status badge should be present (Ready, Provisioning, etc.) const statusTexts = ['Ready', 'Provisioning', 'Active', 'Paused', 'Failed']; const results = await Promise.all( - statusTexts.map((s) => page.getByText(s).isVisible().catch(() => false)) + statusTexts.map((s) => + page + .getByText(s) + .isVisible() + .catch(() => false) + ) ); expect(results.some(Boolean)).toBeTruthy(); } @@ -622,7 +632,12 @@ test.describe('Dedicated databases', () => { await databaseLink.click(); await page.waitForLoadState('networkidle'); - if (await page.getByText('Status', { exact: true }).isVisible().catch(() => false)) { + if ( + await page + .getByText('Status', { exact: true }) + .isVisible() + .catch(() => false) + ) { const refreshButton = page.getByRole('button', { name: /Refresh/ }); await expect(refreshButton).toBeVisible(); } @@ -642,7 +657,12 @@ test.describe('Dedicated databases', () => { await databaseLink.click(); await page.waitForLoadState('networkidle'); - if (await page.getByText('Status', { exact: true }).isVisible().catch(() => false)) { + if ( + await page + .getByText('Status', { exact: true }) + .isVisible() + .catch(() => false) + ) { // Should show either connection details or a provisioning message const connectionTitle = page.getByText('Connection', { exact: true }); const provisioningMessage = page.getByText('Provisioning in progress'); @@ -670,7 +690,12 @@ test.describe('Dedicated databases', () => { await databaseLink.click(); await page.waitForLoadState('networkidle'); - if (await page.getByText('Status', { exact: true }).isVisible().catch(() => false)) { + if ( + await page + .getByText('Status', { exact: true }) + .isVisible() + .catch(() => false) + ) { await expect(page.getByText('High Availability')).toBeVisible(); } }); @@ -687,7 +712,12 @@ test.describe('Dedicated databases', () => { await databaseLink.click(); await page.waitForLoadState('networkidle'); - if (await page.getByText('Status', { exact: true }).isVisible().catch(() => false)) { + if ( + await page + .getByText('Status', { exact: true }) + .isVisible() + .catch(() => false) + ) { await expect(page.getByText('Network', { exact: true })).toBeVisible(); } }); @@ -704,7 +734,12 @@ test.describe('Dedicated databases', () => { await databaseLink.click(); await page.waitForLoadState('networkidle'); - if (await page.getByText('Status', { exact: true }).isVisible().catch(() => false)) { + if ( + await page + .getByText('Status', { exact: true }) + .isVisible() + .catch(() => false) + ) { await expect(page.getByText('Backups', { exact: true }).first()).toBeVisible(); } }); @@ -721,7 +756,12 @@ test.describe('Dedicated databases', () => { await databaseLink.click(); await page.waitForLoadState('networkidle'); - if (await page.getByText('Status', { exact: true }).isVisible().catch(() => false)) { + if ( + await page + .getByText('Status', { exact: true }) + .isVisible() + .catch(() => false) + ) { // Security card was removed (encryption at rest is infra-level) await expect(page.getByText('Encryption at Rest')).not.toBeVisible(); await expect(page.getByText('Key Management')).not.toBeVisible(); @@ -740,7 +780,12 @@ test.describe('Dedicated databases', () => { await databaseLink.click(); await page.waitForLoadState('networkidle'); - if (await page.getByText('Network', { exact: true }).isVisible().catch(() => false)) { + if ( + await page + .getByText('Network', { exact: true }) + .isVisible() + .catch(() => false) + ) { await expect(page.getByText('Connection Timeout')).toBeVisible(); // "Sleep After Idle" was renamed to "Scale-to-Zero After" await expect(page.getByText('Sleep After Idle')).not.toBeVisible(); @@ -753,9 +798,7 @@ test.describe('Dedicated databases', () => { await authenticate(page); }); - test('dedicated database header shows Overview tab instead of Tables', async ({ - page - }) => { + test('dedicated database header shows Overview tab instead of Tables', async ({ page }) => { await page.goto(DATABASES_URL); await page.waitForLoadState('networkidle'); @@ -805,7 +848,13 @@ test.describe('Dedicated databases', () => { // Auth tab only visible for dedicated databases — scope to header tabs area const tabsArea = page.locator('[class*="tabs"], nav').filter({ hasText: 'Backups' }); const authTab = tabsArea.getByRole('link', { name: 'Auth' }); - if (await page.getByRole('link', { name: 'Overview' }).first().isVisible().catch(() => false)) { + if ( + await page + .getByRole('link', { name: 'Overview' }) + .first() + .isVisible() + .catch(() => false) + ) { await expect(authTab).toBeVisible({ timeout: 10_000 }); } }); @@ -933,8 +982,15 @@ test.describe('Dedicated databases', () => { await databaseLink.click(); await page.waitForLoadState('networkidle'); - const sidebarBackups = page.locator('a[href*="/backups"]').filter({ hasText: 'Backups' }); - if (await sidebarBackups.first().isVisible().catch(() => false)) { + const sidebarBackups = page + .locator('a[href*="/backups"]') + .filter({ hasText: 'Backups' }); + if ( + await sidebarBackups + .first() + .isVisible() + .catch(() => false) + ) { await expect(sidebarBackups.first()).toBeVisible(); } }); @@ -952,7 +1008,12 @@ test.describe('Dedicated databases', () => { await page.waitForLoadState('networkidle'); const sidebarAuth = page.locator('a[href*="/auth"]').filter({ hasText: 'Auth' }); - if (await sidebarAuth.first().isVisible().catch(() => false)) { + if ( + await sidebarAuth + .first() + .isVisible() + .catch(() => false) + ) { await expect(sidebarAuth.first()).toBeVisible(); } }); @@ -972,7 +1033,12 @@ test.describe('Dedicated databases', () => { const sidebarMonitoring = page .locator('a[href*="/monitoring"]') .filter({ hasText: 'Monitoring' }); - if (await sidebarMonitoring.first().isVisible().catch(() => false)) { + if ( + await sidebarMonitoring + .first() + .isVisible() + .catch(() => false) + ) { await expect(sidebarMonitoring.first()).toBeVisible(); } }); @@ -992,7 +1058,12 @@ test.describe('Dedicated databases', () => { const sidebarSettings = page .locator('a[href*="/settings"]') .filter({ hasText: 'Settings' }); - if (await sidebarSettings.first().isVisible().catch(() => false)) { + if ( + await sidebarSettings + .first() + .isVisible() + .catch(() => false) + ) { await expect(sidebarSettings.first()).toBeVisible(); } }); @@ -1010,7 +1081,12 @@ test.describe('Dedicated databases', () => { await page.waitForLoadState('networkidle'); const sidebarUsage = page.locator('a[href*="/usage"]').filter({ hasText: 'Usage' }); - if (await sidebarUsage.first().isVisible().catch(() => false)) { + if ( + await sidebarUsage + .first() + .isVisible() + .catch(() => false) + ) { await expect(sidebarUsage.first()).toBeVisible(); } }); @@ -1159,7 +1235,12 @@ test.describe('Dedicated databases', () => { const backupTitle = page .getByText('Backup', { exact: false }) .filter({ hasText: /Backup/ }); - if (await backupTitle.first().isVisible().catch(() => false)) { + if ( + await backupTitle + .first() + .isVisible() + .catch(() => false) + ) { await expect(backupTitle.first()).toBeVisible(); } }); @@ -1192,7 +1273,12 @@ test.describe('Dedicated databases', () => { // UpdatePooler is only rendered for postgres const poolerTitle = page.getByText('Connection pooler', { exact: false }); - if (await poolerTitle.first().isVisible().catch(() => false)) { + if ( + await poolerTitle + .first() + .isVisible() + .catch(() => false) + ) { await expect(poolerTitle.first()).toBeVisible(); } }); @@ -1216,7 +1302,12 @@ test.describe('Dedicated databases', () => { } const replicasTitle = page.getByText('Read replicas', { exact: false }); - if (await replicasTitle.first().isVisible().catch(() => false)) { + if ( + await replicasTitle + .first() + .isVisible() + .catch(() => false) + ) { await expect(replicasTitle.first()).toBeVisible(); } }); @@ -1228,7 +1319,12 @@ test.describe('Dedicated databases', () => { } const crossRegion = page.getByText('Cross-region', { exact: false }); - if (await crossRegion.first().isVisible().catch(() => false)) { + if ( + await crossRegion + .first() + .isVisible() + .catch(() => false) + ) { await expect(crossRegion.first()).toBeVisible(); } }); @@ -1240,7 +1336,12 @@ test.describe('Dedicated databases', () => { } const storageTitle = page.getByText('Storage', { exact: true }); - if (await storageTitle.first().isVisible().catch(() => false)) { + if ( + await storageTitle + .first() + .isVisible() + .catch(() => false) + ) { await expect(storageTitle.first()).toBeVisible(); } }); @@ -1252,7 +1353,12 @@ test.describe('Dedicated databases', () => { } const maintenanceTitle = page.getByText('Maintenance', { exact: false }); - if (await maintenanceTitle.first().isVisible().catch(() => false)) { + if ( + await maintenanceTitle + .first() + .isVisible() + .catch(() => false) + ) { await expect(maintenanceTitle.first()).toBeVisible(); } }); @@ -1264,7 +1370,12 @@ test.describe('Dedicated databases', () => { } const autoscalingTitle = page.getByText('Autoscaling', { exact: false }); - if (await autoscalingTitle.first().isVisible().catch(() => false)) { + if ( + await autoscalingTitle + .first() + .isVisible() + .catch(() => false) + ) { await expect(autoscalingTitle.first()).toBeVisible(); } }); @@ -1400,9 +1511,20 @@ test.describe('Dedicated databases', () => { // For legacy databases, the policies/backups view is shown. // Either way, the page should have loaded successfully. const contentVisibilities = await Promise.all([ - page.getByText('Policies', { exact: true }).isVisible().catch(() => false), - page.getByText('Backups', { exact: true }).first().isVisible().catch(() => false), - page.getByText('Backup', { exact: false }).first().isVisible().catch(() => false) + page + .getByText('Policies', { exact: true }) + .isVisible() + .catch(() => false), + page + .getByText('Backups', { exact: true }) + .first() + .isVisible() + .catch(() => false), + page + .getByText('Backup', { exact: false }) + .first() + .isVisible() + .catch(() => false) ]); const hasContent = contentVisibilities.some(Boolean); @@ -1419,9 +1541,7 @@ test.describe('Dedicated databases', () => { await page.goto(DATABASES_URL); await page.waitForLoadState('networkidle'); - await expect( - page.getByRole('button', { name: /Create database/ }) - ).toBeVisible(); + await expect(page.getByRole('button', { name: /Create database/ })).toBeVisible(); }); test('database list renders database type indicators', async ({ page }) => { diff --git a/eslint.config.js b/eslint.config.js index 841ddf352..8bf40b821 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -36,7 +36,7 @@ export default ts.config( // TODO: @itznotabug, this requires a big refactor! 'svelte/no-navigation-without-resolve': 'warn', 'svelte/prefer-svelte-reactivity': 'warn', - 'svelte/prefer-writable-derived': 'warn' + 'svelte/prefer-writable-derived': 'off' } }, { diff --git a/package.json b/package.json index ede31882f..b5bd2fdae 100644 --- a/package.json +++ b/package.json @@ -20,11 +20,11 @@ }, "dependencies": { "@ai-sdk/svelte": "^1.1.24", - "@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@9fa70f4", + "@appwrite.io/console": "github:appwrite/sdk-for-console#fc5ed94", "@appwrite.io/pink-icons": "0.25.0", - "@appwrite.io/pink-icons-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@b92a389", + "@appwrite.io/pink-icons-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@bfe7ce3", "@appwrite.io/pink-legacy": "^1.0.3", - "@appwrite.io/pink-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@b92a389", + "@appwrite.io/pink-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@bfe7ce3", "@codemirror/autocomplete": "^6.19.0", "@codemirror/commands": "^6.9.0", "@codemirror/lang-javascript": "^6.2.4", @@ -40,10 +40,10 @@ "@popperjs/core": "^2.11.8", "@sentry/sveltekit": "^8.55.1", "@stripe/stripe-js": "^3.5.0", - "@threlte/core": "^8.3.1", - "@threlte/extras": "^9.7.1", - "ai": "^6.0.67", - "analytics": "^0.8.16", + "@threlte/core": "^8.5.2", + "@threlte/extras": "^9.13.0", + "ai": "^6.0.138", + "analytics": "^0.8.19", "codemirror-json5": "^1.0.3", "cron-parser": "^4.9.0", "dayjs": "^1.11.20", @@ -52,7 +52,7 @@ "flatted": "^3.4.2", "ignore": "^6.0.2", "json5": "^2.2.3", - "nanoid": "^5.1.5", + "nanoid": "^5.1.7", "nanotar": "^0.1.1", "pretty-bytes": "^6.1.1", "remarkable": "^2.0.1", @@ -61,8 +61,8 @@ "tippy.js": "^6.3.7" }, "devDependencies": { - "@eslint/compat": "^1.3.1", - "@eslint/js": "^9.31.0", + "@eslint/compat": "^1.4.1", + "@eslint/js": "^9.39.4", "@lezer/common": "^1.5.0", "@melt-ui/pp": "^0.3.2", "@melt-ui/svelte": "^0.86.6", @@ -106,6 +106,6 @@ "minimatch": "10.2.3", "immutable": "^5.1.5", "flatted": "^3.4.2", - "picomatch": "^2.3.2" + "picomatch": "^4.0.4" } } diff --git a/src/lib/actions/analytics.ts b/src/lib/actions/analytics.ts index e193a30a2..93be72800 100644 --- a/src/lib/actions/analytics.ts +++ b/src/lib/actions/analytics.ts @@ -154,8 +154,8 @@ export enum Click { DatabaseRowDelete = 'click_row_delete', DatabaseDatabaseDelete = 'click_database_delete', DatabaseImportCsv = 'click_database_import_csv', - DatabaseImportJson = 'click_database_import_json', DatabaseExportCsv = 'click_database_export_csv', + DatabaseImportJson = 'click_database_import_json', DomainCreateClick = 'click_domain_create', DomainDeleteClick = 'click_domain_delete', DomainRetryDomainVerificationClick = 'click_domain_retry_domain_verification', diff --git a/src/lib/components/customId.svelte b/src/lib/components/customId.svelte index 66a56c2d8..559f6a0fe 100644 --- a/src/lib/components/customId.svelte +++ b/src/lib/components/customId.svelte @@ -12,7 +12,9 @@ id = $bindable(null), autofocus = true, isProject = false, - required = true + required = true, + syncFrom = undefined, + disabled = false }: { show: boolean; name: string; @@ -20,8 +22,29 @@ autofocus?: boolean; isProject?: boolean; required?: boolean; + disabled?: boolean; + syncFrom?: string | undefined; } = $props(); + let touchedId = $state(false); + + function toIdFormat(str: string): string { + return str + .toLowerCase() + .replace(/[^a-z0-9\-_. ]+/g, '') + .replace(/ /g, '_') + .replace(/^-+/, '') + .replace(/\.+$/, '') + .replace(/_{2,}/g, '_') + .slice(0, 36); // max length + } + + function handleInput() { + if (!touchedId) { + touchedId = true; + } + } + $effect(() => { if (!show) { id = null; @@ -37,6 +60,21 @@ trackEvent(Click.ShowCustomIdClick); } }); + + $effect(() => { + if (syncFrom && !touchedId) { + const newId = toIdFormat(syncFrom); + if (id !== newId) { + id = newId; + } + } + }); + + $effect(() => { + if (!show) { + touchedId = false; + } + }); {#if show} @@ -61,9 +99,9 @@ {#if isProject} - + {:else} - + {/if} diff --git a/src/lib/components/filters/content.svelte b/src/lib/components/filters/content.svelte index c4a83c821..20f50a417 100644 --- a/src/lib/components/filters/content.svelte +++ b/src/lib/components/filters/content.svelte @@ -25,7 +25,8 @@ columnId = $bindable(null), arrayValues = $bindable([]), operatorKey = $bindable(null), - singleCondition = false + singleCondition = false, + schema = true }: { // We cast to any to not cause type errors in the input components /* eslint @typescript-eslint/no-explicit-any: 'off' */ @@ -36,11 +37,40 @@ arrayValues?: string[]; operatorKey?: string | null; singleCondition?: boolean; + schema?: boolean; } = $props(); - let columnsArray = $derived($columns); - let column = $derived(columnsArray.find((c) => c.id === columnId)); - let operatorsForColumn = $derived.by(() => { + const systemFieldColumns: Record = { + $id: { id: '$id', title: '$id', type: 'string' }, + $createdAt: { id: '$createdAt', title: '$createdAt', type: 'datetime' }, + $updatedAt: { id: '$updatedAt', title: '$updatedAt', type: 'datetime' } + }; + + const columnsArray = $derived($columns); + const isCustomAttribute = $derived( + !schema && + columnId && + !systemFieldColumns[columnId] && + !columnsArray.find((c) => c.id === columnId) + ); + const column = $derived.by(() => { + if (!schema && columnId) { + if (systemFieldColumns[columnId]) { + return systemFieldColumns[columnId]; + } + const existingColumn = columnsArray.find((c) => c.id === columnId); + if (!existingColumn) { + return { id: columnId, title: columnId, type: 'string' } as Column; + } + return existingColumn; + } + return columnsArray.find((c) => c.id === columnId); + }); + + const operatorsForColumn = $derived.by(() => { + if (!schema && (!column || isCustomAttribute)) { + return Object.entries(operators).map(([k]) => ({ label: k, value: k })); + } if (!column?.type) return []; return Object.entries(operators) .filter(([, v]) => v.types.includes(column.type)) @@ -104,16 +134,46 @@ const dispatch = createEventDispatcher<{ clear: void; apply: { applied: number } }>(); + function coerceValueByOperatorType(value: any, operatorTypes: string[]): any { + if (typeof value !== 'string' || !value) return value; + + if (operatorTypes.includes('integer') || operatorTypes.includes('double')) { + const numValue = Number(value); + if (!isNaN(numValue) && value.trim() !== '') { + return numValue; + } + } else if (operatorTypes.includes('boolean')) { + const lowerValue = value.toLowerCase().trim(); + if (lowerValue === 'true' || lowerValue === '1') { + return true; + } else if (lowerValue === 'false' || lowerValue === '0') { + return false; + } + } + + return value; + } + function addFilterAndReset() { + const columnsWithVirtual = + column && !columnsArray.find((c) => c.id === columnId) + ? [...columnsArray, column] + : columnsArray; + // For distance operators, pass the distance as a separate parameter if (isDistanceOperator && distanceValue !== null && value !== null) { - addFilter(columnsArray, columnId, operatorKey, value, arrayValues, distanceValue); + addFilter(columnsWithVirtual, columnId, operatorKey, value, arrayValues, distanceValue); } else { - const preparedValue = - column?.type === 'datetime' && typeof value === 'string' && value - ? new Date(value).toISOString() - : value; - addFilter(columnsArray, columnId, operatorKey, preparedValue, arrayValues); + let preparedValue = value; + + if (column?.type === 'datetime' && typeof value === 'string' && value) { + preparedValue = new Date(value).toISOString(); + } else if (!schema) { + const operatorTypes = operator?.types || []; + preparedValue = coerceValueByOperatorType(value, operatorTypes); + } + + addFilter(columnsWithVirtual, columnId, operatorKey, preparedValue, arrayValues); } columnId = null; @@ -135,19 +195,23 @@ addFilterAndReset(); }}> - + {#if schema} + + {:else} + + {/if} - {#if column && operator && !operator?.hideInput} + {#if (column || (!schema && columnId)) && operator && !operator?.hideInput} {#if column?.array} {#if column.format === 'enum'} - {#if column.format === 'enum'} + {#if column?.format === 'enum'} - {:else if column.type === 'integer' || column.type === 'double'} + {:else if column?.type === 'integer' || column?.type === 'double'} - {:else if column.type === 'boolean'} + {:else if column?.type === 'boolean'} - {:else if column.type === 'datetime'} + {:else if column?.type === 'datetime'} {#key value} {/key} - {:else if column.type === 'point' || column.type === 'linestring' || column.type === 'polygon'} + {:else if column?.type === 'point' || column?.type === 'linestring' || column?.type === 'polygon'} { diff --git a/src/lib/components/filters/filters.svelte b/src/lib/components/filters/filters.svelte index 4b504a705..6a9e7d359 100644 --- a/src/lib/components/filters/filters.svelte +++ b/src/lib/components/filters/filters.svelte @@ -28,6 +28,7 @@ export let enableApply = false; export let quickFilters = false; export let analyticsSource = ''; + export let schema = true; let displayQuickFilters = quickFilters; const dispatch = createEventDispatcher(); @@ -186,6 +187,7 @@ bind:distanceValue bind:arrayValues {columns} + {schema} {singleCondition} on:apply={afterApply} on:clear={() => (filtersAppliedCount = 0)} /> @@ -243,6 +245,7 @@ {:else} void; + onPreferencesUpdated?: () => void; } let { @@ -36,7 +37,8 @@ allowNoColumns = false, showAnyway = false, disableButton = false, - onCustomOptionClick = null + onCustomOptionClick = null, + onPreferencesUpdated = null }: Props = $props(); let showCountBadge = $state(false); @@ -55,6 +57,11 @@ } } + function handlePreferencesUpdated() { + updateBadgeState(); + onPreferencesUpdated?.(); + } + $effect(() => { showCountBadge = !onlyIcon || !!preferences.getKey(preferenceKey, false); }); @@ -68,7 +75,7 @@ {isCustomTable} {allowNoColumns} {onCustomOptionClick} - onPreferencesUpdated={updateBadgeState}> + onPreferencesUpdated={handlePreferencesUpdated}> {#snippet children(toggle, selectedColumnsNumber)} void; } = $props(); @@ -164,7 +166,12 @@
-