Merge branch 'main' into 'revamp-designs'.

This commit is contained in:
Darshan
2025-11-12 13:10:19 +05:30
79 changed files with 1189 additions and 622 deletions
+28
View File
@@ -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
+104
View File
@@ -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
View File
@@ -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",
+15 -15
View File
@@ -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
+46 -4
View File
@@ -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;
+15
View File
@@ -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>
+10 -12
View File
@@ -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>
+1
View File
@@ -57,6 +57,7 @@
disabled={tooltipDisabled}
portal={tooltipPortal}
delay={tooltipDelay}
maxWidth="500px"
placement={tooltipPlacement}>
<span
data-private
+2 -2
View File
@@ -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);
+2 -1
View File
@@ -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
+1
View File
@@ -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';
+8 -9
View File
@@ -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>
+29 -12
View File
@@ -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}
+24 -24
View File
@@ -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>
+2 -7
View File
@@ -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;
}
+45
View File
@@ -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

+14 -11
View File
@@ -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
View File
@@ -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());
};
}
+10 -10
View File
@@ -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([
@@ -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>
@@ -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
@@ -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>
@@ -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>
@@ -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>
@@ -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'] {
@@ -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) {
@@ -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'] {
@@ -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>
@@ -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;
}
@@ -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);
}
}} />
@@ -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<
@@ -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}
@@ -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>
@@ -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'
@@ -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({
@@ -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([
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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);
}
@@ -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;
@@ -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%;
}
@@ -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();
};
});
@@ -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();
};
});
@@ -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();
};
});
@@ -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();
};
});
@@ -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([
@@ -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 'Preparing for build ... \n';
} else if (deployment.status === 'waiting') {
} else if (effectiveStatus === 'waiting') {
return 'Preparing for build ... \n';
} else if (deployment.status === 'processing') {
} else if (effectiveStatus === 'processing') {
return 'Preparing for build ... \n';
} else {
return emptyCopy;
@@ -62,16 +68,16 @@
Deployment logs
</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);
}
@@ -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);
}
@@ -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);
@@ -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);
}
@@ -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}" ,
@@ -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',
@@ -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>
@@ -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}
@@ -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'
}
@@ -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>
+1 -1
View File
@@ -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"