mirror of
https://github.com/appwrite/console.git
synced 2026-04-07 19:17:46 +00:00
Merge branch 'main' into feat-project-changes
This commit is contained in:
+3
-1
@@ -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=
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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/);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
@@ -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
@@ -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
@@ -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",
|
||||
|
||||
@@ -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'
|
||||
},
|
||||
|
||||
Generated
+65
-70
@@ -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
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
+60
-12
@@ -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}
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
@@ -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" />
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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'];
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,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';
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
{pattern}
|
||||
on:input
|
||||
on:invalid={handleInvalid}
|
||||
type="email"
|
||||
type="text"
|
||||
helper={error}
|
||||
state={error ? 'error' : 'default'}
|
||||
autofocus={autofocus || undefined}
|
||||
|
||||
@@ -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 -->
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => (show = !show)}
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user