Merge branch 'main' into feat-project-changes

This commit is contained in:
Damodar Lohani
2025-05-26 01:45:06 +00:00
989 changed files with 11490 additions and 7527 deletions
+3 -1
View File
@@ -1,4 +1,6 @@
PUBLIC_APPWRITE_ENDPOINT=https://localhost/v1
PUBLIC_CONSOLE_MODE=self-hosted
PUBLIC_APPWRITE_MULTI_REGION=false
PUBLIC_APPWRITE_ENDPOINT=http://localhost/v1
PUBLIC_STRIPE_KEY=
PUBLIC_GROWTH_ENDPOINT=
+41
View File
@@ -39,6 +39,7 @@ jobs:
labels: ${{ steps.meta.outputs.labels }}
build-args: |
"PUBLIC_CONSOLE_MODE=cloud"
"PUBLIC_APPWRITE_MULTI_REGION=true"
"PUBLIC_GROWTH_ENDPOINT=${{ secrets.PUBLIC_GROWTH_ENDPOINT }}"
"PUBLIC_STRIPE_KEY=${{ secrets.PUBLIC_STRIPE_KEY }}"
"SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}"
@@ -77,6 +78,7 @@ jobs:
labels: ${{ steps.meta.outputs.labels }}
build-args: |
"PUBLIC_CONSOLE_MODE=cloud"
"PUBLIC_APPWRITE_MULTI_REGION=true"
"PUBLIC_GROWTH_ENDPOINT=${{ secrets.PUBLIC_GROWTH_ENDPOINT }}"
"PUBLIC_STRIPE_KEY=${{ secrets.PUBLIC_STRIPE_KEY_STAGE }}"
publish-self-hosted:
@@ -113,4 +115,43 @@ jobs:
labels: ${{ steps.meta.outputs.labels }}
build-args: |
"PUBLIC_CONSOLE_MODE=self-hosted"
"PUBLIC_APPWRITE_MULTI_REGION=false"
"PUBLIC_GROWTH_ENDPOINT=${{ secrets.PUBLIC_GROWTH_ENDPOINT }}"
publish-cloud-no-regions:
runs-on: ubuntu-latest
steps:
- name: Checkout the repo
uses: actions/checkout@v2
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: appwrite/console-cloud-no-regions
tags: |
type=semver,pattern={{major}}.{{minor}}.{{patch}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
- name: Build and push Docker image
id: push
uses: docker/build-push-action@v6
with:
context: .
push: true
platforms: linux/amd64,linux/arm64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
"PUBLIC_CONSOLE_MODE=cloud"
"PUBLIC_APPWRITE_MULTI_REGION=false"
"PUBLIC_STRIPE_KEY=${{ secrets.PUBLIC_STRIPE_KEY_STAGE }}"
"PUBLIC_GROWTH_ENDPOINT=${{ secrets.PUBLIC_GROWTH_ENDPOINT }}"
+3 -1
View File
@@ -21,6 +21,7 @@ ADD ./src /app/src
ADD ./static /app/static
ARG PUBLIC_CONSOLE_MODE
ARG PUBLIC_APPWRITE_MULTI_REGION
ARG PUBLIC_APPWRITE_ENDPOINT
ARG PUBLIC_GROWTH_ENDPOINT
ARG PUBLIC_STRIPE_KEY
@@ -30,6 +31,7 @@ ARG SENTRY_RELEASE
ENV PUBLIC_APPWRITE_ENDPOINT=$PUBLIC_APPWRITE_ENDPOINT
ENV PUBLIC_GROWTH_ENDPOINT=$PUBLIC_GROWTH_ENDPOINT
ENV PUBLIC_CONSOLE_MODE=$PUBLIC_CONSOLE_MODE
ENV PUBLIC_APPWRITE_MULTI_REGION=$PUBLIC_APPWRITE_MULTI_REGION
ENV PUBLIC_STRIPE_KEY=$PUBLIC_STRIPE_KEY
ENV SENTRY_AUTH_TOKEN=$SENTRY_AUTH_TOKEN
ENV SENTRY_RELEASE=$SENTRY_RELEASE
@@ -37,7 +39,7 @@ ENV NODE_OPTIONS=--max_old_space_size=8192
RUN pnpm run build
FROM nginx:1.25-alpine
FROM nginx:1.26.3-alpine
EXPOSE 80
+1
View File
@@ -24,6 +24,7 @@ async function main() {
log(bold().magenta('APPWRITE CONSOLE'));
log();
logEnv('CONSOLE MODE', env?.PUBLIC_CONSOLE_MODE);
logEnv('MULTI REGION', env?.PUBLIC_APPWRITE_MULTI_REGION);
logEnv('APPWRITE ENDPOINT', env?.PUBLIC_APPWRITE_ENDPOINT, 'relative');
logEnv('GROWTH ENDPOINT', env?.PUBLIC_GROWTH_ENDPOINT);
log();
+2
View File
@@ -5,6 +5,7 @@ services:
context: .
args:
PUBLIC_CONSOLE_MODE: ${PUBLIC_CONSOLE_MODE}
PUBLIC_APPWRITE_MULTI_REGION: ${PUBLIC_APPWRITE_MULTI_REGION}
PUBLIC_APPWRITE_ENDPOINT: ${PUBLIC_APPWRITE_ENDPOINT}
PUBLIC_GROWTH_ENDPOINT: ${PUBLIC_GROWTH_ENDPOINT}
PUBLIC_STRIPE_KEY: ${PUBLIC_STRIPE_KEY}
@@ -20,6 +21,7 @@ services:
- build/
environment:
- PUBLIC_CONSOLE_MODE
- PUBLIC_APPWRITE_MULTI_REGION
- PUBLIC_APPWRITE_ENDPOINT
- PUBLIC_GROWTH_ENDPOINT
- PUBLIC_STRIPE_KEY
+18 -22
View File
@@ -1,35 +1,31 @@
map $sent_http_content_type $expires {
# cache everything for 1 year
default 1y;
# html files shouldn't be cached for single-page applications
text/html off;
}
server {
listen 80;
server_name localhost;
# serve compressed file if filename.gz exists
gzip_static on;
location /console {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri /console/index.html;
# Set root for all locations
root /usr/share/nginx/html;
# Add cache headers
expires $expires;
# Security headers for all locations
add_header X-UA-Compatible "IE=Edge";
add_header X-Frame-Options SAMEORIGIN;
add_header X-XSS-Protection "1; mode=block;";
add_header X-Content-Type-Options nosniff;
# Only cache files in /console/_app/immutable/ for 1 year
location /console/_app/immutable/ {
try_files $uri =404;
expires 1y;
add_header Pragma public;
add_header Cache-Control "public";
}
# Deny IE browsers from going into quirks mode
add_header X-UA-Compatible "IE=Edge";
# X-Frame-Options is to prevent from clickJacking attack
add_header X-Frame-Options SAMEORIGIN;
# This header enables the Cross-site scripting (XSS) filter
add_header X-XSS-Protection "1; mode=block;";
# disable content-type sniffing on some browsers.
add_header X-Content-Type-Options nosniff;
# All other /console requests (no cache)
location /console {
index index.html index.html;
try_files $uri /console/index.html;
}
location / {
+1 -1
View File
@@ -12,7 +12,7 @@ export function getOrganizationIdFromUrl(pathname: string) {
export function getProjectIdFromUrl(pathname: string) {
// TODO: use base path from svelte here
const regex = /\/console\/project-([^/]+)(\/.*)?/;
const regex = /\/console\/project-(?:[a-z]{2,3}-)?([^/]+)(\/.*)?/;
const match = pathname.match(regex);
if (match) {
+2 -2
View File
@@ -8,12 +8,12 @@ test('upgrade - free tier', async ({ page }) => {
await createFreeProject(page);
await test.step('upgrade project', async () => {
await page.getByRole('link', { name: 'Upgrade', exact: true }).click();
await page.waitForURL('./organization-**/change-plan');
await page.waitForURL(/\/organization-[^/]+\/change-plan/);
await page.locator('input[value="tier-1"]').click();
await page.getByRole('button', { name: 'add' }).first().click();
await enterCreditCard(page);
// skip members
await page.getByRole('button', { name: 'change plan' }).click();
await page.waitForURL('**/console/project-*/overview/platforms');
await page.waitForURL(/\/console\/project-(?:[a-z0-9]+-)?([^/]+)\/get-started/);
});
});
+2 -3
View File
@@ -10,7 +10,6 @@ export function registerUserStep(page: Page): Promise<Metadata> {
return test.step('register user', async () => {
const seed = crypto.randomUUID();
await page.goto('./register');
// await page.getByRole('button', { name: 'only required' }).click();
const inputs = {
name: page.locator('id=name'),
email: page.locator('id=email'),
@@ -25,9 +24,9 @@ export function registerUserStep(page: Page): Promise<Metadata> {
await inputs.name.fill(values.name);
await inputs.email.fill(values.email);
await inputs.password.fill(values.password);
await inputs.terms.check();
await inputs.terms.check({ force: true });
await page.getByRole('button', { name: 'Sign up', exact: true }).click();
await page.waitForURL('./onboarding');
await page.waitForURL('./onboarding/create-project');
return values;
});
+7 -12
View File
@@ -9,23 +9,18 @@ type Metadata = {
export async function createFreeProject(page: Page): Promise<Metadata> {
const organizationId = await test.step('create organization', async () => {
await page.goto('./');
await page.waitForURL('./onboarding');
await page.locator('id=name').fill('test org');
await page.locator('id=plan').selectOption('tier-0');
await page.getByRole('button', { name: 'get started' }).click();
await page.waitForURL('./organization-**');
await page.waitForURL(/\/organization-[^/]+/);
return getOrganizationIdFromUrl(page.url());
});
const projectId = await test.step('create project', async () => {
await page.waitForURL('./organization-**');
await page.waitForURL(/\/organization-[^/]+/);
await page.getByRole('button', { name: 'create project' }).first().click();
await page.locator('id=name').fill('test project');
await page.getByRole('button', { name: 'next' }).click();
await page.locator('label').filter({ hasText: 'Frankfurt' }).click();
await page.getByRole('button', { name: 'create' }).click();
await page.waitForURL('./project-**/overview/platforms');
expect(page.url()).toContain('/console/project-');
const dialog = page.locator('dialog[open]');
await dialog.getByPlaceholder('Project name').fill('test project');
await dialog.getByRole('button', { name: 'create' }).click();
await page.waitForURL(/\/project-fra-[^/]+/);
expect(page.url()).toContain('/console/project-fra-');
return getProjectIdFromUrl(page.url());
});
+12 -14
View File
@@ -19,7 +19,7 @@ export async function enterCreditCard(page: Page) {
await stripe.locator('id=Field-expiryInput').fill('1250');
await stripe.locator('id=Field-cvcInput').fill('123');
await stripe.locator('id=Field-countryInput').selectOption('DE');
await page.getByRole('button', { name: 'Add', exact: true }).click();
await dialog.getByRole('button', { name: 'Add', exact: true }).click();
await dialog.waitFor({
state: 'hidden'
});
@@ -27,30 +27,28 @@ export async function enterCreditCard(page: Page) {
export async function createProProject(page: Page): Promise<Metadata> {
const organizationId = await test.step('create organization', async () => {
await page.goto('./');
await page.waitForURL('./onboarding');
await page.goto('./create-organization');
await page.locator('id=name').fill('test org');
await page.locator('id=plan').selectOption('tier-1');
await page.getByRole('button', { name: 'get started' }).click();
await page.waitForURL('./create-organization**');
await page.getByRole('radio', { name: /^Pro\b/ }).check();
// `create organization` because there's already free created on start!
await page.getByRole('button', { name: 'create organization' }).click();
await page.getByRole('button', { name: 'add' }).first().click();
await enterCreditCard(page);
// skip members
await page.getByRole('button', { name: 'create organization' }).click();
await page.waitForURL('./organization-**');
await page.waitForURL(/\/organization-[^/]+/);
return getOrganizationIdFromUrl(page.url());
});
const projectId = await test.step('create project', async () => {
await page.waitForURL('./organization-**');
await page.waitForURL(/\/organization-[^/]+/);
await page.getByRole('button', { name: 'create project' }).first().click();
await page.getByPlaceholder('project name').fill('test project');
await page.getByRole('button', { name: 'next' }).click();
await page.locator('label').filter({ hasText: 'frankfurt' }).click();
await page.getByRole('button', { name: 'create' }).click();
await page.waitForURL('./project-**/overview/platforms');
expect(page.url()).toContain('/project-');
const dialog = page.locator('dialog[open]');
await dialog.getByPlaceholder('Project name').fill('test project');
await dialog.getByRole('button', { name: 'create' }).click();
await page.waitForURL(/\/project-fra-[^/]+/);
expect(page.url()).toContain('/console/project-fra-');
return getProjectIdFromUrl(page.url());
});
+5 -4
View File
@@ -21,18 +21,18 @@
"e2e:ui": "playwright test --ui"
},
"dependencies": {
"@appwrite.io/console": "https://pkg.pr.new/appwrite/appwrite/@appwrite.io/console@e2f082e",
"@ai-sdk/svelte": "^1.1.24",
"@appwrite.io/console": "https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@e190a19",
"@appwrite.io/pink": "0.25.0",
"@appwrite.io/pink-icons": "0.25.0",
"@appwrite.io/pink-icons-svelte": "https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@bd21ff7f",
"@appwrite.io/pink-icons-svelte": "^2.0.0-RC.1",
"@appwrite.io/pink-legacy": "^1.0.3",
"@appwrite.io/pink-svelte": "https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@cbf05a1",
"@appwrite.io/pink-svelte": "https://try-module.cloud/module/@appwrite/@appwrite.io/pink-svelte@90cb757",
"@popperjs/core": "^2.11.8",
"@sentry/sveltekit": "^8.38.0",
"@stripe/stripe-js": "^3.5.0",
"ai": "^2.2.37",
"analytics": "^0.8.16",
"@ai-sdk/svelte": "^1.1.22",
"cron-parser": "^4.9.0",
"dayjs": "^1.11.13",
"deep-equal": "^2.2.3",
@@ -79,6 +79,7 @@
"svelte-check": "^4.1.5",
"svelte-preprocess": "^6.0.3",
"svelte-sequential-preprocessor": "^2.0.2",
"tldts": "^7.0.7",
"tslib": "^2.8.1",
"typescript": "^5.8.2",
"typescript-eslint": "^8.30.1",
+1
View File
@@ -15,6 +15,7 @@ const config: PlaywrightTestConfig = {
env: {
PUBLIC_APPWRITE_ENDPOINT: 'https://stage.cloud.appwrite.io/v1',
PUBLIC_CONSOLE_MODE: 'cloud',
PUBLIC_APPWRITE_MULTI_REGION: 'true',
PUBLIC_STRIPE_KEY:
'pk_test_51LT5nsGYD1ySxNCyd7b304wPD8Y1XKKWR6hqo6cu3GIRwgvcVNzoZv4vKt5DfYXL1gRGw4JOqE19afwkJYJq1g3K004eVfpdWn'
},
+65 -70
View File
@@ -9,11 +9,11 @@ importers:
.:
dependencies:
'@ai-sdk/svelte':
specifier: ^1.1.22
version: 1.1.22(svelte@5.25.3)(zod@3.24.3)
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/appwrite/@appwrite.io/console@e2f082e
version: https://pkg.pr.new/appwrite/appwrite/@appwrite.io/console@e2f082e
specifier: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@e190a19
version: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@e190a19
'@appwrite.io/pink':
specifier: 0.25.0
version: 0.25.0
@@ -21,14 +21,14 @@ importers:
specifier: 0.25.0
version: 0.25.0
'@appwrite.io/pink-icons-svelte':
specifier: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@bd21ff7f
version: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@bd21ff7f(svelte@5.25.3)
specifier: ^2.0.0-RC.1
version: https://try-module.cloud/module/@appwrite/%40appwrite.io%2Fpink-icons-svelte@12707b9(svelte@5.25.3)
'@appwrite.io/pink-legacy':
specifier: ^1.0.3
version: 1.0.3
'@appwrite.io/pink-svelte':
specifier: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@cbf05a1
version: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@cbf05a1(react-dom@18.3.1(react@18.3.1))(svelte@5.25.3)
specifier: https://try-module.cloud/module/@appwrite/@appwrite.io/pink-svelte@90cb757
version: https://try-module.cloud/module/@appwrite/%40appwrite.io%2Fpink-svelte@90cb757(svelte@5.25.3)
'@popperjs/core':
specifier: ^2.11.8
version: 2.11.8
@@ -177,6 +177,9 @@ importers:
svelte-sequential-preprocessor:
specifier: ^2.0.2
version: 2.0.2
tldts:
specifier: ^7.0.7
version: 7.0.7
tslib:
specifier: ^2.8.1
version: 2.8.1
@@ -198,8 +201,8 @@ packages:
'@adobe/css-tools@4.4.2':
resolution: {integrity: sha512-baYZExFpsdkBNuvGKTKWCwKH57HRZLVtycZS05WTQNVOiXVSeAki3nU35zlRbToeMW8aHlJfyS+1C4BOv27q0A==}
'@ai-sdk/provider-utils@2.1.11':
resolution: {integrity: sha512-lMnXA5KaRJidzW7gQmlo/SnX6D+AKk5GxHFcQtOaGOSJNmu/qcNZc1rGaO7K5qW52OvCLXtnWudR4cc/FvMpVQ==}
'@ai-sdk/provider-utils@2.1.13':
resolution: {integrity: sha512-kLjqsfOdONr6DGcGEntFYM1niXz1H05vyZNf9OAzK+KKKc64izyP4/q/9HX7W4+6g8hm6BnmKxu8vkr6FSOqDg==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.0.0
@@ -207,12 +210,12 @@ packages:
zod:
optional: true
'@ai-sdk/provider@1.0.10':
resolution: {integrity: sha512-pco8Zl9U0xwXI+nCLc0woMtxbvjU8hRmGTseAUiPHFLYAAL8trRPCukg69IDeinOvIeo1SmXxAIdWWPZOLb4Cg==}
'@ai-sdk/provider@1.0.11':
resolution: {integrity: sha512-CPyImHGiT3svyfmvPvAFTianZzWFtm0qK82XjwlQIA1C3IQ2iku/PMQXi7aFyrX0TyMh3VTkJPB03tjU2VXVrw==}
engines: {node: '>=18'}
'@ai-sdk/svelte@1.1.22':
resolution: {integrity: sha512-xtJjvEPHD8GDB1iODvsRLKplZ9CVUN/gT4U+nttuEvjV42qoPXKkrWwjTLEdWKnwTnoHuuI8mnQMje6RhFWtpA==}
'@ai-sdk/svelte@1.1.24':
resolution: {integrity: sha512-nlSSd4FirQyM10MQb9vCzF3e/R4id0od0/cKtB1JdtcIvT4ZaJzyltIK3Q72ceOjEbHXSGbucW6gVyExBzHCLQ==}
engines: {node: '>=18'}
peerDependencies:
svelte: ^3.0.0 || ^4.0.0 || ^5.0.0
@@ -220,8 +223,8 @@ packages:
svelte:
optional: true
'@ai-sdk/ui-utils@1.1.17':
resolution: {integrity: sha512-fCnp/wntZGqPf6tiCmhuQoSDLSBhXoI5DU2JX4As96EO870+jliE6ozvYUwYOZC6Ta2OKAjjWPcSP7HeHX0b+g==}
'@ai-sdk/ui-utils@1.1.19':
resolution: {integrity: sha512-rDHy2uxlPMt3jjS9L6mBrsfhEInZ5BVoWevmD13fsAt2s/XWy2OwwKmgmUQkdLlY4mn/eyeYAfDGK8+5CbOAgg==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.0.0
@@ -254,19 +257,18 @@ packages:
'@analytics/type-utils@0.6.2':
resolution: {integrity: sha512-TD+xbmsBLyYy/IxFimW/YL/9L2IEnM7/EoV9Aeh56U64Ify8o27HJcKjo38XY9Tcn0uOq1AX3thkKgvtWvwFQg==}
'@appwrite.io/console@https://pkg.pr.new/appwrite/appwrite/@appwrite.io/console@e2f082e':
resolution: {tarball: https://pkg.pr.new/appwrite/appwrite/@appwrite.io/console@e2f082e}
version: 1.2.1
'@appwrite.io/console@https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@e190a19':
resolution: {tarball: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@e190a19}
version: 1.8.0
'@appwrite.io/pink-icons-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@bd21ff7f':
resolution: {tarball: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@bd21ff7f}
version: 1.0.0-next.7
'@appwrite.io/pink-icons-svelte@2.0.0-RC.1':
resolution: {integrity: sha512-iLFlV55hj8mGuAbmxJGenxN5RaZMmVT4GJb9dv/MP1xBAtYibFq7JvBcxm18qV2KU8c31Rntf+Ub4GL7HwqTYg==}
peerDependencies:
svelte: ^4.0.0
'@appwrite.io/pink-icons-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@cbf05a106412315e451530f8384924da515b10c8':
resolution: {tarball: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@cbf05a106412315e451530f8384924da515b10c8}
version: 1.0.0-next.7
'@appwrite.io/pink-icons-svelte@https://try-module.cloud/module/@appwrite/%40appwrite.io%2Fpink-icons-svelte@12707b9':
resolution: {tarball: https://try-module.cloud/module/@appwrite/%40appwrite.io%2Fpink-icons-svelte@12707b9}
version: 2.0.0-RC.1
peerDependencies:
svelte: ^4.0.0
@@ -279,11 +281,10 @@ packages:
'@appwrite.io/pink-legacy@1.0.3':
resolution: {integrity: sha512-GGde5fmPhs+s6/3aFeMPc/kKADG/gTFkYQSy6oBN8pK0y0XNCLrZZgBv+EBbdhwdtqVEWXa0X85Mv9w7jcIlwQ==}
'@appwrite.io/pink-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@cbf05a1':
resolution: {tarball: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@cbf05a1}
version: 1.0.0-next.85
'@appwrite.io/pink-svelte@https://try-module.cloud/module/@appwrite/%40appwrite.io%2Fpink-svelte@90cb757':
resolution: {tarball: https://try-module.cloud/module/@appwrite/%40appwrite.io%2Fpink-svelte@90cb757}
version: 2.0.0-RC.2
peerDependencies:
react-dom: ^18.0.0
svelte: ^4.0.0
'@appwrite.io/pink@0.25.0':
@@ -1342,8 +1343,8 @@ packages:
'@types/prop-types@15.7.14':
resolution: {integrity: sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==}
'@types/react@18.3.20':
resolution: {integrity: sha512-IPaCZN7PShZK/3t6Q87pfTkRm6oLTd4vztyoj+cbHUF1g3FfVb2tFIL79uCRKEfv16AhqDMBywP2VW3KIZUvcg==}
'@types/react@18.3.22':
resolution: {integrity: sha512-vUhG0YmQZ7kL/tmKLrD3g5zXbXXreZXB3pmROW8bg3CnLnpjkRVwUlLne7Ufa2r9yJ8+/6B73RzhAek5TBKh2Q==}
'@types/remarkable@2.0.8':
resolution: {integrity: sha512-eKXqPZfpQl1kOADjdKchHrp2gwn9qMnGXhH/AtZe0UrklzhGJkawJo/Y/D0AlWcdWoWamFNIum8+/nkAISQVGg==}
@@ -2854,8 +2855,8 @@ packages:
resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==}
engines: {node: '>=0.4.0'}
property-information@7.0.0:
resolution: {integrity: sha512-7D/qOz/+Y4X/rzSB6jKxKUsQnphO046ei8qxG59mtM3RG3DHgTK81HrxrmoDVINJb8NKT5ZsRbwHvQ6B68Iyhg==}
property-information@7.1.0:
resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==}
proxy-from-env@1.1.0:
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
@@ -2867,11 +2868,6 @@ packages:
queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
react-dom@18.3.1:
resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==}
peerDependencies:
react: ^18.3.1
react-is@17.0.2:
resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==}
@@ -2970,9 +2966,6 @@ packages:
resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==}
engines: {node: '>=v12.22.7'}
scheduler@0.23.2:
resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==}
secure-json-parse@2.7.0:
resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==}
@@ -3249,10 +3242,17 @@ packages:
tldts-core@6.1.85:
resolution: {integrity: sha512-DTjUVvxckL1fIoPSb3KE7ISNtkWSawZdpfxGxwiIrZoO6EbHVDXXUIlIuWympPaeS+BLGyggozX/HTMsRAdsoA==}
tldts-core@7.0.7:
resolution: {integrity: sha512-ECqb8imSroX1UmUuhRBNPkkmtZ8mHEenieim80UVxG0M5wXVjY2Fp2tYXCPvk+nLy1geOhFpeD5YQhM/gF63Jg==}
tldts@6.1.85:
resolution: {integrity: sha512-gBdZ1RjCSevRPFix/hpaUWeak2/RNUZB4/8frF1r5uYMHjFptkiT0JXIebWvgI/0ZHXvxaUDDJshiA0j6GdL3w==}
hasBin: true
tldts@7.0.7:
resolution: {integrity: sha512-ETNXj36ql5BXDa4VVJk3wgqansg8TI1Yqo217twSAPjyDnh/b2T+XzrI0ftn6EnzVPbXpMTZHOWj5s3a8/uGPA==}
hasBin: true
to-regex-range@5.0.1:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'}
@@ -3556,32 +3556,32 @@ snapshots:
'@adobe/css-tools@4.4.2': {}
'@ai-sdk/provider-utils@2.1.11(zod@3.24.3)':
'@ai-sdk/provider-utils@2.1.13(zod@3.24.3)':
dependencies:
'@ai-sdk/provider': 1.0.10
'@ai-sdk/provider': 1.0.11
eventsource-parser: 3.0.0
nanoid: 3.3.11
secure-json-parse: 2.7.0
optionalDependencies:
zod: 3.24.3
'@ai-sdk/provider@1.0.10':
'@ai-sdk/provider@1.0.11':
dependencies:
json-schema: 0.4.0
'@ai-sdk/svelte@1.1.22(svelte@5.25.3)(zod@3.24.3)':
'@ai-sdk/svelte@1.1.24(svelte@5.25.3)(zod@3.24.3)':
dependencies:
'@ai-sdk/provider-utils': 2.1.11(zod@3.24.3)
'@ai-sdk/ui-utils': 1.1.17(zod@3.24.3)
'@ai-sdk/provider-utils': 2.1.13(zod@3.24.3)
'@ai-sdk/ui-utils': 1.1.19(zod@3.24.3)
optionalDependencies:
svelte: 5.25.3
transitivePeerDependencies:
- zod
'@ai-sdk/ui-utils@1.1.17(zod@3.24.3)':
'@ai-sdk/ui-utils@1.1.19(zod@3.24.3)':
dependencies:
'@ai-sdk/provider': 1.0.10
'@ai-sdk/provider-utils': 2.1.11(zod@3.24.3)
'@ai-sdk/provider': 1.0.11
'@ai-sdk/provider-utils': 2.1.13(zod@3.24.3)
zod-to-json-schema: 3.24.5(zod@3.24.3)
optionalDependencies:
zod: 3.24.3
@@ -3625,13 +3625,13 @@ snapshots:
'@analytics/type-utils@0.6.2': {}
'@appwrite.io/console@https://pkg.pr.new/appwrite/appwrite/@appwrite.io/console@e2f082e': {}
'@appwrite.io/console@https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@e190a19': {}
'@appwrite.io/pink-icons-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@bd21ff7f(svelte@5.25.3)':
'@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.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@cbf05a106412315e451530f8384924da515b10c8(svelte@5.25.3)':
'@appwrite.io/pink-icons-svelte@https://try-module.cloud/module/@appwrite/%40appwrite.io%2Fpink-icons-svelte@12707b9(svelte@5.25.3)':
dependencies:
svelte: 5.25.3
@@ -3644,9 +3644,9 @@ snapshots:
'@appwrite.io/pink-icons': 1.0.0
the-new-css-reset: 1.11.3
'@appwrite.io/pink-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@cbf05a1(react-dom@18.3.1(react@18.3.1))(svelte@5.25.3)':
'@appwrite.io/pink-svelte@https://try-module.cloud/module/@appwrite/%40appwrite.io%2Fpink-svelte@90cb757(svelte@5.25.3)':
dependencies:
'@appwrite.io/pink-icons-svelte': https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@cbf05a106412315e451530f8384924da515b10c8(svelte@5.25.3)
'@appwrite.io/pink-icons-svelte': 2.0.0-RC.1(svelte@5.25.3)
'@floating-ui/dom': 1.6.13
'@melt-ui/pp': 0.3.2(@melt-ui/svelte@0.86.6(svelte@5.25.3))(svelte@5.25.3)
'@melt-ui/svelte': 0.86.6(svelte@5.25.3)
@@ -3654,7 +3654,6 @@ snapshots:
d3: 7.9.0
fuse.js: 7.1.0
pretty-bytes: 6.1.1
react-dom: 18.3.1(react@18.3.1)
shiki: 1.29.2
svelte: 5.25.3
svelte-motion: 0.12.2(svelte@5.25.3)
@@ -4788,7 +4787,7 @@ snapshots:
'@types/prop-types@15.7.14': {}
'@types/react@18.3.20':
'@types/react@18.3.22':
dependencies:
'@types/prop-types': 15.7.14
csstype: 3.1.3
@@ -5890,7 +5889,7 @@ snapshots:
hast-util-whitespace: 3.0.0
html-void-elements: 3.0.0
mdast-util-to-hast: 13.2.0
property-information: 7.0.0
property-information: 7.1.0
space-separated-tokens: 2.0.2
stringify-entities: 4.0.4
zwitch: 2.0.4
@@ -6423,7 +6422,7 @@ snapshots:
progress@2.0.3: {}
property-information@7.0.0: {}
property-information@7.1.0: {}
proxy-from-env@1.1.0: {}
@@ -6431,12 +6430,6 @@ snapshots:
queue-microtask@1.2.3: {}
react-dom@18.3.1(react@18.3.1):
dependencies:
loose-envify: 1.4.0
react: 18.3.1
scheduler: 0.23.2
react-is@17.0.2: {}
react@18.3.1:
@@ -6567,10 +6560,6 @@ snapshots:
dependencies:
xmlchars: 2.2.0
scheduler@0.23.2:
dependencies:
loose-envify: 1.4.0
secure-json-parse@2.7.0: {}
semver@6.3.1: {}
@@ -6746,7 +6735,7 @@ snapshots:
svelte-motion@0.12.2(svelte@5.25.3):
dependencies:
'@types/react': 18.3.20
'@types/react': 18.3.22
framesync: 6.1.2
popmotion: 11.0.5
style-value-types: 5.1.2
@@ -6855,10 +6844,16 @@ snapshots:
tldts-core@6.1.85: {}
tldts-core@7.0.7: {}
tldts@6.1.85:
dependencies:
tldts-core: 6.1.85
tldts@7.0.7:
dependencies:
tldts-core: 7.0.7
to-regex-range@5.0.1:
dependencies:
is-number: 7.0.0
+12 -14
View File
@@ -1,7 +1,7 @@
import * as Sentry from '@sentry/sveltekit';
import { isCloud, isProd } from '$lib/system';
import { AppwriteException } from '@appwrite.io/console';
import type { HandleClientError } from '@sveltejs/kit';
import { isCloud, isProd } from '$lib/system';
Sentry.init({
enabled: isCloud && isProd,
@@ -11,17 +11,15 @@ Sentry.init({
replaysOnErrorSampleRate: 0
});
export const handleError: HandleClientError = Sentry.handleErrorWithSentry(
async ({ error, message, status }) => {
console.error(error);
if (error instanceof AppwriteException) {
status = error.code === 0 ? undefined : error.code;
message = error.message;
}
return {
message,
status
};
export const handleError: HandleClientError = ({ error, message, status }) => {
console.error(error);
if (error instanceof AppwriteException) {
status = error.code === 0 ? undefined : error.code;
message = error.message;
}
);
return {
message,
status
};
};
+13
View File
@@ -152,6 +152,7 @@ export enum Click {
DatabaseIndexDelete = 'click_index_delete',
DatabaseCollectionDelete = 'click_collection_delete',
DatabaseDatabaseDelete = 'click_database_delete',
DatabaseImportCsv = 'click_database_import_csv',
DomainCreateClick = 'click_domain_create',
DomainDeleteClick = 'click_domain_delete',
DomainRetryDomainVerificationClick = 'click_domain_retry_domain_verification',
@@ -161,6 +162,7 @@ export enum Click {
FunctionsDeploymentDeleteClick = 'click_deployment_delete',
FunctionsDeploymentCancelClick = 'click_deployment_cancel',
KeyCreateClick = 'click_key_create',
DevKeyCreateClick = 'click_dev_key_create',
MenuDropDownClick = 'click_menu_dropdown',
MenuOverviewClick = 'click_menu_overview',
ModalCloseClick = 'click_close_modal',
@@ -270,6 +272,7 @@ export enum Submit {
DatabaseCreate = 'submit_database_create',
DatabaseDelete = 'submit_database_delete',
DatabaseUpdateName = 'submit_database_update_name',
DatabaseImportCsv = 'submit_database_import_csv',
AttributeCreate = 'submit_attribute_create',
AttributeUpdate = 'submit_attribute_update',
AttributeDelete = 'submit_attribute_delete',
@@ -311,12 +314,19 @@ export enum Submit {
VariableDelete = 'submit_variable_delete',
VariableUpdate = 'submit_variable_update',
VariableEditor = 'submit_variable_editor',
LogDelete = 'submit_log_delete',
KeyCreate = 'submit_key_create',
KeyDelete = 'submit_key_delete',
KeyUpdateName = 'submit_key_update_name',
KeyUpdateScopes = 'submit_key_update_scopes',
KeyUpdateExpire = 'submit_key_update_expire',
DevKeyCreate = 'submit_dev_key_create',
DevKeyDelete = 'submit_dev_key_delete',
DevKeyUpdateName = 'submit_dev_key_update_name',
DevKeyUpdateExpire = 'submit_dev_key_update_expire',
PlatformCreate = 'submit_platform_create',
PlatformDelete = 'submit_platform_delete',
PlatformUpdate = 'submit_platform_update',
@@ -346,6 +356,9 @@ export enum Submit {
FileCreate = 'submit_file_create',
FileDelete = 'submit_file_delete',
FileUpdatePermissions = 'submit_file_update_permissions',
FileTokenCreate = 'submit_file_token',
FileTokenDelete = 'submit_file_delete',
FileTokenUpdate = 'submit_file_update_expiry',
BudgetCapUpdate = 'submit_budget_cap_update',
BudgetAlertsUpdate = 'submit_budget_alert_conditions_update',
CreditRedeem = 'submit_credit_redeem',
@@ -1,6 +1,6 @@
<script lang="ts">
import { initCreateAttribute } from '$routes/(console)/project-[project]/databases/database-[database]/collection-[collection]/+layout.svelte';
import { attributeOptions } from '$routes/(console)/project-[project]/databases/database-[database]/collection-[collection]/attributes/store';
import { initCreateAttribute } from '$routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/+layout.svelte';
import { attributeOptions } from '$routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/attributes/store';
import Template from './template.svelte';
let search = '';
@@ -1,15 +1,15 @@
<script lang="ts">
import { providers } from '$routes/(console)/project-[project]/messaging/providers/store';
import { providers } from '$routes/(console)/project-[region]-[project]/messaging/providers/store';
import {
messageParams,
providerType,
targetsById
} from '$routes/(console)/project-[project]/messaging/wizard/store';
} from '$routes/(console)/project-[region]-[project]/messaging/wizard/store';
import { MessagingProviderType } from '@appwrite.io/console';
import Template from './template.svelte';
import { wizard } from '$lib/stores/wizard';
import Create from '$routes/(console)/project-[project]/messaging/create.svelte';
import { topicsById } from '$routes/(console)/project-[project]/messaging/store';
import Create from '$routes/(console)/project-[region]-[project]/messaging/create.svelte';
import { topicsById } from '$routes/(console)/project-[region]-[project]/messaging/store';
let search = '';
@@ -2,7 +2,7 @@
import {
Platform,
addPlatform
} from '$routes/(console)/project-[project]/overview/platforms/+page.svelte';
} from '$routes/(console)/project-[region]-[project]/overview/platforms/+page.svelte';
import Template from './template.svelte';
import { IconAndroid, IconApple, IconCode, IconFlutter } from '@appwrite.io/pink-icons-svelte';
+17 -11
View File
@@ -1,6 +1,6 @@
import { goto } from '$app/navigation';
import { sdk } from '$lib/stores/sdk';
import { project } from '$routes/(console)/project-[project]/store';
import { project } from '$routes/(console)/project-[region]-[project]/store';
import { Query, type Models } from '@appwrite.io/console';
import { get } from 'svelte/store';
import type { Command, Searcher } from '../commands';
@@ -14,12 +14,13 @@ import {
IconPuzzle,
IconSearch
} from '@appwrite.io/pink-icons-svelte';
import { page } from '$app/state';
const getBucketCommand = (bucket: Models.Bucket, projectId: string) => {
const getBucketCommand = (bucket: Models.Bucket, region: string, projectId: string) => {
return {
label: `${bucket.name}`,
callback() {
goto(`${base}/project-${projectId}/storage/bucket-${bucket.$id}`);
goto(`${base}/project-${region}-${projectId}/storage/bucket-${bucket.$id}`);
},
group: 'buckets',
icon: IconFolder
@@ -27,19 +28,24 @@ const getBucketCommand = (bucket: Models.Bucket, projectId: string) => {
};
export const bucketSearcher = (async (query: string) => {
const { buckets } = await sdk.forProject.storage.listBuckets([Query.orderDesc('$createdAt')]);
const $project = get(project);
const region = page.params.region;
const { buckets } = await sdk
.forProject(page.params.region, page.params.project)
.storage.listBuckets([Query.orderDesc('$createdAt')]);
const filtered = buckets.filter((bucket) => bucket.name.includes(query));
if (filtered.length === 1) {
const bucket = filtered[0];
return [
getBucketCommand(bucket, $project.$id),
getBucketCommand(bucket, region, $project.$id),
{
label: 'Find files',
async callback() {
await goto(`${base}/project-${$project.$id}/storage/bucket-${bucket.$id}`);
await goto(
`${base}/project-${$project.region}-${$project.$id}/storage/bucket-${bucket.$id}`
);
addSubPanel(FilesPanel);
},
group: 'buckets',
@@ -51,7 +57,7 @@ export const bucketSearcher = (async (query: string) => {
label: 'Permissions',
async callback() {
await goto(
`${base}/project-${$project.$id}/storage/bucket-${bucket.$id}/settings#permissions`
`${base}/project-${$project.region}-${$project.$id}/storage/bucket-${bucket.$id}/settings#permissions`
);
scrollBy({ top: -100 });
},
@@ -63,7 +69,7 @@ export const bucketSearcher = (async (query: string) => {
label: 'Extensions',
async callback() {
await goto(
`${base}/project-${$project.$id}/storage/bucket-${bucket.$id}/settings#extensions`
`${base}/project-${$project.region}-${$project.$id}/storage/bucket-${bucket.$id}/settings#extensions`
);
},
group: 'buckets',
@@ -74,7 +80,7 @@ export const bucketSearcher = (async (query: string) => {
label: 'File Security',
async callback() {
await goto(
`${base}/project-${$project.$id}/storage/bucket-${bucket.$id}/settings#file-security`
`${base}/project-${$project.region}-${$project.$id}/storage/bucket-${bucket.$id}/settings#file-security`
);
scrollBy({ top: -100 });
},
@@ -85,5 +91,5 @@ export const bucketSearcher = (async (query: string) => {
];
}
return filtered.map((bucket) => getBucketCommand(bucket, $project.$id));
return filtered.map((bucket) => getBucketCommand(bucket, $project.region, $project.$id));
}) satisfies Searcher;
@@ -1,16 +1,17 @@
import { goto } from '$app/navigation';
import { database } from '$routes/(console)/project-[project]/databases/database-[database]/store';
import { project } from '$routes/(console)/project-[project]/store';
import { database } from '$routes/(console)/project-[region]-[project]/databases/database-[database]/store';
import { get } from 'svelte/store';
import type { Searcher } from '../commands';
import { sdk } from '$lib/stores/sdk';
import { base } from '$app/paths';
import { page } from '$app/state';
export const collectionsSearcher = (async (query: string) => {
const databaseId = get(database).$id;
const { collections } = await sdk.forProject.databases.listCollections(databaseId);
const { collections } = await sdk
.forProject(page.params.region, page.params.project)
.databases.listCollections(databaseId);
const projectId = get(project).$id;
return collections
.filter((col) => col.name.toLowerCase().includes(query.toLowerCase()))
.map(
@@ -20,7 +21,7 @@ export const collectionsSearcher = (async (query: string) => {
label: col.name,
callback: () => {
goto(
`${base}/project-${projectId}/databases/database-${databaseId}/collection-${col.$id}`
`${base}/project-${page.params.region}-${page.params.project}/databases/database-${databaseId}/collection-${col.$id}`
);
}
}) as const
+7 -4
View File
@@ -1,13 +1,14 @@
import { goto } from '$app/navigation';
import { project } from '$routes/(console)/project-[project]/store';
import { get } from 'svelte/store';
import type { Searcher } from '../commands';
import { sdk } from '$lib/stores/sdk';
import { base } from '$app/paths';
import { IconDatabase } from '@appwrite.io/pink-icons-svelte';
import { page } from '$app/state';
export const dbSearcher = (async (query: string) => {
const { databases } = await sdk.forProject.databases.list();
const { databases } = await sdk
.forProject(page.params.region, page.params.project)
.databases.list();
return databases
.filter((db) => db.name.toLowerCase().includes(query.toLowerCase()))
@@ -17,7 +18,9 @@ export const dbSearcher = (async (query: string) => {
group: 'databases',
label: db.name,
callback: () => {
goto(`${base}/project-${get(project).$id}/databases/database-${db.$id}`);
goto(
`${base}/project-${page.params.region}-${page.params.project}/databases/database-${db.$id}`
);
},
icon: IconDatabase
}) as const
+9 -8
View File
@@ -1,27 +1,28 @@
import { sdk } from '$lib/stores/sdk';
import { get } from 'svelte/store';
import type { Searcher } from '../commands';
import { bucket } from '$routes/(console)/project-[project]/storage/bucket-[bucket]/store';
import { bucket } from '$routes/(console)/project-[region]-[project]/storage/bucket-[bucket]/store';
import { Query } from '@appwrite.io/console';
import { goto } from '$app/navigation';
import { project } from '$routes/(console)/project-[project]/store';
import { project } from '$routes/(console)/project-[region]-[project]/store';
import { base } from '$app/paths';
import { IconDocument } from '@appwrite.io/pink-icons-svelte';
import { page } from '$app/state';
export const fileSearcher = (async (query: string) => {
const $bucket = get(bucket);
const $project = get(project);
const { files } = await sdk.forProject.storage.listFiles(
$bucket.$id,
[Query.orderDesc('')],
query || undefined
);
const { files } = await sdk
.forProject(page.params.region, page.params.project)
.storage.listFiles($bucket.$id, [Query.orderDesc('')], query || undefined);
return files.map((file) => ({
label: file.name,
callback: () => {
goto(`${base}/project-${$project.$id}/storage/bucket-${$bucket.$id}/file-${file.$id}`);
goto(
`${base}/project-${$project.region}-${$project.$id}/storage/bucket-${$bucket.$id}/file-${file.$id}`
);
},
icon: IconDocument,
group: 'files'
+26 -16
View File
@@ -1,19 +1,19 @@
import { goto } from '$app/navigation';
import { sdk } from '$lib/stores/sdk';
import { project } from '$routes/(console)/project-[project]/store';
import { project } from '$routes/(console)/project-[region]-[project]/store';
import { get } from 'svelte/store';
import type { Searcher } from '../commands';
import type { Models } from '@appwrite.io/console';
import { page } from '$app/stores';
import { showCreateDeployment } from '$routes/(console)/project-[project]/functions/function-[function]/store';
import { page } from '$app/state';
import { showCreateDeployment } from '$routes/(console)/project-[region]-[project]/functions/function-[function]/store';
import { base } from '$app/paths';
import { IconLightningBolt, IconPlus } from '@appwrite.io/pink-icons-svelte';
const getFunctionCommand = (fn: Models.Function, projectId: string) => {
const getFunctionCommand = (fn: Models.Function, region: string, projectId: string) => {
return {
label: fn.name,
callback: () => {
goto(`${base}/project-${projectId}/functions/function-${fn.$id}`);
goto(`${base}/project-${region}-${projectId}/functions/function-${fn.$id}`);
},
group: 'functions',
icon: IconLightningBolt
@@ -21,23 +21,25 @@ const getFunctionCommand = (fn: Models.Function, projectId: string) => {
};
export const functionsSearcher = (async (query: string) => {
const { functions } = await sdk.forProject.functions.list();
const projectId = get(project).$id;
const { functions } = await sdk
.forProject(page.params.region, page.params.project)
.functions.list();
const filtered = functions.filter((fn) => fn.name.toLowerCase().includes(query.toLowerCase()));
if (filtered.length === 1) {
const func = filtered[0];
return [
getFunctionCommand(func, projectId),
getFunctionCommand(func, page.params.region, projectId),
{
label: 'Create deployment',
nested: true,
async callback() {
const $page = get(page);
if (!$page.url.pathname.endsWith(func.$id)) {
await goto(`${base}/project-${projectId}/functions/function-${func.$id}`);
if (!page.url.pathname.endsWith(func.$id)) {
await goto(
`${base}/project-${page.params.region}-${projectId}/functions/function-${func.$id}`
);
}
showCreateDeployment.set(true);
},
@@ -48,7 +50,9 @@ export const functionsSearcher = (async (query: string) => {
label: 'Go to deployments',
nested: true,
callback() {
goto(`${base}/project-${projectId}/functions/function-${func.$id}`);
goto(
`${base}/project-${page.params.region}-${projectId}/functions/function-${func.$id}`
);
},
group: 'functions'
},
@@ -56,7 +60,9 @@ export const functionsSearcher = (async (query: string) => {
label: 'Go to usage',
nested: true,
callback() {
goto(`${base}/project-${projectId}/functions/function-${func.$id}/usage`);
goto(
`${base}/project-${page.params.region}-${projectId}/functions/function-${func.$id}/usage`
);
},
group: 'functions'
},
@@ -64,7 +70,9 @@ export const functionsSearcher = (async (query: string) => {
label: 'Go to executions',
nested: true,
callback() {
goto(`${base}/project-${projectId}/functions/function-${func.$id}/executions`);
goto(
`${base}/project-${page.params.region}-${projectId}/functions/function-${func.$id}/executions`
);
},
group: 'functions'
},
@@ -72,12 +80,14 @@ export const functionsSearcher = (async (query: string) => {
label: 'Go to settings',
nested: true,
callback() {
goto(`${base}/project-${projectId}/functions/function-${func.$id}/settings`);
goto(
`${base}/project-${page.params.region}-${projectId}/functions/function-${func.$id}/settings`
);
},
group: 'functions'
}
];
}
return filtered.map((fn) => getFunctionCommand(fn, projectId));
return filtered.map((fn) => getFunctionCommand(fn, page.params.region, projectId));
}) satisfies Searcher;
+7 -6
View File
@@ -1,11 +1,10 @@
import { goto } from '$app/navigation';
import { project } from '$routes/(console)/project-[project]/store';
import { get } from 'svelte/store';
import { type Searcher } from '../commands';
import { sdk } from '$lib/stores/sdk';
import { MessagingProviderType, type Models } from '@appwrite.io/console';
import { base } from '$app/paths';
import { IconAnnotation, IconDeviceMobile, IconMail } from '@appwrite.io/pink-icons-svelte';
import { page } from '$app/state';
const getLabel = (message: Models.Message) => {
switch (message.providerType) {
@@ -34,9 +33,9 @@ const getIcon = (message: Models.Message) => {
};
export const messagesSearcher = (async (query: string) => {
const { messages } = await sdk.forProject.messaging.listMessages([], query || undefined);
const projectId = get(project).$id;
const { messages } = await sdk
.forProject(page.params.region, page.params.project)
.messaging.listMessages([], query || undefined);
return messages
.filter((message) => getLabel(message).toLowerCase().includes(query.toLowerCase()))
@@ -46,7 +45,9 @@ export const messagesSearcher = (async (query: string) => {
group: 'messages',
label: getLabel(message),
callback: () => {
goto(`${base}/project-${projectId}/messaging/message-${message.$id}`);
goto(
`${base}/project-${page.params.region}-${page.params.project}/messaging/message-${message.$id}`
);
},
icon: getIcon(message)
}) as const
+1 -1
View File
@@ -18,7 +18,7 @@ export const projectsSearcher = (async (query: string) => {
return {
label: project.name,
callback: () => {
goto(`${base}/project-${project.$id}`);
goto(`${base}/project-${project.region}-${project.$id}`);
},
group: 'projects'
} as const;
+6 -7
View File
@@ -1,10 +1,9 @@
import { goto } from '$app/navigation';
import { project } from '$routes/(console)/project-[project]/store';
import { get } from 'svelte/store';
import type { Searcher } from '../commands';
import { sdk } from '$lib/stores/sdk';
import { getProviderDisplayNameAndIcon } from '$routes/(console)/project-[project]/messaging/provider.svelte';
import { getProviderDisplayNameAndIcon } from '$routes/(console)/project-[region]-[project]/messaging/provider.svelte';
import { base } from '$app/paths';
import { page } from '$app/state';
const getIcon = (provider: string) => {
const { icon } = getProviderDisplayNameAndIcon(provider);
@@ -12,9 +11,9 @@ const getIcon = (provider: string) => {
};
export const providersSearcher = (async (query: string) => {
const { providers } = await sdk.forProject.messaging.listProviders([], query || undefined);
const projectId = get(project).$id;
const { providers } = await sdk
.forProject(page.params.region, page.params.project)
.messaging.listProviders([], query || undefined);
return providers
.filter((provider) => provider.name.toLowerCase().includes(query.toLowerCase()))
@@ -25,7 +24,7 @@ export const providersSearcher = (async (query: string) => {
label: provider.name,
callback: () => {
goto(
`${base}/project-${projectId}/messaging/providers/provider-${provider.$id}`
`${base}/project-${page.params.region}-${page.params.project}/messaging/providers/provider-${provider.$id}`
);
},
image: getIcon(provider.provider)
+14 -10
View File
@@ -1,33 +1,35 @@
import { goto } from '$app/navigation';
import { sdk } from '$lib/stores/sdk';
import { project } from '$routes/(console)/project-[project]/store';
import { get } from 'svelte/store';
import type { Command, Searcher } from '../commands';
import type { Models } from '@appwrite.io/console';
import { base } from '$app/paths';
import { IconUserCircle } from '@appwrite.io/pink-icons-svelte';
import { page } from '$app/state';
const getTeamCommand = (team: Models.Team<Models.Preferences>, projectId: string) =>
const getTeamCommand = (team: Models.Team<Models.Preferences>, region: string, projectId: string) =>
({
label: team.name,
callback: () => {
goto(`${base}/project-${projectId}/auth/teams/team-${team.$id}`);
goto(`${base}/project-${region}-${projectId}/auth/teams/team-${team.$id}`);
},
group: 'teams',
icon: IconUserCircle
}) satisfies Command;
export const teamSearcher = (async (query: string) => {
const { teams } = await sdk.forProject.teams.list([], query);
const projectId = get(project).$id;
const { teams } = await sdk
.forProject(page.params.region, page.params.project)
.teams.list([], query);
if (teams.length === 1) {
return [
getTeamCommand(teams[0], projectId),
getTeamCommand(teams[0], page.params.region, page.params.project),
{
label: 'Go to members',
callback: () => {
goto(`${base}/project-${projectId}/auth/teams/team-${teams[0].$id}/members`);
goto(
`${base}/project-${page.params.region}-${page.params.project}/auth/teams/team-${teams[0].$id}/members`
);
},
group: 'teams',
nested: true
@@ -36,12 +38,14 @@ export const teamSearcher = (async (query: string) => {
{
label: 'Go to activity',
callback: () => {
goto(`${base}/project-${projectId}/auth/teams/team-${teams[0].$id}/activity`);
goto(
`${base}/project-${page.params.region}-${page.params.project}/auth/teams/team-${teams[0].$id}/activity`
);
},
group: 'teams',
nested: true
}
];
}
return teams.map((team) => getTeamCommand(team, projectId));
return teams.map((team) => getTeamCommand(team, page.params.region, page.params.project));
}) satisfies Searcher;
+7 -6
View File
@@ -1,15 +1,14 @@
import { goto } from '$app/navigation';
import { project } from '$routes/(console)/project-[project]/store';
import { get } from 'svelte/store';
import type { Searcher } from '../commands';
import { sdk } from '$lib/stores/sdk';
import { base } from '$app/paths';
import { IconChevronRight } from '@appwrite.io/pink-icons-svelte';
import { page } from '$app/state';
export const topicsSearcher = (async (query: string) => {
const { topics } = await sdk.forProject.messaging.listTopics([], query || undefined);
const projectId = get(project).$id;
const { topics } = await sdk
.forProject(page.params.region, page.params.project)
.messaging.listTopics([], query || undefined);
return topics
.filter((topic) => topic.name.toLowerCase().includes(query.toLowerCase()))
@@ -19,7 +18,9 @@ export const topicsSearcher = (async (query: string) => {
group: 'topics',
label: topic.name,
callback: () => {
goto(`${base}/project-${projectId}/messaging/topics/topic-${topic.$id}`);
goto(
`${base}/project-${page.params.region}-${page.params.project}/messaging/topics/topic-${topic.$id}`
);
},
icon: IconChevronRight // TODO: @itznotabug - 'send' no replacement yet.
}) as const
+18 -12
View File
@@ -1,30 +1,30 @@
import { goto } from '$app/navigation';
import { sdk } from '$lib/stores/sdk';
import { project } from '$routes/(console)/project-[project]/store';
import { get } from 'svelte/store';
import type { Command, Searcher } from '../commands';
import type { Models } from '@appwrite.io/console';
import { promptDeleteUser } from '$routes/(console)/project-[project]/auth/user-[user]/dangerZone.svelte';
import { promptDeleteUser } from '$routes/(console)/project-[region]-[project]/auth/user-[user]/dangerZone.svelte';
import { base } from '$app/paths';
import { IconTrash, IconUserCircle } from '@appwrite.io/pink-icons-svelte';
import { page } from '$app/state';
const getUserCommand = (user: Models.User<Models.Preferences>, projectId: string) =>
const getUserCommand = (user: Models.User<Models.Preferences>, region: string, projectId: string) =>
({
label: user.name,
callback: () => {
goto(`${base}/project-${projectId}/auth/user-${user.$id}`);
goto(`${base}/project-${region}-${projectId}/auth/user-${user.$id}`);
},
group: 'users',
icon: IconUserCircle
}) satisfies Command;
export const userSearcher = (async (query: string) => {
const { users } = await sdk.forProject.users.list([], query || undefined);
const projectId = get(project).$id;
const { users } = await sdk
.forProject(page.params.region, page.params.project)
.users.list([], query || undefined);
if (users.length === 1) {
return [
getUserCommand(users[0], projectId),
getUserCommand(users[0], page.params.region, page.params.project),
{
label: 'Delete user',
callback: () => {
@@ -37,7 +37,9 @@ export const userSearcher = (async (query: string) => {
{
label: 'Go to activity',
callback: () => {
goto(`${base}/project-${projectId}/auth/user-${users[0].$id}/activity`);
goto(
`${base}/project-${page.params.region}-${page.params.project}/auth/user-${users[0].$id}/activity`
);
},
group: 'users',
nested: true
@@ -45,7 +47,9 @@ export const userSearcher = (async (query: string) => {
{
label: 'Go to sessions',
callback: () => {
goto(`${base}/project-${projectId}/auth/user-${users[0].$id}/sessions`);
goto(
`${base}/project-${page.params.region}-${page.params.project}/auth/user-${users[0].$id}/sessions`
);
},
group: 'users',
nested: true
@@ -53,12 +57,14 @@ export const userSearcher = (async (query: string) => {
{
label: 'Go to memberships',
callback: () => {
goto(`${base}/project-${projectId}/auth/user-${users[0].$id}/memberships`);
goto(
`${base}/project-${page.params.region}-${page.params.project}/auth/user-${users[0].$id}/memberships`
);
},
group: 'users',
nested: true
}
];
}
return users.map((user) => getUserCommand(user, projectId));
return users.map((user) => getUserCommand(user, page.params.region, page.params.project));
}) satisfies Searcher;
+7 -1
View File
@@ -1,6 +1,12 @@
<script lang="ts">
import { Avatar } from '@appwrite.io/pink-svelte';
import type { AvatarProps } from '@appwrite.io/pink-svelte/dist/avatar/Avatar.svelte';
type AvatarProps = Partial<{
src: string;
alt: string;
size: 'xs' | 's' | 'm' | 'l' | 'xl';
empty: boolean;
}>;
export let size: AvatarProps['size'] = 'm';
export let src: AvatarProps['src'] = undefined;
+16 -11
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import { sdk } from '$lib/stores/sdk';
import { realtime } from '$lib/stores/sdk';
import { type Payload } from '@appwrite.io/console';
import { onMount } from 'svelte';
import { isCloud, isSelfHosted } from '$lib/system';
@@ -34,6 +34,7 @@
function showRestoreNotification(newDatabaseId: string, newDatabaseName: string) {
if (newDatabaseId && newDatabaseName && lastDatabaseRestorationId !== newDatabaseId) {
const region = page.params.region;
const project = page.params.project;
lastDatabaseRestorationId = newDatabaseId;
@@ -45,7 +46,9 @@
{
name: 'View restored data',
method: () => {
goto(`${base}/project-${project}/databases/database-${newDatabaseId}`);
goto(
`${base}/project-${region}-${project}/databases/database-${newDatabaseId}`
);
}
}
]
@@ -123,16 +126,18 @@
// fast path: don't subscribe if org is on a free plan or is self-hosted.
if (isSelfHosted || (isCloud && $organization.billingPlan === BillingPlan.FREE)) return;
return sdk.forConsole.client.subscribe('console', (response) => {
if (!response.channels.includes(`projects.${getProjectId()}`)) return;
return realtime
.forProject(page.params.region, page.params.project)
.subscribe('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,12 +1,12 @@
<script lang="ts">
import { base } from '$app/paths';
import { page } from '$app/stores';
import { page } from '$app/state';
import { Button } from '$lib/elements/forms';
import { HeaderAlert } from '$lib/layout';
import { failedInvoice } from '$lib/stores/billing';
import { organization } from '$lib/stores/organization';
$: isOnProjects = $page.route.id.includes('project-[project]');
$: isOnProjects = page.route.id.includes('project-[region]-[project]');
</script>
{#if $failedInvoice && $failedInvoice.teamId === $organization.$id && isOnProjects}
+1 -2
View File
@@ -24,8 +24,7 @@
tooltipShow={plan.$id === BillingPlan.FREE && anyOrgFree}
tooltipText={plan.$id === BillingPlan.FREE
? 'You are limited to 1 Free organization per account.'
: ''}
padding={1.5}>
: ''}>
<svelte:fragment slot="custom" let:disabled>
<div
class="u-flex u-flex-vertical u-gap-4 u-width-full-line"
+1 -1
View File
@@ -45,7 +45,7 @@
$: isFree = org.billingPlan === BillingPlan.FREE;
// equal or above means unlimited!
$: getCorrectSeatsCountValue = (count: number): string | number => {
const getCorrectSeatsCountValue = (count: number): string | number => {
// php int max is always larger than js
const exceedsSafeLimit = count >= Number.MAX_SAFE_INTEGER;
return exceedsSafeLimit ? 'Unlimited' : count || 0;
+150 -54
View File
@@ -3,8 +3,9 @@
import { hideNotification, shouldShowNotification } from '$lib/helpers/notifications';
import { app } from '$lib/stores/app';
import {
type BottomModalAlertAction,
type BottomModalAlertItem,
bottomModalAlerts,
bottomModalAlertsConfig,
dismissBottomModalAlert,
hideAllModalAlerts
} from '$lib/stores/bottom-alerts';
@@ -13,15 +14,17 @@
import { BillingPlan } from '$lib/constants';
import { upgradeURL } from '$lib/stores/billing';
import { addBottomModalAlerts } from '$routes/(console)/bottomAlerts';
import { project } from '$routes/(console)/project-[project]/store';
import { project } from '$routes/(console)/project-[region]-[project]/store';
import { page } from '$app/state';
import { Click, trackEvent } from '$lib/actions/analytics';
import { goto } from '$app/navigation';
import { Typography } from '@appwrite.io/pink-svelte';
let currentIndex = 0;
let openModalOnMobile = false;
function getPageScope(pathname: string) {
const isProjectPage = pathname.includes('project-[project]');
const isProjectPage = pathname.includes('project-[region]-[project]');
const isOrganizationPage = pathname.includes('organization-[organization]');
return { isProjectPage, isOrganizationPage };
@@ -33,23 +36,27 @@
return alerts
.sort((a, b) => b.importance - a.importance)
.filter((alert) => {
return (
alert.show &&
shouldShowNotification(alert.id) &&
// if no scope > show in projects & org pages.
((!alert.scope && (isProjectPage || isOrganizationPage)) ||
// project scope, show only in project pages
(isProjectPage && alert.scope === 'project') ||
// organization scope, show only in organization pages
(isOrganizationPage && alert.scope === 'organization'))
);
if (!alert.show || !shouldShowNotification(alert.id)) return false;
switch (alert.scope) {
case 'everywhere':
return true;
case 'project':
return isProjectPage;
case 'organization':
return isOrganizationPage;
default:
return false;
}
});
}
$: filteredModalAlerts = filterModalAlerts($bottomModalAlerts, page.route.id);
$: filteredModalAlerts = filterModalAlerts($bottomModalAlertsConfig.alerts, page.route.id);
$: currentModalAlert = filteredModalAlerts[currentIndex] as BottomModalAlertItem;
$: hasOnlyPrimaryCta = typeof currentModalAlert?.learnMore === 'undefined';
function handleClose() {
filteredModalAlerts.forEach((alert) => {
const modalAlert = alert;
@@ -67,9 +74,76 @@
currentIndex = (currentIndex - 1 + filteredModalAlerts.length) % filteredModalAlerts.length;
}
function getMobileWindowConfig(): {
html: boolean;
cta: boolean;
title: string;
message: string;
} {
const config = $bottomModalAlertsConfig?.mobileSingleLayout;
const visibleAlerts = $bottomModalAlertsConfig.alerts.filter((a) => a.show);
const fallback = {
title: 'New features available',
message: 'Explore new features to enhance your projects and improve security.'
};
const shouldApplyConfig = config?.enabled === true && visibleAlerts.length === 1;
return {
cta: !!(shouldApplyConfig && config.cta),
html: !!(shouldApplyConfig && config.isHtml),
title: shouldApplyConfig && config.title ? config.title : fallback.title,
message: shouldApplyConfig && config.message ? config.message : fallback.message
};
}
function triggerMobileWindowLink() {
handleClose();
const url = $bottomModalAlertsConfig.mobileSingleLayout.cta.link({
organization: $organization,
project: $project
});
if ($bottomModalAlertsConfig.mobileSingleLayout.cta.external) {
window.open(url, '_blank');
} else {
goto(url);
}
}
// the button component cannot have both href and on:click!
function triggerWindowLink(alertAction: BottomModalAlertAction, event?: string) {
const shouldShowUpgrade = showUpgrade();
const url = shouldShowUpgrade
? $upgradeURL
: alertAction.link({
organization: $organization,
project: $project
});
if (!shouldShowUpgrade && alertAction.external) {
window.open(url, '_blank');
} else {
goto(url);
}
if (alertAction?.hideOnClick === true) {
// be careful of this one.
// once clicked, its gone!
handleClose();
}
trackEvent(Click.PromoClick, {
promo: currentModalAlert.id,
type: shouldShowUpgrade ? 'upgrade' : (event ?? `cta_click_${currentModalAlert.id}`)
});
}
function showUpgrade() {
const plan = currentModalAlert.plan;
const organizationPlan = $organization.billingPlan;
const organizationPlan = $organization?.billingPlan;
switch (plan) {
case 'free':
return false;
@@ -87,7 +161,7 @@
});
</script>
{#if filteredModalAlerts.length > 0 && currentModalAlert}
{#if filteredModalAlerts.length > 0 && currentModalAlert && !page.url.pathname.includes('console/onboarding')}
{@const shouldShowUpgrade = showUpgrade()}
<div class="main-alert-wrapper is-not-mobile">
<div class="alert-container">
@@ -150,7 +224,9 @@
{/if}
<div class="u-flex-vertical u-gap-4 u-padding-inline-8">
<h3 class="body-text-2 u-bold">{currentModalAlert.title}</h3>
<Typography.Text variant="m-500" color="--fgcolor-neutral-primary">
{currentModalAlert.title}
</Typography.Text>
<span class="u-width-fit-content">
{#if currentModalAlert.isHtml}
@@ -164,34 +240,23 @@
<div
class="buttons u-flex u-flex-vertical-mobile u-gap-4 u-padding-inline-8 u-padding-block-8">
<Button
secondary
class="button"
href={shouldShowUpgrade
? $upgradeURL
: currentModalAlert.cta.link({
organization: $organization,
project: $project
})}
external={!!currentModalAlert.cta.external}
fullWidthMobile
on:click={() => {
trackEvent(Click.PromoClick, {
promo: currentModalAlert.id,
type: shouldShowUpgrade ? 'upgrade' : 'try_now'
});
}}>
secondary={!hasOnlyPrimaryCta}
class={`${hasOnlyPrimaryCta ? 'only-primary-cta' : ''}`}
on:click={() => triggerWindowLink(currentModalAlert.cta)}>
{currentModalAlert.cta.text}
</Button>
{#if currentModalAlert.learnMore}
<!-- docs, learn-more, etc always external -->
<Button
text
class="button"
external
fullWidthMobile
href={currentModalAlert.learnMore.link({
organization: $organization,
project: $project
project: $project,
organization: $organization
})}>
{currentModalAlert.learnMore.text}
</Button>
@@ -266,7 +331,9 @@
{/if}
<div class="u-flex-vertical u-gap-8 u-padding-inline-8">
<h3 class="body-text-2 u-bold">{currentModalAlert.title}</h3>
<Typography.Text variant="m-500" color="--fgcolor-neutral-primary">
{currentModalAlert.title}
</Typography.Text>
<span class="u-width-fit-content">
{#if currentModalAlert.isHtml}
@@ -280,22 +347,12 @@
<div
class="buttons u-flex u-flex-vertical-mobile u-gap-4 u-padding-inline-8 u-padding-block-8">
<Button
secondary
secondary={!hasOnlyPrimaryCta}
class="button"
href={shouldShowUpgrade
? $upgradeURL
: currentModalAlert.cta.link({
organization: $organization,
project: $project
})}
external={!!currentModalAlert.cta.external}
fullWidthMobile
on:click={() => {
openModalOnMobile = false;
trackEvent(Click.PromoClick, {
promo: currentModalAlert.id,
type: shouldShowUpgrade ? 'upgrade' : 'try_now'
});
triggerWindowLink(currentModalAlert.cta);
}}>
{shouldShowUpgrade
? 'Upgrade plan'
@@ -303,6 +360,7 @@
</Button>
{#if currentModalAlert.learnMore}
<!-- docs, learn-more, etc always external -->
<Button
text
class="button"
@@ -310,8 +368,8 @@
fullWidthMobile
on:click={() => (openModalOnMobile = false)}
href={currentModalAlert.learnMore.link({
organization: $organization,
project: $project
project: $project,
organization: $organization
})}>
{currentModalAlert.learnMore.text}
</Button>
@@ -322,20 +380,41 @@
</article>
</div>
{:else}
<button
{@const mobileConfig = getMobileWindowConfig()}
<!-- we don't need keydown because we show this only on mobile -->
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
tabindex="0"
role="button"
class:showing={!openModalOnMobile}
class="card notification-card u-width-full-line"
on:click={() => (openModalOnMobile = true)}>
on:click={() => {
if (mobileConfig.cta) {
// navigate manually!
triggerMobileWindowLink();
} else {
openModalOnMobile = true;
}
}}>
<div class="u-flex-vertical u-gap-4">
<div class="u-flex u-cross-center u-main-space-between">
<h3 class="body-text-2 u-bold">New features available</h3>
<Typography.Text variant="m-500" color="--fgcolor-neutral-primary">
{currentModalAlert.title}
</Typography.Text>
<button on:click={hideAllModalAlerts} aria-label="Close">
<span class="icon-x"></span>
</button>
</div>
<span class="u-width-fit-content">
Explore new features to enhance your projects and improve security.
{#if mobileConfig.html}
{@html mobileConfig.message}
{:else}
{mobileConfig.message}
{/if}
</span>
</div>
</button>
</div>
{/if}
</div>
{/if}
@@ -364,6 +443,7 @@
top: 1rem;
right: 1rem;
cursor: pointer;
background: #fff;
position: absolute;
display: inline-flex;
@@ -377,6 +457,22 @@
border: hsl(var(--color-neutral-80)) solid 1px;
}
:global(.main-alert-wrapper .only-primary-cta) {
width: 100%;
text-align: center;
justify-content: center;
}
.showcase-image.u-only-light {
border-radius: 8px;
border: 0.795px solid var(--border-neutral-strong, #d8d8db);
}
.showcase-image.u-only-dark {
border-radius: 8px;
border: 0.795px solid var(--border-neutral-strong, #414146);
}
.u-gap-10 {
gap: 0.625rem;
}
+3 -4
View File
@@ -1,10 +1,9 @@
<script lang="ts">
import { Card } from '@appwrite.io/pink-svelte';
import type Base from '@appwrite.io/pink-svelte/dist/card/Base.svelte';
import type { ComponentProps } from 'svelte';
import type { BaseCardProps } from './card.svelte';
export let radius: ComponentProps<Base>['radius'] = 'm';
export let padding: ComponentProps<Base>['padding'] = 's';
export let radius: BaseCardProps['radius'] = 'm';
export let padding: BaseCardProps['padding'] = 's';
</script>
<Card.Base variant="secondary" {radius} {padding}>
+4 -2
View File
@@ -19,6 +19,7 @@
name: string;
$id: string;
isSelected: boolean;
region: string;
};
type Organization = {
name: string;
@@ -139,7 +140,7 @@
if (index < 4) {
return {
name: project.name,
href: `/console/project-${project.$id}/overview`
href: `/console/project-${project.region}-${project.$id}/overview`
};
} else if (index === 4) {
return {
@@ -315,7 +316,8 @@
{#if index < 4}
<div use:melt={$itemProjects}>
<ActionMenu.Root>
<ActionMenu.Item.Anchor href={`/console/project-${project.$id}`}
<ActionMenu.Item.Anchor
href={`/console/project-${project.region}-${project.$id}`}
>{project.name}</ActionMenu.Item.Anchor
></ActionMenu.Root>
</div>
+12 -3
View File
@@ -1,7 +1,16 @@
<script context="module" lang="ts">
export type BaseCardProps = Partial<{
variant: 'primary' | 'secondary';
radius: 's' | 'm' | 'l';
padding: 'none' | 'xxxs' | 'xxs' | 'xs' | 's' | 'm' | 'l';
border: 'solid' | 'dashed';
shadow?: boolean;
disabled?: boolean;
}>;
</script>
<script lang="ts">
import { Card, Layout } from '@appwrite.io/pink-svelte';
import type Base from '@appwrite.io/pink-svelte/dist/card/Base.svelte';
import type { ComponentProps } from 'svelte';
type BaseProps = {
isDashed?: boolean;
@@ -20,7 +29,7 @@
isButton?: never;
};
type $$Props = BaseProps & (ButtonProps | AnchorProps | BaseProps) & ComponentProps<Base>;
type $$Props = BaseProps & (ButtonProps | AnchorProps | BaseProps) & BaseCardProps;
export let isDashed = false;
export let isButton = false;
+2 -1
View File
@@ -9,8 +9,9 @@
export let event: string = null;
export let eventContext = 'click_id_tag';
export let tooltipDisabled = false;
export let copyText: string = 'Click to copy';
let content = 'Click to copy';
let content = copyText;
async function handleClick() {
const success = await copy(value);
@@ -9,7 +9,7 @@
function getCreditCardImage(brand: string, width = 46, height = 32) {
if (!isValueOfStringEnum(CreditCard, brand)) return '';
return sdk.forConsole.avatars.getCreditCard(brand, width, height).toString();
return sdk.forConsole.avatars.getCreditCard(brand, width, height);
}
</script>
+2 -2
View File
@@ -2,9 +2,9 @@
import type { PaymentMethodData } from '$lib/sdk/billing';
import { Badge, Layout, Link, Popover, Table } from '@appwrite.io/pink-svelte';
import CreditCardBrandImage from './creditCardBrandImage.svelte';
import type { RootProp } from '@appwrite.io/pink-svelte/dist/table';
import type { TableRootProp } from '$lib/helpers/types';
export let root: RootProp;
export let root: TableRootProp;
export let paymentMethod: PaymentMethodData;
export let isBackup: boolean = false;
</script>
+242
View File
@@ -0,0 +1,242 @@
<script lang="ts">
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 { goto, invalidate } from '$app/navigation';
import { getProjectId } from '$lib/helpers/project';
import { writable, type Writable } from 'svelte/store';
import { addNotification } from '$lib/stores/notifications';
import { Layout, Typography } from '@appwrite.io/pink-svelte';
import { type Models, type Payload, Query } from '@appwrite.io/console';
type ImportItem = {
status: string;
collection?: string;
};
type ImportItemsMap = Map<string, ImportItem>;
/**
* Keeps a track of the active and ongoing csv migrations.
*
* The structure is as follows -
* `{ migrationId: { status: status, collection: collection } }`
*/
const importItems: Writable<ImportItemsMap> = writable(new Map());
async function showCompletionNotification(
databaseId: string,
collectionId: string,
importData: Payload
) {
await invalidate(Dependencies.DOCUMENTS);
const url = `${base}/project-${page.params.region}-${page.params.project}/databases/database-${databaseId}/collection-${collectionId}`;
// extract clean message from nested backend error.
const match = importData.errors.join('').match(/message: '(.*)' Message:/i);
const errorMessage = match?.[1];
const type = importData.status === 'completed' ? 'success' : 'error';
const message =
importData.status === 'completed'
? 'CSV import finished successfully.'
: `${errorMessage}`;
addNotification({
type,
message,
isHtml: true,
buttons:
collectionId === page.params.collection || type === 'error'
? undefined
: [
{
name: 'View documents',
method: () => goto(url)
}
]
});
}
async function updateOrAddItem(importData: Payload | Models.Migration) {
if (importData.source.toLowerCase() !== 'csv') return;
const status = importData.status;
const resourceId = importData.resourceId ?? '';
const [databaseId, collectionId] = resourceId.split(':') ?? [];
const current = $importItems.get(importData.$id);
let collectionName = current?.collection ?? null;
if (!collectionName && collectionId) {
try {
const collection = await sdk
.forProject(page.params.region, page.params.project)
.databases.getCollection(databaseId, collectionId);
collectionName = collection.name;
} catch {
collectionName = null;
}
}
importItems.update((items) => {
const existing = items.get(importData.$id);
const isDone = (s: string) => s === 'completed' || s === 'failed';
const isInProgress = (s: string) => ['pending', 'processing', 'uploading'].includes(s);
const shouldSkip =
(existing && isDone(existing.status) && isInProgress(status)) ||
existing?.status === status;
if (shouldSkip) return items;
const next = new Map(items);
next.set(importData.$id, { status, collection: collectionName ?? undefined });
return next;
});
if (status === 'completed' || status === 'failed') {
await showCompletionNotification(databaseId, collectionId, importData);
}
}
function clear() {
importItems.update((items) => {
items.clear();
return items;
});
}
function graphSize(status: string): number {
switch (status) {
case 'pending':
return 10;
case 'processing':
return 30;
case 'uploading':
return 60;
case 'completed':
case 'failed':
return 100;
default:
return 30;
}
}
function text(status: string, collectionName = '') {
const name = collectionName ? `<b>${collectionName}</b>` : '';
switch (status) {
case 'completed':
case 'failed':
return `Import to ${name} ${status}`;
case 'processing':
return `Importing CSV file${name ? ` to ${name}` : ''}`;
default:
return 'Preparing CSV for import...';
}
}
onMount(() => {
sdk.forProject(page.params.region, page.params.project)
.migrations.list([
Query.equal('source', 'CSV'),
Query.equal('status', ['pending', 'processing'])
])
.then((migrations) => {
migrations.migrations.forEach(updateOrAddItem);
});
return sdk.forConsole.client.subscribe('console', (response) => {
if (!response.channels.includes(`projects.${getProjectId()}`)) return;
if (response.events.includes('migrations.*')) {
updateOrAddItem(response.payload as Payload);
}
});
});
$: isOpen = true;
$: showCsvImportBox = $importItems.size > 0;
</script>
{#if showCsvImportBox}
<Layout.Stack direction="column" gap="l" alignItems="flex-end">
<section class="upload-box">
<header class="upload-box-header">
<h4 class="upload-box-title">
<Typography.Text variant="m-500">
Importing documents ({$importItems.size})
</Typography.Text>
</h4>
<button
class="upload-box-button"
class:is-open={isOpen}
aria-label="toggle upload box"
on:click={() => (isOpen = !isOpen)}>
<span class="icon-cheveron-up" aria-hidden="true"></span>
</button>
<button
class="upload-box-button"
aria-label="close backup restore box"
on:click={clear}>
<span class="icon-x" aria-hidden="true"></span>
</button>
</header>
{#each [...$importItems.entries()] as [key, value] (key)}
<div class="upload-box-content" class:is-open={isOpen}>
<ul class="upload-box-list">
<li class="upload-box-item">
<section class="progress-bar u-width-full-line">
<div
class="progress-bar-top-line u-flex u-gap-8 u-main-space-between">
<Typography.Text>
{@html text(value.status, value.collection)}
</Typography.Text>
</div>
<div
class="progress-bar-container"
class:is-danger={value.status === 'failed'}
style="--graph-size:{graphSize(value.status)}%">
</div>
</section>
</li>
</ul>
</div>
{/each}
</section>
</Layout.Stack>
{/if}
<style lang="scss">
.upload-box-title {
font-size: 11px;
}
.upload-box-content {
min-width: 400px;
max-width: 100vw;
}
.upload-box-button {
display: flex;
align-items: center;
justify-content: center;
}
.progress-bar-container {
height: 4px;
&::before {
height: 4px;
background-color: var(--bgcolor-neutral-invert);
}
&.is-danger::before {
height: 4px;
background-color: var(--bgcolor-error);
}
}
</style>
@@ -17,7 +17,7 @@
{domain}
</Typography.Text>
{#if verified}
<Badge variant="secondary" type="success" content="Verified" />
<Badge variant="secondary" type="success" size="xs" content="Verified" />
{:else if verified === false}
<Badge variant="secondary" type="warning" size="xs" content="Pending verification" />
{/if}
+26 -14
View File
@@ -1,26 +1,28 @@
<script lang="ts">
import { Link } from '$lib/elements';
import { consoleVariables } from '$routes/(console)/store';
import { IconInfo } from '@appwrite.io/pink-icons-svelte';
import {
Badge,
Layout,
Typography,
Table,
Icon,
InteractiveText
InteractiveText,
Alert
} from '@appwrite.io/pink-svelte';
export let domain: string;
export let verified = false;
export let variant: 'cname' | 'a' | 'aaaa';
export let service: 'sites' | 'general' = 'general';
let subdomain = domain?.split('.')?.slice(0, -2)?.join('.');
function setTarget() {
switch (variant) {
case 'cname':
return $consoleVariables._APP_DOMAIN_TARGET_CNAME;
return service === 'general'
? $consoleVariables._APP_DOMAIN_TARGET_CNAME
: $consoleVariables._APP_DOMAIN_SITES;
case 'a':
return $consoleVariables._APP_DOMAIN_TARGET_A;
case 'aaaa':
@@ -36,9 +38,9 @@
{domain}
</Typography.Text>
{#if verified}
<Badge variant="secondary" type="success" content="Verified" />
<Badge variant="secondary" type="success" size="xs" content="Verified" />
{:else if verified === false}
<Badge variant="secondary" type="error" content="Verification failed" />
<Badge variant="secondary" type="error" size="xs" content="Verification failed" />
{:else}
<Badge
variant="secondary"
@@ -68,13 +70,23 @@
</Table.Row.Base>
</Table.Root>
<Layout.Stack gap="s" direction="row" alignItems="center">
<Icon icon={IconInfo} size="s" color="--fgcolor-neutral-secondary" />
<Typography.Text variant="m-400" color="--fgcolor-neutral-secondary">
A list of all domain providers and their DNS setting is available <Link
variant="muted"
external
href="https://appwrite.io/docs/advanced/platform/custom-domains">here</Link
>.
</Typography.Text>
{#if variant === 'cname'}
<Alert.Inline>
If your domain uses CAA records, ensure certainly.com is authorized — otherwise, SSL
setup may fail. A list of all domain providers and their DNS setting is available <Link
variant="muted"
external
href="https://appwrite.io/docs/advanced/platform/custom-domains">here</Link
>.
</Alert.Inline>
{:else}
<Typography.Text variant="m-400" color="--fgcolor-neutral-secondary">
A list of all domain providers and their DNS setting is available <Link
variant="muted"
external
href="https://appwrite.io/docs/advanced/platform/custom-domains">here</Link
>.
</Typography.Text>
{/if}
</Layout.Stack>
</Layout.Stack>
+1 -1
View File
@@ -110,7 +110,7 @@
<InteractiveText
isVisible
variant="copy"
text={toLocaleDateTime(time, 'UTC')}
text={toLocaleDateTime(time, false, 'UTC')}
value={toISOString(time)} />
<Badge variant="secondary" content="UTC" size="xs" />
@@ -4,7 +4,6 @@
import { Card, Layout, Typography } from '@appwrite.io/pink-svelte';
export let source = 'empty_state_card';
export let noAspectRatio = false;
</script>
<Card.Base variant="secondary" padding="s" radius="s">
@@ -1,5 +1,12 @@
<script context="module" lang="ts">
export type ExpirationOptions = {
label: string;
value: string;
};
</script>
<script lang="ts">
import { Helper, InputDateTime, InputSelect } from '$lib/elements/forms';
import { InputDateTime, InputSelect } from '$lib/elements/forms';
import { isSameDay, isValidDate, toLocaleDate } from '$lib/helpers/date';
function incrementToday(value: number, type: 'day' | 'month' | 'year'): string {
@@ -19,7 +26,7 @@
return date.toISOString();
}
const options = [
const defaultOptions: ExpirationOptions[] = [
{
label: 'Never',
value: null
@@ -46,10 +53,37 @@
}
];
const limitedOptions: ExpirationOptions[] = [
{
label: '1 Day',
value: incrementToday(1, 'day')
},
{
label: '7 Days',
value: incrementToday(7, 'day')
},
{
label: '30 days',
value: incrementToday(30, 'day')
}
];
export let value: string | null = null;
export let dateSelectorLabel: string | undefined = undefined;
export let selectorLabel: string | undefined = 'Expiration date';
export let resourceType: string | 'key' | 'token' | undefined = 'key';
export let expiryOptions: 'default' | 'limited' | ExpirationOptions[] = 'default';
const options = Array.isArray(expiryOptions)
? expiryOptions
: expiryOptions === 'default'
? defaultOptions
: limitedOptions;
function initExpirationSelect() {
if (value === null || !isValidDate(value)) return null;
if (value === null || !isValidDate(value)) {
return options[0]?.value ?? null;
}
let result = 'custom';
for (const option of options) {
@@ -63,23 +97,37 @@
return result;
}
/**
* Custom picker only supports `dd/mm/yy` format.
*/
function splitDateValue(value: string): string {
return value.slice(0, 10);
}
let expirationSelect = initExpirationSelect();
let expirationCustom: string | null = value ?? null;
let expirationCustom: string | null = value ? splitDateValue(value) : null;
$: {
if (!isSameDay(new Date(expirationSelect), new Date(value))) {
value = expirationSelect === 'custom' ? expirationCustom : expirationSelect;
}
}
$: helper =
expirationSelect !== 'custom' && expirationSelect !== null
? `Your ${resourceType} will expire in ${toLocaleDate(value)}`
: null;
</script>
<InputSelect bind:value={expirationSelect} {options} id="preset" label="Expiration date">
<svelte:fragment slot="helper">
{#if expirationSelect !== 'custom' && expirationSelect !== null}
<Helper type="neutral">Your key will expire in {toLocaleDate(value)}</Helper>
{/if}
</svelte:fragment>
</InputSelect>
<InputSelect
required
{helper}
{options}
id="preset"
label={selectorLabel}
bind:value={expirationSelect} />
{#if expirationSelect === 'custom'}
<InputDateTime required id="expire" label="" bind:value={expirationCustom} />
<InputDateTime required id="expire" label={dateSelectorLabel} bind:value={expirationCustom} />
{/if}
+1 -1
View File
@@ -11,7 +11,7 @@
import { addNotification } from '$lib/stores/notifications';
import { page } from '$app/state';
import { Typography } from '@appwrite.io/pink-svelte';
import { project } from '$routes/(console)/project-[project]/store';
import { project } from '$routes/(console)/project-[region]-[project]/store';
$: $selectedFeedback = feedbackOptions.find((option) => option.type === $feedback.type);
+487 -236
View File
@@ -1,36 +1,45 @@
<script lang="ts">
import { Id } from '.';
import { Button } from '$lib/elements/forms';
import { sdk } from '$lib/stores/sdk';
import { ID, Query, Permission, Role } from '@appwrite.io/console';
import type { Models } from '@appwrite.io/console';
import { calculateSize } from '$lib/helpers/sizeConvertion';
import InputSearch from '$lib/elements/forms/inputSearch.svelte';
import { writable } from 'svelte/store';
import { EmptySearch, Id } from '.';
import { onMount } from 'svelte';
import { base } from '$app/paths';
import { page } from '$app/state';
import { sdk } from '$lib/stores/sdk';
import { goto } from '$app/navigation';
import { writable } from 'svelte/store';
import { Button, InputSelect } from '$lib/elements/forms';
import DualTimeView from './dualTimeView.svelte';
import type { Models } from '@appwrite.io/console';
import { calculateSize } from '$lib/helpers/sizeConvertion';
import InputSearch from '$lib/elements/forms/inputSearch.svelte';
import { ID, Query, Permission, Role } from '@appwrite.io/console';
import {
Layout,
Typography,
Modal,
ActionMenu,
Table,
Spinner,
ToggleButton,
Selector,
Card,
Divider,
Empty,
Card
Layout,
Modal,
Selector,
Spinner,
Table,
ToggleButton,
Tooltip,
Typography
} from '@appwrite.io/pink-svelte';
import Form from '$lib/elements/forms/form.svelte';
import { IconCheck, IconViewGrid, IconViewList } from '@appwrite.io/pink-icons-svelte';
import { isSmallViewport } from '$lib/stores/viewport';
import { IconViewGrid, IconViewList } from '@appwrite.io/pink-icons-svelte';
import { showCreateBucket } from '$routes/(console)/project-[region]-[project]/storage/+page.svelte';
export let show: boolean;
export let mimeTypeQuery: string = 'image/';
export let allowedExtension: string = '*';
export let selectedBucket: string = null;
export let selectedFile: string = null;
export let onSelect: (e: Models.File) => void;
export let gridImageDimensions: { imageHeight?: number; imageWidth?: number } = {
imageHeight: 148
};
let search = writable('');
let searchEnabled = false;
@@ -49,20 +58,21 @@
function getPreview(bucketId: string, fileId: string, size: number = 64) {
return (
sdk.forProject.storage.getFilePreview(bucketId, fileId, size, size).toString() +
'&mode=admin'
sdk
.forProject(page.params.region, page.params.project)
.storage.getFilePreview(bucketId, fileId, size, size)
.toString() + '&mode=admin'
);
}
async function uploadFile() {
try {
uploading = true;
const file = await sdk.forProject.storage.createFile(
selectedBucket,
ID.unique(),
fileSelector.files[0],
[Permission.read(Role.any())]
);
const file = await sdk
.forProject(page.params.region, page.params.project)
.storage.createFile(selectedBucket, ID.unique(), fileSelector.files[0], [
Permission.read(Role.any())
]);
search.set($search === null ? '' : null);
selectFile(file);
} catch (e) {
@@ -110,7 +120,9 @@
let buckets: Promise<Models.BucketList> = loadBuckets();
async function loadBuckets() {
const response = await sdk.forProject.storage.listBuckets();
const response = await sdk
.forProject(page.params.region, page.params.project)
.storage.listBuckets();
const bucket = response.buckets[0] ?? null;
if (bucket) {
currentBucket = bucket;
@@ -120,14 +132,28 @@
return response;
}
function truncatedFilename(file: Models.File, max: number = 15): string {
const length = file.name.length;
return length > 15 ? `${file.name.substring(0, max)}...` : file.name;
}
function getProperQuery(): string[] {
let query = [Query.orderDesc('$createdAt')];
if (allowedExtension === '*') {
query.push(Query.startsWith('mimeType', mimeTypeQuery));
} else {
query.push(Query.endsWith('name', `.${allowedExtension}`));
}
return query;
}
$: files =
currentBucket &&
sdk.forProject.storage
.listFiles(
currentBucket.$id,
[Query.startsWith('mimeType', mimeTypeQuery), Query.orderDesc('$createdAt')],
$search || undefined
)
sdk
.forProject(page.params.region, page.params.project)
.storage.listFiles(currentBucket.$id, getProperQuery(), $search || undefined)
.then((response) => {
if ($search === '') {
searchEnabled = response.total > 0;
@@ -139,236 +165,461 @@
$: if ($search) {
resetFile();
}
$: extension = allowedExtension === '*' ? mimeTypeQuery : `.${allowedExtension}`;
</script>
<svelte:document on:visibilitychange={handleVisibilityChange} />
<Form {onSubmit}>
<Form {onSubmit} isModal class="file-picker-modal-form">
<Modal bind:open={show} title="Select file" size="l">
<Layout.Stack direction="row" height="50vh">
<Layout.Stack direction={$isSmallViewport ? 'column' : 'row'} height="50vh" gap="none">
<!-- min-width to avoid a layout-shift -->
<aside>
<Typography.Caption variant="500">Buckets</Typography.Caption>
{#if !$isSmallViewport}
<Typography.Caption variant="500">Buckets</Typography.Caption>
{/if}
{#await buckets}
<div class="u-flex u-main-center">
<div class="loader"></div>
</div>
{#if $isSmallViewport}
<!-- disabled state -->
<div style:padding-block-start="1rem">
<InputSelect
required
disabled
id="bucket"
value={null}
options={[]}
label="Bucket"
placeholder="Loading buckets..." />
</div>
{/if}
{:then response}
<ActionMenu.Root>
{#each response.buckets as bucket}
{@const isSelected = bucket.$id === selectedBucket}
<ActionMenu.Item.Button
on:click={() => selectBucket(bucket)}
leadingIcon={isSelected ? IconCheck : undefined}>
{bucket.name}
</ActionMenu.Item.Button>
{:else}
<ActionMenu.Item.Button>No buckets found</ActionMenu.Item.Button>
{/each}
</ActionMenu.Root>
{#if $isSmallViewport}
<div style:padding-block-start="1rem">
<InputSelect
required
id="bucket"
label="Bucket"
bind:value={selectedBucket}
placeholder="Select bucket"
on:change={(event) => {
const bucketId = event.detail;
const bucket = response.buckets.find(
(bucket) => bucket.$id === bucketId
);
selectBucket(bucket);
}}
options={response.buckets.map((bucket) => {
return {
value: bucket.$id,
label: `${bucket.name}`
};
})} />
</div>
{:else}
<div class="action-menu-holder">
<ActionMenu.Root width="180px">
{#each response.buckets as bucket}
{@const isSelected = bucket.$id === selectedBucket}
<div class="action-button" class:active-item={isSelected}>
<ActionMenu.Item.Button
on:click={() => selectBucket(bucket)}>
{bucket.name}
</ActionMenu.Item.Button>
</div>
{:else}
<ActionMenu.Item.Button
>No buckets found</ActionMenu.Item.Button>
{/each}
</ActionMenu.Root>
</div>
{/if}
{/await}
</aside>
<Layout.Stack>
{#await buckets then response}
{#if response?.total}
{#if currentBucket}
<Layout.Stack>
<Layout.Stack direction="row" alignItems="center">
<Typography.Title>{currentBucket?.name}</Typography.Title>
<Id value={currentBucket?.$id} event="bucket">
{currentBucket?.$id}
</Id>
</Layout.Stack>
<Layout.Stack direction="row" alignItems="center">
<InputSearch
placeholder="Search files"
bind:value={$search}
disabled={!searchEnabled} />
<ToggleButton
bind:active={view}
buttons={[
{
id: 'list',
label: 'list view',
disabled: !searchEnabled,
icon: IconViewList
},
{
id: 'grid',
label: 'grid view',
disabled: !searchEnabled,
icon: IconViewGrid
}
]} />
<Button
secondary
disabled={uploading}
on:click={() => fileSelector.click()}>
<input
tabindex="-1"
type="file"
accept="image/*"
class="u-hide"
on:change={uploadFile}
bind:this={fileSelector} />
{#if uploading}
<div class="loader is-small"></div>
<span>Uploading</span>
{:else}
<span class="icon-upload" aria-hidden="true"></span>
<span>Upload</span>
{/if}
</Button>
</Layout.Stack>
</Layout.Stack>
<div style:padding-inline-start="1rem" style:opacity={$isSmallViewport ? 0 : 1}>
<Divider vertical />
</div>
{#if files}
{#await files}
<Layout.Stack
justifyContent="center"
alignContent="center"
alignItems="center"
height="100%">
<Spinner size="l" />
<span>Loading files...</span>
</Layout.Stack>
{:then response}
{#if response?.files?.length}
{#if view === 'grid'}
<Layout.Grid
columnsXXS={1}
columnsXS={2}
columnsS={3}
columns={4}>
{#each response?.files as file}
<Card.Selector
group="files"
name="files"
title="files"
value={file.$id}
src={getPreview(
currentBucket.$id,
file.$id,
360
)}
on:click={() => selectFile(file)} />
{/each}
</Layout.Grid>
<div class="files-section">
<Layout.Stack gap="l">
{#if $isSmallViewport}
<Button
secondary
disabled={uploading}
on:click={() => fileSelector.click()}>
<input
type="file"
tabindex="-1"
class="u-hide"
accept={extension}
on:change={uploadFile}
bind:this={fileSelector} />
{#if uploading}
<div class="loader is-small"></div>
<span>Uploading</span>
{:else}
<span class="icon-upload" aria-hidden="true"></span>
<span>Upload</span>
{/if}
</Button>
{/if}
{#await buckets then response}
{#if response?.total}
{#if currentBucket}
<Layout.Stack>
{#if !$isSmallViewport}
{#key currentBucket?.$id}
<Layout.Stack direction="row" alignItems="center">
<Typography.Title size="s"
>{currentBucket?.name}</Typography.Title>
<Id value={currentBucket?.$id} event="bucket">
{currentBucket?.$id}
</Id>
</Layout.Stack>
{/key}
{/if}
<Layout.Stack direction="row" alignItems="center">
<InputSearch
placeholder="Search files"
bind:value={$search}
disabled={!searchEnabled} />
<ToggleButton
bind:active={view}
buttons={[
{
id: 'list',
label: 'list view',
disabled: !searchEnabled,
icon: IconViewList
},
{
id: 'grid',
label: 'grid view',
disabled: !searchEnabled,
icon: IconViewGrid
}
]} />
{#if !$isSmallViewport}
<Button
secondary
disabled={uploading}
on:click={() => fileSelector.click()}>
<input
type="file"
tabindex="-1"
class="u-hide"
accept={extension}
on:change={uploadFile}
bind:this={fileSelector} />
{#if uploading}
<div class="loader is-small"></div>
<span>Uploading</span>
{:else}
<span class="icon-upload" aria-hidden="true"
></span>
<span>Upload</span>
{/if}
</Button>
{/if}
{#if view === 'list'}
<Table.Root
let:root
columns={[
{ id: 'filename', width: { min: 140 } },
{ id: 'id', width: { min: 100 } },
{ id: 'type', width: { min: 100 } },
{ id: 'size', width: { min: 100 } },
{ id: 'created', width: { min: 120 } }
]}>
<svelte:fragment slot="header" let:root>
<Table.Header.Cell column="filename" {root}>
Filename
</Table.Header.Cell>
<Table.Header.Cell column="id" {root}>
ID
</Table.Header.Cell>
<Table.Header.Cell column="type" {root}>
Type
</Table.Header.Cell>
<Table.Header.Cell column="size" {root}>
Size
</Table.Header.Cell>
<Table.Header.Cell column="created" {root}>
Created
</Table.Header.Cell>
</svelte:fragment>
{#each response?.files as file}
<Table.Row.Button
{root}
on:click={() => selectFile(file)}>
<Table.Cell column="filename" {root}>
<div
class="u-inline-flex u-cross-center u-gap-12">
<Selector.Radio
name="file"
group="file"
</Layout.Stack>
</Layout.Stack>
{#if files}
{#await files}
<Layout.Stack
justifyContent="center"
alignContent="center"
alignItems="center"
gap="xl"
height="100%">
<Spinner size="l" />
<span>Loading files...</span>
</Layout.Stack>
{:then response}
{#if response?.files?.length}
{#if view === 'grid'}
{#if $isSmallViewport}
<Layout.Stack gap="l">
{#each response?.files as file}
<Card.Selector
radius="s"
name="files"
padding="xxs"
imageHeight={32}
imageWidth={32}
imageRadius="xs"
bind:group={selectedFile}
title={truncatedFilename(file, 14)}
value={file.$id}
src={getPreview(
currentBucket.$id,
file.$id,
360
)}
on:click={() => selectFile(file)} />
{/each}
</Layout.Stack>
{:else}
<Layout.Grid
columnsXXS={1}
columnsXS={2}
columnsS={3}
columns={4}>
{#each response?.files as file}
<div class="image-selector">
<Card.Selector
radius="s"
name="files"
padding="xxs"
imageWidth={gridImageDimensions.imageWidth}
imageHeight={gridImageDimensions.imageHeight}
imageRadius="xs"
bind:group={selectedFile}
title={truncatedFilename(
file,
14
)}
value={file.$id}
checked={file.$id ===
selectedFile} />
<img
style:border-radius="var(--border-radius-xsmall)"
width="28"
height="28"
src={getPreview(
currentBucket.$id,
file.$id
file.$id,
360
)}
alt={file.name} />
<Typography.Text truncate>
{file.name}
</Typography.Text>
on:click={() =>
selectFile(file)} />
</div>
</Table.Cell>
<Table.Cell column="id" {root}>
<Id value={file.$id}>{file.$id}</Id>
</Table.Cell>
<Table.Cell column="type" {root}>
{file.mimeType}
</Table.Cell>
<Table.Cell column="size" {root}>
{calculateSize(file.sizeOriginal)}
</Table.Cell>
<Table.Cell column="created" {root}>
<DualTimeView time={file.$createdAt} />
</Table.Cell>
</Table.Row.Button>
{/each}
</Table.Root>
{/each}
</Layout.Grid>
{/if}
{/if}
{#if view === 'list'}
<Table.Root
let:root
columns={[
{ id: 'filename', width: { min: 225 } },
{ id: 'id', width: { min: 200 } },
{ id: 'type', width: { min: 100 } },
{ id: 'size', width: { min: 120 } },
{ id: 'created', width: { min: 140 } }
]}>
<svelte:fragment slot="header" let:root>
<Table.Header.Cell column="filename" {root}>
Filename
</Table.Header.Cell>
<Table.Header.Cell column="id" {root}>
ID
</Table.Header.Cell>
<Table.Header.Cell column="type" {root}>
Type
</Table.Header.Cell>
<Table.Header.Cell column="size" {root}>
Size
</Table.Header.Cell>
<Table.Header.Cell column="created" {root}>
Created
</Table.Header.Cell>
</svelte:fragment>
{#each response?.files as file (file.$id)}
<Table.Row.Button
{root}
on:click={() => selectFile(file)}>
<Table.Cell column="filename" {root}>
<div
class="u-inline-flex u-cross-center u-gap-12">
<Selector.Radio
size="s"
name="file"
value={file.$id}
bind:group={selectedFile} />
<div class="preview-block">
<img
alt={file.name}
src={getPreview(
currentBucket.$id,
file.$id
)} />
</div>
<Tooltip
disabled={file.name.length <
15}
maxWidth="fit-content">
<Typography.Text truncate>
{truncatedFilename(
file
)}
</Typography.Text>
<span slot="tooltip">
{file.name}
</span>
</Tooltip>
</div>
</Table.Cell>
<Table.Cell column="id" {root}>
<Id value={file.$id}>{file.$id}</Id>
</Table.Cell>
<Table.Cell column="type" {root}>
{file.mimeType}
</Table.Cell>
<Table.Cell column="size" {root}>
{calculateSize(file.sizeOriginal)}
</Table.Cell>
<Table.Cell column="created" {root}>
<DualTimeView
time={file.$createdAt} />
</Table.Cell>
</Table.Row.Button>
{/each}
</Table.Root>
{/if}
{:else if $search}
<EmptySearch
hidePages
hidePagination
bind:search={$search}
target="files">
<Button secondary on:click={() => ($search = '')}>
Clear search
</Button>
</EmptySearch>
{:else}
<Card.Base padding="none">
<Empty
title="No files found within this bucket."
description="Need a hand? Learn more in our documentation.">
<slot name="actions" slot="actions">
<Button
text
external
size="s"
event="empty_documentation"
href="https://appwrite.io/docs/products/storage/upload-download"
ariaLabel="create document"
>Documentation</Button>
<Button
secondary
disabled={uploading}
on:click={() => fileSelector.click()}
>Upload file
</Button>
</slot>
</Empty>
</Card.Base>
{/if}
{:else if $search}
<Empty
type="secondary"
title={`Sorry we couldn't find "${$search}"`}
description="There are no files that match your search.">
<Button
secondary
slot="actions"
on:click={() => ($search = '')}
>Clear search</Button>
</Empty>
{:else}
<Empty title="No files found within this bucket.">
<Button
secondary
slot="actions"
disabled={uploading}
on:click={() => fileSelector.click()}
>Upload file</Button>
</Empty>
{/if}
{/await}
{/await}
{/if}
{/if}
{:else}
<Card.Base padding="none">
<Empty
title="No buckets found"
description="Need a hand? Learn more in our documentation.">
<slot name="actions" slot="actions">
<Button
text
external
size="s"
event="empty_documentation"
href="https://appwrite.io/docs/products/storage/buckets"
ariaLabel="create document">Documentation</Button>
<Button
secondary
on:click={async () => {
await goto(
`${base}/project-${page.params.region}-${page.params.project}/storage`
);
$showCreateBucket = true;
}}>
Create bucket
</Button>
</slot>
</Empty>
</Card.Base>
{/if}
{:else}
<Empty title="No buckets found">
<Button
slot="actions"
secondary
external
href={`${base}/project-${page.params.project}/storage`}>
Create bucket
</Button>
</Empty>
{/if}
{/await}
</Layout.Stack>
</Layout.Stack>
{/await}
</Layout.Stack>
</div></Layout.Stack>
<svelte:fragment slot="footer">
<Layout.Stack direction="row" justifyContent="flex-end">
<Button text on:click={closeModal}>Cancel</Button>
<Button submit disabled={selectedBucket === null || selectedFile === null}
>Select</Button>
>Select
</Button>
</Layout.Stack>
</svelte:fragment>
</Modal>
</Form>
<style>
:global(.file-picker-modal-form dialog .content) {
overflow: hidden;
padding: unset !important;
/* multiple scroll bars from `.content` and `.files-section` look very odd */
scrollbar-width: none;
-ms-overflow-style: none;
}
aside {
min-width: 200px;
padding: var(--space-7);
@media (max-width: 768px) {
padding-block: unset;
padding-inline: var(--space-7);
}
}
.files-section {
width: 100%;
height: 100%;
overflow: auto;
padding: var(--space-8);
background: var(--bgcolor-neutral-default);
&::-webkit-scrollbar {
display: none;
}
}
.action-menu-holder :global(div:first-of-type) {
padding-inline: unset;
}
.action-button {
width: 100%;
margin-block: 0.125rem;
& :global(button) {
width: 100%;
}
&.active-item {
border-radius: var(--border-radius-s);
background-color: var(--bgcolor-neutral-secondary, #f4f4f7);
}
}
.preview-block {
width: 1.5rem;
height: 1.5rem;
border-radius: 50%;
border: var(--border-width-S, 1px) solid var(--border-neutral-strong, #d8d8db);
& img {
border-radius: 50%;
align-self: center;
}
}
.image-selector :global(img) {
border: 1px solid var(--border-neutral);
}
</style>
@@ -12,7 +12,9 @@
<ActionMenu.Item.Button
on:click={() => {
show = true;
}}>Custom filters</ActionMenu.Item.Button>
}}>
Custom filters
</ActionMenu.Item.Button>
</ActionMenu.Root>
{#if show}
@@ -111,8 +111,6 @@
return arrayValue;
}
}
$inspect(subSheets);
</script>
<BottomSheet.Menu bind:isOpen={openBottomSheet} menu={filtersBottomSheet} />
+13 -4
View File
@@ -11,11 +11,12 @@
} from '$lib/elements/forms';
import type { Writable } from 'svelte/store';
import Modal from '../modal.svelte';
import { addFilter, generateTag, operators, queries, type TagValue } from './store';
import { generateTag, operators, ValidOperators, type TagValue } from './store';
import type { Column } from '$lib/helpers/types';
import { TagList } from '.';
import { Icon, Layout } from '@appwrite.io/pink-svelte';
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
import { addFilterAndApply } from './quickFilters';
export let show = false;
export let columns: Writable<Column[]>;
@@ -35,9 +36,17 @@
function apply() {
localQueries.forEach((query) => {
addFilter($columns, query.id, query.operator, query.value, query.arrayValues);
addFilterAndApply(
query.id,
$columns.find((c) => c.id === query.id).title,
query.operator as ValidOperators,
query.value,
query.arrayValues,
$columns,
analyticsSource
);
});
queries.apply();
localTags = [];
localQueries = [];
trackEvent(Submit.FilterApply, {
@@ -48,7 +57,7 @@
}
function addCondition() {
const newTag = generateTag(selectedColumn, operatorKey, value, arrayValues);
const newTag = generateTag(selectedColumn, operatorKey, value || arrayValues);
if (localTags.some((t) => t.tag === newTag.tag && t.value === newTag.value)) {
return;
} else {
+56 -95
View File
@@ -1,107 +1,68 @@
<script lang="ts">
import { afterNavigate } from '$app/navigation';
import { queryParamToMap } from '$lib/components/filters/store';
import type { Column } from '$lib/helpers/types';
import { type Writable } from 'svelte/store';
import { CustomFilters, FiltersBottomSheet } from '$lib/components/filters';
import { addFilterAndApply, buildFilterCol } from './quickFilters';
import { parsedTags, setFilters } from './setFilters';
import { CustomFilters } from '$lib/components/filters';
import { addFilterAndApply, type FilterData } from './quickFilters';
import { parsedTags } from './setFilters';
import Menu from '../menu/menu.svelte';
import { Button } from '$lib/elements/forms';
import { Icon } from '@appwrite.io/pink-svelte';
import { IconFilterLine } from '@appwrite.io/pink-icons-svelte';
import QuickfiltersSubMenu from './quickfiltersSubMenu.svelte';
import { isSmallViewport } from '$lib/stores/viewport';
export let columns: Writable<Column[]>;
export let analyticsSource: string;
export let openBottomSheet = false;
//TODO: remove this when all header are replace with `ResponsiveContainerHeader`
let filterCols = $columns
.map((col) => (col.filter !== false ? buildFilterCol(col) : null))
.filter((f) => f?.options);
afterNavigate((p) => {
const paramQueries = p.to.url.searchParams.get('query');
const localQueries = queryParamToMap(paramQueries || '[]');
const localTags = Array.from(localQueries.keys());
// console.log(paramQueries, localQueries, localTags);
setFilters(localTags, filterCols, $columns);
filterCols = filterCols;
});
let {
columns,
filterCols,
analyticsSource
}: {
columns: Writable<Column[]>;
filterCols: FilterData[];
analyticsSource?: string;
} = $props();
</script>
{#if $isSmallViewport}
{#if $parsedTags?.length}
<Button
secondary
badge={`${$parsedTags?.length}`}
on:click={() => (openBottomSheet = true)}>
<Icon icon={IconFilterLine} slot="start" size="s" />
Filters
</Button>
{:else}
<Button secondary on:click={() => (openBottomSheet = true)}>
<Icon icon={IconFilterLine} slot="start" size="s" />
Filters
</Button>
{/if}
{:else}
<Menu>
{#if $parsedTags?.length}
<Button secondary badge={`${$parsedTags?.length}`}>
<Icon icon={IconFilterLine} slot="start" size="s" />
Filters
</Button>
{:else}
<Button secondary>
<Icon icon={IconFilterLine} slot="start" size="s" />
Filters
</Button>
{/if}
<svelte:fragment slot="menu">
{#each filterCols as filter (filter.title + filter.id)}
{#if filter.options}
<QuickfiltersSubMenu
{filter}
variant={filter?.array ? 'checkbox' : 'radio'}
on:add={(e) => {
addFilterAndApply(
filter.id,
filter.title,
filter.operator,
e.detail.value,
filter?.array
? (filter.options
.filter((opt) => opt.checked)
.map((opt) => opt.value) ?? [])
: [],
$columns,
analyticsSource
);
}}
on:clear={() => {
addFilterAndApply(
filter.id,
filter.title,
filter.operator,
null,
[],
$columns,
analyticsSource
);
}} />
{/if}
{/each}
</svelte:fragment>
<Menu>
<Button secondary badge={$parsedTags?.length ? `${$parsedTags.length}` : undefined}>
<Icon icon={IconFilterLine} slot="start" size="s" />
Filters
</Button>
<svelte:fragment slot="menu">
{#each filterCols.filter((f) => f?.options) as filter (filter.title + filter.id)}
{#if filter.options}
<QuickfiltersSubMenu
{filter}
variant={filter?.array ? 'checkbox' : 'radio'}
on:add={(e) => {
addFilterAndApply(
filter.id,
filter.title,
filter.operator,
e.detail.value,
filter?.array
? (filter.options
.filter((opt) => opt.checked)
.map((opt) => opt.value) ?? [])
: [],
$columns,
analyticsSource
);
}}
on:clear={() => {
addFilterAndApply(
filter.id,
filter.title,
filter.operator,
null,
[],
$columns,
analyticsSource
);
}} />
{/if}
{/each}
</svelte:fragment>
<svelte:fragment slot="end">
<CustomFilters {columns} />
</svelte:fragment>
</Menu>
{/if}
{#if $isSmallViewport && openBottomSheet}
<FiltersBottomSheet bind:openBottomSheet {columns} {analyticsSource} bind:filterCols />
{/if}
<svelte:fragment slot="end">
<CustomFilters {columns} />
</svelte:fragment>
</Menu>
@@ -34,9 +34,10 @@
</script>
<div use:melt={$subTrigger}>
<ActionMenu.Root noPadding>
<ActionMenu.Item.Button trailingIcon={IconChevronRight}
>{filter.title}</ActionMenu.Item.Button>
<ActionMenu.Root>
<ActionMenu.Item.Button trailingIcon={IconChevronRight}>
{filter.title}
</ActionMenu.Item.Button>
</ActionMenu.Root>
</div>
+18 -16
View File
@@ -9,17 +9,18 @@ export function setFilters(localTags: TagValue[], filterCols: FilterData[], $col
if (!localTags?.length) {
filterCols.forEach((filter) => {
resetOptions(filter);
cleanOldTags(filter.title);
cleanOldTags(filter?.title);
});
} else {
filterCols.forEach((filter) => {
if (filter.id.toLowerCase().includes('duration')) {
const id = filter?.id?.toLowerCase();
if (id?.includes('duration')) {
setTimeFilter(filter, $columns);
} else if (filter.id.toLocaleLowerCase().includes('size')) {
} else if (id?.includes('size')) {
setSizeFilter(filter, $columns);
} else if (filter.id.toLocaleLowerCase().includes('statuscode')) {
} else if (id?.includes('statuscode')) {
setStatusCodeFilter(filter, $columns);
} else if (filter.id === '$createdAt' || filter.id === '$updatedAt') {
} else if (id === '$createdat' || id === '$updatedat') {
setDateFilter(filter, $columns);
} else {
setFilterData(filter);
@@ -40,7 +41,7 @@ export function setFilterData(filter: FilterData) {
option.checked = values.includes(option.value);
});
}
cleanOldTags(filter.title);
cleanOldTags(filter?.title);
const newTag = {
tag: tagData.tag.replace(',', ' or '),
value: tagData.value
@@ -52,7 +53,7 @@ export function setFilterData(filter: FilterData) {
});
} else {
resetOptions(filter);
cleanOldTags(filter.title);
cleanOldTags(filter?.title);
}
}
@@ -76,7 +77,7 @@ export function setTimeFilter(filter: FilterData, columns: Column[]) {
value: timeTag.value
};
cleanOldTags(filter.title);
cleanOldTags(filter?.title);
parsedTags.update((tags) => {
tags.push(newTag);
@@ -84,7 +85,7 @@ export function setTimeFilter(filter: FilterData, columns: Column[]) {
});
}
} else {
cleanOldTags(filter.title);
cleanOldTags(filter?.title);
}
}
@@ -102,7 +103,7 @@ export function setSizeFilter(filter: FilterData, columns: Column[]) {
return prev;
});
if (sizeRange) {
cleanOldTags(filter.title);
cleanOldTags(filter?.title);
const newTag = {
tag: `**${filter.title}** is **${sizeRange.label}**`,
@@ -114,7 +115,7 @@ export function setSizeFilter(filter: FilterData, columns: Column[]) {
});
}
} else {
cleanOldTags(filter.title);
cleanOldTags(filter?.title);
}
}
@@ -127,19 +128,18 @@ export function setStatusCodeFilter(filter: FilterData, columns: Column[]) {
const codeRange = ranges.find((c) => c?.value && c.value === statusCodeTag.value);
if (codeRange) {
cleanOldTags(filter.title);
cleanOldTags(filter?.title);
const newTag = {
tag: `**${filter.title}** is **${codeRange.label}**`,
value: statusCodeTag.value
};
console.log(codeRange);
parsedTags.update((tags) => {
tags.push(newTag);
return tags;
});
}
} else {
cleanOldTags(filter.title);
cleanOldTags(filter?.title);
}
}
@@ -158,7 +158,7 @@ export function setDateFilter(filter: FilterData, columns: Column[]) {
return prev;
});
if (dateRange) {
cleanOldTags(filter.title);
cleanOldTags(filter?.title);
const newTag = {
tag: `**${filter.title}** is **${dateRange.label}**`,
value: dateTag.value
@@ -169,11 +169,12 @@ export function setDateFilter(filter: FilterData, columns: Column[]) {
});
}
} else {
cleanOldTags(filter.title);
cleanOldTags(filter?.title);
}
}
function cleanOldTags(title: string) {
if (!title) return;
parsedTags.update((tags) => {
tags = tags.filter((tag) => !tag.tag.includes(`**${title}**`));
return tags;
@@ -181,6 +182,7 @@ function cleanOldTags(title: string) {
}
export function resetOptions(filter: FilterData) {
if (!filter?.options) return;
filter.options.forEach((option) => {
option.checked = false;
});
@@ -12,6 +12,7 @@
import { addNotification } from '$lib/stores/notifications';
import { Click, trackEvent } from '$lib/actions/analytics';
import RepositoryBehaviour from '$lib/components/git/repositoryBehaviour.svelte';
import { page } from '$app/state';
let {
show = $bindable(false),
@@ -38,12 +39,14 @@
let error = $state('');
onMount(async () => {
installations = await sdk.forProject.vcs.listInstallations();
installations = await sdk
.forProject(page.params.region, page.params.project)
.vcs.listInstallations();
if (!$installation?.$id && installations?.total) {
$installation = installations.installations[0];
}
selectedInstallationId = installations.total ? installations.installations[0]?.$id : '';
if (!!installations?.total) {
if (installations?.total) {
repositoryBehaviour = 'existing';
}
});
@@ -51,11 +54,9 @@
async function connectRepo() {
try {
if (repositoryBehaviour === 'new') {
const repo = await sdk.forProject.vcs.createRepository(
$installation.$id,
repositoryName,
repositoryPrivate
);
const repo = await sdk
.forProject(page.params.region, page.params.project)
.vcs.createRepository($installation.$id, repositoryName, repositoryPrivate);
repository.set(repo);
selectedRepository = repo.id;
}
@@ -42,12 +42,12 @@
</Link>
{#if sortedDomains.length > 1}
<Popover padding="none" let:toggle>
<Popover padding="none" let:toggle placement="bottom-end">
<Tag size="xs" on:click={toggle}>
+{sortedDomains.length - 1}
</Tag>
<svelte:fragment slot="tooltip">
<ActionMenu.Root>
<ActionMenu.Root width="20px">
{#each sortedDomains as rule, i}
{#if i !== 0}
<ActionMenu.Item.Anchor
@@ -1,9 +1,10 @@
<script lang="ts">
import { Button, InputSelect, InputText } from '$lib/elements/forms';
import { Button, InputText } from '$lib/elements/forms';
import { Fieldset, Input, Layout, Selector, Skeleton } from '@appwrite.io/pink-svelte';
import SelectRootModal from './selectRootModal.svelte';
import { sdk } from '$lib/stores/sdk';
import { sortBranches } from '$lib/stores/vcs';
import { page } from '$app/state';
export let branch = 'main';
export let rootDir: string;
@@ -15,10 +16,9 @@
let show = false;
async function loadBranches() {
const { branches } = await sdk.forProject.vcs.listRepositoryBranches(
installationId,
repositoryId
);
const { branches } = await sdk
.forProject(page.params.region, page.params.project)
.vcs.listRepositoryBranches(installationId, repositoryId);
const sorted = sortBranches(branches);
branch = sorted[0]?.name ?? null;
+156 -131
View File
@@ -3,7 +3,7 @@
import { Button, InputSearch, InputSelect } from '$lib/elements/forms';
import { timeFromNow } from '$lib/helpers/date';
import { sdk } from '$lib/stores/sdk';
import { repositories } from '$routes/(console)/project-[project]/functions/function-[function]/store';
import { repositories } from '$routes/(console)/project-[region]-[project]/functions/function-[function]/store';
import { installation, installations, repository } from '$lib/stores/vcs';
import {
Layout,
@@ -11,7 +11,6 @@
Typography,
Icon,
Avatar,
Skeleton,
Button as PinkButton
} from '@appwrite.io/pink-svelte';
import { IconLockClosed, IconPlus } from '@appwrite.io/pink-icons-svelte';
@@ -20,6 +19,10 @@
import { VCSDetectionType, type Models } from '@appwrite.io/console';
import { getFrameworkIcon } from '$lib/stores/sites';
import { connectGitHub } from '$lib/stores/git';
import { page } from '$app/state';
import Card from '../card.svelte';
import SkeletonRepoList from './skeletonRepoList.svelte';
import { untrack } from 'svelte';
let {
action = $bindable('select'),
@@ -41,11 +44,12 @@
let search = $state('');
let selectedInstallation = $state(null);
let isLoadingRepositories = $state(null);
async function loadInstallations() {
if (installationList) {
if (installationList.installations.length) {
selectedInstallation = installationList.installations[0].$id;
untrack(() => (selectedInstallation = installationList.installations[0].$id));
installation.set(
installationList.installations.find(
(entry) => entry.$id === selectedInstallation
@@ -54,9 +58,11 @@
}
return installationList.installations;
} else {
const { installations } = await sdk.forProject.vcs.listInstallations();
const { installations } = await sdk
.forProject(page.params.region, page.params.project)
.vcs.listInstallations();
if (installations.length) {
selectedInstallation = installations[0].$id;
untrack(() => (selectedInstallation = installations[0].$id));
installation.set(installations.find((entry) => entry.$id === selectedInstallation));
}
return installations;
@@ -72,9 +78,6 @@
await fetchRepos(installationId, search);
}
$repositories.search = search;
$repositories.installationId = installationId;
if ($repositories.repositories.length && action === 'select') {
selectedRepository = $repositories.repositories[0].id;
$repository = $repositories.repositories[0];
@@ -85,22 +88,31 @@
async function fetchRepos(installationId: string, search: string) {
if (product === 'functions') {
$repositories.repositories = (
(await sdk.forProject.vcs.listRepositories(
installationId,
VCSDetectionType.Runtime,
search || undefined
)) as unknown as Models.ProviderRepositoryRuntimeList
(await sdk
.forProject(page.params.region, page.params.project)
.vcs.listRepositories(
installationId,
VCSDetectionType.Runtime,
search || undefined
)) as unknown as Models.ProviderRepositoryRuntimeList
).runtimeProviderRepositories; //TODO: remove forced cast after backend fixes
} else {
$repositories.repositories = (
(await sdk.forProject.vcs.listRepositories(
installationId,
VCSDetectionType.Framework,
search || undefined
)) as unknown as Models.ProviderRepositoryFrameworkList
(await sdk
.forProject(page.params.region, page.params.project)
.vcs.listRepositories(
installationId,
VCSDetectionType.Framework,
search || undefined
)) as unknown as Models.ProviderRepositoryFrameworkList
).frameworkProviderRepositories;
}
$repositories.search = search;
$repositories.installationId = installationId;
}
selectedRepository;
</script>
{#if hasInstallations}
@@ -145,132 +157,145 @@
installation.set(
installations.find((entry) => entry.$id === selectedInstallation)
);
isLoadingRepositories = true;
loadRepositories(selectedInstallation, search).then(() => {
isLoadingRepositories = false;
});
}}
bind:value={selectedInstallation} />
<InputSearch placeholder="Search repositories" bind:value={search} />
<InputSearch
placeholder="Search repositories"
bind:value={search}
disabled={!search && !$repositories?.repositories?.length} />
</Layout.Stack>
{/await}
</Layout.Stack>
{#if selectedInstallation}
{#await loadRepositories(selectedInstallation, search)}
<Table.Root columns={1} let:root>
{#each Array(4) as _}
<Table.Row.Base {root}>
<Table.Cell {root}>
<Layout.Stack direction="row" alignItems="center">
<Skeleton variant="circle" width={24} />
<Layout.Stack gap="s" direction="row" alignItems="center">
<Skeleton variant="line" width={200} height={20} />
</Layout.Stack>
<Skeleton variant="line" width={76} height={32} />
</Layout.Stack>
</Table.Cell>
</Table.Row.Base>
{/each}
</Table.Root>
{:then response}
{#if response?.length}
<Paginator items={response} hideFooter={response?.length <= 6} limit={6}>
{#snippet children(
paginatedItems: Models.ProviderRepositoryRuntime[] &
Models.ProviderRepositoryFramework[]
)}
<Table.Root columns={1} let:root>
{#each paginatedItems as repo}
<Table.Row.Base {root}>
<Table.Cell {root}>
<Layout.Stack
direction="row"
alignItems="center"
gap="s">
{#if action === 'select'}
<input
class="is-small u-margin-inline-end-8"
type="radio"
name="repositories"
bind:group={selectedRepository}
onchange={() => repository.set(repo)}
value={repo.id} />
{/if}
{#if product === 'sites'}
{#if repo?.framework && repo.framework !== 'other'}
<Avatar size="xs" alt={repo.name}>
<!-- manual installation change -->
{#if isLoadingRepositories}
<SkeletonRepoList />
{:else}
{#await loadRepositories(selectedInstallation, search)}
<SkeletonRepoList />
{:then response}
{#if response?.length}
<Paginator items={response} hideFooter={response?.length <= 6} limit={6}>
{#snippet children(
paginatedItems: Models.ProviderRepositoryRuntime[] &
Models.ProviderRepositoryFramework[]
)}
<Table.Root columns={1} let:root>
{#each paginatedItems as repo}
<Table.Row.Base {root}>
<Table.Cell {root}>
<Layout.Stack
direction="row"
alignItems="center"
gap="s">
{#if action === 'select'}
<input
class="is-small u-margin-inline-end-8"
type="radio"
name="repositories"
bind:group={selectedRepository}
onchange={() => repository.set(repo)}
value={repo.id} />
{/if}
{#if product === 'sites'}
{#if repo?.framework && repo.framework !== 'other'}
<Avatar size="xs" alt={repo.name}>
<SvgIcon
name={getFrameworkIcon(
repo.framework
)}
iconSize="small" />
</Avatar>
{:else}
<Avatar
size="xs"
alt={repo.name}
empty />
{/if}
{:else}
{@const iconName = repo?.runtime
? repo.runtime.split('-')[0]
: undefined}
<Avatar
size="xs"
alt={repo.name}
empty={!iconName}>
<SvgIcon
name={getFrameworkIcon(
repo.framework
)}
name={iconName}
iconSize="small" />
</Avatar>
{:else}
<Avatar size="xs" alt={repo.name} empty />
{/if}
{:else}
{@const iconName = repo?.runtime
? repo.runtime.split('-')[0]
: undefined}
<Avatar
size="xs"
alt={repo.name}
empty={!iconName}>
<SvgIcon name={iconName} iconSize="small" />
</Avatar>
{/if}
<Layout.Stack
gap="s"
direction="row"
alignItems="center">
<Typography.Text
truncate
color="--fgcolor-neutral-secondary">
{repo.name}
</Typography.Text>
{#if repo.private}
<Icon
size="s"
icon={IconLockClosed}
color="--fgcolor-neutral-tertiary" />
{/if}
<time datetime={repo.pushedAt}>
<Typography.Caption
variant="400"
truncate
color="--fgcolor-neutral-tertiary">
{timeFromNow(repo.pushedAt)}
</Typography.Caption>
</time>
</Layout.Stack>
{#if action === 'button'}
<Layout.Stack
gap="s"
direction="row"
justifyContent="flex-end">
<PinkButton.Button
size="xs"
variant="secondary"
on:click={() => connect(repo)}>
Connect
</PinkButton.Button>
alignItems="center">
<Typography.Text
truncate
color="--fgcolor-neutral-secondary">
{repo.name}
</Typography.Text>
{#if repo.private}
<Icon
size="s"
icon={IconLockClosed}
color="--fgcolor-neutral-tertiary" />
{/if}
<time datetime={repo.pushedAt}>
<Typography.Caption
variant="400"
truncate
color="--fgcolor-neutral-tertiary">
{timeFromNow(repo.pushedAt)}
</Typography.Caption>
</time>
</Layout.Stack>
{/if}
</Layout.Stack>
</Table.Cell>
</Table.Row.Base>
{/each}
</Table.Root>
{/snippet}
</Paginator>
{:else}
<EmptySearch hidePages hidePagination bind:search target="repositories">
<svelte:fragment slot="actions">
{#if search}
<Button secondary on:click={() => (search = '')}>
Clear search
</Button>
{/if}
</svelte:fragment>
</EmptySearch>
{/if}
{/await}
{#if action === 'button'}
<Layout.Stack
direction="row"
justifyContent="flex-end">
<PinkButton.Button
size="xs"
variant="secondary"
on:click={() => connect(repo)}>
Connect
</PinkButton.Button>
</Layout.Stack>
{/if}
</Layout.Stack>
</Table.Cell>
</Table.Row.Base>
{/each}
</Table.Root>
{/snippet}
</Paginator>
{:else if search}
<EmptySearch hidePages hidePagination bind:search target="repositories">
<svelte:fragment slot="actions">
{#if search}
<Button secondary on:click={() => (search = '')}>
Clear search
</Button>
{/if}
</svelte:fragment>
</EmptySearch>
{:else}
<Card>
<Layout.Stack alignItems="center" justifyContent="center">
<Typography.Text
variation="m-500"
color="--fgcolor-neutral-tertiary">
No repositories available
</Typography.Text>
</Layout.Stack>
</Card>
{/if}
{/await}
{/if}
{/if}
</Layout.Stack>
{:else}
+19 -22
View File
@@ -1,4 +1,5 @@
<script lang="ts">
import { page } from '$app/state';
import { Modal } from '$lib/components';
import { Button } from '$lib/elements/forms';
import { iconPath } from '$lib/stores/app';
@@ -39,12 +40,9 @@
onMount(async () => {
try {
const content = await sdk.forProject.vcs.getRepositoryContents(
$installation.$id,
$repository.id,
currentPath
);
// console.log(content);
const content = await sdk
.forProject(page.params.region, page.params.project)
.vcs.getRepositoryContents($installation.$id, $repository.id, currentPath);
directories[0].fileCount = content.contents?.length ?? 0;
directories[0].children = content.contents
.filter((e) => e.isDirectory)
@@ -56,10 +54,9 @@
loading: false
}));
currentDir = directories[0];
// console.log(directories);
isLoading = false;
} catch (e) {
console.log(e);
} catch {
return;
}
});
@@ -83,12 +80,10 @@
directories = [...directories];
try {
const content = await sdk.forProject.vcs.getRepositoryContents(
$installation.$id,
$repository.id,
path
);
// console.log(content);
const content = await sdk
.forProject(page.params.region, page.params.project)
.vcs.getRepositoryContents($installation.$id, $repository.id, path);
const fileCount = content.contents?.length ?? 0;
const contentDirectories = content.contents.filter((e) => e.isDirectory);
@@ -103,12 +98,14 @@
fileCount: undefined,
thumbnailUrl: undefined
}));
const runtime = await sdk.forProject.vcs.createRepositoryDetection(
$installation.$id,
$repository.id,
product === 'sites' ? VCSDetectionType.Framework : VCSDetectionType.Runtime,
path
);
const runtime = await sdk
.forProject(page.params.region, page.params.project)
.vcs.createRepositoryDetection(
$installation.$id,
$repository.id,
product === 'sites' ? VCSDetectionType.Framework : VCSDetectionType.Runtime,
path
);
if (product === 'sites') {
currentDir.children.forEach((dir) => {
dir.thumbnailUrl = $iconPath(runtime.framework, 'color');
@@ -145,6 +142,6 @@
<svelte:fragment slot="footer">
<Button secondary on:click={() => (show = false)}>Cancel</Button>
<Button submit>Save</Button>
<Button submit disabled={isLoading}>Save</Button>
</svelte:fragment>
</Modal>
@@ -0,0 +1,20 @@
<script lang="ts">
import { Layout, Table, Skeleton } from '@appwrite.io/pink-svelte';
</script>
<Table.Root columns={1} let:root>
{#each Array(4) as _}
<Table.Row.Base {root}>
<Table.Cell {root}>
<Layout.Stack direction="row" alignItems="center">
<Skeleton variant="circle" width={24} />
<Layout.Stack gap="s" direction="row" alignItems="center">
<Skeleton variant="line" width={200} height={20} />
</Layout.Stack>
<Skeleton variant="line" width={76} height={32} />
</Layout.Stack>
</Table.Cell>
</Table.Row.Base>
{/each}
</Table.Root>
+11 -9
View File
@@ -1,12 +1,5 @@
<script lang="ts">
import { Icon, Tag } from '@appwrite.io/pink-svelte';
import { Copy } from '.';
import { IconDuplicate } from '@appwrite.io/pink-icons-svelte';
export let value: string;
export let event: string = null;
function truncateText(node: HTMLElement) {
<script context="module" lang="ts">
export function truncateText(node: HTMLElement) {
const MAX_TRIES = 100;
let originalText = node.textContent;
function checkOverflow() {
@@ -48,6 +41,15 @@
}
</script>
<script lang="ts">
import { Icon, Tag } from '@appwrite.io/pink-svelte';
import { Copy } from '.';
import { IconDuplicate } from '@appwrite.io/pink-icons-svelte';
export let value: string;
export let event: string = null;
</script>
<Copy {value} {event}>
<Tag size="xs" variant="code">
<Icon icon={IconDuplicate} size="s" slot="start" />
+2
View File
@@ -82,3 +82,5 @@ 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 RegionEndpoint } from './regionEndpoint.svelte';
export { default as ExpirationInput } from './expirationInput.svelte';
+18 -3
View File
@@ -1,9 +1,24 @@
<script lang="ts">
import { Card, Tooltip } from '@appwrite.io/pink-svelte';
import { type ComponentProps } from 'svelte';
import type Selector from '@appwrite.io/pink-svelte/dist/card/Selector.svelte';
import type { HTMLAttributes } from 'svelte/elements';
import type { BaseCardProps } from './card.svelte';
import type { ComponentType } from 'svelte';
type Props = ComponentProps<Selector>;
type Props = BaseCardProps &
HTMLAttributes<HTMLInputElement> & {
name: string;
value: string;
group: string;
title: string;
info?: string | undefined;
icon?: ComponentType;
imageHeight?: number;
imageWidth?: number;
imageRadius?: 'xxs' | 'xs' | 's' | 'm' | 'l';
disabled?: boolean;
src?: string;
alt?: string | undefined;
};
export let group: string;
export let value: string;
+2 -2
View File
@@ -11,7 +11,7 @@
<Alert.Inline status="info" title="Your logs are disabled">
To view logs and errors, enable them in your
<Link
href={`${base}/project-${page.params.project}/sites/site-${page.params.site}/settings`}>
href={`${base}/project-${page.params.region}-${page.params.project}/sites/site-${page.params.site}/settings`}>
site settings</Link
>.
</Alert.Inline>
@@ -19,7 +19,7 @@
<Alert.Inline status="info" title="Your execution logs are disabled">
To view execution logs and errors, enable them in your
<Link
href={`${base}/project-${page.params.project}/functions/function-${page.params.function}/settings`}>
href={`${base}/project-${page.params.region}-${page.params.project}/functions/function-${page.params.function}/settings`}>
function settings</Link
>.
</Alert.Inline>
+16 -5
View File
@@ -13,11 +13,22 @@
} from '@appwrite.io/pink-svelte';
import { onMount } from 'svelte';
export let selectedLog: Models.Execution;
let {
selectedLog,
product
}: {
selectedLog: Models.Execution;
product: 'site' | 'function';
} = $props();
let requestTab: 'parameters' | 'headers' = 'parameters';
let requestTab: 'parameters' | 'headers' = $state('parameters');
let parameters = [];
let parameters = $state([]);
const href =
product === 'site'
? 'https://appwrite.io/docs/products/sites/logs#log-details'
: 'https://appwrite.io/docs/products/functions/develop#logging';
onMount(() => {
try {
@@ -121,8 +132,8 @@
<Input.Helper state="default">
<span>
Missing headers? Check the <Link variant="muted" href="#" external>docs</Link> to
see the supported data and how to log it.
Missing headers? Check the <Link variant="muted" {href} external>docs</Link> to see
the supported data and how to log it.
</span>
</Input.Helper>
{:else}
+14 -20
View File
@@ -16,10 +16,6 @@
import { onMount } from 'svelte';
import LoggingAlert from './loggingAlert.svelte';
// export let selectedLog: Models.Execution;
// export let product: 'site' | 'function';
// export let logging: boolean;
let {
selectedLog,
product,
@@ -32,6 +28,11 @@
let responseTab: 'logs' | 'errors' | 'headers' | 'body' = $state('logs');
const href =
product === 'site'
? 'https://appwrite.io/docs/products/sites/logs#log-details'
: 'https://appwrite.io/docs/products/functions/develop#logging';
onMount(() => {
if (selectedLog?.errors) {
responseTab = 'errors';
@@ -52,14 +53,12 @@
on:click={() => (responseTab = 'logs')}>
Logs
</Tabs.Item.Button>
{#if product !== 'site'}
<Tabs.Item.Button
{root}
active={responseTab === 'errors'}
on:click={() => (responseTab = 'errors')}>
Errors
</Tabs.Item.Button>
{/if}
<Tabs.Item.Button
{root}
active={responseTab === 'errors'}
on:click={() => (responseTab = 'errors')}>
Errors
</Tabs.Item.Button>
<Tabs.Item.Button
{root}
active={responseTab === 'headers'}
@@ -128,8 +127,8 @@
<Input.Helper state="default">
<span>
Missing headers? Check the <Link variant="muted" href="#" external>docs</Link> to
see the supported data and how to log it.
Missing headers? Check the <Link variant="muted" {href} external>docs</Link> to see
the supported data and how to log it.
</span>
</Input.Helper>
{:else}
@@ -146,12 +145,7 @@
Body data is not captured by Appwrite for your user's security and privacy. To
display body data in the Logs tab, use <InlineCode
code="context.log()"
size="s" />. <Link
external
href="https://appwrite.io/docs/products/functions/develop#logging"
variant="muted">
Learn more</Link
>.
size="s" />. <Link external {href} variant="muted">Learn more</Link>.
</Typography.Text>
</Card>
{/if}
+20 -4
View File
@@ -1,23 +1,39 @@
<script lang="ts">
import { setContext, onMount } from 'svelte';
import { Card } from '@appwrite.io/pink-svelte';
import { createMenubar, melt } from '@melt-ui/svelte';
import { menuOpen } from '$lib/components/menu/store';
import { activeMenuId, menuOpen } from '$lib/components/menu/store';
const menuId = Math.random().toString(36).slice(2);
const {
elements: { menubar },
builders: { createMenu }
} = createMenubar();
const {
elements: { trigger: trigger, menu: menu, separator: separator },
states: { open }
elements: { trigger, menu, separator },
states: { open },
builders // for submenu for same toggle state
} = createMenu();
function toggle() {
open.update((state) => !state);
}
$: menuOpen.set($open);
open.subscribe((state) => {
if (state) activeMenuId.set(menuId);
else activeMenuId.update((current) => (current === menuId ? null : current));
});
onMount(() => {
return activeMenuId.subscribe((id) => {
if (id !== menuId) open.set(false);
});
});
$: menuOpen.set($open as boolean);
setContext('menuBuilder', { builders, separator });
</script>
<div use:melt={$menubar}>
+14
View File
@@ -1,3 +1,17 @@
import { writable } from 'svelte/store';
import type { createMenubar } from '@melt-ui/svelte';
export const menuOpen = writable(false);
export const activeMenuId = writable<string | null>(null);
/**
* Full menu context passed to submenus
* includes `builders` and shared `separator`.
*/
export type MenuContext = {
separator: ReturnType<
ReturnType<typeof createMenubar>['builders']['createMenu']
>['elements']['separator'];
builders: ReturnType<ReturnType<typeof createMenubar>['builders']['createMenu']>['builders'];
};
+6 -10
View File
@@ -1,15 +1,12 @@
<script lang="ts">
import { getContext } from 'svelte';
import { melt } from '@melt-ui/svelte';
import type { MenuContext } from './store';
import { Card } from '@appwrite.io/pink-svelte';
import { createMenubar, melt } from '@melt-ui/svelte';
const {
builders: { createMenu }
} = createMenubar();
const {
elements: { separator: separator },
builders: { createSubmenu: createSubmenu }
} = createMenu();
// get parent builder for toggle state!
const { builders, separator } = getContext<MenuContext>('menuBuilder');
const { createSubmenu } = builders;
const {
elements: { subMenu: subMenu, subTrigger: subTrigger }
@@ -37,7 +34,6 @@
<style>
.subMenu {
min-width: 244px;
margin-inline: -4px;
margin-block: -4px;
}
</style>
+11 -8
View File
@@ -1,7 +1,8 @@
<script lang="ts" context="module">
import { page } from '$app/state';
import { parseIfString } from '$lib/helpers/object';
import { getProjectId } from '$lib/helpers/project';
import { sdk } from '$lib/stores/sdk';
import { realtime } from '$lib/stores/sdk';
import type { Models } from '@appwrite.io/console';
import { onMount } from 'svelte';
import { writable } from 'svelte/store';
@@ -49,13 +50,15 @@
})();
onMount(() => {
return sdk.forConsole.client.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, 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;
}
});
});
</script>
+2 -1
View File
@@ -6,6 +6,7 @@
export let show = false;
export let size: 'small' | 'big' | 'huge' = null;
export let closable = true;
export let closeByEscape = true;
export let headerDivider = true;
export let style = '';
@@ -48,7 +49,7 @@
}
function handleKeydown(event: KeyboardEvent) {
if (event.key === 'Escape') {
if (event.key === 'Escape' && closeByEscape) {
event.preventDefault();
trackEvent(Click.ModalCloseClick, {
from: 'escape'
+12 -2
View File
@@ -2,10 +2,20 @@
export type NavbarProject = {
name: string;
$id: string;
region: string;
isSelected: boolean;
platformCount: number;
pingCount: number;
};
export type BaseNavbarProps = HTMLAttributes<HTMLHeadElement> & {
logo: {
src: string;
alt: string;
};
avatar: string;
sideBarIsOpen: boolean;
};
</script>
<script lang="ts">
@@ -23,7 +33,6 @@
Typography
} from '@appwrite.io/pink-svelte';
import { toggleCommandCenter } from '$lib/commandCenter/commandCenter.svelte';
import type { BaseNavbarProps } from '@appwrite.io/pink-svelte/dist/navbar/Base.svelte';
import {
IconChevronRight,
IconLogoutRight,
@@ -45,6 +54,7 @@
import { isCloud } from '$lib/system.js';
import { user } from '$lib/stores/user';
import { Click, trackEvent } from '$lib/actions/analytics';
import type { HTMLAttributes } from 'svelte/elements';
let showSupport = false;
@@ -125,7 +135,7 @@
{#if selectedProject && selectedProject.pingCount === 0}
<div class="only-desktop" style:margin-inline-start="-16px">
<Button.Anchor
href={`${base}/project-${selectedProject.$id}/get-started`}
href={`${base}/project-${selectedProject.region}-${selectedProject.$id}/get-started`}
variant="secondary"
size="xs">Connect</Button.Anchor>
</div>
+24 -18
View File
@@ -12,7 +12,7 @@
$: totalPages = Math.ceil(total / limit);
$: currentPage = Math.floor(offset / limit + 1);
$: pages = pagination(currentPage, totalPages);
// $: pages = pagination(currentPage, totalPages);
function handleOptionClick(e: CustomEvent) {
if (currentPage !== e.detail) {
@@ -35,26 +35,32 @@
}
}
function pagination(page: number, total: number) {
const pagesShown = 5;
const start = Math.max(
1,
Math.min(page - Math.floor((pagesShown - 3) / 2), total - pagesShown + 2)
);
const end = Math.min(
total,
Math.max(page + Math.floor((pagesShown - 2) / 2), pagesShown - 1)
);
return [
...(start > 2 ? [1, '...'] : start > 1 ? [1] : []),
...Array.from({ length: end + 1 - start }, (_, i) => i + start),
...(end < total - 1 ? ['...', total] : end < total ? [total] : [])
];
}
// function pagination(page: number, total: number) {
// const pagesShown = 5;
// const start = Math.max(
// 1,
// Math.min(page - Math.floor((pagesShown - 3) / 2), total - pagesShown + 2)
// );
// const end = Math.min(
// total,
// Math.max(page + Math.floor((pagesShown - 2) / 2), pagesShown - 1)
// );
// return [
// ...(start > 2 ? [1, '...'] : start > 1 ? [1] : []),
// ...Array.from({ length: end + 1 - start }, (_, i) => i + start),
// ...(end < total - 1 ? ['...', total] : end < total ? [total] : [])
// ];
// }
</script>
{#if !hidePages}
<Pagination {limit} page={currentPage} {total} type="button" on:page={handleOptionClick} />
<Pagination
{limit}
page={currentPage}
{total}
type="button"
on:page={handleOptionClick}
createLink={undefined as never} />
{:else}
<Layout.Stack direction="row" inline>
<Button.Button
+2 -2
View File
@@ -12,6 +12,7 @@
hasLimit = false,
name = 'items',
gap = 's',
offset = $bindable(0),
children
}: {
items: T[];
@@ -23,13 +24,12 @@
gap?:
| ('none' | 'xxxs' | 'xxs' | 'xs' | 's' | 'm' | 'l' | 'xl' | 'xxl' | 'xxxl')
| undefined;
offset?: number;
children: Snippet<[T[], number]>;
} = $props();
let total = $derived(items.length);
let offset = $state(0);
let paginatedItems = $derived(items.slice(offset, offset + limit));
</script>
+34 -12
View File
@@ -12,6 +12,7 @@
export let showTeam: boolean;
export let showLabel: boolean;
export let showCustom: boolean;
export let hideOnClick: boolean = false;
export let groups: Writable<Map<string, Permission>>;
const dispatch = createEventDispatcher();
@@ -19,31 +20,52 @@
<Popover let:toggle padding="none" placement="bottom-start">
<slot {toggle} />
<svelte:fragment slot="tooltip">
<svelte:fragment slot="tooltip" let:hide>
<ActionMenu.Root>
<ActionMenu.Item.Button
disabled={$groups.has('any')}
on:click={() => dispatch('create', ['any'])}>
on:click={(e) => {
if (hideOnClick) hide(e);
dispatch('create', ['any']);
}}>
Any
</ActionMenu.Item.Button>
<ActionMenu.Item.Button
disabled={$groups.has('guests')}
on:click={() => dispatch('create', ['guests'])}>
on:click={(e) => {
if (hideOnClick) hide(e);
dispatch('create', ['guests']);
}}>
All guests
</ActionMenu.Item.Button>
<ActionMenu.Item.Button
disabled={$groups.has('users')}
on:click={() => dispatch('create', ['users'])}>
on:click={(e) => {
if (hideOnClick) hide(e);
dispatch('create', ['users']);
}}>
All users
</ActionMenu.Item.Button>
<ActionMenu.Item.Button on:click={() => (showUser = true)}
>Select users</ActionMenu.Item.Button>
<ActionMenu.Item.Button on:click={() => (showTeam = true)}
>Select teams</ActionMenu.Item.Button>
<ActionMenu.Item.Button on:click={() => (showLabel = true)}
>Label</ActionMenu.Item.Button>
<ActionMenu.Item.Button on:click={() => (showCustom = true)}
>Custom permission</ActionMenu.Item.Button>
<ActionMenu.Item.Button
on:click={(e) => {
showUser = true;
if (hideOnClick) hide(e);
}}>Select users</ActionMenu.Item.Button>
<ActionMenu.Item.Button
on:click={(e) => {
showTeam = true;
if (hideOnClick) hide(e);
}}>Select teams</ActionMenu.Item.Button>
<ActionMenu.Item.Button
on:click={(e) => {
showLabel = true;
if (hideOnClick) hide(e);
}}>Label</ActionMenu.Item.Button>
<ActionMenu.Item.Button
on:click={(e) => {
showCustom = true;
if (hideOnClick) hide(e);
}}>Custom permission</ActionMenu.Item.Button>
</ActionMenu.Root>
</svelte:fragment>
</Popover>
@@ -21,6 +21,7 @@
import type { PinkColumn } from '$lib/helpers/types';
export let withCreate = false;
export let hideOnClick = false;
export let permissions: string[] = [];
let showUser = false;
@@ -127,11 +128,11 @@
}
const columns: PinkColumn[] = [
{ id: 'role', width: { min: 100 } },
{ id: 'create', width: { min: 100 }, hide: !withCreate },
{ id: 'read', width: { min: 100 } },
{ id: 'update', width: { min: 100 } },
{ id: 'delete', width: { min: 100 } },
{ id: 'role', width: { min: 80 } },
{ id: 'create', width: { min: 80 }, hide: !withCreate },
{ id: 'read', width: { min: 80 } },
{ id: 'update', width: { min: 80 } },
{ id: 'delete', width: { min: 80 } },
{ id: 'action', width: 40 }
];
</script>
@@ -195,6 +196,7 @@
bind:showTeam
bind:showUser
{groups}
{hideOnClick}
on:create={create}
let:toggle>
<Button secondary on:click={toggle}>
@@ -213,6 +215,7 @@
bind:showTeam
bind:showUser
{groups}
{hideOnClick}
on:create={create}
let:toggle>
<Button compact icon on:click={toggle}>
+10 -4
View File
@@ -27,10 +27,16 @@
const role = permission.split(':')[0];
const id = permission.split(':')[1].split('/')[0];
if (role === 'user') {
return await sdk.forProject.users.get(id);
const user = await sdk
.forProject(page.params.region, page.params.project)
.users.get(id);
return user;
}
if (role === 'team') {
return await sdk.forProject.teams.get(id);
const team = await sdk
.forProject(page.params.region, page.params.project)
.teams.get(id);
return team;
}
}
</script>
@@ -82,7 +88,7 @@
{/if}
<div>
<Button.Anchor
href={`${base}/project-${page.params.project}/auth/user-${data?.$id}`}
href={`${base}/project-${page.params.region}-${page.params.project}/auth/user-${data?.$id}`}
size="xs"
target="_blank"
variant="secondary">
@@ -94,7 +100,7 @@
<Typography.Text>Members: {data?.total}</Typography.Text>
<div>
<Button.Anchor
href={`${base}/project-${page.params.project}/auth/teams/team-${data?.$id}`}
href={`${base}/project-${page.params.region}-${page.params.project}/auth/teams/team-${data?.$id}`}
size="s"
target="_blank"
variant="secondary">
+4 -4
View File
@@ -15,6 +15,7 @@
Table,
Typography
} from '@appwrite.io/pink-svelte';
import { page } from '$app/state';
export let show: boolean;
export let groups: Writable<Map<string, Permission>>;
@@ -40,10 +41,9 @@
async function request() {
if (!show) return;
results = await sdk.forProject.teams.list(
[Query.limit(5), Query.offset(offset)],
search || undefined
);
results = await sdk
.forProject(page.params.region, page.params.project)
.teams.list([Query.limit(5), Query.offset(offset)], search || undefined);
}
function onSelection(role: string) {
+4 -4
View File
@@ -18,6 +18,7 @@
Typography
} from '@appwrite.io/pink-svelte';
import { IconAnonymous, IconMinusSm } from '@appwrite.io/pink-icons-svelte';
import { page } from '$app/state';
export let show: boolean;
export let groups: Writable<Map<string, Permission>>;
@@ -43,10 +44,9 @@
async function request() {
if (!show) return;
results = await sdk.forProject.users.list(
[Query.limit(5), Query.offset(offset)],
search || undefined
);
results = await sdk
.forProject(page.params.region, page.params.project)
.users.list([Query.limit(5), Query.offset(offset)], search || undefined);
}
function onSelection(role: string) {
+42
View File
@@ -0,0 +1,42 @@
<script lang="ts">
import { Copy } from '.';
import { sdk } from '$lib/stores/sdk';
import { Flag } from '@appwrite.io/console';
import { truncateText } from '$lib/components/id.svelte';
import { isValueOfStringEnum } from '$lib/helpers/types';
import { getProjectEndpoint } from '$lib/helpers/project';
import { projectRegion } from '$routes/(console)/project-[region]-[project]/store';
$: flagSrc =
$projectRegion && isValueOfStringEnum(Flag, $projectRegion.flag)
? sdk.forConsole.avatars.getFlag($projectRegion.flag, 30, 20, 100)
: '';
</script>
{#if $projectRegion}
<Copy value={getProjectEndpoint()} copyText="Copy endpoint">
<div
class="flex u-gap-8 u-cross-center interactive-text-output is-buttons-on-top u-text-center"
style:min-inline-size="0"
style:display="inline-flex">
<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">
{$projectRegion?.name}
</span>
{#if flagSrc}
<img
style="border-radius: 2.5px"
src={flagSrc}
alt={$projectRegion?.name}
width={16}
height={12} />
{/if}
</div>
</Copy>
{/if}
+1 -1
View File
@@ -1,7 +1,7 @@
<script lang="ts">
import { page } from '$app/state';
import { goto } from '$app/navigation';
import { onMount, onDestroy } from 'svelte';
import { onDestroy } from 'svelte';
import { trackEvent } from '$lib/actions/analytics';
import { Icon, Input } from '@appwrite.io/pink-svelte';
import { IconSearch, IconX } from '@appwrite.io/pink-icons-svelte';
+26 -15
View File
@@ -12,7 +12,8 @@
Button,
Layout,
Avatar,
Typography
Typography,
Badge
} from '@appwrite.io/pink-svelte';
import {
@@ -38,10 +39,11 @@
import { Click, trackEvent } from '$lib/actions/analytics';
import type { HTMLAttributes } from 'svelte/elements';
import type { NavbarProject } from '$lib/components/navbar.svelte';
type $$Props = HTMLAttributes<HTMLElement> & {
state?: 'closed' | 'open' | 'icons';
project: { $id: string } | undefined;
project: NavbarProject | undefined;
avatar: string;
progressCard?: {
title: string;
@@ -77,7 +79,7 @@
{ name: 'Functions', icon: IconLightningBolt, slug: 'functions', category: 'build' },
{ name: 'Messaging', icon: IconChatBubble, slug: 'messaging', category: 'build' },
{ name: 'Storage', icon: IconFolder, slug: 'storage', category: 'build' },
{ name: 'Sites', icon: IconGlobeAlt, slug: 'sites', category: 'deploy' }
{ name: 'Sites', icon: IconGlobeAlt, slug: 'sites', category: 'deploy', badge: 'New' }
];
</script>
@@ -117,7 +119,7 @@
<Tooltip placement="right" disabled={state !== 'icons'}>
<a
class="progress-card"
href={`/console/project-${project.$id}/get-started`}
href={`/console/project-${project.region}-${project.$id}/get-started`}
on:click={() => {
trackEvent('click_menu_get_started');
sideBarIsOpen = false;
@@ -141,7 +143,7 @@
<Layout.Stack direction="column" gap="s">
<Tooltip placement="right" disabled={state !== 'icons'}>
<a
href={`/console/project-${project.$id}/overview`}
href={`/console/project-${project.region}-${project.$id}/overview`}
class="link"
class:active={page.url.pathname.includes('overview')}
on:click={() => {
@@ -171,7 +173,7 @@
{#each buildProjectOptions as projectOption}
<Tooltip placement="right" disabled={state !== 'icons'}>
<a
href={`/console/project-${project.$id}/${projectOption.slug}`}
href={`/console/project-${project.region}-${project.$id}/${projectOption.slug}`}
class="link"
class:active={page.url.pathname.includes(projectOption.slug)}
on:click={() => {
@@ -202,20 +204,29 @@
{#each deployProjectOptions as projectOption}
<Tooltip placement="right" disabled={state !== 'icons'}>
<a
href={`/console/project-${project.$id}/${projectOption.slug}`}
href={`/console/project-${project.region}-${project.$id}/${projectOption.slug}`}
class="link"
class:active={page.url.pathname.includes(projectOption.slug)}
on:click={() => {
trackEvent(`click_menu_${projectOption.slug}`);
sideBarIsOpen = false;
}}
><span class="link-icon"
><Icon icon={projectOption.icon} size="s" />
</span><span
}}>
<span class="link-icon">
<Icon icon={projectOption.icon} size="s" />
</span>
<span
class:no-text={state === 'icons'}
class:has-text={state === 'open'}
class="link-text">{projectOption.name}</span
></a>
class="link-text">
{projectOption.name}
{#if projectOption?.badge}
<Badge
variant="secondary"
content={projectOption.badge}
size="xs" />
{/if}
</span>
</a>
<span slot="tooltip">{projectOption.name}</span>
</Tooltip>
{/each}
@@ -225,7 +236,7 @@
<div class="only-mobile">
<Tooltip placement="right" disabled={state !== 'icons'}>
<a
href={`/console/project-${project.$id}/settings`}
href={`/console/project-${project.region}-${project.$id}/settings`}
on:click={() => {
trackEvent('click_menu_settings');
}}
@@ -283,7 +294,7 @@
<div class="only-desktop">
<Tooltip placement="right" disabled={state !== 'icons'}>
<a
href={`/console/project-${project.$id}/settings`}
href={`/console/project-${project.region}-${project.$id}/settings`}
class="link"
on:click={() => {
trackEvent('click_menu_settings');
+17 -5
View File
@@ -5,18 +5,30 @@
import { isSupportOnline, showSupportModal } from '$routes/(console)/wizard/support/store';
import { Click, trackEvent } from '$lib/actions/analytics';
import { localeShortTimezoneName, utcHourToLocaleHour } from '$lib/helpers/date';
import { upgradeURL } from '$lib/stores/billing';
import { plansInfo } from '$lib/stores/billing';
import { Card } from '$lib/components/index';
import { app } from '$lib/stores/app';
import { currentPlan } from '$lib/stores/organization';
import { currentPlan, type Organization, organizationList } from '$lib/stores/organization';
import { isCloud } from '$lib/system';
import { Typography } from '@appwrite.io/pink-svelte';
import { base } from '$app/paths';
export let show = false;
export let showHeader = true;
$: hasPremiumSupport = $currentPlan?.premiumSupport ?? false;
$: hasPremiumSupport = $currentPlan?.premiumSupport ?? allOrgsHavePremiumSupport ?? false;
$: allOrgsHavePremiumSupport = $organizationList.teams.every(
(team) => $plansInfo.get((team as Organization).billingPlan)?.premiumSupport
);
// there can only be one free organization
$: freeOrganization = $organizationList.teams.find(
(team) => !$plansInfo.get((team as Organization).billingPlan)?.premiumSupport
);
$: upgradeURL = `${base}/organization-${freeOrganization?.$id}/change-plan`;
$: supportTimings = `${utcHourToLocaleHour('16:00')} - ${utcHourToLocaleHour('00:00')} ${localeShortTimezoneName()}`;
@@ -55,7 +67,7 @@
}
];
const showCloudSupport = (index) => {
const showCloudSupport = (index: number) => {
return (index === 0 && isCloud) || index > 0;
};
</script>
@@ -79,7 +91,7 @@
<div class="u-flex u-gap-12 u-cross-center">
{#if !hasPremiumSupport}
<Button
href={$upgradeURL}
href={upgradeURL}
on:click={() => {
trackEvent(Click.OrganizationClickUpgrade, {
from: 'button',
+1 -2
View File
@@ -5,13 +5,12 @@
import { getElementDir } from '$lib/helpers/style';
import { waitUntil } from '$lib/helpers/waitUntil';
import { Tabs } from '@appwrite.io/pink-svelte';
import type { Variant } from '@appwrite.io/pink-svelte/dist/tabs/types';
export let selected = false;
export let href: string = null;
export let event: string = null;
export let noscroll = false;
export let root: { variant: Variant; stretch: boolean } = {
export let root: { variant: 'primary' | 'secondary'; stretch: boolean } = {
variant: 'primary',
stretch: false
};
+23
View File
@@ -3,6 +3,14 @@ export const CARD_LIMIT = 6; // default card limit
export const INTERVAL = 5 * 60000; // default interval to check for feedback
export const NEW_DEV_PRO_UPGRADE_COUPON = 'appw50';
export const REGION_FRA = 'fra';
export const REGION_SYD = 'syd';
export const REGION_NYC = 'nyc';
export const SUBDOMAIN_FRA = 'fra.';
export const SUBDOMAIN_SYD = 'syd.';
export const SUBDOMAIN_NYC = 'nyc.';
export enum Dependencies {
FACTORS = 'dependency:factors',
IDENTITIES = 'dependency:identities',
@@ -34,6 +42,7 @@ export enum Dependencies {
DOCUMENTS = 'dependency:documents',
BUCKET = 'dependency:bucket',
FILE = 'dependency:file',
FILE_TOKENS = 'dependency:file_tokens',
FILES = 'dependency:files',
FUNCTION = 'dependency:function',
FUNCTION_DOMAINS = 'dependency:function_domains',
@@ -47,6 +56,8 @@ export enum Dependencies {
PLATFORMS = 'dependency:platforms',
KEY = 'dependency:key',
KEYS = 'dependency:keys',
DEV_KEY = 'dependency:dev_key',
DEV_KEYS = 'dependency:dev_keys',
DOMAINS = 'dependency:domains',
DOMAIN = 'dependency:domains',
WEBHOOK = 'dependency:webhook',
@@ -367,6 +378,18 @@ export const scopes: {
category: 'Other',
icon: 'globe'
},
{
scope: 'tokens.read',
description: "Access to read your project's file tokens",
category: 'Other',
icon: 'globe'
},
{
scope: 'tokens.write',
description: 'Access to create file tokens',
category: 'Other',
icon: 'globe'
},
{
scope: 'sites.read',
description: "Access to read your project's sites and deployments",
+1
View File
@@ -2,6 +2,7 @@
import { isValueOfStringEnum } from '$lib/helpers/types';
import { sdk } from '$lib/stores/sdk';
import { Flag } from '@appwrite.io/console';
export let flag: string;
export let name: string = flag;
export let width = 40;
+1 -1
View File
@@ -49,7 +49,7 @@
{pattern}
on:input
on:invalid={handleInvalid}
type="email"
type="text"
helper={error}
state={error ? 'error' : 'default'}
autofocus={autofocus || undefined}
+40 -36
View File
@@ -1,12 +1,10 @@
<script lang="ts">
import { Input } from '@appwrite.io/pink-svelte';
import { onMount } from 'svelte';
import { Input, Layout, Selector } from '@appwrite.io/pink-svelte';
export let label: string = undefined;
export let id: string;
export let name: string = id;
export let helper: string = undefined;
export let value = '';
export let placeholder = '';
export let label: string = '';
export let value: string;
export let required = false;
export let nullable = false;
export let disabled = false;
@@ -16,18 +14,23 @@
export let step: number | 'any' = 0.001;
let error: string;
let element: HTMLInputElement;
function handleInvalid(event: Event) {
event.preventDefault();
const inputNode = event.currentTarget as HTMLInputElement;
if (inputNode.validity.valueMissing) {
error = 'This field is required';
return;
onMount(() => {
if (element && autofocus) {
element.focus();
}
});
error = inputNode.validationMessage;
let prevValue = '';
function handleNullChange(e: CustomEvent<boolean>) {
const isNull = e.detail;
if (isNull) {
prevValue = value;
value = null;
} else {
value = prevValue;
}
}
$: if (value) {
@@ -35,24 +38,25 @@
}
</script>
<Input.DateTime
{id}
{name}
{placeholder}
{disabled}
{required}
{label}
{step}
{nullable}
{readonly}
autofocus={autofocus || undefined}
autocomplete={autocomplete ? 'on' : 'off'}
helper={error || helper}
state={error ? 'error' : 'default'}
on:invalid={handleInvalid}
on:input
bind:value>
<slot name="start" slot="start" />
<slot name="info" slot="info" />
<slot name="end" slot="end" />
</Input.DateTime>
<Layout.Stack gap="s" direction="row">
<Input.DateTime
{id}
{label}
{disabled}
{readonly}
{required}
{value}
{step}
helper={error}
on:change={(event) => (value = (event.target as HTMLInputElement).value)}
autocomplete={autocomplete ? 'on' : 'off'}>
{#if nullable}
<Selector.Checkbox
size="s"
slot="end"
label="NULL"
checked={value === null}
on:change={handleNullChange} />
{/if}
</Input.DateTime>
</Layout.Stack>
@@ -25,7 +25,7 @@
<Label {optionalText} {tooltip} hide={!label}>
{label}{#if $$slots.popover && isPopoverDefined}
<Drop bind:show display="inline-block">
<!-- TODO: make unclicked icon greyed out and hover and clicked filled -->
<!-- TODO: make un-clicked icon greyed out and hover and clicked filled -->
&nbsp;<button
type="button"
on:click={() => (show = !show)}
+2 -1
View File
@@ -5,6 +5,7 @@
export let id: string;
export let label: string | undefined = undefined;
export let value: string | number | boolean | null;
export let helper: string | undefined = undefined;
export let optionalText: string | number | boolean | null | undefined = undefined;
export let placeholder = '';
export let required = false;
@@ -54,8 +55,8 @@
{placeholder}
{disabled}
{isSearchable}
helper={error ?? helper}
{required}
helper={error}
state={error ? 'error' : 'default'}
on:invalid={handleInvalid}
on:input
@@ -1,7 +1,7 @@
<script lang="ts">
import { DropList } from '$lib/components';
import { SelectSearchCheckbox } from '..';
import { Icon, Layout, Tag } from '@appwrite.io/pink-svelte';
import { Icon, Tag } from '@appwrite.io/pink-svelte';
import { IconChevronDown, IconChevronUp } from '@appwrite.io/pink-icons-svelte';
type Option = {
+2 -1
View File
@@ -6,7 +6,7 @@
export let name: string = id;
export let helper: string = undefined;
export let value = '';
export let pattern: string = null; //TODO: implement pattern check
export let pattern: string = undefined; //TODO: implement pattern check
export let patternError: string = '';
export let placeholder = '';
export let required = false;
@@ -53,6 +53,7 @@
{label}
{nullable}
{readonly}
{pattern}
autofocus={autofocus || undefined}
autocomplete={autocomplete ? 'on' : 'off'}
helper={error || helper}
+8 -2
View File
@@ -15,13 +15,17 @@ if (browser) {
dayjs.extend(relativeTime);
}
export const toLocaleDate = (datetime: string) => {
export const toLocaleDate = (datetime: string, format: 'dd mm yyyy' | 'default' = 'default') => {
const date = new Date(datetime);
if (isNaN(date.getTime())) {
return 'n/a';
}
if (format === 'dd mm yyyy') {
return `${date.getDate().toString().padStart(2, '0')} ${date.toLocaleString('en', { month: 'short' })} ${date.getFullYear()}`;
}
const options: Intl.DateTimeFormatOptions = {
year: 'numeric',
month: 'short',
@@ -33,6 +37,7 @@ export const toLocaleDate = (datetime: string) => {
export const toLocaleDateTime = (
datetime: string | number,
is12HourFormat: boolean = false,
timeZone: string | undefined = undefined
) => {
const date = new Date(datetime);
@@ -48,7 +53,8 @@ export const toLocaleDateTime = (
day: 'numeric',
hour: 'numeric',
minute: 'numeric',
hourCycle: 'h23'
hourCycle: is12HourFormat ? 'h12' : 'h23',
...(is12HourFormat && { hour12: true })
};
return date.toLocaleDateString('en', options);
+1 -1
View File
@@ -32,8 +32,8 @@ export async function createRecord(record: Partial<DnsRecord>, domainId: string)
domainId,
record.name,
record.value,
record.priority || 10,
record.ttl,
record.priority || 10,
record?.comment || undefined
);
case 'TXT':
+5 -5
View File
@@ -36,7 +36,6 @@ export async function processFileList(files: FileList): Promise<FileData[]> {
buffer: buffer
};
} catch (e) {
// console.log(file);
return null;
}
});
@@ -46,7 +45,6 @@ export async function processFileList(files: FileList): Promise<FileData[]> {
export async function gzipUpload(files: FileList) {
let uploadFile: File;
const tick = performance.now();
if (!files?.length) return;
// If the file is a tar.gz file, then return it as is
@@ -79,9 +77,6 @@ export async function gzipUpload(files: FileList) {
});
}
}
console.log(uploadFile);
const tock = performance.now();
console.log('Time taken to process files:', tock - tick);
return uploadFile;
}
@@ -97,6 +92,11 @@ export function removeFile(file: File, files: FileList) {
return dataTransfer.files;
}
export enum InvalidFileType {
SIZE = 'invalid_size',
EXTENSION = 'invalid_extension'
}
export const defaultIgnore = `
### Node ###
# Logs
+1 -1
View File
@@ -4,5 +4,5 @@ import { sdk } from '$lib/stores/sdk';
export function getFlagUrl(countryCode: string) {
if (!isValueOfStringEnum(Flag, countryCode)) return '';
return sdk.forProject.avatars.getFlag(countryCode, 22, 15, 100)?.toString();
return sdk.forConsole.avatars.getFlag(countryCode, 22, 15, 100)?.toString();
}
+40 -2
View File
@@ -1,6 +1,44 @@
export function getProjectId() {
import { page } from '$app/state';
import { get } from 'svelte/store';
import { sdk } from '$lib/stores/sdk';
import { projectRegion } from '$routes/(console)/project-[region]-[project]/store';
/**
* Returns the current project ID.
*
* The function first checks for a `project` parameter in the Svelte `page` store.
* If not found, it extracts the project ID from the pathname.
*
* Supports:
* - Legacy structure: `/project-{projectID}/`
* - Multi-region structure: `/project-{region}-{projectID}/`
*
* Example:
* - `/project-console/` `console`
* - `/project-fra-console/` `console`
* - `/project-nyc-console/` `console`
*/
export function getProjectId(): string | null {
// safety check!
const projectFromParams = page?.params?.project;
if (projectFromParams) {
return projectFromParams;
}
const pathname = window.location.pathname + '/';
const projectMatch = pathname.match(/\/project-(.*?)\//);
const projectMatch = pathname.match(/\/project-(?:[a-z]{2,3}-)?([^/]+)/);
return projectMatch?.[1] || null;
}
/**
* Returns the correct API endpoint for the project based on the current project region.
*
* @returns {string} The project-specific API endpoint.
*/
export function getProjectEndpoint(): string {
const currentProjectRegion = get(projectRegion);
const { protocol, hostname, href } = new URL(sdk.forConsole.client.config.endpoint);
return currentProjectRegion ? `${protocol}//${currentProjectRegion.$id}.${hostname}/v1` : href;
}
+7
View File
@@ -53,3 +53,10 @@ export function formatNum(number: number): string {
* Returns a regex to check hostname validity. Supports wildcards too!
*/
export const hostnameRegex = String.raw`(\*)|(\*\.)?(?!-)[A-Za-z0-9\-]+([\-\.]{1}[a-z0-9]+)*\.[A-Za-z]{2,18}|localhost`;
/**
* Returns a regex to check hostname validity.
*
* Supports domains, localhost, wildcards, ip-addresses and Chrome extension IDs!
*/
export const extendedHostnameRegex = String.raw`(\*)|(\*\.)?((?!-)[A-Za-z0-9\-]+([\-\.]{1}[a-z0-9]+)*\.[A-Za-z]{2,18}|localhost|(\d{1,3}\.){3}\d{1,3}|[a-z0-9]{32})`;

Some files were not shown because too many files have changed in this diff Show More