mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
Merge branch 'main' into 'revamp-designs'.
This commit is contained in:
@@ -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
|
||||
@@ -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.
|
||||
+3
-3
@@ -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",
|
||||
|
||||
Generated
+15
-15
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
<script lang="ts">
|
||||
import { Copy } from '.';
|
||||
import { Icon, Tag } from '@appwrite.io/pink-svelte';
|
||||
import { IconDuplicate } from '@appwrite.io/pink-icons-svelte';
|
||||
import { getProjectEndpoint } from '$lib/helpers/project';
|
||||
</script>
|
||||
|
||||
<Copy value={getProjectEndpoint()} copyText="Copy endpoint">
|
||||
<Tag size="xs" variant="code">
|
||||
<Icon icon={IconDuplicate} size="s" slot="start" />
|
||||
<span style:white-space="nowrap" style:overflow="hidden" style:word-break="break-all">
|
||||
API endpoint
|
||||
</span>
|
||||
</Tag>
|
||||
</Copy>
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -57,6 +57,7 @@
|
||||
disabled={tooltipDisabled}
|
||||
portal={tooltipPortal}
|
||||
delay={tooltipDelay}
|
||||
maxWidth="500px"
|
||||
placement={tooltipPlacement}>
|
||||
<span
|
||||
data-private
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
import { onMount } from 'svelte';
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/state';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { realtime, sdk } from '$lib/stores/sdk';
|
||||
import { goto, invalidate } from '$app/navigation';
|
||||
import { getProjectId } from '$lib/helpers/project';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
@@ -187,7 +187,7 @@
|
||||
migrations.migrations.forEach(updateOrAddItem);
|
||||
});
|
||||
|
||||
return sdk.forConsoleIn(page.params.region).realtime.subscribe('console', (response) => {
|
||||
return realtime.forConsole(page.params.region, 'console', (response) => {
|
||||
if (!response.channels.includes(`projects.${getProjectId()}`)) return;
|
||||
if (response.events.includes('migrations.*')) {
|
||||
updateOrAddItem(response.payload as Payload);
|
||||
|
||||
@@ -95,10 +95,11 @@
|
||||
export let tooltipPortal = false;
|
||||
export let tooltipDelay: number = 0;
|
||||
export let tooltipPlacement: TooltipPlacement = undefined;
|
||||
export let copyText: string | undefined = undefined;
|
||||
</script>
|
||||
|
||||
{#key value}
|
||||
<Copy {value} {event} {tooltipPortal} {tooltipDelay} {tooltipPlacement}>
|
||||
<Copy {value} {event} {tooltipPortal} {tooltipDelay} {tooltipPlacement} {copyText}>
|
||||
<Tag size="xs" variant="code">
|
||||
<Icon icon={IconDuplicate} size="s" slot="start" />
|
||||
<span
|
||||
|
||||
@@ -82,6 +82,7 @@ export { default as BottomSheet } from './bottom-sheet/index';
|
||||
export { default as Confirm } from './confirm.svelte';
|
||||
export { default as UsageCard } from './usageCard.svelte';
|
||||
export { default as ViewToggle } from './viewToggle.svelte';
|
||||
export { default as ApiEndpoint } from './apiEndpoint.svelte';
|
||||
export { default as RegionEndpoint } from './regionEndpoint.svelte';
|
||||
export { default as ExpirationInput } from './expirationInput.svelte';
|
||||
export { default as EstimatedCard } from './estimatedCard.svelte';
|
||||
|
||||
@@ -50,15 +50,14 @@
|
||||
})();
|
||||
|
||||
onMount(() => {
|
||||
return realtime
|
||||
.forProject(page.params.region, page.params.project)
|
||||
.subscribe<Models.Migration>(['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;
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -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 @@
|
||||
</script>
|
||||
|
||||
{#if [...$groups]?.length}
|
||||
<div class="table-wrapper">
|
||||
<TableScroll>
|
||||
<Table.Root {columns} let:root>
|
||||
<svelte:fragment slot="header" let:root>
|
||||
<Table.Header.Cell column="role" {root}>Role</Table.Header.Cell>
|
||||
@@ -139,7 +140,7 @@
|
||||
{#each [...$groups] as [role, permission] (role)}
|
||||
<Table.Row.Base {root}>
|
||||
<Table.Cell column="role" {root}>
|
||||
<Row {role} />
|
||||
<Row {role} onNotFound={() => deleteRole(role)} />
|
||||
</Table.Cell>
|
||||
<Table.Cell column="create" {root}>
|
||||
<Selector.Checkbox
|
||||
@@ -174,7 +175,7 @@
|
||||
</Table.Row.Base>
|
||||
{/each}
|
||||
</Table.Root>
|
||||
</div>
|
||||
</TableScroll>
|
||||
|
||||
<div>
|
||||
<Actions
|
||||
@@ -210,14 +211,3 @@
|
||||
</Layout.Stack>
|
||||
</Card>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.table-wrapper {
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { type ComponentProps, type Snippet } from 'svelte';
|
||||
import { type ComponentProps, type Snippet, onMount } from 'svelte';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { AvatarInitials } from '../';
|
||||
@@ -34,9 +34,10 @@
|
||||
role: string;
|
||||
placement?: ComponentProps<Popover>['placement'];
|
||||
children?: Snippet;
|
||||
onNotFound?: (role: string) => void;
|
||||
}
|
||||
|
||||
let { role, placement = 'bottom-start', children }: Props = $props();
|
||||
let { role, placement = 'bottom-start', children, onNotFound }: Props = $props();
|
||||
|
||||
type ParsedPermission = {
|
||||
type: 'user' | 'team' | 'other';
|
||||
@@ -111,6 +112,21 @@
|
||||
return fetchPromise;
|
||||
}
|
||||
|
||||
async function verifyExistence() {
|
||||
try {
|
||||
const data = await getData(role);
|
||||
if (data?.notFound) {
|
||||
onNotFound?.(role);
|
||||
}
|
||||
} catch {
|
||||
// Intentionally ignore fetch/parse errors; UI handles missing data state
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
verifyExistence();
|
||||
});
|
||||
|
||||
let isMouseOverTooltip = $state(false);
|
||||
function hidePopover(hideTooltip: () => void, timeout = true) {
|
||||
if (!timeout) {
|
||||
@@ -162,7 +178,7 @@
|
||||
{:then data}
|
||||
{formatName(
|
||||
data.name ?? data?.email ?? data?.phone ?? '-',
|
||||
$isSmallViewport ? 16 : 18
|
||||
$isSmallViewport ? 16 : 20
|
||||
)}
|
||||
{/await}
|
||||
</Typography.Text>
|
||||
@@ -281,20 +297,21 @@
|
||||
<Divider />
|
||||
<Layout.Stack gap="xxs" alignItems="flex-start">
|
||||
{#if data.email}
|
||||
<Typography.Text
|
||||
size="xs"
|
||||
variant="m-400"
|
||||
<Typography.Caption
|
||||
variant="400"
|
||||
color="--fgcolor-neutral-secondary">
|
||||
Email: {data.email}
|
||||
</Typography.Text>
|
||||
Email: {formatName(
|
||||
data.email,
|
||||
$isSmallViewport ? 24 : 32
|
||||
)}
|
||||
</Typography.Caption>
|
||||
{/if}
|
||||
{#if data.phone}
|
||||
<Typography.Text
|
||||
size="xs"
|
||||
variant="m-400"
|
||||
<Typography.Caption
|
||||
variant="400"
|
||||
color="--fgcolor-neutral-secondary">
|
||||
Phone: {data.phone}
|
||||
</Typography.Text>
|
||||
</Typography.Caption>
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
{/if}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { Copy } from '.';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { Layout, Tag } from '@appwrite.io/pink-svelte';
|
||||
import { Icon, Tag } from '@appwrite.io/pink-svelte';
|
||||
import { IconDuplicate } from '@appwrite.io/pink-icons-svelte';
|
||||
import { Flag, type Models } from '@appwrite.io/console';
|
||||
import { truncateText } from '$lib/components/id.svelte';
|
||||
import { isValueOfStringEnum } from '$lib/helpers/types';
|
||||
import { getProjectEndpoint } from '$lib/helpers/project';
|
||||
|
||||
@@ -21,28 +21,28 @@
|
||||
</script>
|
||||
|
||||
{#if region}
|
||||
<Copy value={getProjectEndpoint()} copyText="Copy endpoint">
|
||||
<Tag size="xs" variant="default">
|
||||
<Layout.Stack direction="row" gap="s" alignItems="center" inline>
|
||||
{#if flagSrc}
|
||||
<img
|
||||
width={16}
|
||||
height={12}
|
||||
src={flagSrc}
|
||||
alt={region?.name}
|
||||
style:border-radius="2.5px" />
|
||||
{/if}
|
||||
|
||||
<span
|
||||
style:white-space="nowrap"
|
||||
class="text u-line-height-1-5"
|
||||
style:overflow="hidden"
|
||||
style:word-break="break-all"
|
||||
use:truncateText
|
||||
style:font-family="unset">
|
||||
{region?.name}
|
||||
</span>
|
||||
</Layout.Stack>
|
||||
<Copy value={getProjectEndpoint()} copyText={`Copy endpoint (${region.name})`}>
|
||||
<Tag size="xs" variant="code">
|
||||
<Icon icon={IconDuplicate} size="s" slot="start" />
|
||||
<span class="endpoint-label"> API endpoint </span>
|
||||
{#if flagSrc}
|
||||
<img class="region-flag" src={flagSrc} alt={region?.name} />
|
||||
{/if}
|
||||
</Tag>
|
||||
</Copy>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.endpoint-label {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.region-flag {
|
||||
width: 16px;
|
||||
height: 12px;
|
||||
border-radius: 2.5px;
|
||||
margin-inline-start: 6px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 221 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 86 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 91 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 83 KiB |
@@ -76,7 +76,7 @@
|
||||
<Icon size="s" icon={IconCloud} />
|
||||
{/if}
|
||||
|
||||
{#if $version}
|
||||
{#if $version && !isCloud}
|
||||
<Link.Anchor
|
||||
size="s"
|
||||
variant="quiet"
|
||||
@@ -137,7 +137,7 @@
|
||||
</Link.Anchor>
|
||||
{/if}
|
||||
{#if $isSmallViewport}
|
||||
{#if $version}
|
||||
{#if $version && !isCloud}
|
||||
<span class="divider-wrapper">
|
||||
<Divider vertical />
|
||||
</span>
|
||||
@@ -151,15 +151,18 @@
|
||||
style="white-space: nowrap;">
|
||||
Version {$version}
|
||||
</Link.Anchor>
|
||||
{#if isCloud}
|
||||
<Icon size="s" icon={IconCloud} />
|
||||
<Badge
|
||||
size="xs"
|
||||
type="success"
|
||||
variant="secondary"
|
||||
content="Generally Available"
|
||||
style="white-space: nowrap;" />
|
||||
{/if}
|
||||
{/if}
|
||||
{#if isCloud}
|
||||
<span class="divider-wrapper">
|
||||
<Divider vertical />
|
||||
</span>
|
||||
<Icon size="s" icon={IconCloud} />
|
||||
<Badge
|
||||
size="xs"
|
||||
type="success"
|
||||
variant="secondary"
|
||||
content="Generally Available"
|
||||
style="white-space: nowrap;" />
|
||||
{/if}
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
|
||||
+43
-12
@@ -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<ReturnType<Realtime['subscribe']>>;
|
||||
|
||||
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());
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { Card, Layout, Button } from '@appwrite.io/pink-svelte';
|
||||
import { Form } from '$lib/elements/forms';
|
||||
import { isCloud } from '$lib/system';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { ID, Region } from '@appwrite.io/console';
|
||||
@@ -97,24 +98,22 @@
|
||||
class="u-only-dark"
|
||||
alt="Appwrite Logo" />
|
||||
<Card.Base variant="primary" padding="l">
|
||||
<CreateProject
|
||||
showTitle
|
||||
bind:projectName
|
||||
bind:id={projectId}
|
||||
bind:region={projectRegion}
|
||||
regions={$regionsStore?.regions}>
|
||||
{#snippet submit()}
|
||||
<Layout.Stack direction="row" justifyContent="flex-end">
|
||||
<Button.Button
|
||||
on:click={createProject}
|
||||
type="submit"
|
||||
variant="primary"
|
||||
size="s">
|
||||
Create
|
||||
</Button.Button>
|
||||
</Layout.Stack>
|
||||
{/snippet}
|
||||
</CreateProject>
|
||||
<Form noStyle onSubmit={createProject}>
|
||||
<CreateProject
|
||||
showTitle
|
||||
bind:projectName
|
||||
bind:id={projectId}
|
||||
bind:region={projectRegion}
|
||||
regions={$regionsStore?.regions}>
|
||||
{#snippet submit()}
|
||||
<Layout.Stack direction="row" justifyContent="flex-end">
|
||||
<Button.Button type="submit" variant="primary" size="s">
|
||||
Create
|
||||
</Button.Button>
|
||||
</Layout.Stack>
|
||||
{/snippet}
|
||||
</CreateProject>
|
||||
</Form>
|
||||
</Card.Base>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -27,15 +27,13 @@
|
||||
import CsvImportBox from '$lib/components/csvImportBox.svelte';
|
||||
|
||||
onMount(() => {
|
||||
return realtime
|
||||
.forProject(page.params.region, page.params.project)
|
||||
.subscribe(['project', 'console'], (response) => {
|
||||
if (response.events.includes('stats.connections')) {
|
||||
for (const [projectId, value] of Object.entries(response.payload)) {
|
||||
stats.add(projectId, [new Date(response.timestamp).toISOString(), value]);
|
||||
}
|
||||
return realtime.forProject(page.params.region, ['project', 'console'], (response) => {
|
||||
if (response.events.includes('stats.connections')) {
|
||||
for (const [projectId, value] of Object.entries(response.payload)) {
|
||||
stats.add(projectId, [new Date(response.timestamp).toISOString(), value]);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$: $registerCommands([
|
||||
|
||||
+9
-6
@@ -28,6 +28,12 @@
|
||||
}
|
||||
|
||||
async function triggerColumnSuggestions() {
|
||||
// set table info. first!
|
||||
$tableColumnSuggestions.table = {
|
||||
id: page.params.table,
|
||||
name: page.data.table?.name ?? 'Table'
|
||||
};
|
||||
|
||||
if (!isOnRowsPage) {
|
||||
await goto(
|
||||
resolve(
|
||||
@@ -42,13 +48,10 @@
|
||||
);
|
||||
}
|
||||
|
||||
show = false;
|
||||
$tableColumnSuggestions.enabled = true;
|
||||
$tableColumnSuggestions.table = {
|
||||
id: page.params.table,
|
||||
name: page.data.table?.name ?? 'Table'
|
||||
};
|
||||
$tableColumnSuggestions.force = true;
|
||||
$tableColumnSuggestions.enabled = true;
|
||||
|
||||
show = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+7
-12
@@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
+112
-44
@@ -20,7 +20,7 @@
|
||||
<script lang="ts">
|
||||
import { goto, invalidate } from '$app/navigation';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { realtime, sdk } from '$lib/stores/sdk';
|
||||
import { type RealtimeResponse, realtime, sdk } from '$lib/stores/sdk';
|
||||
import { onMount } from 'svelte';
|
||||
import {
|
||||
table,
|
||||
@@ -36,6 +36,7 @@
|
||||
expandTabs,
|
||||
databaseRelatedRowSheetOptions,
|
||||
rowPermissionSheet,
|
||||
isWaterfallFromFaker,
|
||||
type Columns
|
||||
} from './store';
|
||||
import { addSubPanel, registerCommands, updateCommandGroupRanks } from '$lib/commandCenter';
|
||||
@@ -46,14 +47,21 @@
|
||||
import { page } from '$app/state';
|
||||
import { base } from '$app/paths';
|
||||
import { canWriteTables } from '$lib/stores/roles';
|
||||
import { IconEye, IconLockClosed, IconPlus, IconPuzzle } from '@appwrite.io/pink-icons-svelte';
|
||||
import {
|
||||
IconChevronDown,
|
||||
IconChevronUp,
|
||||
IconEye,
|
||||
IconLockClosed,
|
||||
IconPlus,
|
||||
IconPuzzle
|
||||
} from '@appwrite.io/pink-icons-svelte';
|
||||
import SideSheet from './layout/sidesheet.svelte';
|
||||
import EditRow from './rows/edit.svelte';
|
||||
import EditRelatedRow from './rows/editRelated.svelte';
|
||||
import EditColumn from './columns/edit.svelte';
|
||||
import RowActivity from './rows/activity.svelte';
|
||||
import EditRowPermissions from './rows/editPermissions.svelte';
|
||||
import { Dialog, Layout, Typography, Selector } from '@appwrite.io/pink-svelte';
|
||||
import { Dialog, Layout, Typography, Selector, Icon } from '@appwrite.io/pink-svelte';
|
||||
import { Button, Seekbar } from '$lib/elements/forms';
|
||||
import { generateFakeRecords, generateColumns } from '$lib/helpers/faker';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
@@ -64,14 +72,10 @@
|
||||
import { chunks } from '$lib/helpers/array';
|
||||
import { Submit, trackEvent } from '$lib/actions/analytics';
|
||||
|
||||
import { isTabletViewport } from '$lib/stores/viewport';
|
||||
import IndexesSuggestions from '../(suggestions)/indexes.svelte';
|
||||
import ColumnsSuggestions from '../(suggestions)/columns.svelte';
|
||||
import {
|
||||
showColumnsSuggestionsModal,
|
||||
showIndexesSuggestions,
|
||||
tableColumnSuggestions
|
||||
} from '../(suggestions)';
|
||||
import type { RealtimeResponseEvent } from '@appwrite.io/console';
|
||||
import { showColumnsSuggestionsModal } from '../(suggestions)';
|
||||
|
||||
let editRow: EditRow;
|
||||
let editRelatedRow: EditRelatedRow;
|
||||
@@ -82,41 +86,48 @@
|
||||
let selectedOption: Option['name'] = 'String';
|
||||
let createMoreColumns = false;
|
||||
|
||||
/**
|
||||
* adding a lot of fake data will trigger the realtime below
|
||||
* and will keep invalidating the `Dependencies.TABLE` making a lot of API noise!
|
||||
*/
|
||||
let isWaterfallFromFaker = false;
|
||||
let columnCreationHandler: ((response: RealtimeResponse) => void) | null = null;
|
||||
|
||||
let columnCreationHandler: ((response: RealtimeResponseEvent<unknown>) => void) | null = null;
|
||||
// manual management of focus is needed!
|
||||
const autoFocusAction = (node: HTMLElement, shouldFocus: boolean) => {
|
||||
const button = node.querySelector('button');
|
||||
if (!button) return;
|
||||
|
||||
const handleBlur = () => button.classList.remove('focus-visible');
|
||||
const applyFocus = (focus: boolean) => {
|
||||
if (focus) {
|
||||
button.classList.add('focus-visible');
|
||||
button.focus();
|
||||
} else {
|
||||
button.classList.remove('focus-visible');
|
||||
}
|
||||
};
|
||||
|
||||
button.addEventListener('blur', handleBlur);
|
||||
applyFocus(shouldFocus);
|
||||
|
||||
return {
|
||||
update: applyFocus,
|
||||
destroy() {
|
||||
button.removeEventListener('blur', handleBlur);
|
||||
button.classList.remove('focus-visible');
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
expandTabs.set(preferences.getKey('tableHeaderExpanded', true));
|
||||
|
||||
return realtime
|
||||
.forProject(page.params.region, page.params.project)
|
||||
.subscribe(['project', 'console'], (response) => {
|
||||
if (
|
||||
response.events.includes('databases.*.tables.*.columns.*') ||
|
||||
response.events.includes('databases.*.tables.*.indexes.*')
|
||||
) {
|
||||
if (isWaterfallFromFaker) {
|
||||
columnCreationHandler?.(response);
|
||||
}
|
||||
|
||||
// don't invalidate when -
|
||||
// 1. from faker
|
||||
// 2. ai columns creation
|
||||
// 3. ai indexes creation
|
||||
if (
|
||||
!isWaterfallFromFaker &&
|
||||
!$showIndexesSuggestions &&
|
||||
!$tableColumnSuggestions.table
|
||||
) {
|
||||
invalidate(Dependencies.TABLE);
|
||||
}
|
||||
return realtime.forProject(page.params.region, ['project', 'console'], (response) => {
|
||||
if (
|
||||
response.events.includes('databases.*.tables.*.columns.*') ||
|
||||
response.events.includes('databases.*.tables.*.indexes.*')
|
||||
) {
|
||||
if ($isWaterfallFromFaker) {
|
||||
columnCreationHandler?.(response);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: use route ids instead of pathname
|
||||
@@ -273,7 +284,7 @@
|
||||
const availableColumns = new Set<string>();
|
||||
const waitPromise = new Promise<void>((resolve) => (resolvePromise = resolve));
|
||||
|
||||
columnCreationHandler = (response) => {
|
||||
columnCreationHandler = (response: RealtimeResponse) => {
|
||||
const { events, payload } = response;
|
||||
|
||||
if (
|
||||
@@ -316,7 +327,7 @@
|
||||
}
|
||||
|
||||
async function createFakeData() {
|
||||
isWaterfallFromFaker = true;
|
||||
isWaterfallFromFaker.set(true);
|
||||
|
||||
$spreadsheetLoading = true;
|
||||
$randomDataModalState.show = false;
|
||||
@@ -394,10 +405,8 @@
|
||||
$randomDataModalState.value = 25;
|
||||
}
|
||||
|
||||
/* api is too fast! */
|
||||
// await sleep(1250);
|
||||
$spreadsheetLoading = false;
|
||||
isWaterfallFromFaker = false;
|
||||
isWaterfallFromFaker.set(false);
|
||||
|
||||
spreadsheetRenderKey.set(hash(rowIds));
|
||||
}
|
||||
@@ -476,11 +485,63 @@
|
||||
show: !!currentRowId,
|
||||
value: buildRowUrl(currentRowId)
|
||||
}}>
|
||||
{#snippet topEndActions()}
|
||||
{@const rows = $databaseRowSheetOptions.rows ?? []}
|
||||
{@const currentIndex = $databaseRowSheetOptions.rowIndex ?? -1}
|
||||
{@const isFirstRow = currentIndex <= 0}
|
||||
{@const isLastRow = currentIndex >= rows.length - 1}
|
||||
|
||||
{#if !$isTabletViewport}
|
||||
{@const shouldFocusPrev = !$databaseRowSheetOptions.autoFocus && !isFirstRow}
|
||||
{@const shouldFocusNext =
|
||||
!$databaseRowSheetOptions.autoFocus && isFirstRow && !isLastRow}
|
||||
|
||||
<div use:autoFocusAction={shouldFocusPrev} class:nav-button-wrapper={shouldFocusPrev}>
|
||||
<Button
|
||||
icon
|
||||
text
|
||||
size="xs"
|
||||
on:click={() => {
|
||||
if (currentIndex > 0) {
|
||||
databaseRowSheetOptions.update((opts) => ({
|
||||
...opts,
|
||||
row: rows[currentIndex - 1],
|
||||
rowIndex: currentIndex - 1
|
||||
}));
|
||||
}
|
||||
}}
|
||||
disabled={isFirstRow}>
|
||||
<Icon icon={IconChevronUp} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div use:autoFocusAction={shouldFocusNext} class:nav-button-wrapper={shouldFocusNext}>
|
||||
<Button
|
||||
icon
|
||||
text
|
||||
size="xs"
|
||||
on:click={() => {
|
||||
if (currentIndex < rows.length - 1) {
|
||||
databaseRowSheetOptions.update((opts) => ({
|
||||
...opts,
|
||||
row: rows[currentIndex + 1],
|
||||
rowIndex: currentIndex + 1
|
||||
}));
|
||||
}
|
||||
}}
|
||||
disabled={isLastRow}>
|
||||
<Icon icon={IconChevronDown} />
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
||||
{#key currentRowId}
|
||||
<EditRow
|
||||
bind:this={editRow}
|
||||
bind:row={$databaseRowSheetOptions.row}
|
||||
bind:rowId={$databaseRowSheetOptions.rowId} />
|
||||
bind:rowId={$databaseRowSheetOptions.rowId}
|
||||
autoFocus={$databaseRowSheetOptions.autoFocus} />
|
||||
{/key}
|
||||
</SideSheet>
|
||||
|
||||
@@ -552,3 +613,10 @@
|
||||
<ColumnsSuggestions bind:show={$showColumnsSuggestionsModal} />
|
||||
|
||||
<IndexesSuggestions />
|
||||
|
||||
<style lang="scss">
|
||||
// not the best solution but needed!
|
||||
.nav-button-wrapper :global(button.focus-visible) {
|
||||
outline: var(--border-width-l) solid var(--border-focus);
|
||||
}
|
||||
</style>
|
||||
|
||||
+43
-4
@@ -31,13 +31,16 @@
|
||||
IconChevronDown,
|
||||
IconChevronUp,
|
||||
IconPlus,
|
||||
IconViewBoards
|
||||
IconViewBoards,
|
||||
IconRefresh
|
||||
} from '@appwrite.io/pink-icons-svelte';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import EmptySheet from './layout/emptySheet.svelte';
|
||||
import CreateRow from './rows/create.svelte';
|
||||
import { onDestroy } from 'svelte';
|
||||
import { isCloud } from '$lib/system';
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import {
|
||||
Empty as SuggestionsEmptySheet,
|
||||
tableColumnSuggestions,
|
||||
@@ -48,6 +51,7 @@
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
let isRefreshing = false;
|
||||
let showImportCSV = false;
|
||||
|
||||
// todo: might need a type fix here.
|
||||
@@ -165,9 +169,9 @@
|
||||
justifyContent="flex-end"
|
||||
style="padding-right: 40px;">
|
||||
<Layout.Stack
|
||||
gap="s"
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
gap="s"
|
||||
justifyContent="flex-end">
|
||||
<Button
|
||||
secondary
|
||||
@@ -196,9 +200,31 @@
|
||||
preferences.setKey('tableHeaderExpanded', $expandTabs);
|
||||
}}>
|
||||
<Icon
|
||||
icon={!$expandTabs ? IconChevronDown : IconChevronUp}
|
||||
size="s" />
|
||||
size="s"
|
||||
icon={!$expandTabs ? IconChevronDown : IconChevronUp} />
|
||||
</Button>
|
||||
|
||||
<Tooltip disabled={isRefreshing || !data.rows.total} placement="top">
|
||||
<Button
|
||||
icon
|
||||
size="s"
|
||||
secondary
|
||||
disabled={isRefreshing ||
|
||||
!data.rows.total ||
|
||||
!(hasColumns && hasValidColumns)}
|
||||
class="small-button-dimensions"
|
||||
on:click={async () => {
|
||||
isRefreshing = true;
|
||||
await invalidate(Dependencies.TABLE);
|
||||
isRefreshing = false;
|
||||
}}>
|
||||
<div style:line-height="0px" class:rotating={isRefreshing}>
|
||||
<Icon icon={IconRefresh} size="s" />
|
||||
</div>
|
||||
</Button>
|
||||
|
||||
<svelte:fragment slot="tooltip">Refresh</svelte:fragment>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
</Layout.Stack>
|
||||
@@ -350,4 +376,17 @@
|
||||
width: 32px !important;
|
||||
height: 32px !important;
|
||||
}
|
||||
|
||||
:global(.rotating) {
|
||||
animation: rotate 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
+14
@@ -23,6 +23,7 @@
|
||||
type ColumnsWidth,
|
||||
indexes,
|
||||
isCsvImportInProgress,
|
||||
isWaterfallFromFaker,
|
||||
reorderItems,
|
||||
showCreateIndexSheet
|
||||
} from '../store';
|
||||
@@ -55,6 +56,9 @@
|
||||
import { page } from '$app/state';
|
||||
import { debounce } from '$lib/helpers/debounce';
|
||||
import type { PageData } from './$types';
|
||||
import { realtime } from '$lib/stores/sdk';
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
|
||||
const {
|
||||
data
|
||||
@@ -140,6 +144,16 @@
|
||||
onMount(() => {
|
||||
columnsOrder = preferences.getColumnOrder(tableId);
|
||||
columnsWidth = preferences.getColumnWidths(tableId + '#columns');
|
||||
|
||||
return realtime.forProject(page.params.region, ['project', 'console'], async (response) => {
|
||||
if (
|
||||
response.events.includes('databases.*.tables.*.columns.*.delete') ||
|
||||
(response.events.includes('databases.*.tables.*.columns.*.update') &&
|
||||
!$isWaterfallFromFaker)
|
||||
) {
|
||||
await invalidate(Dependencies.TABLE);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function getColumnStatusBadge(status: string): ComponentProps<Badge>['type'] {
|
||||
|
||||
-3
@@ -1,5 +1,4 @@
|
||||
<script lang="ts">
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { page } from '$app/state';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { table } from '../store';
|
||||
@@ -9,7 +8,6 @@
|
||||
import { isRelationship } from '../rows/store';
|
||||
import Confirm from '$lib/components/confirm.svelte';
|
||||
import { Layout } from '@appwrite.io/pink-svelte';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
|
||||
let {
|
||||
@@ -59,7 +57,6 @@
|
||||
: `${selectedColumns.length} columns have been deleted`
|
||||
});
|
||||
|
||||
await invalidate(Dependencies.TABLE);
|
||||
showDelete = false;
|
||||
selectedColumn = Array.isArray(selectedColumn) ? [] : null;
|
||||
} catch (e) {
|
||||
|
||||
+9
@@ -42,6 +42,9 @@
|
||||
import IconAI from '../../(suggestions)/icon/aiForButton.svelte';
|
||||
import EmptySheetCards from '../layout/emptySheetCards.svelte';
|
||||
import { isCloud } from '$lib/system';
|
||||
import { realtime } from '$lib/stores/sdk';
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
|
||||
let {
|
||||
data
|
||||
@@ -101,6 +104,12 @@
|
||||
|
||||
onMount(() => {
|
||||
columnsWidth = preferences.getColumnWidths(tableId + '#indexes');
|
||||
|
||||
return realtime.forProject(page.params.region, ['project', 'console'], (response) => {
|
||||
if (response.events.includes('databases.*.tables.*.indexes.*')) {
|
||||
invalidate(Dependencies.TABLE);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function getColumnStatusBadge(status: string): ComponentProps<Badge>['type'] {
|
||||
|
||||
+9
-1
@@ -18,6 +18,7 @@
|
||||
footer = null,
|
||||
titleBadge = null,
|
||||
topAction = null,
|
||||
topEndActions = null,
|
||||
...restProps
|
||||
}: {
|
||||
show: boolean;
|
||||
@@ -48,7 +49,8 @@
|
||||
}
|
||||
| undefined;
|
||||
children?: Snippet;
|
||||
footer?: Snippet | null;
|
||||
footer?: Snippet;
|
||||
topEndActions?: Snippet;
|
||||
} & HTMLAttributes<HTMLDivElement> = $props();
|
||||
|
||||
let form: Form;
|
||||
@@ -88,6 +90,12 @@
|
||||
{/if}
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
|
||||
{#if topEndActions}
|
||||
<Layout.Stack direction="row" gap="xs" alignItems="center" inline>
|
||||
{@render topEndActions()}
|
||||
</Layout.Stack>
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
</div>
|
||||
|
||||
|
||||
+17
-3
@@ -10,19 +10,27 @@
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { table, type Columns, PROHIBITED_ROW_KEYS } from '../store';
|
||||
import ColumnItem from './columns/columnItem.svelte';
|
||||
import { buildWildcardColumnsQuery, isRelationship, isRelationshipToMany } from './store';
|
||||
import {
|
||||
buildWildcardColumnsQuery,
|
||||
isRelationship,
|
||||
isRelationshipToMany,
|
||||
isSpatialType
|
||||
} from './store';
|
||||
import { Layout, Skeleton } from '@appwrite.io/pink-svelte';
|
||||
import { deepClone } from '$lib/helpers/object';
|
||||
import deepEqual from 'deep-equal';
|
||||
|
||||
const tableId = page.params.table;
|
||||
const databaseId = page.params.database;
|
||||
|
||||
let {
|
||||
row = $bindable(),
|
||||
rowId = $bindable(null)
|
||||
rowId = $bindable(null),
|
||||
autoFocus = true
|
||||
}: {
|
||||
row?: Models.Row | null;
|
||||
rowId?: string | null;
|
||||
autoFocus?: boolean;
|
||||
} = $props();
|
||||
|
||||
let loading = $state(false);
|
||||
@@ -76,7 +84,9 @@
|
||||
$effect(() => {
|
||||
if (row) {
|
||||
work = initWork();
|
||||
requestAnimationFrame(() => focusFirstInput());
|
||||
if (autoFocus) {
|
||||
requestAnimationFrame(() => focusFirstInput());
|
||||
}
|
||||
} else {
|
||||
work = null;
|
||||
}
|
||||
@@ -90,6 +100,10 @@
|
||||
const workColumn = $work?.[column.key];
|
||||
const currentColumn = $doc?.[column.key];
|
||||
|
||||
if (isSpatialType(column)) {
|
||||
return deepEqual(workColumn, currentColumn);
|
||||
}
|
||||
|
||||
if (column.array) {
|
||||
return !symmetricDifference(Array.from(workColumn), Array.from(currentColumn)).length;
|
||||
}
|
||||
|
||||
+20
-6
@@ -104,11 +104,17 @@
|
||||
};
|
||||
|
||||
$: rows = writable(data.rows);
|
||||
$: if (rows) {
|
||||
$: if ($rows) {
|
||||
paginatedRows.clear();
|
||||
paginatedRows.setPage(1, $rows.rows);
|
||||
|
||||
/* reset ui when the underlying data changes */
|
||||
spreadsheetRenderKey.set(hash(Date.now().toString()));
|
||||
}
|
||||
|
||||
// create index map for O(1) row lookups, reactive!
|
||||
$: rowIndexMap = new Map($paginatedRows.items.map((row, index) => [row.$id, index]));
|
||||
|
||||
const tableId = page.params.table;
|
||||
const databaseId = page.params.database;
|
||||
const organizationId = data.organization.$id ?? data.project.teamId;
|
||||
@@ -475,7 +481,7 @@
|
||||
message: 'Column deleted'
|
||||
});
|
||||
|
||||
invalidate(Dependencies.TABLE);
|
||||
await invalidate(Dependencies.TABLE);
|
||||
|
||||
$databaseColumnSheetOptions.column = null;
|
||||
} catch (error) {
|
||||
@@ -543,11 +549,14 @@
|
||||
} else if (type === 'row') {
|
||||
if (action === 'update') {
|
||||
databaseRowSheetOptions.update((opts) => {
|
||||
const rowIndex = rowIndexMap.get(row.$id) ?? -1;
|
||||
return {
|
||||
...opts,
|
||||
row,
|
||||
rowIndex,
|
||||
show: true,
|
||||
title: 'Update row'
|
||||
title: 'Update row',
|
||||
rows: $paginatedRows.items
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -797,9 +806,10 @@
|
||||
expandKbdShortcut="Cmd+Enter"
|
||||
on:expandKbdShortcut={({ detail }) => {
|
||||
const focusedRowId = detail.rowId;
|
||||
const focusedRow = $rows.rows.find((row) => row.$id === focusedRowId);
|
||||
const focusedRow = $paginatedRows.items.find((row) => row.$id === focusedRowId);
|
||||
|
||||
previouslyFocusedElement = document.activeElement;
|
||||
$databaseRowSheetOptions.autoFocus = false;
|
||||
onSelectSheetOption('update', null, 'row', focusedRow);
|
||||
}}>
|
||||
<svelte:fragment slot="header" let:root>
|
||||
@@ -927,6 +937,7 @@
|
||||
hide();
|
||||
previouslyFocusedElement =
|
||||
document.activeElement;
|
||||
$databaseRowSheetOptions.autoFocus = false;
|
||||
onSelectSheetOption(
|
||||
'update',
|
||||
null,
|
||||
@@ -977,8 +988,10 @@
|
||||
<SheetOptions
|
||||
type="row"
|
||||
column={rowColumn}
|
||||
onSelect={(option) =>
|
||||
onSelectSheetOption(option, null, 'row', row)}
|
||||
onSelect={(option) => {
|
||||
$databaseRowSheetOptions.autoFocus = true;
|
||||
onSelectSheetOption(option, null, 'row', row);
|
||||
}}
|
||||
onVisibilityChanged={(visible) => {
|
||||
canShowDatetimePopover = !visible;
|
||||
}}>
|
||||
@@ -1105,6 +1118,7 @@
|
||||
rowColumn
|
||||
);
|
||||
} else {
|
||||
$databaseRowSheetOptions.autoFocus = true;
|
||||
onSelectSheetOption('update', null, 'row', row);
|
||||
}
|
||||
}} />
|
||||
|
||||
+13
-1
@@ -28,6 +28,12 @@ export const table = derived(page, ($page) => $page.data.table as Table);
|
||||
export const columns = derived(page, ($page) => $page.data.table.columns as Columns[]);
|
||||
export const indexes = derived(page, ($page) => $page.data.table.indexes as Models.ColumnIndex[]);
|
||||
|
||||
/**
|
||||
* adding a lot of fake data will trigger the realtime below
|
||||
* and will keep invalidating the `Dependencies.TABLE` making a lot of API noise!
|
||||
*/
|
||||
export const isWaterfallFromFaker = writable(false);
|
||||
|
||||
export const tableColumns = writable<Column[]>([]);
|
||||
|
||||
export const isCsvImportInProgress = writable(false);
|
||||
@@ -64,12 +70,18 @@ export const databaseRowSheetOptions = writable<
|
||||
DatabaseSheetOptions & {
|
||||
row: Models.Row;
|
||||
rowId?: string;
|
||||
rows: Models.Row[];
|
||||
rowIndex?: number;
|
||||
autoFocus?: boolean;
|
||||
}
|
||||
>({
|
||||
title: null,
|
||||
show: false,
|
||||
row: null,
|
||||
rowId: null // for loading from a given id
|
||||
rowId: null, // for loading from a given id
|
||||
rows: [],
|
||||
rowIndex: -1,
|
||||
autoFocus: true
|
||||
});
|
||||
|
||||
export const databaseRelatedRowSheetOptions = writable<
|
||||
|
||||
+7
-2
@@ -15,6 +15,8 @@
|
||||
import { DeploymentSource, DeploymentCreatedBy, DeploymentDomains } from '$lib/components/git';
|
||||
import { func } from '../store';
|
||||
import { capitalize } from '$lib/helpers/string';
|
||||
import { getEffectiveBuildStatus } from '$lib/helpers/buildTimeout';
|
||||
import { regionalConsoleVariables } from '$routes/(console)/project-[region]-[project]/store';
|
||||
import { isCloud } from '$lib/system';
|
||||
import { IconInfo } from '@appwrite.io/pink-icons-svelte';
|
||||
import Link from '$lib/elements/link.svelte';
|
||||
@@ -36,6 +38,9 @@
|
||||
footer?: Snippet;
|
||||
} = $props();
|
||||
|
||||
let effectiveStatus = $derived(
|
||||
getEffectiveBuildStatus(deployment.status, deployment.$createdAt, $regionalConsoleVariables)
|
||||
);
|
||||
let totalSize = $derived(humanFileSize(deployment?.totalSize ?? 0));
|
||||
</script>
|
||||
|
||||
@@ -122,11 +127,11 @@
|
||||
</Layout.Stack>
|
||||
|
||||
<Layout.Stack direction="row" gap="xl">
|
||||
{#if deployment.status === 'failed'}
|
||||
{#if effectiveStatus === 'failed'}
|
||||
<Layout.Stack gap="xxs" inline>
|
||||
{@render titleSnippet('Status')}
|
||||
<Typography.Text variant="m-400" color="--fgcolor-neutral-primary">
|
||||
<Status status={deployment.status} label={deployment.status} />
|
||||
<Status status={effectiveStatus} label={effectiveStatus} />
|
||||
</Typography.Text>
|
||||
</Layout.Stack>
|
||||
{:else}
|
||||
|
||||
+3
-2
@@ -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'}
|
||||
<SubMenu>
|
||||
<ActionMenu.Root noPadding>
|
||||
<ActionMenu.Item.Button trailingIcon={IconDownload}>Download</ActionMenu.Item.Button>
|
||||
<ActionMenu.Item.Button leadingIcon={IconDownload} trailingIcon={IconChevronRight}
|
||||
>Download</ActionMenu.Item.Button>
|
||||
</ActionMenu.Root>
|
||||
<svelte:fragment slot="menu">
|
||||
<ActionMenu.Root>
|
||||
|
||||
+9
-9
@@ -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'
|
||||
|
||||
+8
-1
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { goto, invalidate } from '$app/navigation';
|
||||
import { base } from '$app/paths';
|
||||
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
|
||||
import Confirm from '$lib/components/confirm.svelte';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
@@ -20,6 +21,12 @@
|
||||
functionId: selectedDeployment.resourceId,
|
||||
deploymentId: selectedDeployment.$id
|
||||
});
|
||||
if (page.route.id?.includes('deployment-[deployment]')) {
|
||||
goto(
|
||||
`${base}/project-${page.params.region}-${page.params.project}/functions/function-${page.params.function}/deployments`
|
||||
);
|
||||
return;
|
||||
}
|
||||
await invalidate(Dependencies.FUNCTION);
|
||||
showDelete = false;
|
||||
addNotification({
|
||||
|
||||
+23
-24
@@ -21,30 +21,29 @@
|
||||
|
||||
onMount(() => {
|
||||
let previousStatus = null;
|
||||
return realtime
|
||||
.forProject(page.params.region, page.params.project)
|
||||
.subscribe<Models.Deployment>('console', (message) => {
|
||||
if (
|
||||
message.payload.status !== 'ready' &&
|
||||
previousStatus === message.payload.status
|
||||
) {
|
||||
return;
|
||||
}
|
||||
previousStatus = message.payload.status;
|
||||
if (message.events.includes('functions.*.deployments.*.create')) {
|
||||
invalidate(Dependencies.DEPLOYMENTS);
|
||||
return;
|
||||
}
|
||||
if (message.events.includes('functions.*.deployments.*.update')) {
|
||||
invalidate(Dependencies.DEPLOYMENTS);
|
||||
invalidate(Dependencies.FUNCTION);
|
||||
return;
|
||||
}
|
||||
if (message.events.includes('functions.*.deployments.*.delete')) {
|
||||
invalidate(Dependencies.DEPLOYMENTS);
|
||||
return;
|
||||
}
|
||||
});
|
||||
return realtime.forProject(page.params.region, 'console', (response) => {
|
||||
const payload = response.payload as Models.Deployment;
|
||||
if (payload.status !== 'ready' && previousStatus === payload.status) {
|
||||
return;
|
||||
}
|
||||
|
||||
previousStatus = payload.status;
|
||||
if (response.events.includes('functions.*.deployments.*.create')) {
|
||||
invalidate(Dependencies.DEPLOYMENTS);
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.events.includes('functions.*.deployments.*.update')) {
|
||||
invalidate(Dependencies.DEPLOYMENTS);
|
||||
invalidate(Dependencies.FUNCTION);
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.events.includes('functions.*.deployments.*.delete')) {
|
||||
invalidate(Dependencies.DEPLOYMENTS);
|
||||
return;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$: $registerCommands([
|
||||
|
||||
+2
-2
@@ -113,7 +113,7 @@
|
||||
placement={'bottom'}>
|
||||
<div>
|
||||
<ActionMenu.Item.Button
|
||||
trailingIcon={IconRefresh}
|
||||
leadingIcon={IconRefresh}
|
||||
disabled={activeDeployment.sourceSize === 0}
|
||||
on:click={() => {
|
||||
selectedDeployment = activeDeployment;
|
||||
@@ -134,7 +134,7 @@
|
||||
deployment={activeDeployment}
|
||||
{toggle} />
|
||||
<ActionMenu.Item.Anchor
|
||||
trailingIcon={IconTerminal}
|
||||
leadingIcon={IconTerminal}
|
||||
href={`${base}/project-${page.params.region}-${page.params.project}/functions/function-${page.params.function}/deployment-${activeDeployment.$id}`}>
|
||||
Build logs
|
||||
</ActionMenu.Item.Anchor>
|
||||
|
||||
-39
@@ -1,39 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
|
||||
import Confirm from '$lib/components/confirm.svelte';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { page } from '$app/state';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
|
||||
export let showDelete = false;
|
||||
export let selectedDeployment: Models.Deployment = null;
|
||||
let error: string;
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
await sdk
|
||||
.forProject(page.params.region, page.params.project)
|
||||
.functions.deleteDeployment({
|
||||
functionId: selectedDeployment.resourceId,
|
||||
deploymentId: selectedDeployment.$id
|
||||
});
|
||||
await invalidate(Dependencies.FUNCTION);
|
||||
showDelete = false;
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: `Deployment has been deleted`
|
||||
});
|
||||
trackEvent(Submit.DeploymentDelete);
|
||||
} catch (e) {
|
||||
error = e.message;
|
||||
trackError(e, Submit.DeploymentDelete);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Confirm onSubmit={handleSubmit} title="Delete deployment" bind:open={showDelete} bind:error>
|
||||
Are you sure you want to delete this deployment?
|
||||
</Confirm>
|
||||
+43
-28
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { Container } from '$lib/layout';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { realtime } from '$lib/stores/sdk';
|
||||
import { onMount } from 'svelte';
|
||||
import { type Models } from '@appwrite.io/console';
|
||||
import { page } from '$app/state';
|
||||
@@ -24,9 +24,11 @@
|
||||
} from '@appwrite.io/pink-svelte';
|
||||
import { capitalize } from '$lib/helpers/string';
|
||||
import { formatTimeDetailed } from '$lib/helpers/timeConversion';
|
||||
import { getEffectiveBuildStatus } from '$lib/helpers/buildTimeout';
|
||||
import { regionalConsoleVariables } from '$routes/(console)/project-[region]-[project]/store';
|
||||
import { timer } from '$lib/actions/timer';
|
||||
import { app } from '$lib/stores/app';
|
||||
import { IconDotsHorizontal, IconRefresh } from '@appwrite.io/pink-icons-svelte';
|
||||
import { IconDotsHorizontal, IconRefresh, IconTrash } from '@appwrite.io/pink-icons-svelte';
|
||||
import { Menu } from '$lib/components/menu';
|
||||
import { canWriteFunctions } from '$lib/stores/roles';
|
||||
import { Click, trackEvent } from '$lib/actions/analytics';
|
||||
@@ -36,32 +38,33 @@
|
||||
import { readOnly } from '$lib/stores/billing';
|
||||
import RedeployModal from '../(modals)/redeployModal.svelte';
|
||||
|
||||
export let data;
|
||||
let { data } = $props();
|
||||
|
||||
let showDelete = false;
|
||||
let showCancel = false;
|
||||
let showActivate = false;
|
||||
let showRedeploy = false;
|
||||
let effectiveStatus = $derived(
|
||||
getEffectiveBuildStatus(
|
||||
data.deployment.status,
|
||||
data.deployment.$createdAt,
|
||||
$regionalConsoleVariables
|
||||
)
|
||||
);
|
||||
let showDelete = $state(false);
|
||||
let showCancel = $state(false);
|
||||
let showActivate = $state(false);
|
||||
let showRedeploy = $state(false);
|
||||
|
||||
onMount(() => {
|
||||
const unsubscribe = sdk.forConsole.client.subscribe<Models.Deployment>(
|
||||
'console',
|
||||
(message) => {
|
||||
if (
|
||||
message.events.includes(
|
||||
`functions.${page.params.function}.deployments.${page.params.deployment}.update`
|
||||
)
|
||||
) {
|
||||
if (message.payload.status === 'ready') {
|
||||
invalidate(Dependencies.DEPLOYMENT);
|
||||
}
|
||||
return realtime.forConsole(page.params.region, 'console', (message) => {
|
||||
if (
|
||||
message.events.includes(
|
||||
`functions.${page.params.function}.deployments.${page.params.deployment}.update`
|
||||
)
|
||||
) {
|
||||
const payload = message.payload as Models.Deployment;
|
||||
if (['ready', 'failed'].includes(payload.status)) {
|
||||
invalidate(Dependencies.DEPLOYMENT);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
export function badgeTypeDeployment(status: string) {
|
||||
@@ -84,7 +87,7 @@
|
||||
<DeploymentCard proxyRuleList={data.proxyRuleList} deployment={data.deployment}>
|
||||
{#snippet footer()}
|
||||
<Layout.Stack direction="row" alignItems="center" inline>
|
||||
{#if data.deployment.status === 'processing' || data.deployment.status === 'building' || data.deployment.status === 'waiting'}
|
||||
{#if effectiveStatus === 'processing' || effectiveStatus === 'building' || effectiveStatus === 'waiting'}
|
||||
<Button
|
||||
text
|
||||
on:click={() => {
|
||||
@@ -104,7 +107,7 @@
|
||||
placement={'bottom'}>
|
||||
<div>
|
||||
<ActionMenu.Item.Button
|
||||
trailingIcon={IconRefresh}
|
||||
leadingIcon={IconRefresh}
|
||||
disabled={data.deployment.sourceSize === 0}
|
||||
on:click={() => {
|
||||
showRedeploy = true;
|
||||
@@ -121,6 +124,18 @@
|
||||
{#if !!data.deployment?.sourceSize || !!data.deployment?.sourceSize}
|
||||
<DownloadActionMenuItem deployment={data.deployment} {toggle} />
|
||||
{/if}
|
||||
{#if $canWriteFunctions && ['ready', 'failed'].includes(data.deployment.status)}
|
||||
<ActionMenu.Item.Button
|
||||
status="danger"
|
||||
leadingIcon={IconTrash}
|
||||
on:click={() => {
|
||||
showDelete = true;
|
||||
toggle();
|
||||
}}
|
||||
style="width: 100%">
|
||||
Delete
|
||||
</ActionMenu.Item.Button>
|
||||
{/if}
|
||||
</ActionMenu.Root>
|
||||
</svelte:fragment>
|
||||
</Menu>
|
||||
@@ -146,9 +161,9 @@
|
||||
<Card.Base padding="s">
|
||||
<Accordion
|
||||
title="Deployment logs"
|
||||
badge={capitalize(data.deployment.status)}
|
||||
badge={capitalize(effectiveStatus)}
|
||||
open
|
||||
badgeType={badgeTypeDeployment(data.deployment.status)}
|
||||
badgeType={badgeTypeDeployment(effectiveStatus)}
|
||||
hideDivider>
|
||||
<Layout.Stack gap="xl">
|
||||
{#key data.deployment.buildLogs}
|
||||
@@ -161,7 +176,7 @@
|
||||
|
||||
<svelte:fragment slot="end">
|
||||
<Layout.Stack direction="row" alignItems="center" inline>
|
||||
{#if ['processing', 'building'].includes(data.deployment.status)}
|
||||
{#if ['processing', 'building'].includes(effectiveStatus)}
|
||||
<Typography.Code color="--fgcolor-neutral-secondary">
|
||||
<Layout.Stack direction="row" alignItems="center" inline>
|
||||
<p use:timer={{ start: data.deployment.$createdAt }}></p>
|
||||
|
||||
+9
-11
@@ -1,18 +1,16 @@
|
||||
<script>
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { page } from '$app/state';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { realtime } from '$lib/stores/sdk';
|
||||
import { onMount } from 'svelte';
|
||||
import { page } from '$app/state';
|
||||
import { realtime } from '$lib/stores/sdk';
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
|
||||
onMount(() => {
|
||||
return realtime
|
||||
.forProject(page.params.region, page.params.project)
|
||||
.subscribe('functions.*.executions', (response) => {
|
||||
if (response.events.includes('functions.*.executions.*')) {
|
||||
invalidate(Dependencies.EXECUTIONS);
|
||||
}
|
||||
});
|
||||
return realtime.forProject(page.params.region, 'functions.*.executions', (response) => {
|
||||
if (response.events.includes('functions.*.executions.*')) {
|
||||
invalidate(Dependencies.EXECUTIONS);
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
+5
-3
@@ -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);
|
||||
}
|
||||
|
||||
+16
-12
@@ -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}
|
||||
<Table.Header.Cell column="actions" {root} />
|
||||
{/snippet}
|
||||
|
||||
{#snippet children(root)}
|
||||
{#each data.deploymentList.deployments as deployment (deployment.$id)}
|
||||
{@const effectiveStatus = getEffectiveBuildStatus(
|
||||
deployment.status,
|
||||
deployment.$createdAt,
|
||||
$regionalConsoleVariables
|
||||
)}
|
||||
<Table.Row.Link
|
||||
{root}
|
||||
id={deployment.$id}
|
||||
@@ -96,23 +102,21 @@
|
||||
<Id value={deployment.$id}>{deployment.$id}</Id>
|
||||
{/key}
|
||||
{:else if column.id === 'status'}
|
||||
{@const status = deployment.status}
|
||||
|
||||
{#if data?.activeDeployment?.$id === deployment?.$id}
|
||||
<Status status="complete" label="Active" />
|
||||
{:else}
|
||||
<Status
|
||||
status={deploymentStatusConverter(status)}
|
||||
label={capitalize(status)} />
|
||||
status={deploymentStatusConverter(effectiveStatus)}
|
||||
label={capitalize(effectiveStatus)} />
|
||||
{/if}
|
||||
{:else if column.id === 'type'}
|
||||
<DeploymentSource {deployment} />
|
||||
{:else if column.id === '$updatedAt'}
|
||||
<DeploymentCreatedBy {deployment} />
|
||||
{: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)}
|
||||
<span use:timer={{ start: deployment.$createdAt }}></span>
|
||||
{:else}
|
||||
{formatTimeDetailed(deployment.buildDuration)}
|
||||
@@ -139,7 +143,7 @@
|
||||
placement={'bottom'}>
|
||||
<div>
|
||||
<ActionMenu.Item.Button
|
||||
trailingIcon={IconRefresh}
|
||||
leadingIcon={IconRefresh}
|
||||
disabled={deployment.sourceSize === 0}
|
||||
on:click={() => {
|
||||
selectedDeployment = deployment;
|
||||
@@ -155,7 +159,7 @@
|
||||
</Tooltip>
|
||||
{#if deployment.status === 'ready' && deployment.$id !== $func.deploymentId}
|
||||
<ActionMenu.Item.Button
|
||||
trailingIcon={IconLightningBolt}
|
||||
leadingIcon={IconLightningBolt}
|
||||
on:click={() => {
|
||||
selectedDeployment = deployment;
|
||||
showActivate = true;
|
||||
@@ -167,7 +171,7 @@
|
||||
|
||||
<DownloadActionMenuItem {deployment} {toggle} />
|
||||
|
||||
{#if deployment.status === 'processing' || deployment.status === 'building' || deployment.status === 'waiting'}
|
||||
{#if effectiveStatus === 'processing' || effectiveStatus === 'building' || effectiveStatus === 'waiting'}
|
||||
<ActionMenu.Item.Button
|
||||
trailingIcon={IconXCircle}
|
||||
on:click={() => {
|
||||
@@ -180,9 +184,9 @@
|
||||
Cancel
|
||||
</ActionMenu.Item.Button>
|
||||
{/if}
|
||||
{#if deployment.status !== 'building' && deployment.status !== 'processing' && deployment.status !== 'waiting'}
|
||||
{#if effectiveStatus !== 'building' && effectiveStatus !== 'processing' && effectiveStatus !== 'waiting'}
|
||||
<ActionMenu.Item.Button
|
||||
trailingIcon={IconTrash}
|
||||
leadingIcon={IconTrash}
|
||||
status="danger"
|
||||
on:click={() => {
|
||||
selectedDeployment = deployment;
|
||||
|
||||
+17
-2
@@ -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({
|
||||
|
||||
@@ -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;
|
||||
</script>
|
||||
@@ -12,5 +16,26 @@
|
||||
<CoverTitle href={`${base}/project-${page.params.region}-${projectId}/overview/api-keys`}>
|
||||
{$key?.name}
|
||||
</CoverTitle>
|
||||
<Layout.Stack direction="row" inline>
|
||||
{#if $key?.secret}
|
||||
<Copy value={$key.secret} copyText="Copy API secret">
|
||||
<Tag size="xs" variant="code">
|
||||
<Icon icon={IconDuplicate} size="s" slot="start" />
|
||||
<span class="api-secret-label"> API secret </span>
|
||||
</Tag>
|
||||
</Copy>
|
||||
{/if}
|
||||
{#if $projectRegion}
|
||||
<RegionEndpoint region={$projectRegion} />
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
</svelte:fragment>
|
||||
</Cover>
|
||||
|
||||
<style>
|
||||
.api-secret-label {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
word-break: break-all;
|
||||
}
|
||||
</style>
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 308 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
@@ -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
|
||||
})
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -24,12 +29,15 @@
|
||||
<Layout.Stack alignItems="baseline" direction={$isSmallViewport ? 'column' : 'row'}>
|
||||
<Typography.Title color="--fgcolor-neutral-primary" size="xl" truncate>
|
||||
<span class="project-title">
|
||||
{$project?.name}
|
||||
{page.data.project?.name}
|
||||
</span>
|
||||
</Typography.Title>
|
||||
<Layout.Stack direction="row" inline>
|
||||
<Id value={$project.$id}>{$project.$id}</Id>
|
||||
<RegionEndpoint region={$projectRegion} />
|
||||
<Id value={page.params.project} copyText="Copy project ID"
|
||||
>{page.params.project}</Id>
|
||||
{#if $projectRegion}
|
||||
<RegionEndpoint region={$projectRegion} />
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
</Layout.Stack>
|
||||
</svelte:fragment>
|
||||
@@ -53,7 +61,7 @@
|
||||
>Follow a few quick steps to get started with Appwrite</Typography.Text>
|
||||
</Layout.Stack>
|
||||
<div class="dashboard-header-button">
|
||||
{#if !hasOnboardingDismissed($project.$id, $user)}
|
||||
{#if !hasOnboardingDismissed(page.params.project, $user)}
|
||||
<Button.Button size="s" variant="secondary" on:click={dismissOnboarding}>
|
||||
Dismiss this page
|
||||
</Button.Button>
|
||||
|
||||
@@ -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 @@
|
||||
</Layout.Stack>
|
||||
</div>
|
||||
<Layout.Stack gap="l">
|
||||
<Layout.Stack gap="l" direction="row">
|
||||
<Layout.Stack
|
||||
gap="l"
|
||||
direction={$isSmallViewport ? 'column' : 'row'}>
|
||||
<Card.Button
|
||||
on:click={() => {
|
||||
openPlatformWizard(0, platformMap.get('Web'));
|
||||
@@ -341,17 +339,34 @@
|
||||
</Layout.Stack>
|
||||
</Card.Button>
|
||||
</Layout.Stack>
|
||||
<Layout.Stack direction="row" gap="xxs" alignItems="center">
|
||||
<Typography.Text>Or connect</Typography.Text>
|
||||
<Link.Button variant="muted" on:click={createKey}
|
||||
>server side</Link.Button>
|
||||
<div style:padding-inline-start="8px">
|
||||
<AvatarGroup
|
||||
icons={[IconPython, IconNodeJs, IconPhp]}
|
||||
total={7}
|
||||
size="s" />
|
||||
</div>
|
||||
</Layout.Stack>
|
||||
<span class="with-separators eyebrow-heading-3">or</span>
|
||||
|
||||
<Card.Button on:click={createKey} padding="none">
|
||||
<Layout.Stack gap="xl">
|
||||
<div
|
||||
class="card-top-image api-key-card-image"
|
||||
style:background-image={`url('${
|
||||
$app.themeInUse === 'dark'
|
||||
? PlatformSdkImgSourceDark
|
||||
: PlatformSdkImgSource
|
||||
}')`}>
|
||||
<Layout.Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
justifyContent="space-between">
|
||||
<Layout.Stack gap="xxs">
|
||||
<Typography.Title size="s"
|
||||
>Create API key</Typography.Title>
|
||||
<Typography.Text
|
||||
>Connect your server or backend to Appwrite</Typography.Text>
|
||||
</Layout.Stack>
|
||||
<div class="arrow-icon">
|
||||
<Icon icon={IconArrowRight} size="s" />
|
||||
</div>
|
||||
</Layout.Stack>
|
||||
</div>
|
||||
</Layout.Stack>
|
||||
</Card.Button>
|
||||
</Layout.Stack>
|
||||
</Layout.Stack></Step.Item>
|
||||
<Step.Item state="next"
|
||||
@@ -684,6 +699,24 @@
|
||||
background-position: bottom;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
.api-key-card-image {
|
||||
background-size: cover;
|
||||
background-position: right center;
|
||||
background-repeat: no-repeat;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 160px;
|
||||
border-radius: var(--border-radius-m);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
align-items: flex-start;
|
||||
padding: var(--base-16, 16px);
|
||||
@media (min-width: 1200px) {
|
||||
min-height: 187px;
|
||||
}
|
||||
}
|
||||
.full-height-card {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
+14
-15
@@ -17,7 +17,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';
|
||||
@@ -63,8 +63,10 @@ const val APPWRITE_PUBLIC_ENDPOINT = "${sdk.forProject(page.params.region, page.
|
||||
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({
|
||||
@@ -81,20 +83,17 @@ const val APPWRITE_PUBLIC_ENDPOINT = "${sdk.forProject(page.params.region, page.
|
||||
}
|
||||
|
||||
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();
|
||||
};
|
||||
});
|
||||
|
||||
+14
-15
@@ -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();
|
||||
};
|
||||
});
|
||||
|
||||
+14
-15
@@ -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();
|
||||
};
|
||||
});
|
||||
|
||||
+14
-15
@@ -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();
|
||||
};
|
||||
});
|
||||
|
||||
+20
-18
@@ -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();
|
||||
};
|
||||
});
|
||||
</script>
|
||||
@@ -271,7 +272,8 @@ ${prefix}APPWRITE_ENDPOINT = "${sdk.forProject(page.params.region, page.params.p
|
||||
protocol or port number required.
|
||||
</span>
|
||||
</Tooltip>
|
||||
</InputText></Fieldset>
|
||||
</InputText>
|
||||
</Fieldset>
|
||||
<Layout.Stack direction="row" justifyContent="flex-end">
|
||||
<Button submit disabled={!selectedFramework}>Create platform</Button>
|
||||
</Layout.Stack>
|
||||
|
||||
@@ -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([
|
||||
|
||||
+12
-5
@@ -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 @@
|
||||
</Button>
|
||||
<svelte:fragment slot="menu" let:toggle>
|
||||
<ActionMenu.Root>
|
||||
{@const effectiveStatus = getEffectiveBuildStatus(
|
||||
deployment.status,
|
||||
deployment.$createdAt,
|
||||
$regionalConsoleVariables
|
||||
)}
|
||||
{#if !inCard}
|
||||
<Tooltip disabled={selectedDeployment?.sourceSize !== 0} placement={'bottom'}>
|
||||
<div>
|
||||
@@ -70,7 +77,7 @@
|
||||
<div slot="tooltip">Source is empty</div>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
{#if deployment?.status === 'ready' && deployment?.$id !== activeDeployment}
|
||||
{#if effectiveStatus === 'ready' && deployment?.$id !== activeDeployment}
|
||||
<ActionMenu.Item.Button
|
||||
leadingIcon={IconLightningBolt}
|
||||
on:click={(e) => {
|
||||
@@ -82,7 +89,7 @@
|
||||
Activate
|
||||
</ActionMenu.Item.Button>
|
||||
{/if}
|
||||
{#if deployment?.status === 'ready' || deployment?.status === 'failed' || deployment?.status === 'building'}
|
||||
{#if effectiveStatus === 'ready' || effectiveStatus === 'failed' || effectiveStatus === 'building'}
|
||||
<SubMenu>
|
||||
<ActionMenu.Root noPadding>
|
||||
<ActionMenu.Item.Button
|
||||
@@ -101,7 +108,7 @@
|
||||
</ActionMenu.Item.Anchor>
|
||||
|
||||
<ActionMenu.Item.Anchor
|
||||
disabled={deployment?.status !== 'ready'}
|
||||
disabled={effectiveStatus !== 'ready'}
|
||||
on:click={toggle}
|
||||
href={getOutputDownload(deployment.$id)}
|
||||
external>
|
||||
@@ -112,7 +119,7 @@
|
||||
</SubMenu>
|
||||
{/if}
|
||||
|
||||
{#if deployment?.status === 'processing' || deployment?.status === 'building' || deployment.status === 'waiting'}
|
||||
{#if effectiveStatus === 'processing' || effectiveStatus === 'building' || effectiveStatus === 'waiting'}
|
||||
<ActionMenu.Item.Button
|
||||
leadingIcon={IconXCircle}
|
||||
status="danger"
|
||||
@@ -124,7 +131,7 @@
|
||||
Cancel
|
||||
</ActionMenu.Item.Button>
|
||||
{/if}
|
||||
{#if deployment.status !== 'building' && deployment.status !== 'processing' && deployment?.status !== 'waiting'}
|
||||
{#if ['ready', 'failed'].includes(deployment.status)}
|
||||
<ActionMenu.Item.Button
|
||||
status="danger"
|
||||
leadingIcon={IconTrash}
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
<script lang="ts">
|
||||
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 '[37mPreparing for build ... [0m\n';
|
||||
} else if (deployment.status === 'waiting') {
|
||||
} else if (effectiveStatus === 'waiting') {
|
||||
return '[37mPreparing for build ... [0m\n';
|
||||
} else if (deployment.status === 'processing') {
|
||||
} else if (effectiveStatus === 'processing') {
|
||||
return '[37mPreparing for build ... [0m\n';
|
||||
} else {
|
||||
return emptyCopy;
|
||||
@@ -62,16 +68,16 @@
|
||||
Deployment logs
|
||||
</Typography.Text>
|
||||
<Badge
|
||||
content={capitalize(deployment.status)}
|
||||
content={capitalize(effectiveStatus)}
|
||||
size="xs"
|
||||
variant="secondary"
|
||||
type={badgeTypeDeployment(deployment.status)} />
|
||||
type={badgeTypeDeployment(effectiveStatus)} />
|
||||
</Layout.Stack>
|
||||
<LogsTimer status={deployment.status} {deployment} />
|
||||
<LogsTimer status={effectiveStatus} {deployment} />
|
||||
</Layout.Stack>
|
||||
{/if}
|
||||
|
||||
{#if ['waiting', 'processing'].includes(deployment.status) || (deployment.status === 'building' && !deployment?.buildLogs?.length)}
|
||||
{#if ['waiting', 'processing'].includes(effectiveStatus) || (effectiveStatus === 'building' && !deployment?.buildLogs?.length)}
|
||||
<Card.Base variant="secondary">
|
||||
<Layout.Stack direction="row" justifyContent="center" gap="s">
|
||||
<Spinner /> Waiting for build to start...
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
<script lang="ts">
|
||||
import { timer } from '$lib/actions/timer';
|
||||
import { formatTimeDetailed } from '$lib/helpers/timeConversion';
|
||||
import { getEffectiveBuildStatus } from '$lib/helpers/buildTimeout';
|
||||
import { regionalConsoleVariables } from '$routes/(console)/project-[region]-[project]/store';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
|
||||
import { Layout, Spinner, Typography } from '@appwrite.io/pink-svelte';
|
||||
|
||||
export let status: string;
|
||||
export let deployment: Models.Deployment;
|
||||
let { status, deployment }: { status: string; deployment: Models.Deployment } = $props();
|
||||
|
||||
let effectiveStatus = $derived(
|
||||
getEffectiveBuildStatus(status, deployment.$createdAt, $regionalConsoleVariables)
|
||||
);
|
||||
</script>
|
||||
|
||||
<Layout.Stack direction="row" alignItems="center" inline>
|
||||
{#if ['processing', 'building'].includes(status)}
|
||||
{#if ['processing', 'building'].includes(effectiveStatus)}
|
||||
<Typography.Code color="--fgcolor-neutral-secondary">
|
||||
<Layout.Stack direction="row" alignItems="center" inline>
|
||||
<p use:timer={{ start: deployment.$createdAt }}></p>
|
||||
|
||||
@@ -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 @@
|
||||
</Layout.Stack>
|
||||
|
||||
<Layout.Stack direction="row" gap="xl">
|
||||
{#if deployment.status === 'failed'}
|
||||
{#if effectiveStatus === 'failed'}
|
||||
<Layout.Stack gap="xxs" inline>
|
||||
<Typography.Text variant="m-400" color="--fgcolor-neutral-tertiary">
|
||||
Status
|
||||
</Typography.Text>
|
||||
<Typography.Text variant="m-400" color="--fgcolor-neutral-primary">
|
||||
<Status
|
||||
status={deployment.status}
|
||||
label={capitalize(deployment.status)} />
|
||||
status={effectiveStatus}
|
||||
label={capitalize(effectiveStatus)} />
|
||||
</Typography.Text>
|
||||
</Layout.Stack>
|
||||
{:else}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
+20
-26
@@ -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();
|
||||
};
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
+7
-4
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { page } from '$app/state';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { realtime } from '$lib/stores/sdk';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { invalidate, goto } from '$app/navigation';
|
||||
import { registerCommands } from '$lib/commandCenter';
|
||||
@@ -13,11 +13,14 @@
|
||||
|
||||
onMount(() => {
|
||||
let previousStatus: string = null;
|
||||
return sdk.forConsole.realtime.subscribe<Models.Deployment>('console', (message) => {
|
||||
if (message.payload.status !== 'ready' && previousStatus === message.payload.status) {
|
||||
return realtime.forConsole(page.params.region, 'console', (message) => {
|
||||
const payload = message.payload as Models.Deployment;
|
||||
if (payload.status !== 'ready' && previousStatus === payload.status) {
|
||||
return;
|
||||
}
|
||||
previousStatus = message.payload.status;
|
||||
|
||||
previousStatus = payload.status;
|
||||
|
||||
if (message.events.includes('sites.*.deployments.*.create')) {
|
||||
invalidate(Dependencies.DEPLOYMENTS);
|
||||
|
||||
|
||||
+2
-6
@@ -13,7 +13,7 @@
|
||||
import DeploymentMetrics from './deploymentMetrics.svelte';
|
||||
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { realtime, sdk } from '$lib/stores/sdk';
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import CreateCliModal from './createCliModal.svelte';
|
||||
@@ -32,11 +32,7 @@
|
||||
let showAlert = true;
|
||||
|
||||
onMount(() => {
|
||||
if (page.url.searchParams.has('createDeployment')) {
|
||||
showConnectRepo = true;
|
||||
}
|
||||
|
||||
return sdk.forConsole.realtime.subscribe('console', (response) => {
|
||||
return realtime.forConsole(page.params.region, 'console', (response) => {
|
||||
if (response.events.includes('sites.*.deployments.*')) {
|
||||
invalidate(Dependencies.DEPLOYMENTS);
|
||||
}
|
||||
|
||||
+7
-7
@@ -37,7 +37,7 @@
|
||||
|
||||
return {
|
||||
Unix: {
|
||||
code: `appwrite client --projectId="${projectId}" && \\
|
||||
code: `appwrite client --project-id="${projectId}" && \\
|
||||
appwrite sites create-deployment \\
|
||||
--site-id="${siteId}" \\
|
||||
--code="${codePath}" \\
|
||||
@@ -49,9 +49,9 @@ appwrite sites create-deployment \\
|
||||
},
|
||||
|
||||
CMD: {
|
||||
code: `appwrite client --projectId="${projectId}" && ^
|
||||
appwrite sites createDeployment ^
|
||||
--siteId=${siteId} ^
|
||||
code: `appwrite client --project-id="${projectId}" && ^
|
||||
appwrite sites create-deployment ^
|
||||
--site-id=${siteId} ^
|
||||
--code="${codePath}" ^
|
||||
--activate ^
|
||||
--build-command="${buildCommand}" ^
|
||||
@@ -61,9 +61,9 @@ appwrite sites createDeployment ^
|
||||
},
|
||||
|
||||
PowerShell: {
|
||||
code: `appwrite client --projectId="${projectId}" ;
|
||||
appwrite sites createDeployment ,
|
||||
--siteId=${siteId} ,
|
||||
code: `appwrite client --project-id="${projectId}" ;
|
||||
appwrite sites create-deployment ,
|
||||
--site-id=${siteId} ,
|
||||
--code="${codePath}" ,
|
||||
--activate ,
|
||||
--build-command="${buildCommand}" ,
|
||||
|
||||
+6
-5
@@ -22,15 +22,16 @@
|
||||
siteId: selectedDeployment.resourceId,
|
||||
deploymentId: selectedDeployment.$id
|
||||
});
|
||||
if (page.route.id?.includes('deployment-[deployment]')) {
|
||||
goto(
|
||||
`${base}/project-${page.params.region}-${page.params.project}/sites/site-${page.params.site}/deployments`
|
||||
);
|
||||
return;
|
||||
}
|
||||
await Promise.all([
|
||||
invalidate(Dependencies.DEPLOYMENTS),
|
||||
invalidate(Dependencies.SITE)
|
||||
]);
|
||||
if (page.url.href.includes(`deployment-${selectedDeployment.$id}`)) {
|
||||
await goto(
|
||||
`${base}/project-${page.params.region}-${page.params.project}/sites/site-${page.params.site}/deployments`
|
||||
);
|
||||
}
|
||||
showDelete = false;
|
||||
addNotification({
|
||||
type: 'success',
|
||||
|
||||
+20
-22
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { Container } from '$lib/layout';
|
||||
import { type AppwriteRealtimeSubscription, sdk } from '$lib/stores/sdk';
|
||||
import { realtime } from '$lib/stores/sdk';
|
||||
import { onMount } from 'svelte';
|
||||
import SiteCard from '../../../(components)/siteCard.svelte';
|
||||
import Logs, { badgeTypeDeployment } from '../../../(components)/logs.svelte';
|
||||
@@ -13,6 +13,8 @@
|
||||
import ActivateDeploymentModal from '../../../activateDeploymentModal.svelte';
|
||||
import { Accordion, Tooltip } from '@appwrite.io/pink-svelte';
|
||||
import { capitalize } from '$lib/helpers/string';
|
||||
import { getEffectiveBuildStatus } from '$lib/helpers/buildTimeout';
|
||||
import { regionalConsoleVariables } from '$routes/(console)/project-[region]-[project]/store';
|
||||
import LogsTimer from '../../../(components)/logsTimer.svelte';
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
@@ -24,6 +26,9 @@
|
||||
let { data }: PageProps = $props();
|
||||
|
||||
let deployment = $derived(data.deployment);
|
||||
let effectiveStatus = $derived(
|
||||
getEffectiveBuildStatus(deployment.status, deployment.$createdAt, $regionalConsoleVariables)
|
||||
);
|
||||
|
||||
let showRedeploy = $state(false);
|
||||
let showActivate = $state(false);
|
||||
@@ -31,29 +36,22 @@
|
||||
let showCancel = $state(false);
|
||||
|
||||
onMount(() => {
|
||||
let subscription: AppwriteRealtimeSubscription;
|
||||
sdk.forConsoleIn(page.params.region)
|
||||
.realtime.subscribe('console', async (response) => {
|
||||
if (
|
||||
response.events.includes(
|
||||
`sites.${page.params.site}.deployments.${page.params.deployment}.update`
|
||||
)
|
||||
) {
|
||||
await invalidate(Dependencies.DEPLOYMENT);
|
||||
}
|
||||
})
|
||||
.then((realtime) => (subscription = realtime));
|
||||
|
||||
return () => {
|
||||
subscription?.close();
|
||||
};
|
||||
return realtime.forConsole(page.params.region, 'console', async (response) => {
|
||||
if (
|
||||
response.events.includes(
|
||||
`sites.${page.params.site}.deployments.${page.params.deployment}.update`
|
||||
)
|
||||
) {
|
||||
await invalidate(Dependencies.DEPLOYMENT);
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<Container>
|
||||
<SiteCard {deployment} proxyRuleList={data.proxyRuleList}>
|
||||
{#snippet footer()}
|
||||
{#if deployment?.status === 'ready' && data.proxyRuleList?.total}
|
||||
{#if effectiveStatus === 'ready' && data.proxyRuleList?.total}
|
||||
<Button
|
||||
external
|
||||
href={`${$regionalProtocol}${data.proxyRuleList.rules[0]?.domain}`}>
|
||||
@@ -86,13 +84,13 @@
|
||||
<Card padding="s">
|
||||
<Accordion
|
||||
title="Deployment logs"
|
||||
badge={capitalize(deployment.status)}
|
||||
badge={capitalize(effectiveStatus)}
|
||||
open
|
||||
badgeType={badgeTypeDeployment(deployment.status)}
|
||||
badgeType={badgeTypeDeployment(effectiveStatus)}
|
||||
hideDivider>
|
||||
<Logs {deployment} hideTitle hideScrollButtons fullHeight />
|
||||
<Logs {deployment} hideTitle fullHeight />
|
||||
<svelte:fragment slot="end">
|
||||
<LogsTimer status={deployment.status} {deployment} />
|
||||
<LogsTimer status={effectiveStatus} {deployment} />
|
||||
</svelte:fragment>
|
||||
</Accordion>
|
||||
</Card>
|
||||
|
||||
+11
-12
@@ -17,6 +17,8 @@
|
||||
import { capitalize } from '$lib/helpers/string';
|
||||
import DeploymentActionMenu from '../../(components)/deploymentActionMenu.svelte';
|
||||
import { deploymentStatusConverter } from '$lib/stores/git';
|
||||
import { getEffectiveBuildStatus } from '$lib/helpers/buildTimeout';
|
||||
import { regionalConsoleVariables } from '$routes/(console)/project-[region]-[project]/store';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { invalidate } from '$app/navigation';
|
||||
@@ -69,9 +71,13 @@
|
||||
{/each}
|
||||
<Table.Header.Cell column="actions" {root} />
|
||||
{/snippet}
|
||||
|
||||
{#snippet children(root)}
|
||||
{#each data.deploymentList.deployments as deployment (deployment.$id)}
|
||||
{@const effectiveStatus = getEffectiveBuildStatus(
|
||||
deployment.status,
|
||||
deployment.$createdAt,
|
||||
$regionalConsoleVariables
|
||||
)}
|
||||
<Table.Row.Link
|
||||
{root}
|
||||
id={deployment.$id}
|
||||
@@ -83,22 +89,21 @@
|
||||
<Id value={deployment.$id}>{deployment.$id}</Id>
|
||||
{/key}
|
||||
{:else if column.id === 'status'}
|
||||
{@const status = deployment.status}
|
||||
{#if data?.activeDeployment?.$id === deployment?.$id}
|
||||
<Status status="complete" label="Active" />
|
||||
{:else}
|
||||
<Status
|
||||
status={deploymentStatusConverter(status)}
|
||||
label={capitalize(status)} />
|
||||
status={deploymentStatusConverter(effectiveStatus)}
|
||||
label={capitalize(effectiveStatus)} />
|
||||
{/if}
|
||||
{:else if column.id === 'type'}
|
||||
<DeploymentSource {deployment} />
|
||||
{:else if column.id === '$updatedAt'}
|
||||
<DeploymentCreatedBy {deployment} />
|
||||
{: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)}
|
||||
<span use:timer={{ start: deployment.$createdAt }}></span>
|
||||
{:else}
|
||||
{formatTimeDetailed(deployment.buildDuration)}
|
||||
@@ -127,12 +132,6 @@
|
||||
</Table.Row.Link>
|
||||
{/each}
|
||||
{/snippet}
|
||||
|
||||
{#snippet deleteContent(count)}
|
||||
Are you sure you want to delete <strong>{count}</strong>
|
||||
{count > 1 ? 'deployments' : 'deployment'} from your site -
|
||||
<strong>{page.data.site.name}</strong>?
|
||||
{/snippet}
|
||||
</MultiSelectionTable>
|
||||
|
||||
{#if selectedDeployment}
|
||||
|
||||
+9
-2
@@ -11,6 +11,8 @@
|
||||
import ActivateDeploymentModal from '../activateDeploymentModal.svelte';
|
||||
import CancelDeploymentModal from './deployments/cancelDeploymentModal.svelte';
|
||||
import { capitalize } from '$lib/helpers/string';
|
||||
import { getEffectiveBuildStatus } from '$lib/helpers/buildTimeout';
|
||||
import { regionalConsoleVariables } from '$routes/(console)/project-[region]-[project]/store';
|
||||
import DeleteDeploymentModal from './deployments/deleteDeploymentModal.svelte';
|
||||
import DeploymentActionMenu from '../(components)/deploymentActionMenu.svelte';
|
||||
import { deploymentStatusConverter } from '$lib/stores/git';
|
||||
@@ -72,12 +74,17 @@
|
||||
</Table.Cell>
|
||||
<Table.Cell {root}>
|
||||
{@const status = deployment.status}
|
||||
{@const effectiveStatus = getEffectiveBuildStatus(
|
||||
status,
|
||||
deployment.$createdAt,
|
||||
$regionalConsoleVariables
|
||||
)}
|
||||
{#if activeDeployment?.$id === deployment?.$id}
|
||||
<Status status="complete" label="Active" />
|
||||
{:else}
|
||||
<Status
|
||||
status={deploymentStatusConverter(status)}
|
||||
label={capitalize(status)} />
|
||||
status={deploymentStatusConverter(effectiveStatus)}
|
||||
label={capitalize(effectiveStatus)} />
|
||||
{/if}
|
||||
</Table.Cell>
|
||||
|
||||
|
||||
@@ -4,16 +4,17 @@
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { Container, ResponsiveContainerHeader } from '$lib/layout';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { realtime } from '$lib/stores/sdk';
|
||||
import { onMount } from 'svelte';
|
||||
import Table from './table.svelte';
|
||||
import { Card, Empty } from '@appwrite.io/pink-svelte';
|
||||
import { columns } from './store';
|
||||
import { page } from '$app/state';
|
||||
|
||||
export let data;
|
||||
|
||||
onMount(() => {
|
||||
return sdk.forConsole.realtime.subscribe('console', (response) => {
|
||||
return realtime.forConsole(page.params.region, 'console', (response) => {
|
||||
if (response.events.includes('sites.*.executions.*')) {
|
||||
invalidate(Dependencies.EXECUTIONS);
|
||||
}
|
||||
|
||||
@@ -69,12 +69,12 @@ export const adapterDataList = [
|
||||
{
|
||||
framework: 'nextjs',
|
||||
ssr: {
|
||||
desc: "Ensure you don't set $ in $ file.",
|
||||
code: ['output', 'next.config.js'],
|
||||
desc: 'Set $ in $ file.',
|
||||
code: ["output: 'standalone'", 'next.config.js'],
|
||||
url: 'https://nextjs.org/docs/pages/building-your-application/deploying'
|
||||
},
|
||||
static: {
|
||||
desc: 'Set $ in $ file',
|
||||
desc: 'Set $ in $ file.',
|
||||
code: ["output: 'export'", 'next.config.js'],
|
||||
url: 'https://nextjs.org/docs/pages/building-your-application/deploying/static-exports'
|
||||
}
|
||||
@@ -82,12 +82,12 @@ export const adapterDataList = [
|
||||
{
|
||||
framework: 'analog',
|
||||
ssr: {
|
||||
desc: 'Set $ in $ plugin in $',
|
||||
desc: 'Set $ in $ plugin in $.',
|
||||
code: ['ssr: true', 'analog', 'vite.config.ts'],
|
||||
url: 'https://analogjs.org/docs/features/server/server-side-rendering'
|
||||
},
|
||||
static: {
|
||||
desc: 'Set $ in $ plugin in $',
|
||||
desc: 'Set $ in $ plugin in $.',
|
||||
code: ['static: true', 'analog', 'vite.config.ts'],
|
||||
url: 'https://analogjs.org/docs/features/server/static-site-generation'
|
||||
}
|
||||
|
||||
+48
-15
@@ -36,21 +36,34 @@
|
||||
frameworks.find((framework) => framework.key === site.framework)
|
||||
);
|
||||
let showFallback = $derived(adapter === Adapter.Static);
|
||||
let hasChanges = $derived(
|
||||
|
||||
let isUntouched = $derived(
|
||||
installCommand === site?.installCommand &&
|
||||
buildCommand === site?.buildCommand &&
|
||||
outputDirectory === site?.outputDirectory &&
|
||||
selectedFramework?.key === site?.framework &&
|
||||
fallback === (site?.fallbackFile || undefined) &&
|
||||
adapter === site?.adapter
|
||||
(fallback ?? '') === (site?.fallbackFile ?? '') &&
|
||||
(adapter ?? '') === (site?.adapter ?? '')
|
||||
);
|
||||
let frameworkAdapterData = $derived(
|
||||
selectedFramework.adapters.find((a) => a.key === adapter) ?? selectedFramework.adapters[0]
|
||||
);
|
||||
|
||||
$effect(() => {
|
||||
if (adapter) {
|
||||
const data = selectedFramework.adapters.find((a) => a.key === adapter);
|
||||
if (data) {
|
||||
installCommand = data.installCommand;
|
||||
buildCommand = data.buildCommand;
|
||||
outputDirectory = data.outputDirectory;
|
||||
fallback = data.fallbackFile;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (selectedFramework?.key !== site.framework) {
|
||||
//Update adapter
|
||||
// Update adapter
|
||||
const singleAdapter = selectedFramework?.adapters?.length <= 1;
|
||||
if (singleAdapter) {
|
||||
const hasSSR = selectedFramework?.adapters?.some((a) => a?.key === Adapter.Ssr);
|
||||
@@ -65,11 +78,13 @@
|
||||
}
|
||||
|
||||
//Update values
|
||||
const data = selectedFramework.adapters.find((a) => a.key === adapter);
|
||||
const data =
|
||||
selectedFramework.adapters.find((a) => a.key === adapter) ??
|
||||
selectedFramework.adapters[0];
|
||||
installCommand = data.installCommand;
|
||||
buildCommand = data.buildCommand;
|
||||
outputDirectory = data.outputDirectory;
|
||||
adapter = selectedFramework.adapters[0].key as Adapter;
|
||||
adapter = data.key as Adapter;
|
||||
fallback = data.fallbackFile;
|
||||
} else {
|
||||
adapter = site.adapter as Adapter;
|
||||
@@ -157,11 +172,11 @@
|
||||
const data = selectedFramework.adapters.find((a) => a.key === adapter);
|
||||
|
||||
if (type === 'installCommand') {
|
||||
installCommand = site?.installCommand ?? data.installCommand;
|
||||
installCommand = data.installCommand;
|
||||
} else if (type === 'buildCommand') {
|
||||
buildCommand = site?.buildCommand ?? data.buildCommand;
|
||||
buildCommand = data.buildCommand;
|
||||
} else if (type === 'outputDirectory') {
|
||||
outputDirectory = site?.outputDirectory ?? data.outputDirectory;
|
||||
outputDirectory = data.outputDirectory;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -216,7 +231,8 @@
|
||||
{adapterData.ssr.desc}
|
||||
{/if}
|
||||
{#if adapterData?.ssr?.url}
|
||||
<Link external href={adapterData.ssr.url}>Learn more</Link>
|
||||
<Link variant="muted" size="m" external href={adapterData.ssr.url}
|
||||
>Learn more</Link>
|
||||
{/if}
|
||||
</Card.Selector>
|
||||
<Card.Selector
|
||||
@@ -243,7 +259,11 @@
|
||||
{adapterData.static.desc}
|
||||
{/if}
|
||||
{#if adapterData?.static?.url}
|
||||
<Link external href={adapterData.static.url}>Learn more</Link>
|
||||
<Link
|
||||
variant="muted"
|
||||
size="m"
|
||||
external
|
||||
href={adapterData.static.url}>Learn more</Link>
|
||||
{/if}
|
||||
</Card.Selector>
|
||||
</Layout.Grid>
|
||||
@@ -258,7 +278,11 @@
|
||||
placeholder={frameworkAdapterData?.installCommand ||
|
||||
'Enter install command'} />
|
||||
|
||||
<Button secondary size="s" on:click={() => reset('installCommand')}>
|
||||
<Button
|
||||
secondary
|
||||
size="s"
|
||||
on:click={() => reset('installCommand')}
|
||||
disabled={installCommand === frameworkAdapterData?.installCommand}>
|
||||
Reset
|
||||
</Button>
|
||||
</Layout.Stack>
|
||||
@@ -269,7 +293,11 @@
|
||||
bind:value={buildCommand}
|
||||
placeholder={frameworkAdapterData?.buildCommand ||
|
||||
'Enter build command'} />
|
||||
<Button secondary size="s" on:click={() => reset('buildCommand')}>
|
||||
<Button
|
||||
secondary
|
||||
size="s"
|
||||
disabled={buildCommand === frameworkAdapterData?.buildCommand}
|
||||
on:click={() => reset('buildCommand')}>
|
||||
Reset
|
||||
</Button>
|
||||
</Layout.Stack>
|
||||
@@ -280,7 +308,12 @@
|
||||
bind:value={outputDirectory}
|
||||
placeholder={frameworkAdapterData?.outputDirectory ||
|
||||
'Enter output directory'} />
|
||||
<Button secondary size="s" on:click={() => reset('outputDirectory')}>
|
||||
<Button
|
||||
secondary
|
||||
size="s"
|
||||
disabled={(outputDirectory ?? '') ===
|
||||
(frameworkAdapterData?.outputDirectory ?? '')}
|
||||
on:click={() => reset('outputDirectory')}>
|
||||
Reset
|
||||
</Button>
|
||||
</Layout.Stack>
|
||||
@@ -305,7 +338,7 @@
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="actions">
|
||||
<Button disabled={hasChanges} submit>Update</Button>
|
||||
<Button disabled={isUntouched} submit>Update</Button>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
</Form>
|
||||
|
||||
@@ -125,11 +125,11 @@
|
||||
</Layout.Stack>
|
||||
</Layout.Stack>
|
||||
<InputSelect
|
||||
required
|
||||
id="project"
|
||||
label="Choose a project"
|
||||
options={projectOptions ?? []}
|
||||
bind:value={$supportData.project}
|
||||
required={false}
|
||||
placeholder="Select project" />
|
||||
<InputText
|
||||
id="subject"
|
||||
|
||||
Reference in New Issue
Block a user