Merge branch 'main' into 'fix-relationship-set-and-view'.

This commit is contained in:
Darshan
2025-05-28 18:17:38 +05:30
104 changed files with 2239 additions and 1464 deletions
+10 -1
View File
@@ -4,7 +4,6 @@ server {
gzip_static on;
# Set root for all locations
root /usr/share/nginx/html;
# Security headers for all locations
@@ -22,10 +21,20 @@ server {
add_header Cache-Control "public";
}
# Cache, but revalidate, for images, css, fonts, and icons folders
location ~* ^/console/(images|css|fonts|icons)/ {
expires 1d;
add_header Cache-Control "public, must-revalidate";
}
# All other /console requests (no cache)
location /console {
index index.html index.html;
try_files $uri /console/index.html;
expires 0;
add_header Cache-Control "no-cache, no-store";
add_header Pragma "no-cache";
}
location / {
+1 -1
View File
@@ -14,6 +14,6 @@ test('upgrade - free tier', async ({ page }) => {
await enterCreditCard(page);
// skip members
await page.getByRole('button', { name: 'change plan' }).click();
await page.waitForURL(/\/console\/project-(?:[a-z0-9]+-)?([^/]+)\/overview\/platforms/);
await page.waitForURL(/\/console\/project-(?:[a-z0-9]+-)?([^/]+)\/get-started/);
});
});
+4 -3
View File
@@ -19,7 +19,7 @@ export async function enterCreditCard(page: Page) {
await stripe.locator('id=Field-expiryInput').fill('1250');
await stripe.locator('id=Field-cvcInput').fill('123');
await stripe.locator('id=Field-countryInput').selectOption('DE');
await page.getByRole('button', { name: 'Add', exact: true }).click();
await dialog.getByRole('button', { name: 'Add', exact: true }).click();
await dialog.waitFor({
state: 'hidden'
});
@@ -29,8 +29,9 @@ export async function createProProject(page: Page): Promise<Metadata> {
const organizationId = await test.step('create organization', async () => {
await page.goto('./create-organization');
await page.locator('id=name').fill('test org');
await page.getByLabel('pro').check();
await page.getByRole('button', { name: 'get started' }).click();
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
+2 -3
View File
@@ -22,12 +22,11 @@
},
"dependencies": {
"@ai-sdk/svelte": "^1.1.24",
"@appwrite.io/console": "https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@1959",
"@appwrite.io/pink": "0.25.0",
"@appwrite.io/console": "https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@e190a19",
"@appwrite.io/pink-icons": "0.25.0",
"@appwrite.io/pink-icons-svelte": "^2.0.0-RC.1",
"@appwrite.io/pink-legacy": "^1.0.3",
"@appwrite.io/pink-svelte": "https://try-module.cloud/module/@appwrite/%40appwrite.io%2Fpink-svelte@510c1a7",
"@appwrite.io/pink-svelte": "https://try-module.cloud/-/@appwrite/@appwrite.io/pink-svelte@7b28443",
"@popperjs/core": "^2.11.8",
"@sentry/sveltekit": "^8.38.0",
"@stripe/stripe-js": "^3.5.0",
+11 -28
View File
@@ -12,11 +12,8 @@ importers:
specifier: ^1.1.24
version: 1.1.24(svelte@5.25.3)(zod@3.24.3)
'@appwrite.io/console':
specifier: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@1959
version: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@1959
'@appwrite.io/pink':
specifier: 0.25.0
version: 0.25.0
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-icons':
specifier: 0.25.0
version: 0.25.0
@@ -27,8 +24,8 @@ importers:
specifier: ^1.0.3
version: 1.0.3
'@appwrite.io/pink-svelte':
specifier: https://try-module.cloud/module/@appwrite/%40appwrite.io%2Fpink-svelte@510c1a7
version: https://try-module.cloud/module/@appwrite/%40appwrite.io%2Fpink-svelte@510c1a7(svelte@5.25.3)
specifier: https://try-module.cloud/-/@appwrite/@appwrite.io/pink-svelte@7b28443
version: https://try-module.cloud/-/@appwrite/%40appwrite.io%2Fpink-svelte@7b28443(svelte@5.25.3)
'@popperjs/core':
specifier: ^2.11.8
version: 2.11.8
@@ -257,9 +254,9 @@ packages:
'@analytics/type-utils@0.6.2':
resolution: {integrity: sha512-TD+xbmsBLyYy/IxFimW/YL/9L2IEnM7/EoV9Aeh56U64Ify8o27HJcKjo38XY9Tcn0uOq1AX3thkKgvtWvwFQg==}
'@appwrite.io/console@https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@1959':
resolution: {tarball: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@1959}
version: 1.5.2
'@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@2.0.0-RC.1':
resolution: {integrity: sha512-iLFlV55hj8mGuAbmxJGenxN5RaZMmVT4GJb9dv/MP1xBAtYibFq7JvBcxm18qV2KU8c31Rntf+Ub4GL7HwqTYg==}
@@ -281,15 +278,12 @@ packages:
'@appwrite.io/pink-legacy@1.0.3':
resolution: {integrity: sha512-GGde5fmPhs+s6/3aFeMPc/kKADG/gTFkYQSy6oBN8pK0y0XNCLrZZgBv+EBbdhwdtqVEWXa0X85Mv9w7jcIlwQ==}
'@appwrite.io/pink-svelte@https://try-module.cloud/module/@appwrite/%40appwrite.io%2Fpink-svelte@510c1a7':
resolution: {tarball: https://try-module.cloud/module/@appwrite/%40appwrite.io%2Fpink-svelte@510c1a7}
'@appwrite.io/pink-svelte@https://try-module.cloud/-/@appwrite/%40appwrite.io%2Fpink-svelte@7b28443':
resolution: {tarball: https://try-module.cloud/-/@appwrite/%40appwrite.io%2Fpink-svelte@7b28443}
version: 2.0.0-RC.2
peerDependencies:
svelte: ^4.0.0
'@appwrite.io/pink@0.25.0':
resolution: {integrity: sha512-9z4G/iAhjvVpM7tkNqzGqrokst4hvNt3Fkf2jD4W6n8j//EJbQ3mJ0I+5dye5Sry0a33wUanq5DSztqFNYq5zw==}
'@asamuzakjp/css-color@3.1.1':
resolution: {integrity: sha512-hpRD68SV2OMcZCsrbdkccTw5FXjNDLo5OuqSHyHZfwweGsDWZwDJ2+gONyNAbazZclobMirACLw0lk8WVxIqxA==}
@@ -2664,9 +2658,6 @@ packages:
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
engines: {node: '>=0.10.0'}
normalize.css@8.0.1:
resolution: {integrity: sha512-qizSNPO93t1YUuUhP22btGOo3chcvDFqFaj2TRybP0DMxkHOCTYwp3n34fel4a31ORXy4m1Xq0Gyqpb5m33qIg==}
nwsapi@2.2.20:
resolution: {integrity: sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA==}
@@ -3625,7 +3616,7 @@ snapshots:
'@analytics/type-utils@0.6.2': {}
'@appwrite.io/console@https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@1959': {}
'@appwrite.io/console@https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@e190a19': {}
'@appwrite.io/pink-icons-svelte@2.0.0-RC.1(svelte@5.25.3)':
dependencies:
@@ -3644,7 +3635,7 @@ snapshots:
'@appwrite.io/pink-icons': 1.0.0
the-new-css-reset: 1.11.3
'@appwrite.io/pink-svelte@https://try-module.cloud/module/@appwrite/%40appwrite.io%2Fpink-svelte@510c1a7(svelte@5.25.3)':
'@appwrite.io/pink-svelte@https://try-module.cloud/-/@appwrite/%40appwrite.io%2Fpink-svelte@7b28443(svelte@5.25.3)':
dependencies:
'@appwrite.io/pink-icons-svelte': 2.0.0-RC.1(svelte@5.25.3)
'@floating-ui/dom': 1.6.13
@@ -3659,12 +3650,6 @@ snapshots:
svelte-motion: 0.12.2(svelte@5.25.3)
svelte-sonner: 0.3.28(svelte@5.25.3)
'@appwrite.io/pink@0.25.0':
dependencies:
'@appwrite.io/pink-icons': 0.25.0
normalize.css: 8.0.1
the-new-css-reset: 1.11.3
'@asamuzakjp/css-color@3.1.1':
dependencies:
'@csstools/css-calc': 2.1.2(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3)
@@ -6254,8 +6239,6 @@ snapshots:
normalize-path@3.0.0: {}
normalize.css@8.0.1: {}
nwsapi@2.2.20: {}
object-inspect@1.13.4: {}
+14 -5
View File
@@ -11,6 +11,7 @@
import { IconArrowSmRight } from '@appwrite.io/pink-icons-svelte';
import { Icon, Keyboard, Layout } from '@appwrite.io/pink-svelte';
import { Submit, trackEvent } from '$lib/actions/analytics';
import type { Action } from 'svelte/action';
/* eslint no-undef: "off" */
type Option = $$Generic<Omit<Command, 'group'> & { group?: string }>;
@@ -247,6 +248,10 @@
return isGroup(nextItem) || !nextItem?.nested;
};
const autofocus: Action<HTMLInputElement> = (node) => {
if (node) node.focus();
};
</script>
<svelte:window on:keydown={handleKeyDown} />
@@ -271,8 +276,11 @@
<slot name="search">
<div class="u-flex default-search u-width-full-line">
<!-- svelte-ignore a11y-autofocus -->
<input type="text" placeholder={searchPlaceholder} autofocus bind:value={search} />
<input
type="text"
placeholder={searchPlaceholder}
use:autofocus
bind:value={search} />
</div>
</slot>
</div>
@@ -305,7 +313,7 @@
on:mouseleave={getOptionBlurHandler()}
on:focus={getOptionFocusHandler(item)}>
<slot name="option" option={castOption(item)}>
<Layout.Stack direction="column" gap="s">
<Layout.Stack direction="row" gap="s" alignItems="center">
{#if item.icon}
<Icon
icon={item.icon}
@@ -448,8 +456,9 @@
.default-search {
input {
margin: -1rem;
padding: 1rem;
width: 100%;
margin: 0rem;
padding: 0rem;
border: none;
background-color: transparent;
}
@@ -7,6 +7,8 @@
import { organization } from '$lib/stores/organization';
import { activeHeaderAlert } from '$routes/(console)/store';
import GradientBanner from '../gradientBanner.svelte';
import { isSmallViewport } from '$lib/stores/viewport';
import { Layout, Typography } from '@appwrite.io/pink-svelte';
let show = true;
@@ -23,8 +25,13 @@
{#if show && $organization?.$id && $organization?.billingPlan === BillingPlan.FREE && !page.url.pathname.includes('/console/account')}
<GradientBanner on:close={handleClose}>
<div class="u-flex u-gap-24 u-main-center u-cross-center u-flex-vertical-mobile">
<span class="body-text-1">Get $50 Cloud credits for Appwrite Pro.</span>
<Layout.Stack
gap="m"
alignItems="center"
alignContent="center"
direction={$isSmallViewport ? 'column' : 'row'}>
<Typography.Text>Get $50 Cloud credits for Appwrite Pro.</Typography.Text>
<Button
secondary
fullWidthMobile
@@ -39,6 +46,6 @@
}}>
Claim credits
</Button>
</div>
</Layout.Stack>
</GradientBanner>
{/if}
@@ -1,11 +1,41 @@
<script lang="ts">
import { base } from '$app/paths';
import { createEventDispatcher } from 'svelte';
import { isTabletViewport } from '$lib/stores/viewport';
import { createEventDispatcher, onMount, onDestroy } from 'svelte';
import { Button } from '$lib/elements/forms';
import { Icon } from '@appwrite.io/pink-svelte';
import { IconX } from '@appwrite.io/pink-icons-svelte';
let container: HTMLElement;
const dispatch = createEventDispatcher();
const queryLayoutElements = () => ({
header: document.querySelector<HTMLElement>('main > header'),
sidebar: document.querySelector<HTMLElement>('main > div > nav'),
content: document.querySelector<HTMLElement>('main > div > section')
});
const setNavigationHeight = () => {
const alertHeight = container?.getBoundingClientRect()?.height || 0;
const { header, sidebar, content } = queryLayoutElements();
const headerHeight = header?.getBoundingClientRect().height || 0;
const offset = alertHeight + (!isTabletViewport && header ? headerHeight : 0);
if (header) header.style.top = `${alertHeight}px`;
if (sidebar) {
sidebar.style.top = `${offset}px`;
sidebar.style.height = `calc(100vh - ${offset}px)`;
}
if (content) content.style.paddingBlockStart = `${alertHeight}px`;
};
onMount(setNavigationHeight);
onDestroy(setNavigationHeight);
</script>
<div class="top-banner">
<svelte:window on:resize={setNavigationHeight} />
<div bind:this={container} class="top-banner alert is-action is-action-and-top-sticky">
<div class="top-banner-bg">
<div class="top-banner-bg-1">
<img
@@ -22,14 +52,28 @@
alt="" />
</div>
</div>
<div class="top-banner-content u-color-text-primary">
<slot />
</div>
<button
on:click|preventDefault={() => dispatch('close')}
type="button"
class="top-banner-button"
aria-label="close upgrade message">
<span class="icon-x" aria-hidden="true"></span>
</button>
<Button icon extraCompact class="top-banner-button position" on:click={() => dispatch('close')}>
<Icon icon={IconX} size="s" />
</Button>
</div>
<style>
.alert {
top: 0;
width: 100%;
z-index: 100;
position: fixed;
}
@media (max-width: 768px) {
:global(.top-banner-button.position) {
position: absolute;
padding-block-end: 3.7625rem;
}
}
</style>
@@ -21,6 +21,7 @@
async function cardSaved(event: CustomEvent<PaymentMethodData>) {
value = event.detail.$id;
invalidate(Dependencies.UPGRADE_PLAN);
invalidate(Dependencies.CREATE_ORGANIZATION);
}
onMount(() => {
+24 -41
View File
@@ -17,31 +17,6 @@
? new Date(new Date().getFullYear(), new Date().getMonth() + 1, 1).toString()
: org?.billingNextInvoiceDate;
const planData = [
{
id: 'members',
resource: 'Organization members',
unit: 'member'
},
{ id: 'bandwidth', resource: 'Bandwidth', unit: 'GB' },
{ id: 'storage', resource: 'Storage', unit: 'GB' },
{
id: 'executions',
resource: 'Function executions',
unit: ' executions'
},
{
id: 'users',
resource: 'Users',
unit: ' Users'
},
{
id: 'realtime',
resource: 'Concurrent connections',
unit: ' connections'
}
];
$: isFree = org.billingPlan === BillingPlan.FREE;
// equal or above means unlimited!
@@ -50,6 +25,10 @@
const exceedsSafeLimit = count >= Number.MAX_SAFE_INTEGER;
return exceedsSafeLimit ? 'Unlimited' : count || 0;
};
function getPlanLimit(key: string): number | false {
return plan[key] || false;
}
</script>
<Modal bind:show title="Usage rates">
@@ -77,29 +56,33 @@
<Table.Header.Cell column="limit" {root}>Limit</Table.Header.Cell>
<Table.Header.Cell column="rate" {root}>Rate</Table.Header.Cell>
</svelte:fragment>
{#each planData as usage}
{#if usage['id'] === 'members'}
<Table.Row.Base {root}>
<Table.Cell column="resource" {root}>{usage.resource}</Table.Cell>
<Table.Cell column="limit" {root}>
{getCorrectSeatsCountValue(plan.addons.seats.limit)}
</Table.Cell>
{#each Object.values(plan.addons) as addon}
<Table.Row.Base {root}>
<Table.Cell column="resource" {root}>{addon.invoiceDesc}</Table.Cell>
<Table.Cell column="limit" {root}>
{getCorrectSeatsCountValue(addon.limit)}
</Table.Cell>
{#if !isFree}
<Table.Cell column="rate" {root}>
{formatCurrency(plan.addons?.seats?.price)}/{usage?.unit}
{formatCurrency(addon.price)}/member
</Table.Cell>
</Table.Row.Base>
{:else}
{@const addon = plan.addons[usage.id]}
{/if}
</Table.Row.Base>
{/each}
{#each Object.entries(plan.usage) as [key, usage]}
{@const limit = getPlanLimit(key)}
{@const show = limit !== false}
{#if show}
<Table.Row.Base {root}>
<Table.Cell column="resource" {root}>{usage.resource}</Table.Cell>
<Table.Cell column="resource" {root}>{usage.name}</Table.Cell>
<Table.Cell column="limit" {root}>
{abbreviateNumber(plan[usage.id])}{usage?.unit}
{abbreviateNumber(limit)}{usage.unit}
</Table.Cell>
{#if !isFree}
<Table.Cell column="rate" {root}>
{formatCurrency(addon?.price)}/{['MB', 'GB', 'TB'].includes(addon?.unit)
? addon?.value
: abbreviateNumber(addon?.value, 0)}{usage?.unit}
{formatCurrency(usage.price)}/{abbreviateNumber(
usage.value
)}{usage.unit}
</Table.Cell>
{/if}
</Table.Row.Base>
+8 -3
View File
@@ -2,10 +2,11 @@
import { page } from '$app/state';
import { Empty } from '$lib/components';
import { CARD_LIMIT } from '$lib/constants';
import { getServiceLimit, type PlanServices } from '$lib/stores/billing';
import { preferences } from '$lib/stores/preferences';
import { isCloud } from '$lib/system';
import CardPlanLimit from './cardPlanLimit.svelte';
import { preferences } from '$lib/stores/preferences';
import { isSmallViewport } from '$lib/stores/viewport';
import { getServiceLimit, type PlanServices } from '$lib/stores/billing';
export let disableEmpty = true;
export let offset = 0;
@@ -17,9 +18,13 @@
$: planLimit = getServiceLimit(serviceId) || Infinity;
$: limit = preferences.get(page.route)?.limit ?? CARD_LIMIT;
$: gridItemStyle = $isSmallViewport
? undefined
: `--grid-item-size: ${total > 3 ? '22rem' : '25rem'}`;
</script>
<ul class="grid-box" style={`--grid-item-size:${total > 3 ? '22rem' : '25rem'};`} data-private>
<ul data-private class="grid-box" style={gridItemStyle}>
<slot />
{#if total > 3 ? total < limit + offset : total % 2 !== 0}
+16 -5
View File
@@ -13,11 +13,22 @@
} from '@appwrite.io/pink-svelte';
import { onMount } from 'svelte';
export let selectedLog: Models.Execution;
let {
selectedLog,
product
}: {
selectedLog: Models.Execution;
product: 'site' | 'function';
} = $props();
let requestTab: 'parameters' | 'headers' = 'parameters';
let requestTab: 'parameters' | 'headers' = $state('parameters');
let parameters = [];
let parameters = $state([]);
const href =
product === 'site'
? 'https://appwrite.io/docs/products/sites/logs#log-details'
: 'https://appwrite.io/docs/products/functions/develop#logging';
onMount(() => {
try {
@@ -121,8 +132,8 @@
<Input.Helper state="default">
<span>
Missing headers? Check the <Link variant="muted" href="#" external>docs</Link> to
see the supported data and how to log it.
Missing headers? Check the <Link variant="muted" {href} external>docs</Link> to see
the supported data and how to log it.
</span>
</Input.Helper>
{:else}
+2 -3
View File
@@ -28,11 +28,10 @@
let responseTab: 'logs' | 'errors' | 'headers' | 'body' = $state('logs');
let href = $derived(
const href =
product === 'site'
? 'https://appwrite.io/docs/products/sites/logs#log-details'
: 'https://appwrite.io/docs/products/functions/develop#logging'
);
: 'https://appwrite.io/docs/products/functions/develop#logging';
onMount(() => {
if (selectedLog?.errors) {
@@ -3,7 +3,7 @@
export async function verify(challenge: Models.MfaChallenge, code: string) {
try {
if (challenge == null) {
if (challenge === null) {
challenge = await sdk.forConsole.account.createMfaChallenge(
AuthenticationFactor.Totp
);
@@ -59,9 +59,9 @@
onMount(async () => {
const enabledNonRecoveryFactors = enabledFactors.filter(
([factor, _]) => factor != 'recoveryCode'
([factor, _]) => factor !== 'recoveryCode'
);
if (enabledNonRecoveryFactors.length == 1) {
if (enabledNonRecoveryFactors.length === 1) {
if (factors.phone) {
createChallenge(AuthenticationFactor.Phone);
} else if (factors.email) {
@@ -77,7 +77,7 @@
</Typography.Text>
<InputText id="recovery-code" bind:value={code} required autofocus />
{:else}
{#if factors.totp && (challengeType == AuthenticationFactor.Totp || challengeType == null)}
{#if factors.totp && (challengeType === AuthenticationFactor.Totp || challengeType === null)}
<Typography.Text align="center"
>Enter below a 6-digit one-time code generated by your authentication app.</Typography.Text>
{:else if challengeType === AuthenticationFactor.Email}
+3 -3
View File
@@ -39,11 +39,11 @@
import { Click, trackEvent } from '$lib/actions/analytics';
import type { HTMLAttributes } from 'svelte/elements';
import type { NavbarProject } from '$lib/components/navbar.svelte';
import type { Models } from '@appwrite.io/console';
type $$Props = HTMLAttributes<HTMLElement> & {
state?: 'closed' | 'open' | 'icons';
project: NavbarProject | undefined;
project: Models.Project | undefined;
avatar: string;
progressCard?: {
title: string;
@@ -84,7 +84,7 @@
</script>
<div
class:only-mobile-tablet={project === undefined}
class:only-mobile-tablet={!project}
style:--overlay-on-neutral={$app.themeInUse === 'dark'
? 'var(--neutral-750)'
: 'var(--neutral-100)'}>
+1 -1
View File
@@ -28,7 +28,7 @@
(team) => !$plansInfo.get((team as Organization).billingPlan)?.premiumSupport
);
$: upgradeURL = `${base}/organization-${freeOrganization.$id}/change-plan`;
$: upgradeURL = `${base}/organization-${freeOrganization?.$id}/change-plan`;
$: supportTimings = `${utcHourToLocaleHour('16:00')} - ${utcHourToLocaleHour('00:00')} ${localeShortTimezoneName()}`;
+1
View File
@@ -18,6 +18,7 @@ export enum Dependencies {
INVOICES = 'dependency:invoices',
ADDRESS = 'dependency:address',
UPGRADE_PLAN = 'dependency:upgrade_plan',
CREATE_ORGANIZATION = 'dependency:create_organization',
PAYMENT_METHODS = 'dependency:paymentMethods',
ORGANIZATION = 'dependency:organization',
MEMBERS = 'dependency:members',
+15
View File
@@ -124,3 +124,18 @@
<slot slot="end" name="end" />
</Button.Button>
{/if}
<!-- built-in classes don't seem to work -->
<style lang="scss">
:global {
.is-full-width {
width: 100% !important;
}
@media (max-width: 768px) {
.is-full-width-mobile {
width: 100% !important;
}
}
}
</style>
+27
View File
@@ -0,0 +1,27 @@
import { getFlagUrl } from '$lib/helpers/flag';
import type { Models } from '@appwrite.io/console';
type RegionOption = {
label: string;
value: string;
disabled: boolean;
leadingHtml: string;
};
export function filterRegions(regions: Models.ConsoleRegion[]): RegionOption[] {
if (!regions.length) return [];
return regions
.filter((region) => region.$id !== 'default')
.sort((a, b) => {
if (a.disabled && !b.disabled) return 1;
if (!a.disabled && b.disabled) return -1;
return a.name.localeCompare(b.name);
})
.map((region) => ({
label: region.name,
value: region.$id,
disabled: region.disabled || !region.available,
leadingHtml: `<img src='${getFlagUrl(region.flag)}' alt='Region flag'/>`
}));
}
+30
View File
@@ -0,0 +1,30 @@
import { get } from 'svelte/store';
import { user } from '$lib/stores/user';
import { sdk } from '$lib/stores/sdk';
const userPreferences = () => get(user)?.prefs;
export const joinWaitlistSites = () => {
const prefs = userPreferences();
const newPrefs = {
...prefs,
joinWaitlistSites: true
};
sdk.forConsole.account.updatePrefs(newPrefs);
if (sessionStorage) {
sessionStorage.setItem('joinWaitlistSites', 'true');
}
};
export const isOnWaitlistSites = (): boolean => {
const prefs = userPreferences();
const joinedInPrefs = 'joinWaitlistSites' in prefs;
let joinedInSession = false;
if (sessionStorage) {
joinedInSession = sessionStorage.getItem('joinWaitlistSites') === 'true';
}
return joinedInSession || joinedInPrefs;
};
Binary file not shown.

Before

Width:  |  Height:  |  Size: 506 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 578 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 296 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 429 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 472 KiB

+1 -1
View File
@@ -11,7 +11,7 @@
<div class="top-cover-console" style:block-size={blocksize}>
<div class="cover-container" {style}>
<Layout.Stack direction="row" alignItems="center">
<Layout.Stack direction="row" alignItems="baseline">
<slot name="header" />
</Layout.Stack>
<slot />
+18 -12
View File
@@ -1,18 +1,24 @@
<script lang="ts">
import { isSmallViewport } from '$lib/stores/viewport';
import { isSmallViewport, isTabletViewport } from '$lib/stores/viewport';
import { IconChevronLeft } from '@appwrite.io/pink-icons-svelte';
import { Typography, Button, Icon } from '@appwrite.io/pink-svelte';
import { Typography, Button, Icon, Layout } from '@appwrite.io/pink-svelte';
export let href: string = null;
</script>
{#if href}
<span style:position="absolute" style:left="-2.75rem">
<Button.Anchor size="s" icon variant="text" {href} aria-label="page back">
<Icon icon={IconChevronLeft} />
</Button.Anchor>
</span>
{/if}
<Typography.Title truncate color="--fgcolor-neutral-primary" size={$isSmallViewport ? 'm' : 'l'}>
<slot />
</Typography.Title>
<Layout.Stack justifyContent="center" alignItems="center" direction="row" gap="xs" inline>
{#if href}
<span
style={$isTabletViewport ? 'position: relative' : 'position: absolute; left: -2.75rem'}>
<Button.Anchor size="s" icon variant="text" {href} aria-label="page back">
<Icon icon={IconChevronLeft} />
</Button.Anchor>
</span>
{/if}
<Typography.Title
truncate
color="--fgcolor-neutral-primary"
size={$isSmallViewport ? 'm' : 'l'}>
<slot />
</Typography.Title>
</Layout.Stack>
+3 -20
View File
@@ -5,6 +5,7 @@
import { getFlagUrl } from '$lib/helpers/flag';
import { isCloud } from '$lib/system.js';
import type { Models } from '@appwrite.io/console';
import { filterRegions } from '$lib/helpers/regions';
export let projectName: string;
export let id: string;
@@ -13,25 +14,6 @@
export let showTitle = true;
let showCustomId = false;
function getRegions() {
return regions
.filter((region) => region.$id !== 'default')
.sort((regionA, regionB) => {
if (regionA.disabled && !regionB.disabled) {
return 1;
}
return regionA.name > regionB.name ? 1 : -1;
})
.map((region) => {
return {
label: region.name,
value: region.$id,
leadingHtml: `<img src='${getFlagUrl(region.flag)}' alt='Region flag'/>`,
disabled: region.disabled
};
});
}
</script>
<svelte:head>
@@ -39,6 +21,7 @@
<link rel="preload" as="image" href={getFlagUrl(region.flag)} />
{/each}
</svelte:head>
<form on:submit|preventDefault>
<Layout.Stack direction="column" gap="xxl">
{#if showTitle}
@@ -68,7 +51,7 @@
<Input.Select
bind:value={region}
placeholder="Select a region"
options={getRegions()}
options={filterRegions(regions)}
label="Region" />
<Typography.Text>Region cannot be changed after creation</Typography.Text>
</Layout.Stack>
+3 -6
View File
@@ -11,21 +11,20 @@
import { sdk } from '$lib/stores/sdk';
import { user } from '$lib/stores/user';
import { tierToPlan } from '$lib/stores/billing';
import { type Models } from '@appwrite.io/console';
import { isCloud } from '$lib/system';
import SideNavigation from '$lib/layout/navigation.svelte';
import { hasOnboardingDismissed } from '$lib/helpers/onboarding';
import { isSidebarOpen } from '$lib/stores/sidebar';
import { BillingPlan } from '$lib/constants';
import { page } from '$app/stores';
import type { Models } from '@appwrite.io/console';
export let showSideNavigation = false;
export let showHeader = true;
export let showFooter = true;
export let loadedProjects: Array<NavbarProject> = [];
export let projects: Array<Models.Project> = [];
export let selectedProject: Models.Project | null = null;
$: selectedProject = loadedProjects.find((project) => project.isSelected);
let yOnMenuOpen: number;
let showContentTransition = false;
let timeoutId: ReturnType<typeof setTimeout>;
@@ -120,11 +119,9 @@
const progressCard = function getProgressCard() {
if (selectedProject && !hasOnboardingDismissed(selectedProject.$id)) {
const currentProject = projects.find((project) => project.$id === selectedProject.$id);
return {
title: 'Get started',
percentage: currentProject && currentProject.platforms.length ? 100 : 33
percentage: selectedProject && selectedProject.platforms.length ? 100 : 33
};
}
+1
View File
@@ -278,6 +278,7 @@ export type AddressesList = {
};
export type AdditionalResource = {
name: string;
currency: string;
invoiceDesc: string;
price: number;
+6 -8
View File
@@ -1,23 +1,21 @@
import { StatusCode } from '@appwrite.io/console';
export const statusCodeOptions = [
{
label: '301 Moved permanently',
value: 301
value: StatusCode.MovedPermanently301
},
{
label: '302 Found',
value: 302
},
{
label: '303 See other',
value: 303
value: StatusCode.Found302
},
{
label: '307 Temporary redirect',
value: 307
value: StatusCode.TemporaryRedirect307
},
{
label: '308 Permanent redirect',
value: 308
value: StatusCode.PermanentRedirect308
}
];
+1 -1
View File
@@ -61,4 +61,4 @@ export const organization = derived(page, ($page) => $page.data?.organization as
export const currentPlan = derived(page, ($page) => $page.data?.currentPlan as Plan);
export const members = derived(page, ($page) => $page.data.members as Models.MembershipList);
export const regions = writable<Models.ConsoleRegionList | undefined>(undefined);
export const regions = writable<Models.ConsoleRegionList>({ total: 0, regions: [] });
+1 -1
View File
@@ -43,7 +43,7 @@ export function getApiEndpoint(region?: string): string {
VARS.APPWRITE_ENDPOINT ? VARS.APPWRITE_ENDPOINT : globalThis?.location?.toString()
);
const protocol = url.protocol;
const hostname = url.hostname;
const hostname = url.host; // "hostname:port" (or just "hostname" if no port)
// If instance supports multi-region, add the region subdomain.
const subdomain = isMultiRegionSupported(url) ? getSubdomain(region) : '';
@@ -3,6 +3,7 @@ import { wizard } from '$lib/stores/wizard';
import { requestedMigration } from '$routes/store';
import { get, writable } from 'svelte/store';
import Wizard from './wizard.svelte';
import { Region } from '@appwrite.io/console';
export const formData = createMigrationFormStore();
@@ -18,5 +19,6 @@ export function openMigrationWizard() {
}
export const selectedProject = writable<string>(null);
export const selectedRegion = writable<Region>(Region.Fra);
export const provider = createMigrationProviderStore();
@@ -1,66 +1,27 @@
<script lang="ts">
import { Alert, Box, EyebrowHeading } from '$lib/components';
import { Button } from '$lib/elements/forms';
import { deepMap } from '$lib/helpers/object';
import { Alert, Divider, Layout } from '@appwrite.io/pink-svelte';
import { onMount } from 'svelte';
import {
createMigrationFormStore,
createMigrationProviderStore,
isVersionAtLeast,
type MigrationFormData,
providerResources,
resourcesToMigrationForm
} from '$lib/stores/migration';
import { Button } from '$lib/elements/forms';
import { wizard } from '$lib/stores/wizard';
import type { Models } from '@appwrite.io/console';
import type { sdk } from '$lib/stores/sdk';
import type { WritableValue } from '$lib/helpers/types';
import ImportReport from '$routes/(console)/project-[region]-[project]/settings/migrations/(import)/importReport.svelte';
export let errorInResources: boolean | undefined = undefined;
export let formData: ReturnType<typeof createMigrationFormStore>;
export let provider: ReturnType<typeof createMigrationProviderStore>;
export let projectSdk: ReturnType<typeof sdk.forProject>;
type ValueOf<T> = T[keyof T];
type FormData = WritableValue<typeof formData>;
// TL;DR of this type: It gets a object with two levels (in this case FormData)
// And returns a join of the keys with a dot between them.
// e.g. If FormData was { users: { root: boolean, teams: boolean } }
// The type would be 'users.root' | 'users.teams'
type FormKeys = ValueOf<{
[K in keyof FormData]: ValueOf<{
[L in keyof FormData[K]]: L extends string ? `${K}.${L}` : never;
}>;
}>;
/**
*
* @param field {string} The field to update.
* Represents the path to the field in the formData object e.g. 'users.root'
*/
function handleInputChange(field: FormKeys) {
return (event: Event) => {
const checked = (event.target as HTMLInputElement).checked;
// For each entry in formData, if the root is changed, change all children to the same value
// otherwise, if a child is changed to true, change the root to true
const [parent, child] = field.split('.');
if (child === 'root') {
formData.update((data) => {
data[parent].root = checked;
for (const key in data[parent]) {
if (key !== 'root') {
data[parent][key] = checked;
}
}
return data;
});
} else if (checked) {
formData.update((data) => {
data[parent].root = checked;
return data;
});
}
};
}
export let migrationType: 'cloud' | 'provider' = 'provider';
function deselectAll() {
$formData = deepMap($formData, (v) => {
@@ -75,14 +36,15 @@
$formData = resourcesToMigrationForm(resources, version);
}
let report: Models.MigrationReport;
$: version = report?.version || '0.0.0';
let isOpen = false;
let error = false;
let isOpen = false;
let report: Models.MigrationReport;
onMount(async () => {
isOpen = true;
deselectAll();
try {
switch ($provider.provider) {
case 'appwrite':
@@ -109,7 +71,6 @@
providerResources.firebase,
$provider.serviceAccount
);
break;
case 'nhost':
report = await projectSdk.migrations.getNHostReport(
@@ -130,322 +91,84 @@
isOpen = false;
});
$: resources = providerResources[$provider.provider];
$: errorInResources = error;
$: wizard.setNextDisabled(!report);
$: resources = providerResources[$provider.provider];
const shouldRenderGroup = (groupKey: string): boolean => {
if (groupKey === 'functions') {
return resources.includes('function') && isVersionAtLeast(version, '1.4.0');
}
if (groupKey === 'storage') {
return resources.includes('bucket') && resources.includes('file');
}
return resources.includes(groupKey.slice(0, -1));
};
// no typecasting in svelte context!
function getAsType(groupKey: string) {
return groupKey as keyof MigrationFormData;
}
const getReportKey = (groupKey: string) => {
const map: Record<string, string> = {
users: 'user',
databases: 'database',
functions: 'function',
storage: 'bucket'
};
return map[groupKey] || groupKey;
};
</script>
<Box radius="s">
<div class="u-flex u-flex-vertical u-gap-16">
<EyebrowHeading class="eyebrow" tag="h3" size={3}>Good to know</EyebrowHeading>
<div class="u-flex u-gap-16">
<div class="circled">
<i class="icon-cog"></i>
</div>
<div>
<p class="u-bold">Project settings are not imported</p>
<p>You will need to set service and project settings manually</p>
</div>
</div>
<div class="u-flex u-gap-16">
<div class="circled">
<i class="icon-trending-up"></i>
</div>
<div>
<p class="u-bold">Keep your organization plan's limits in mind</p>
<p>
Make sure to have enough storage in your organization plan when importing files.
</p>
</div>
</div>
{#if $provider.provider === 'firebase'}
<div class="u-flex u-gap-16">
<div class="circled">
<i class="icon-exclamation u-color-text-warning"></i>
</div>
<div>
<p class="u-bold">Possible charges by Firebase</p>
<p>
Appwrite does not impose charges for importing data, but please note that
Firebase may have its own pricing for this service
</p>
</div>
</div>
{:else}
<div class="u-flex u-gap-16">
<div class="circled">
<i class="icon-currency-dollar"></i>
</div>
<div>
<p class="u-bold">Transfer is free of charge</p>
<p>You won't be charged for Appwrite bandwidth usage for importing data</p>
</div>
</div>
{/if}
</div>
</Box>
{#if report && !isVersionAtLeast(version, '1.4.0') && $provider.provider === 'appwrite'}
<div class="u-margin-block-start-24">
<Alert
type="warning"
isStandalone
buttons={[
{
slot: 'Learn more',
onClick() {
wizard.updateStep((p) => p - 1);
}
}
]}>
<Layout.Stack gap="l">
{#if report && !isVersionAtLeast(version, '1.4.0') && $provider.provider === 'appwrite'}
<Alert.Inline status="warning">
<svelte:fragment slot="title">Functions not available for import</svelte:fragment>
To migrate your functions, update the version of the Appwrite instance you're importing from
to a version newer than 1.4
</Alert>
</div>
{/if}
</Alert.Inline>
{/if}
{#if error}
<div class="u-margin-block-start-24">
<Alert
type="error"
isStandalone
buttons={[
{
slot: 'Edit credentials',
onClick() {
wizard.updateStep((p) => p - 1);
}
}
]}>
<svelte:fragment slot="title">Request failed</svelte:fragment>
Please check if your credentials are filled in correctly in the previous step
</Alert>
</div>
{/if}
<ul class="buttons-list u-margin-block-start-32 u-main-end">
<li class="buttons-list-item">
<Button text on:click={deselectAll}>Deselect all</Button>
</li>
<li class="buttons-list-item">
<Button text on:click={selectAll}>Select all</Button>
</li>
</ul>
<ul class="u-flex u-flex-vertical u-margin-block-start-16">
{#if resources?.includes('user')}
<li class="checkbox-field">
<input
type="checkbox"
bind:checked={$formData.users.root}
on:change={handleInputChange('users.root')} />
<div class="u-flex u-gap-4 u-cross-center">
<span class="u-bold">Users</span>
{#if $provider.provider !== 'firebase'}
{#if report?.user !== undefined}
<span class="inline-tag">{report.user}</span>
{:else if !error}
<span class="loader is-small u-margin-inline-start-4"></span>
{/if}
{/if}
</div>
<div></div>
<span>Import all users</span>
{#if resources?.includes('team')}
<ul>
<li class="checkbox-field">
<input
type="checkbox"
bind:checked={$formData.users.teams}
on:change={handleInputChange('users.teams')} />
<div class="u-flex u-gap-4 u-cross-center">
<span class="u-bold">Include teams</span>
{#if $provider.provider === 'firebase'}
{#if report?.team !== undefined}
<span class="inline-tag">{report.team}</span>
{:else if !error}
<span class="loader is-small u-margin-inline-start-4"></span>
{/if}
{/if}
</div>
<div></div>
<span>Import all teams and the team memberships of your users</span>
</li>
</ul>
{#if error}
<Alert.Inline status="error" title="Couldn’t load resources">
{#if migrationType === 'provider'}
Please double-check your credentials from the previous step and try again.
{:else}
The API key required for the migration may no longer be valid or has expired, please
verify and start migration again.
{/if}
</li>
{/if}
</Alert.Inline>
{:else}
<Layout.Stack gap="l">
<Layout.Stack direction="row">
<Button
compact
on:click={(event) => {
event.preventDefault();
deselectAll();
}}>Deselect all</Button>
{#if resources?.includes('database')}
<li class="checkbox-field">
<input
type="checkbox"
bind:checked={$formData.databases.root}
on:change={handleInputChange('databases.root')} />
<div class="u-flex u-gap-4 u-cross-center">
<span class="u-bold">Databases</span>
{#if $provider.provider !== 'firebase'}
{#if report?.database !== undefined}
<span class="inline-tag">{report.database}</span>
{:else if !error}
<span class="loader is-small u-margin-inline-start-4"></span>
{/if}
{/if}
</div>
<div></div>
<span>Import all databases, including collections, indexes and attributes</span>
<Button
compact
on:click={(event) => {
event.preventDefault();
selectAll();
}}>Select all</Button>
</Layout.Stack>
<Divider />
</Layout.Stack>
{#if resources?.includes('document')}
<ul>
<li class="checkbox-field">
<input
type="checkbox"
bind:checked={$formData.databases.documents}
on:change={handleInputChange('databases.documents')} />
<div class="u-flex u-gap-4 u-cross-center">
<span class="u-bold">Include documents</span>
{#if $provider.provider !== 'firebase'}
{#if report?.document !== undefined}
<span class="inline-tag">{report.document}</span>
{:else if !error}
<span class="loader is-small u-margin-inline-start-4"></span>
{/if}
{/if}
</div>
<div></div>
<span>Import all of your documents</span>
</li>
</ul>
{#each Object.entries($formData) as [groupKey, formGroup]}
{#if shouldRenderGroup(groupKey)}
<ImportReport
{error}
{formGroup}
groupKey={getAsType(groupKey)}
reportValue={report?.[getReportKey(groupKey)]} />
{/if}
</li>
{/each}
{/if}
{#if resources?.includes('function') && isVersionAtLeast(version, '1.4.0')}
<li class="checkbox-field">
<input
type="checkbox"
bind:checked={$formData.functions.root}
on:change={handleInputChange('functions.root')} />
<div class="u-flex u-gap-4 u-cross-center">
<span class="u-bold">Functions</span>
{#if $provider.provider !== 'firebase'}
{#if report?.function !== undefined}
<span class="inline-tag">{report.function}</span>
{:else if !error}
<span class="loader is-small u-margin-inline-start-4"></span>
{/if}
{/if}
</div>
<div></div>
<span>Import all functions and their active deployment</span>
<ul>
{#if resources?.includes('environment-variable')}
<li class="checkbox-field">
<input
type="checkbox"
bind:checked={$formData.functions.env}
on:change={handleInputChange('functions.env')} />
<div class="u-flex u-gap-4">
<span class="u-bold">Include environment variables</span>
</div>
<div></div>
<span>Import all environment variables</span>
</li>
{/if}
{#if resources?.includes('deployment')}
<li class="checkbox-field">
<input
type="checkbox"
bind:checked={$formData.functions.inactive}
on:change={handleInputChange('functions.inactive')} />
<div class="u-flex u-gap-4">
<span class="u-bold">Include inactive deployments</span>
</div>
<div></div>
<span>Import all deployments that are not currently active</span>
</li>
{/if}
</ul>
</li>
{/if}
{#if resources?.includes('bucket') && resources?.includes('file')}
<li class="checkbox-field">
<input
type="checkbox"
bind:checked={$formData.storage.root}
on:change={handleInputChange('storage.root')} />
<div class="u-flex u-gap-4 u-cross-center">
<span class="u-bold">Storage</span>
{#if $provider.provider !== 'firebase'}
{#if report?.size !== undefined}
<span class="inline-tag">{`${report.size.toFixed(2)}MB`}</span>
{:else if !error}
<span class="loader is-small u-margin-inline-start-4"></span>
{/if}
{/if}
</div>
<div></div>
<p>
Import all buckets
{#if $provider.provider !== 'firebase' && report?.bucket}
<span class="inline-tag">{report.bucket}</span>
{/if}
and files
{#if $provider.provider !== 'firebase' && report?.file}
<span class="inline-tag">{report.file}</span>
{/if}
</p>
</li>
{/if}
</ul>
<!-- svelte-ignore css-unused-selector -->
<style lang="scss">
.box :global(.eyebrow) {
font-weight: 500;
color: hsl(var(--color-neutral-70));
}
.circled {
width: 1.5rem;
height: 1.5rem;
flex-shrink: 0;
border-radius: 100%;
border: 1px solid hsl(var(--border));
position: relative;
i {
position: absolute;
left: 50%;
top: 50%;
translate: -50% -50%;
font-size: 1rem;
}
}
.buttons-list {
border-block-end: 1px solid hsl(var(--border));
padding-block-end: 0.625rem;
}
.checkbox-field {
display: grid;
grid-template-columns: auto 1fr;
gap: 0.25rem 1rem;
align-items: center;
padding-block-end: 1rem;
ul {
grid-column-start: 2;
li {
margin-block-start: 1rem;
}
}
}
</style>
</Layout.Stack>
@@ -1,52 +1,388 @@
<script lang="ts">
import { goto, invalidateAll } from '$app/navigation';
import type { WizardStepsType } from '$lib/layout/wizardWithSteps.svelte';
import Wizard from '$lib/layout/wizardWithSteps.svelte';
import { migrationFormToResources } from '$lib/stores/migration';
import { wizard } from '$lib/stores/wizard';
import { onDestroy } from 'svelte';
import { formData, provider, selectedProject } from '.';
import Step1 from './step1.svelte';
import Step2 from './step2.svelte';
import { requestedMigration } from '$routes/store';
import { base } from '$app/paths';
import { page } from '$app/state';
import { Wizard } from '$lib/layout';
import { sdk } from '$lib/stores/sdk';
import { capitalize } from '$lib/helpers/string';
import ResourceForm from './resource-form.svelte';
import { requestedMigration } from '$routes/store';
import { formData, provider, selectedProject, selectedRegion } from '.';
import { ID, type Models, Query } from '@appwrite.io/console';
import { InputSelect, InputText } from '$lib/elements/forms';
import {
Button,
Card,
Fieldset,
Icon,
Input,
Layout,
Skeleton,
Spinner,
Typography
} from '@appwrite.io/pink-svelte';
import { isCloud } from '$lib/system';
import { regions } from '$lib/stores/organization';
import { addNotification } from '$lib/stores/notifications';
import { goto, invalidate } from '$app/navigation';
import { base } from '$app/paths';
import { migrationFormToResources } from '$lib/stores/migration';
import { EyebrowHeading } from '$lib/components';
import {
IconAppwrite,
IconCog,
IconCurrencyDollar,
IconExclamation,
IconTrendingUp
} from '@appwrite.io/pink-icons-svelte';
import { Dependencies } from '$lib/constants';
import { filterRegions } from '$lib/helpers/regions';
const onExit = () => {
formData.reset();
requestedMigration.set(null);
};
const organizations = page.data.organizations
.teams as Models.TeamList<Models.Preferences>['teams'];
let showExitModal = false;
let showResources = false;
let loadingProjects = false;
let creatingProject = false;
let errorInResources = false;
let migrationStarted = false;
let projectSdkInstance: ReturnType<typeof sdk.forProject> | null = null;
let projects = [] as Models.ProjectList['projects'];
let selectedOrg = organizations.length ? organizations[0].$id : null;
let newProjName = '';
let projectType: 'existing' | 'new' = 'existing';
async function getProjects(orgId: string | null) {
if (!orgId) {
projects = [];
} else {
loadingProjects = true;
projects = await sdk.forConsole.projects
.list([Query.equal('teamId', orgId), Query.orderDesc('$createdAt')])
.then((res) => res.projects);
loadingProjects = false;
projectType = projects.length ? 'existing' : 'new';
if (projectType === 'existing') {
const first = projects[0];
$selectedProject = first.$id;
projectSdkInstance = sdk.forProject(first.region, first.region);
}
}
}
function getProjectName(): string {
return isExisting ? currentSelectedProject.name : newProjName || 'Appwrite project';
}
async function createNewProject() {
creatingProject = true;
try {
return await sdk.forConsole.projects.create(
ID.unique(),
newProjName,
selectedOrg,
$selectedRegion
);
} catch (error) {
addNotification({
type: 'error',
message: error.message
});
return null;
} finally {
creatingProject = false;
}
}
const onFinish = async () => {
if ($provider.provider !== 'appwrite') return;
migrationStarted = true;
const resources = migrationFormToResources($formData, $provider.provider);
await sdk
.forProject('fra1', $selectedProject)
.migrations.createAppwriteMigration(
try {
await projectSdkInstance.migrations.createAppwriteMigration(
resources,
$provider.endpoint,
$provider.projectID,
$provider.apiKey
);
await invalidateAll();
wizard.hide();
goto(`${base}/project-${$selectedProject}/settings/migrations`);
addNotification({
type: 'success',
message: 'Migration started'
});
onExit();
await invalidate(Dependencies.PROJECTS);
await goto(
`${base}/project-${currentSelectedProject.region}-${currentSelectedProject.$id}/settings/migrations`
);
} catch (error) {
addNotification({
type: 'error',
message: error.message
});
} finally {
migrationStarted = false;
}
};
onDestroy(onExit);
$: getProjects(selectedOrg);
const steps: WizardStepsType = new Map();
steps.set(1, {
label: 'Project',
component: Step1
});
steps.set(2, {
label: 'Resources',
component: Step2
});
$: currentSelectedProject = projects.find((project) => project.$id === $selectedProject);
$: isExisting = projectType === 'existing';
$: if (isExisting && $selectedProject) {
projectSdkInstance = sdk.forProject(
currentSelectedProject.region,
currentSelectedProject.$id
);
}
$: disableNextButton = isExisting
? !$selectedProject
: newProjName.trim() === '' || creatingProject;
$: isFinalsButtonEnabled =
showResources &&
!errorInResources &&
!disableNextButton &&
Object.values($formData).some((category) =>
Object.values(category).some((value) => value === true)
);
</script>
<Wizard title="Create Migration" {steps} on:exit={onExit} on:finish={onFinish} />
<Wizard title="Create migration" bind:showExitModal confirmExit {onExit}>
<Layout.Stack gap="xl">
{#if !showResources}
<Layout.Stack gap="xxl">
{#if organizations.length >= 1}
<Fieldset legend="Organization">
<InputSelect
required
id="organization"
bind:value={selectedOrg}
label="Select organization"
options={organizations.map((project) => ({
label: project.name,
value: project.$id
}))}
disabled={loadingProjects} />
</Fieldset>
{/if}
<Fieldset legend="Project">
{#if loadingProjects}
<Layout.Stack gap="xl">
<Layout.Grid rowGap="l" columns={2} columnsS={1}>
<Skeleton variant="line" width="100%" height={70} />
<Skeleton variant="line" width="100%" height={70} />
</Layout.Grid>
<Skeleton variant="line" width="100%" height={50} />
</Layout.Stack>
{:else}
<Layout.Stack gap="xl">
<Layout.Grid rowGap="l" columns={2} columnsXS={1}>
<Card.Selector
bind:group={projectType}
name="Existing project"
id="existing"
value="existing"
title="Existing project"
imageRadius="s">
Import data to an existing project
</Card.Selector>
<Card.Selector
bind:group={projectType}
name="Existing project"
id="new"
value="new"
title="Create new project"
imageRadius="s">
Import data to a new project
</Card.Selector>
</Layout.Grid>
{#if projectType === 'existing'}
<InputSelect
required
id="project"
label="Select a project"
bind:value={$selectedProject}
options={projects.map((p) => ({
label: p.name,
value: p.$id
}))} />
{:else}
<Layout.Stack gap="l">
<InputText
required
id="project_name"
label="Project name"
bind:value={newProjName}
placeholder="Enter project name" />
{#if isCloud && $regions.regions.length > 0}
<Input.Select
required
label="Region"
options={filterRegions($regions.regions)}
bind:value={$selectedRegion}
placeholder="Select a region" />
<Typography.Text
>Region cannot be changed after creation</Typography.Text>
{/if}
</Layout.Stack>
{/if}
<Layout.Stack direction="row" justifyContent="flex-end">
<Button.Button
size="s"
disabled={disableNextButton}
on:click={async () => {
if (isExisting) {
showResources = true;
} else {
const project = await createNewProject();
if (project !== null) {
projectSdkInstance = sdk.forProject(
project.region,
project.$id
);
showResources = true;
}
}
}}>
{#if creatingProject}
<Spinner size="s" />
{/if}
Next
</Button.Button>
</Layout.Stack>
</Layout.Stack>
{/if}
</Fieldset>
</Layout.Stack>
{:else}
<Card.Base radius="s" padding="s">
<Layout.Stack
direction="row"
justifyContent="space-between"
alignItems="center"
gap="xs">
<Layout.Stack gap="s" direction="row" alignItems="center">
<Icon icon={IconAppwrite} color="--fgcolor-neutral-primary" />
<Typography.Text variant="m-500">
{capitalize(getProjectName())}
</Typography.Text>
</Layout.Stack>
<Button.Button
size="s"
variant="secondary"
on:click={() => (showResources = !showResources)}>
Update
</Button.Button>
</Layout.Stack>
</Card.Base>
{/if}
{#if showResources}
<Fieldset legend="Resources">
<Layout.Stack gap="xl">
<ResourceForm
{provider}
{formData}
bind:errorInResources
migrationType="cloud"
projectSdk={projectSdkInstance} />
</Layout.Stack>
</Fieldset>
{/if}
</Layout.Stack>
<svelte:fragment slot="aside">
<Card.Base variant="primary" radius="s" padding="m">
<Layout.Stack gap="l">
<EyebrowHeading class="eyebrow" tag="h3" size={3}>Good to know</EyebrowHeading>
<!-- tip 1 -->
<Layout.Stack gap="l" direction="row" wrap="normal">
<span style:padding-block="2px">
<Icon icon={IconCog} size="s" />
</span>
<Layout.Stack gap="none">
<Typography.Text variant="m-600"
>Project settings are not imported</Typography.Text>
You will need to set service and project settings manually.
</Layout.Stack>
</Layout.Stack>
<!-- tip 2 -->
<Layout.Stack gap="l" direction="row" wrap="normal">
<span style:padding-block="2px">
<Icon icon={IconTrendingUp} size="s" />
</span>
<Layout.Stack gap="none">
<Typography.Text variant="m-600"
>Keep your organization plan's limits in mind</Typography.Text>
Make sure to have enough storage in your organization plan when importing files.
</Layout.Stack>
</Layout.Stack>
<!-- tip 3 -->
<Layout.Stack gap="l" direction="row" wrap="normal">
{@const isFirebase = $provider.provider === 'firebase'}
<span style:padding-block="3px">
<Icon
icon={isFirebase ? IconExclamation : IconCurrencyDollar}
size="s"
color={isFirebase ? '--fgcolor-warning' : undefined} />
</span>
<Layout.Stack gap="none">
<Typography.Text variant="m-600">
{isFirebase
? 'Possible charges by Firebase'
: 'Transfer is free of charge'}
</Typography.Text>
{#if isFirebase}
Appwrite does not impose charges for importing data, but please note
that Firebase may have its own pricing for this service
{:else}
You won't be charged for Appwrite bandwidth usage for importing data
{/if}
</Layout.Stack>
</Layout.Stack>
</Layout.Stack>
</Card.Base>
</svelte:fragment>
<svelte:fragment slot="footer">
<Button.Button variant="secondary" on:click={() => (showExitModal = true)}>
Cancel
</Button.Button>
<Button.Button
variant="primary"
on:click={onFinish}
disabled={!isFinalsButtonEnabled || migrationStarted}>
{#if migrationStarted}
<Spinner size="s" />
{/if}
Create
</Button.Button>
</svelte:fragment>
</Wizard>
+7 -5
View File
@@ -52,6 +52,7 @@
IconSparkles,
IconSwitchHorizontal
} from '@appwrite.io/pink-icons-svelte';
import type { LayoutData } from './$types';
function kebabToSentenceCase(str: string) {
return str
@@ -62,7 +63,8 @@
const isAssistantEnabled = $consoleVariables?._APP_ASSISTANT_ENABLED === true;
export let data;
export let data: LayoutData;
$: loadedProjects = data.projects.map((project) => {
return {
name: project?.name,
@@ -338,10 +340,10 @@
!page.url.pathname.includes('/console/account') &&
!page.url.pathname.includes('/console/card') &&
!page.url.pathname.includes('/console/onboarding')}
showHeader={!page.url.pathname.includes('/console/onboarding')}
showFooter={!page.url.pathname.includes('/console/onboarding')}
bind:loadedProjects
bind:projects={data.projects}>
showHeader={!page.url.pathname.includes('/console/onboarding/create-project')}
showFooter={!page.url.pathname.includes('/console/onboarding/create-project')}
{loadedProjects}
selectedProject={page.data?.project}>
<!-- <Header slot="header" />-->
<slot />
<Footer slot="footer" />
+3 -3
View File
@@ -4,7 +4,7 @@ import type { Tier } from '$lib/stores/billing';
import { sdk } from '$lib/stores/sdk';
import { isCloud } from '$lib/system';
import type { LayoutLoad } from './$types';
import { Query } from '@appwrite.io/console';
import { Query, type Models } from '@appwrite.io/console';
export const load: LayoutLoad = async ({ params, fetch, depends, parent }) => {
await parent();
@@ -38,7 +38,7 @@ export const load: LayoutLoad = async ({ params, fetch, depends, parent }) => {
? await sdk.forConsole.teams.list()
: await sdk.forConsole.billing.listOrganization();
let projects = [];
let projects: Models.Project[] = [];
let currentOrgId = params.organization ? params.organization : prefs.organization;
if (!currentOrgId && organizations.teams.length > 0) {
@@ -47,7 +47,7 @@ export const load: LayoutLoad = async ({ params, fetch, depends, parent }) => {
if (currentOrgId) {
const orgProjects = await sdk.forConsole.projects.list([
Query.equal('teamId', currentOrgId),
Query.limit(100),
Query.limit(5),
Query.orderDesc('$updatedAt')
]);
projects = orgProjects.projects.length > 0 ? orgProjects.projects : [];
+1 -1
View File
@@ -75,7 +75,7 @@
{/await}
{:else}
<label for="code">Enter the 6-digit one-time code generated by the app</label>
<InputDigits bind:value={code} autofocus />
<InputDigits required bind:value={code} autofocus />
{/if}
{/key}
<svelte:fragment slot="footer">
@@ -71,11 +71,11 @@
const sessionId = url.searchParams.get('sessionId');
if (sessionId) {
logoutSessionId(sessionId);
}
// Clear the sessionId param from the URL
url.searchParams.delete('sessionId');
goto(url.pathname + url.search, { replaceState: true });
// Clear the sessionId param from the URL
url.searchParams.delete('sessionId');
goto(url.pathname + url.search, { replaceState: true });
}
});
});
</script>
@@ -90,9 +90,9 @@
let:root
columns={[
{ id: 'client' },
{ id: 'location' },
{ id: 'ip' },
{ id: 'actions', width: 40 }
{ id: 'location', width: 120 },
{ id: 'ip', width: 120 },
{ id: 'actions', width: 70 }
]}>
<svelte:fragment slot="header" let:root>
<Table.Header.Cell column="client" {root}>Client</Table.Header.Cell>
+5 -110
View File
@@ -1,124 +1,19 @@
import Init3KeysPromo from '$lib/images/promos/init3-keys.png';
import Init3SitesPromo from '$lib/images/promos/init3-sites.png';
import Init3FlutterPromo from '$lib/images/promos/init3-flutter.png';
import Init3ImagesPromo from '$lib/images/promos/init3-images.png';
import Init3TokensPromo from '$lib/images/promos/init3-tokens.png';
import {
type BottomModalAlertItem,
setMobileSingleAlertLayout,
showBottomModalAlert
} from '$lib/stores/bottom-alerts';
import { isCloud } from '$lib/system';
import { isSameDay } from '$lib/helpers/date';
import { type BottomModalAlertItem, showBottomModalAlert } from '$lib/stores/bottom-alerts';
const listOfPromotions: BottomModalAlertItem[] = [];
if (!isCloud) {
const getSrc = (image: string) => {
return {
dark: image,
light: image
};
};
const sharedCTA = (url: string) => ({
external: true,
link: () => url,
hideOnClick: true,
text: 'Read the announcement'
});
const promos: BottomModalAlertItem[] = [
{
id: 'modal:init3_sites',
src: getSrc(Init3SitesPromo),
title: 'Announcing: Sites',
message:
'The open-source Vercel alternative. With Sites, you deploy and host your websites and web apps right inside Appwrite.',
plan: 'free',
importance: 8,
scope: 'everywhere',
cta: sharedCTA('https://apwr.dev/sites1'),
// Day 1 = Monday 9 AM CET
show: isPromoLive('2025-05-19', '09:00')
},
{
id: 'modal:init3_flutter',
src: getSrc(Init3FlutterPromo),
title: 'Announcing: Hosting for Flutter Web',
message: 'Deploy your Flutter web apps directly from Appwrite Sites.',
plan: 'free',
importance: 8,
scope: 'everywhere',
cta: sharedCTA('https://apwr.dev/sitesfl'),
// Day 2 = Tuesday 3 PM CET
show: isPromoLive('2025-05-20', '15:00')
},
{
id: 'modal:init3_keys',
src: getSrc(Init3KeysPromo),
title: 'Announcing: Dev Keys',
message:
'A new Appwrite feature that lets you bypass rate limits during local development.',
plan: 'free',
importance: 8,
scope: 'everywhere',
cta: sharedCTA('https://apwr.dev/devkeys'),
// Day 3 = Wednesday 3 PM CET
show: isPromoLive('2025-05-21', '15:00')
},
{
id: 'modal:init3_images',
src: getSrc(Init3ImagesPromo),
title: 'Announcing: New image formats',
message:
'We have added support for two new image formats in Appwrite Storage: HEIC and AVIF.',
plan: 'free',
importance: 8,
scope: 'everywhere',
cta: sharedCTA('https://apwr.dev/imagefo'),
// Day 4 = Thursday 3 PM CET
show: isPromoLive('2025-05-22', '15:00')
},
{
id: 'modal:init3_file_tokens',
src: getSrc(Init3TokensPromo),
title: 'Announcing: File Tokens',
message:
'File tokens let you share files easily and securely, without modifying permissions or changing project access.',
plan: 'free',
importance: 8,
scope: 'everywhere',
cta: sharedCTA('https://apwr.dev/filet'),
// Day 5 = Friday 3 PM CET
show: isPromoLive('2025-05-23', '15:00')
}
];
const currentPromo = promos.find((promo) => promo.show);
if (currentPromo) {
listOfPromotions.push(currentPromo);
setMobileSingleAlertLayout({
title: currentPromo.title,
message: currentPromo.message,
enabled: true,
cta: currentPromo.cta
});
}
if (isCloud) {
/* add promos here */
}
export function addBottomModalAlerts() {
listOfPromotions.forEach((promotion) => showBottomModalAlert(promotion));
}
// use this for time based promo handling
// noinspection JSUnusedGlobalSymbols
export function isPromoLive(
date: string,
time: string,
@@ -195,35 +195,47 @@
required />
</Fieldset>
<Fieldset legend="Select plan">
<Typography.Text>
For more details on our plans, visit our
<Link.Anchor
href="https://appwrite.io/pricing"
target="_blank"
rel="noopener noreferrer">pricing page</Link.Anchor
>.
</Typography.Text>
<PlanSelection
bind:billingPlan={selectedPlan}
anyOrgFree={data.hasFreeOrganizations}
isNewOrg />
<Layout.Stack>
<Typography.Text>
For more details on our plans, visit our
<Link.Anchor
href="https://appwrite.io/pricing"
target="_blank"
rel="noopener noreferrer">pricing page</Link.Anchor
>.
</Typography.Text>
<PlanSelection
bind:billingPlan={selectedPlan}
anyOrgFree={data.hasFreeOrganizations}
isNewOrg />
</Layout.Stack>
</Fieldset>
{#if selectedPlan !== BillingPlan.FREE}
<Fieldset legend="Payment">
<SelectPaymentMethod
methods={data.paymentMethods}
bind:value={paymentMethodId}
bind:taxId>
<svelte:fragment slot="actions">
{#if !selectedCoupon?.code}
<Divider vertical style="height: 2rem;" />
<Button compact on:click={() => (showCreditModal = true)}>
<Icon icon={IconPlus} slot="start" size="s" />
Add credits
</Button>
{/if}
</svelte:fragment>
</SelectPaymentMethod>
<Layout.Stack gap="s" alignItems="flex-start">
<SelectPaymentMethod
methods={data.paymentMethods}
bind:value={paymentMethodId}
bind:taxId>
<svelte:fragment slot="actions">
{#if !selectedCoupon?.code && paymentMethodId}
<Divider vertical style="height: 2rem;" />
<Button compact on:click={() => (showCreditModal = true)}>
<Icon icon={IconPlus} slot="start" size="s" />
Add credits
</Button>
{/if}
</svelte:fragment>
</SelectPaymentMethod>
{#if !selectedCoupon?.code && !paymentMethodId}
<Button compact on:click={() => (showCreditModal = true)}>
<Icon icon={IconPlus} slot="start" size="s" />
Add credits
</Button>
{/if}
</Layout.Stack>
</Fieldset>
<Fieldset legend="Invite members">
<InputTags
@@ -1,10 +1,11 @@
import { BillingPlan } from '$lib/constants';
import { BillingPlan, Dependencies } from '$lib/constants';
import { sdk } from '$lib/stores/sdk';
import type { PageLoad } from './$types';
import type { Coupon } from '$lib/sdk/billing';
import type { Organization } from '$lib/stores/organization';
export const load: PageLoad = async ({ url, parent }) => {
export const load: PageLoad = async ({ url, parent, depends }) => {
depends(Dependencies.CREATE_ORGANIZATION);
const { organizations } = await parent();
const [coupon, paymentMethods] = await Promise.all([
getCoupon(url),
@@ -0,0 +1,115 @@
<script lang="ts">
import { isCloud } from '$lib/system';
import { sdk } from '$lib/stores/sdk';
import { ID } from '@appwrite.io/console';
import { BillingPlan } from '$lib/constants';
import { tierToPlan } from '$lib/stores/billing';
import { addNotification } from '$lib/stores/notifications';
import { loadAvailableRegions } from '$routes/(console)/regions';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { Button, Card, Layout, Input, Typography, Spinner } from '@appwrite.io/pink-svelte';
import { Form } from '$lib/elements/forms/index.js';
import { goto } from '$app/navigation';
import { base } from '$app/paths';
let isLoading = false;
let organizationName = 'Personal Projects';
async function createOrganization() {
isLoading = true;
let organization = null;
try {
if (isCloud) {
organization = await sdk.forConsole.billing.createOrganization(
ID.unique(),
organizationName,
BillingPlan.FREE,
null,
null
);
trackEvent(Submit.OrganizationCreate, {
plan: tierToPlan(BillingPlan.FREE)?.name,
budget_cap_enabled: false,
members_invited: 0
});
} else {
organization = await sdk.forConsole.teams.create(ID.unique(), organizationName);
}
} catch (e) {
trackError(e, Submit.OrganizationCreate);
addNotification({
type: 'error',
message: e.message
});
} finally {
isLoading = false;
if (organization) {
loadAvailableRegions(organization?.$id).then();
await goto(`${base}/organization-${organization.$id}`);
}
}
}
</script>
<svelte:head>
<title>Create organization - Appwrite</title>
</svelte:head>
<Form onSubmit={createOrganization}>
<div class="viewport-center">
<div class="page-container u-cross-child-center">
<Card.Base variant="primary" padding="l">
<Layout.Stack direction="column" gap="xxl" alignItems="flex-start">
<Typography.Title size="l">Create an organization</Typography.Title>
<Input.Text
required
disabled={isLoading}
label="Organization name"
bind:value={organizationName}
placeholder="Personal Projects" />
<Layout.Stack direction="row" justifyContent="flex-end">
<Button.Button
size="s"
autofocus
type="submit"
variant="primary"
disabled={isLoading}>
{#if isLoading}
<Spinner size="s" />
{/if}
Create
</Button.Button>
</Layout.Stack>
</Layout.Stack>
</Card.Base>
</div>
</div>
</Form>
<style lang="scss">
@use '@appwrite.io/pink-legacy/src/abstract/variables/devices';
:global(body) {
margin: 0;
background: var(--bgcolor-neutral-default, #19191c);
}
.viewport-center {
height: 60vh;
display: flex;
align-items: center;
justify-content: center;
}
.page-container {
width: calc(100% - 2rem);
@media #{devices.$break2open} {
width: 700px;
}
}
</style>
@@ -10,6 +10,8 @@
import { base } from '$app/paths';
import { addNotification } from '$lib/stores/notifications';
import CreateProject from '$lib/layout/createProject.svelte';
import { loadAvailableRegions } from '$routes/(console)/regions';
import { regions as regionsStore } from '$lib/stores/organization';
let isLoading = false;
let id: string;
@@ -49,6 +51,9 @@
});
}
}
// safe side!
loadAvailableRegions(data.organization?.$id);
</script>
<svelte:head>
@@ -74,7 +79,7 @@
alt="Appwrite Logo" />
<Card.Base variant="primary" padding="l">
<CreateProject
regions={isCloud ? data.regions.regions : []}
regions={$regionsStore?.regions}
bind:projectName
bind:id
bind:region
@@ -3,72 +3,71 @@ import { isCloud } from '$lib/system';
import { sdk } from '$lib/stores/sdk';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { isOrganization, tierToPlan } from '$lib/stores/billing';
import { ID, Query } from '@appwrite.io/console';
import { ID, Query, type Models } from '@appwrite.io/console';
import { BillingPlan } from '$lib/constants';
import { redirect } from '@sveltejs/kit';
import { base } from '$app/paths';
// TODO: this needs to be cleaned up!
export const load: PageLoad = async ({ parent }) => {
const { organizations } = await parent();
try {
if (!organizations?.total) {
try {
if (isCloud) {
const org = await sdk.forConsole.billing.createOrganization(
ID.unique(),
'Personal projects',
BillingPlan.FREE,
null,
null
);
trackEvent(Submit.OrganizationCreate, {
plan: tierToPlan(BillingPlan.FREE)?.name,
budget_cap_enabled: false,
members_invited: 0
});
if (isOrganization(org)) {
return {
organization: org,
regions: await sdk.forConsole.billing.listRegions(org.$id)
};
} else {
const e = new Error(org.message, {
cause: org
});
trackError(e, Submit.OrganizationCreate);
}
} else {
if (!organizations?.total) {
try {
if (isCloud) {
const org = await sdk.forConsole.billing.createOrganization(
ID.unique(),
'Personal projects',
BillingPlan.FREE,
null,
null
);
trackEvent(Submit.OrganizationCreate, {
plan: tierToPlan(BillingPlan.FREE)?.name,
budget_cap_enabled: false,
members_invited: 0
});
if (isOrganization(org)) {
return {
organization: await sdk.forConsole.teams.create(
ID.unique(),
'Personal projects'
),
regions: null
organization: org
};
} else {
const e = new Error(org.message, {
cause: org
});
trackError(e, Submit.OrganizationCreate);
}
} catch (e) {
trackError(e, Submit.OrganizationCreate);
} else {
return {
organization: await sdk.forConsole.teams.create(
ID.unique(),
'Personal projects'
)
};
}
} else if (organizations?.total === 1) {
const org = organizations.teams[0];
const projects = await sdk.forConsole.projects.list([
} catch (e) {
trackError(e, Submit.OrganizationCreate);
}
} else if (organizations?.total === 1) {
const org = organizations.teams[0];
let projects: Models.ProjectList = null;
try {
projects = await sdk.forConsole.projects.list([
Query.equal('teamId', org.$id),
Query.limit(1)
]);
if (!projects.total) {
const regions = isCloud ? await sdk.forConsole.billing.listRegions(org.$id) : null;
return {
organization: org,
regions
};
} else {
redirect(303, `${base}/console/organization-${org.$id}`);
}
} else {
redirect(303, `${base}/console/organization-${organizations.teams[0].$id}`);
} catch (e) {
redirect(303, `${base}/organization-${org.$id}`);
}
} catch (error) {
console.error(error);
if (!projects?.total) {
return {
organization: org
};
} else {
redirect(303, `${base}/organization-${org.$id}`);
}
} else {
redirect(303, `${base}/organization-${organizations.teams[0].$id}`);
}
};
@@ -17,7 +17,6 @@
} from '$lib/components';
import { goto } from '$app/navigation';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { services } from '$lib/stores/project-services';
import { sdk } from '$lib/stores/sdk';
import { loading } from '$routes/store';
import type { Models } from '@appwrite.io/console';
@@ -39,7 +38,7 @@
} from '@appwrite.io/pink-icons-svelte';
import { getPlatformInfo } from '$lib/helpers/platform';
import CreateProjectCloud from './createProjectCloud.svelte';
import { organization, regions as regionsStore } from '$lib/stores/organization';
import { regions as regionsStore } from '$lib/stores/organization';
export let data;
@@ -47,23 +46,6 @@
let showCreateProjectCloud = false;
let addOrganization = false;
async function allServiceDisabled(project: Models.Project) {
let disabled = true;
try {
if (!project.$id) return false;
if (!project.platforms.length) return false;
services.load(project);
$services.list.forEach((service) => {
if (service.value) {
disabled = false;
}
});
return disabled;
} catch (e) {
return true;
}
}
function filterPlatforms(platforms: { name: string; icon: string }[]) {
return platforms.filter(
(value, index, self) => index === self.findIndex((t) => t.name === value.name)
@@ -130,17 +112,11 @@
}
};
onMount(async () => {
if (isCloud && $organization.$id) {
const regions = await sdk.forConsole.billing.listRegions($organization.$id);
regionsStore.set(regions);
checkPricingRefAndRedirect(page.url.searchParams);
}
checkPricingRefAndRedirect(page.url.searchParams);
});
function findRegion(project: Models.Project) {
return $regionsStore?.regions?.find(
(region) => region.$id === (project as Models.Project & { region: string }).region
);
return $regionsStore.regions.find((region) => region.$id === project.region);
}
</script>
@@ -186,15 +162,6 @@
<svelte:fragment slot="title">
{project.name}
</svelte:fragment>
{#await allServiceDisabled(project) then isDisabled}
{#if isDisabled}
<p>
<span class="icon-pause" aria-hidden="true"></span> All services are
disabled.
</p>
{/if}
{/await}
{#each platforms as platform, i}
{#if i < 3}
{@const icon = getIconForPlatform(platform.icon)}
@@ -5,7 +5,7 @@
import { toLocaleDate } from '$lib/helpers/date';
import { formatCurrency } from '$lib/helpers/numbers';
import type { Invoice, InvoiceList } from '$lib/sdk/billing';
import { sdk } from '$lib/stores/sdk';
import { getApiEndpoint, sdk } from '$lib/stores/sdk';
import { Query } from '@appwrite.io/console';
import { onMount } from 'svelte';
import { trackEvent } from '$lib/actions/analytics';
@@ -27,7 +27,6 @@
IconExternalLink,
IconRefresh
} from '@appwrite.io/pink-icons-svelte';
import { base } from '$app/paths';
let offset = 0;
let invoiceList: InvoiceList = {
@@ -36,6 +35,7 @@
};
const limit = 5;
const endpoint = getApiEndpoint();
onMount(request);
@@ -134,12 +134,12 @@
<ActionMenu.Item.Anchor
leadingIcon={IconExternalLink}
external
href={`${base}/organizations/${page.params.organization}/invoices/${invoice.$id}/view`}>
href={`${endpoint}/organizations/${page.params.organization}/invoices/${invoice.$id}/view`}>
View invoice
</ActionMenu.Item.Anchor>
<ActionMenu.Item.Anchor
leadingIcon={IconDownload}
href={`${base}/organizations/${page.params.organization}/invoices/${invoice.$id}/download`}>
href={`${endpoint}/organizations/${page.params.organization}/invoices/${invoice.$id}/download`}>
Download PDF
</ActionMenu.Item.Anchor>
{#if status === 'overdue' || status === 'failed'}
@@ -7,7 +7,6 @@
import { organization } from '$lib/stores/organization';
import type { Aggregation, CreditList, Invoice, Plan } from '$lib/sdk/billing';
import { abbreviateNumber, formatCurrency, formatNumberWithCommas } from '$lib/helpers/numbers';
import { humanFileSize } from '$lib/helpers/sizeConvertion';
import { BillingPlan } from '$lib/constants';
import { Click, trackEvent } from '$lib/actions/analytics';
import {
@@ -35,21 +34,6 @@
new Date($organization?.billingStartDate).getTime() - today.getTime() > 0 &&
$plansInfo.get($organization.billingPlan)?.trialDays;
const extraUsage = currentInvoice ? currentInvoice.amount - currentPlan?.price : 0;
function usageNameToLabel(name: string): string {
switch (name) {
case 'executions':
return 'Executions';
case 'storage':
return 'Storage';
case 'users':
return 'Users';
case 'bandwidth':
return 'Bandwidth';
default:
return '';
}
}
</script>
{#if $organization}
@@ -112,69 +96,29 @@
{#if i > 0 || currentAggregation.additionalMembers}
<Divider />
{/if}
{#if ['storage', 'bandwidth'].includes(excess.name)}
{@const excessValue = humanFileSize(excess.value)}
<Layout.Stack gap="xxxs">
<Layout.Stack
direction="row"
justifyContent="space-between">
<Typography.Text
color="--fgcolor-neutral-primary"
>{usageNameToLabel(
excess.name
)}</Typography.Text>
<Typography.Text
>{formatCurrency(
excess.amount
)}</Typography.Text>
</Layout.Stack>
<Layout.Stack direction="row">
<Layout.Stack direction="row">
<Tooltip
placement="bottom"
disabled={excess.value <=
1_000_000_000}>
<svelte:fragment slot="tooltip">
{formatNumberWithCommas(
excess.value ?? 0
)} bytes
</svelte:fragment>
<span
>{excessValue.value ??
0}{excessValue.unit}</span>
</Tooltip>
</Layout.Stack>
</Layout.Stack>
<Layout.Stack gap="xxxs">
<Layout.Stack
direction="row"
justifyContent="space-between">
<Typography.Text color="--fgcolor-neutral-primary">
{excess.name}
</Typography.Text>
<Typography.Text>
{formatCurrency(excess.amount)}
</Typography.Text>
</Layout.Stack>
{/if}
{#if ['users', 'executions'].includes(excess.name)}
<Layout.Stack gap="xxxs">
<Layout.Stack
direction="row"
justifyContent="space-between">
<Typography.Text
color="--fgcolor-neutral-primary"
>{usageNameToLabel(
excess.name
)}</Typography.Text>
<Typography.Text
>{formatCurrency(
excess.amount
)}</Typography.Text>
</Layout.Stack>
<Layout.Stack direction="row">
<Tooltip
placement="bottom"
disabled={excess.value <= 1000}>
<svelte:fragment slot="tooltip">
{formatNumberWithCommas(excess.value)}
</svelte:fragment>
<span
>{abbreviateNumber(excess.value)}</span>
</Tooltip>
</Layout.Stack>
<Layout.Stack direction="row">
<Tooltip
placement="bottom"
disabled={excess.value <= 1000}>
<svelte:fragment slot="tooltip">
{formatNumberWithCommas(excess.value)}
</svelte:fragment>
<span>{abbreviateNumber(excess.value)}</span>
</Tooltip>
</Layout.Stack>
{/if}
</Layout.Stack>
{/each}
{/if}
</Layout.Stack>
@@ -241,9 +241,9 @@
}
}
$: isUpgrade = $plansInfo.get(selectedPlan).order > $currentPlan.order;
$: isDowngrade = $plansInfo.get(selectedPlan).order < $currentPlan.order;
$: isButtonDisabled = $organization.billingPlan === selectedPlan;
$: isUpgrade = $plansInfo.get(selectedPlan).order > $currentPlan?.order;
$: isDowngrade = $plansInfo.get(selectedPlan).order < $currentPlan?.order;
$: isButtonDisabled = $organization?.billingPlan === selectedPlan;
</script>
<svelte:head>
@@ -6,8 +6,7 @@
import { Dependencies } from '$lib/constants';
import { page } from '$app/state';
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
import { ID, Region as ConsoleRegion, type Models } from '@appwrite.io/console';
import { createProject } from './wizard/store';
import { ID, Region as ConsoleRegion, type Models, Region } from '@appwrite.io/console';
import { Button } from '@appwrite.io/pink-svelte';
import { base } from '$app/paths';
import CreateProject from '$lib/layout/createProject.svelte';
@@ -17,6 +16,10 @@
export let regions: Array<Models.ConsoleRegion> = [];
export let showCreateProjectCloud: boolean;
let id: string = null;
let name: string = 'Appwrite project';
let region: string = Region.Fra;
async function onFinish() {
await invalidate(Dependencies.FUNCTIONS);
}
@@ -25,19 +28,19 @@
try {
// TODO: fix typing once SDK is updated
const project = await sdk.forConsole.projects.create(
$createProject?.id ?? ID.unique(),
$createProject.name,
id ?? ID.unique(),
name,
teamId,
$createProject.region as ConsoleRegion
region as ConsoleRegion
);
trackEvent(Submit.ProjectCreate, {
customId: !!$createProject?.id,
customId: !!id,
teamId,
region: $createProject.region
region: region
});
addNotification({
type: 'success',
message: `${$createProject.name} has been created`
message: `${name} has been created`
});
await onFinish();
await goto(`${base}/project-${project.region}-${project.$id}`);
@@ -51,21 +54,14 @@
}
onDestroy(() => {
$createProject = {
id: null,
name: null,
region: 'fra'
};
id = null;
name = null;
region = 'fra';
});
</script>
<Modal bind:show={showCreateProjectCloud} title={'Create project'} onSubmit={create}>
<CreateProject
showTitle={false}
bind:id={$createProject.id}
bind:projectName={$createProject.name}
bind:region={$createProject.region}
{regions}>
<CreateProject showTitle={false} bind:id bind:projectName={name} bind:region {regions}>
</CreateProject>
<svelte:fragment slot="footer">
<Button.Button type="submit" variant="primary" size="s">Create</Button.Button>
@@ -4,11 +4,11 @@
import { toLocaleDate } from '$lib/helpers/date';
import { Layout, Status } from '@appwrite.io/pink-svelte';
export let domain: Domain;
let { domain }: { domain: Domain } = $props();
$: isDomainVerified = domain.nameservers.toLocaleLowerCase() === 'appwrite';
const isDomainVerified = domain.nameservers.toLocaleLowerCase() === 'appwrite';
let metrics = [
const metrics = [
{
value: isDomainVerified ? 'Verified' : 'Not verified',
description: 'Status'
@@ -18,11 +18,11 @@
description: 'Registrar'
},
{
value: domain?.nameservers ? domain?.nameservers : '-',
value: domain?.nameservers || '-',
description: 'Nameservers'
},
{
value: domain?.expiry ? toLocaleDate(domain?.expiry) : '-',
value: domain?.expiry ? toLocaleDate(domain.expiry) : '-',
description: 'Expiry date'
},
{
@@ -16,13 +16,18 @@
Input,
InteractiveText
} from '@appwrite.io/pink-svelte';
import { base } from '$app/paths';
import { page } from '$app/state';
import { goto } from '$app/navigation';
export let domain: Domain;
let verified = undefined;
const routeBase = `${base}/organization-${page.params.organization}/domains/domain-${domain.$id}`;
// TODO: split _APP_DOMAINS_NAMESERVERS?
let nameservers = $consoleVariables?._APP_DOMAINS_NAMESERVERS.split(',') ?? [
const nameservers = $consoleVariables?._APP_DOMAINS_NAMESERVERS.split(',') ?? [
'ns1.appwrite.io',
'ns2.appwrite.io'
];
@@ -31,6 +36,19 @@
try {
domain = await sdk.forConsole.domains.updateNameservers(domain.$id);
verified = domain.nameservers === 'Appwrite';
if (verified) {
addNotification({
type: 'success',
message: 'Domain verified'
});
await goto(routeBase);
} else {
addNotification({
type: 'error',
message: 'Domain not verified'
});
}
} catch (error) {
addNotification({
type: 'error',
@@ -13,7 +13,7 @@
export let show = false;
export let selectedDomain: Domain;
let nameservers = $consoleVariables?._APP_DOMAINS_NAMESERVERS.split(',') ?? [
const nameservers = $consoleVariables?._APP_DOMAINS_NAMESERVERS.split(',') ?? [
'ns1.appwrite.io',
'ns2.appwrite.io'
];
@@ -30,7 +30,7 @@
});
$: avatars = $members.memberships?.map((m) => m.userName || m.userEmail) ?? [];
$: organizationId = page.params.organization;
$: organizationId = $organization?.$id ?? page.params.organization;
$: path = `${base}/organization-${organizationId}`;
$: tabs = [
{
@@ -75,7 +75,7 @@
].filter((tab) => !tab.disabled);
</script>
{#if $organization?.$id}
{#if $organization.$id}
<Cover>
<svelte:fragment slot="header">
<span class="u-flex u-cross-center u-gap-8 u-min-width-0">
@@ -39,7 +39,7 @@
if ($organizationList?.total > 1) {
await goto(`${base}/account/organizations`);
} else {
await goto(`${base}/onboarding/create-project`);
await goto(`${base}/onboarding/create-organization`);
}
await invalidate(Dependencies.ACCOUNT);
await invalidate(Dependencies.ORGANIZATION);
@@ -6,6 +6,7 @@
import { onMount } from 'svelte';
import { Accordion, Table } from '@appwrite.io/pink-svelte';
import { base } from '$app/paths';
import type { PageData } from './$types';
type Metric =
| 'users'
@@ -21,7 +22,7 @@
type DatabaseOperationMetric = Extract<Metric, 'databasesReads' | 'databasesWrites'>;
export let data;
export let data: PageData;
export let projects: OrganizationUsage['projects'];
export let metric: Metric | undefined = undefined;
export let estimate: Estimate | undefined = undefined;
@@ -137,7 +138,7 @@
{#if !$canSeeProjects}
<Table.Row.Base {root}>
<Table.Cell column="project" {root}>
{data.projectNames[project.projectId]?.name ?? 'Unknown'}
{data.projects[project.projectId]?.name ?? 'Unknown'}
</Table.Cell>
<Table.Cell column="reads" {root}>
{format(project.databasesReads ?? 0)}
@@ -155,7 +156,7 @@
{:else}
<Table.Row.Link href={getProjectUsageLink(project.projectId)} {root}>
<Table.Cell column="project" {root}>
{data.projectNames[project.projectId]?.name ?? 'Unknown'}
{data.projects[project.projectId]?.name ?? 'Unknown'}
</Table.Cell>
<Table.Cell column="reads" {root}>
{format(project.databasesReads ?? 0)}
@@ -1,11 +0,0 @@
import { writable } from 'svelte/store';
export const createProject = writable<{
id?: string;
name: string;
region: string;
}>({
id: null,
name: 'Appwrite project',
region: 'fra'
});
@@ -1,11 +1,13 @@
<script lang="ts" context="module">
import { get } from 'svelte/store';
import { page } from '$app/state';
import { base } from '$app/paths';
import { goto } from '$app/navigation';
let showDelete = writable(false);
export const promptDeleteUser = (id: string) => {
showDelete.set(true);
goto(`${base}/project-${get(project).$id}/auth/user-${id}`);
goto(`${base}/project-${page.params.region}-${page.params.project}/auth/user-${id}`);
};
</script>
@@ -15,10 +17,7 @@
import { writable } from 'svelte/store';
import DeleteUser from './deleteUser.svelte';
import { user } from './store';
import { goto } from '$app/navigation';
import { project } from '../../store';
import { toLocaleDate } from '$lib/helpers/date';
import { base } from '$app/paths';
$: accessedAt = ($user as unknown as { accessedAt: string }).accessedAt;
</script>
@@ -15,6 +15,7 @@
let selectedMembership: Models.Membership;
let showDelete = false;
const region = page.params.region;
const project = page.params.project;
</script>
@@ -37,7 +38,7 @@
{#each data.memberships.memberships as membership}
<Table.Row.Link
{root}
href={`${base}/project-${project}/auth/teams/team-${membership.teamId}`}>
href={`${base}/project-${region}-${project}/auth/teams/team-${membership.teamId}`}>
<Table.Cell column="name" {root}>
<Layout.Stack direction="row" alignItems="center">
<AvatarInitials size="xs" name={membership.teamName} />
@@ -22,7 +22,8 @@
Button,
Link,
Badge,
FloatingActionBar
FloatingActionBar,
Typography
} from '@appwrite.io/pink-svelte';
import DualTimeView from '$lib/components/dualTimeView.svelte';
@@ -102,6 +103,7 @@
type: attribute.type as ColumnType,
show: selected?.includes(attribute.key) ?? true,
array: attribute?.array,
width: { min: 168 },
format:
'format' in attribute && attribute?.format === 'enum' ? attribute.format : null,
elements: 'elements' in attribute ? attribute.elements : null
@@ -166,7 +168,12 @@
let:root
allowSelection
bind:selectedRows
columns={[{ id: '$id', width: 200 }, ...$columns, { id: '$created' }, { id: '$updated' }]}>
columns={[
{ id: '$id', width: 200 },
...$columns,
{ id: '$created', width: 200 },
{ id: '$updated', width: 200 }
]}>
<svelte:fragment slot="header" let:root>
<Table.Header.Cell column="$id" {root}>Document ID</Table.Header.Cell>
{#each $columns as column}
@@ -238,10 +245,13 @@
{:else}
{@const formatted = formatColumn(document[id])}
<Tooltip disabled={!formatted.truncated} placement="bottom">
<span>
<Typography.Text truncate>
{formatted.value}
</span>
<span style:white-space="pre-wrap" slot="tooltip">
</Typography.Text>
<span
style:white-space="pre-wrap"
style:word-break="break-all"
slot="tooltip">
{formatted.whole}
</span>
</Tooltip>
@@ -8,7 +8,7 @@
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
import { Icon, Layout, Tooltip, Typography, Upload } from '@appwrite.io/pink-svelte';
import { writable } from 'svelte/store';
import { get, writable } from 'svelte/store';
import { ID, Runtime } from '@appwrite.io/console';
import type { Models } from '@appwrite.io/console';
import { consoleVariables } from '$routes/(console)/store';
@@ -20,15 +20,22 @@
import Configuration from './configuration.svelte';
import { getIconFromRuntime } from '$lib/stores/runtimes';
import { InvalidFileType, removeFile } from '$lib/helpers/files';
import { isCloud } from '$lib/system';
import { humanFileSize } from '$lib/helpers/sizeConvertion';
import { currentPlan } from '$lib/stores/organization';
export let data;
const iconFor = get(iconPath);
const runtimeOptions = data.runtimesList.runtimes.map((runtime) => {
return {
value: runtime.$id,
label: `${runtime.name} - ${runtime.version}`,
leadingHtml: `<img src='${$iconPath(getIconFromRuntime(runtime.key), 'color')}' style='inline-size: var(--icon-size-m)' />`
};
const { $id: value, name, version, key } = runtime;
const label = `${name} - ${version}`;
const iconName = getIconFromRuntime(key);
const iconSrc = iconFor(iconName, 'color');
const leadingHtml = `<img src='${iconSrc}' alt='${iconName}' style='inline-size: var(--icon-size-m)' />`;
return { value, label, leadingHtml };
});
const specificationOptions = data.specificationsList.specifications.map((size) => ({
@@ -153,6 +160,13 @@
};
})
: [];
$: maxSize =
isCloud && $currentPlan
? $currentPlan.deploymentSize * 1000000
: $consoleVariables._APP_COMPUTE_SIZE_LIMIT; // already in MB
$: readableMaxSize = humanFileSize(maxSize);
</script>
<svelte:head>
@@ -176,7 +190,7 @@
bind:files
title="Upload function"
extensions={['gz', 'tar']}
maxSize={10000000}
{maxSize}
required
on:invalid={handleInvalid}>
<Layout.Stack alignItems="center" gap="s">
@@ -201,12 +215,12 @@
</Tooltip>
</Layout.Stack>
<Typography.Caption variant="400"
>Max file size 10MB</Typography.Caption>
>Max file size: {readableMaxSize.value +
readableMaxSize.unit}</Typography.Caption>
</Layout.Stack>
</Layout.Stack>
</Upload.Dropzone>
{#if files?.length}
<!-- TODO: torsten, the types issue with FileList-->
<Upload.List
bind:files={filesList}
on:remove={(e) => (files = removeFile(e.detail, files))} />
@@ -14,6 +14,7 @@
import { consoleVariables } from '$routes/(console)/store';
import { currentPlan } from '$lib/stores/organization';
import { isCloud } from '$lib/system';
import { humanFileSize } from '$lib/helpers/sizeConvertion';
export let show = false;
@@ -21,12 +22,12 @@
let error: string = '';
$: maxSize =
isCloud && $currentPlan
? $currentPlan?.deploymentSize === 0
? 0
: $currentPlan?.deploymentSize * 1000000
isCloud && $currentPlan?.deploymentSize
? $currentPlan.deploymentSize * 1000000
: $consoleVariables._APP_COMPUTE_SIZE_LIMIT; // already in MB
$: readableMaxSize = humanFileSize(maxSize);
async function create() {
try {
await sdk
@@ -100,7 +101,9 @@
>Only .tar.gz files allowed</svelte:fragment>
</Tooltip>
</Layout.Stack>
<Typography.Caption variant="400">Max file size 10MB</Typography.Caption>
<Typography.Caption variant="400"
>Max file size: {readableMaxSize.value +
readableMaxSize.unit}</Typography.Caption>
</Layout.Stack>
</Layout.Stack>
</Upload.Dropzone>
@@ -18,7 +18,7 @@
let showDelete = $state(false);
let showRetry = $state(false);
let selectedDomain: Models.ProxyRule = null;
let selectedProxyRule: Models.ProxyRule = null;
</script>
<Container>
@@ -35,14 +35,14 @@
Add domain
</Button>
</Layout.Stack>
{#if data.domains.total}
<Table domains={data.domains} />
{#if data.proxyRules.total}
<Table proxyRules={data.proxyRules} />
<PaginationWithLimit
name="Domains"
limit={data.limit}
offset={data.offset}
total={data.domains.total} />
total={data.proxyRules.total} />
{:else if data?.search}
<EmptySearch hidePages bind:search={data.search} target="domains" hidePagination>
<svelte:fragment slot="actions">
@@ -83,9 +83,9 @@
</Container>
{#if showDelete}
<DeleteDomainModal bind:show={showDelete} {selectedDomain} />
<DeleteDomainModal bind:show={showDelete} {selectedProxyRule} />
{/if}
{#if showRetry}
<RetryDomainModal bind:show={showRetry} {selectedDomain} />
<RetryDomainModal bind:show={showRetry} {selectedProxyRule} />
{/if}
@@ -26,7 +26,7 @@ export const load: PageLoad = async ({ depends, params, url, route }) => {
limit,
query,
search,
domains: await sdk
proxyRules: await sdk
.forProject(params.region, params.project)
.proxy.listRules(
[
@@ -11,15 +11,16 @@
import { page } from '$app/state';
export let show = false;
export let selectedDomain: Models.ProxyRule;
let confirm = false;
export let selectedProxyRule: Models.ProxyRule;
let error = '';
let confirm = false;
async function deleteDomain() {
try {
await sdk
.forProject(page.params.region, page.params.project)
.proxy.deleteRule(selectedDomain.$id);
.proxy.deleteRule(selectedProxyRule.$id);
await invalidate(Dependencies.FUNCTION_DOMAINS);
show = false;
addNotification({
@@ -35,13 +36,13 @@
</script>
<Confirm title="Delete domain" bind:open={show} onSubmit={deleteDomain} bind:error>
{#if selectedDomain}
{#if selectedProxyRule}
<Typography.Text variant="m-400">
Are you sure you want to delete this domain? You will no longer be able to execute your
function by visiting:
</Typography.Text>
<Typography.Text variant="m-500">
{selectedDomain.domain}
{selectedProxyRule.domain}
</Typography.Text>
{/if}
<InputCheckbox
@@ -1,6 +1,5 @@
<script lang="ts">
import { Link } from '$lib/elements';
import type { Domain } from '$lib/sdk/domains';
import { IconInfo } from '@appwrite.io/pink-icons-svelte';
import {
Badge,
@@ -14,7 +13,7 @@
import type { Models } from '@appwrite.io/console';
import { consoleVariables } from '$routes/(console)/store';
export let domain: Domain | Models.ProxyRule;
export let proxyRule: Models.ProxyRule;
</script>
<Fieldset legend="Verification">
@@ -22,7 +21,7 @@
<Layout.Stack gap="xl">
<Layout.Stack gap="s">
<Layout.Stack gap="s" direction="row" alignItems="center">
<Typography.Text variant="l-500">{domain?.domain}</Typography.Text>
<Typography.Text variant="l-500">{proxyRule?.domain}</Typography.Text>
<Badge variant="secondary" type="warning" content="Pending verification" />
</Layout.Stack>
<Typography.Text variant="m-400">
@@ -39,7 +38,7 @@
</svelte:fragment>
<Table.Row.Base {root}>
<Table.Cell {root}>CNAME</Table.Cell>
<Table.Cell {root}>{domain?.domain}</Table.Cell>
<Table.Cell {root}>{proxyRule?.domain}</Table.Cell>
<Table.Cell {root}>
<InteractiveText
variant="copy"
@@ -7,24 +7,23 @@
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
import { Dependencies } from '$lib/constants';
import RecordsCard from './recordsCard.svelte';
import type { Domain } from '$lib/sdk/domains';
import type { Models } from '@appwrite.io/console';
import { page } from '$app/state';
export let show = false;
export let selectedDomain: Domain | Models.ProxyRule;
export let selectedProxyRule: Models.ProxyRule;
let error = null;
async function retryDomain() {
async function retryProxyRule() {
try {
await sdk
.forProject(page.params.region, page.params.project)
.proxy.updateRuleVerification(selectedDomain.$id);
.proxy.updateRuleVerification(selectedProxyRule.$id);
await invalidate(Dependencies.SITES_DOMAINS);
show = false;
addNotification({
type: 'success',
message: `${selectedDomain.domain} has been verified`
message: `${selectedProxyRule.domain} has been verified`
});
trackEvent(Submit.DomainUpdateVerification);
} catch (e) {
@@ -38,9 +37,9 @@
}
</script>
<Modal title="Retry verification" bind:show onSubmit={retryDomain} bind:error>
{#if selectedDomain}
<RecordsCard domain={selectedDomain} />
<Modal title="Retry verification" bind:show onSubmit={retryProxyRule} bind:error>
{#if selectedProxyRule}
<RecordsCard proxyRule={selectedProxyRule} />
{/if}
<svelte:fragment slot="footer">
@@ -19,14 +19,22 @@
import { columns } from './store';
let {
domains
proxyRules
}: {
domains: Models.ProxyRuleList;
proxyRules: Models.ProxyRuleList;
} = $props();
let showDelete = $state(false);
let showRetry = $state(false);
let selectedDomain: Models.ProxyRule = $state(null);
let selectedProxyRule: Models.ProxyRule = $state(null);
const proxyTarget = (proxy: Models.ProxyRule) => {
return proxy?.redirectUrl
? 'Redirect to ' + proxy.redirectUrl
: proxy?.deploymentVcsProviderBranch
? 'Deployed from' + proxy.deploymentVcsProviderBranch
: 'Active deployment';
};
</script>
<Table.Root columns={[...$columns, { id: 'actions', width: 40 }]} let:root>
@@ -38,24 +46,24 @@
{/each}
<Table.Header.Cell column="actions" {root} />
</svelte:fragment>
{#each domains.rules as domain}
{#each proxyRules.rules as proxyRule}
<Table.Row.Base {root}>
{#each $columns as column}
<Table.Cell column={column.id} {root}>
{#if column.id === 'domain'}
<Layout.Stack direction="row" gap="xs">
<Link external href={`${$protocol}${domain.domain}`} variant="quiet">
<Link external href={`${$protocol}${proxyRule.domain}`} variant="quiet">
<Typography.Text truncate>
{domain.domain}
{proxyRule.domain}
</Typography.Text>
</Link>
{#if domain.status === 'verifying'}
{#if proxyRule.status === 'verifying'}
<Badge
variant="secondary"
type="warning"
content="Verifying"
size="s" />
{:else if domain.status !== 'verified'}
{:else if proxyRule.status !== 'verified'}
<Badge
variant="secondary"
type="error"
@@ -64,11 +72,7 @@
{/if}
</Layout.Stack>
{:else if column.id === 'target'}
{domain?.redirectUrl
? 'Redirect to ' + domain.redirectUrl
: domain?.deploymentVcsProviderBranch
? 'Deployed from' + domain.deploymentVcsProviderBranch
: '-'}
{proxyTarget(proxyRule)}
{/if}
</Table.Cell>
{/each}
@@ -87,11 +91,11 @@
<svelte:fragment slot="tooltip" let:toggle>
<ActionMenu.Root>
{#if domain.status !== 'verified'}
{#if proxyRule.status !== 'verified'}
<ActionMenu.Item.Button
leadingIcon={IconRefresh}
on:click={(e) => {
selectedDomain = domain;
selectedProxyRule = proxyRule;
showRetry = true;
toggle(e);
}}>
@@ -102,7 +106,7 @@
status="danger"
leadingIcon={IconTrash}
on:click={(e) => {
selectedDomain = domain;
selectedProxyRule = proxyRule;
showDelete = true;
toggle(e);
trackEvent(Click.DomainDeleteClick, {
@@ -121,9 +125,9 @@
</Table.Root>
{#if showDelete}
<DeleteDomainModal bind:show={showDelete} {selectedDomain} />
<DeleteDomainModal bind:show={showDelete} {selectedProxyRule} />
{/if}
{#if showRetry}
<RetryDomainModal bind:show={showRetry} {selectedDomain} />
<RetryDomainModal bind:show={showRetry} {selectedProxyRule} />
{/if}
@@ -167,7 +167,7 @@
</Layout.Stack>
</Accordion>
<Accordion title="Request" open>
<LogsRequest {selectedLog} />
<LogsRequest {selectedLog} product="function" />
</Accordion>
<Accordion title="Response" open hideDivider>
<LogsResponse {selectedLog} product="function" {logging} />
@@ -149,16 +149,18 @@
<svelte:fragment slot="menu" let:toggle>
<ActionMenu.Root>
<ActionMenu.Item.Button
trailingIcon={IconRefresh}
on:click={() => {
selectedDeployment = deployment;
showRedeploy = true;
toggle();
trackEvent(Click.FunctionsRedeployClick);
}}>
Redeploy
</ActionMenu.Item.Button>
{#if deployment.sourceSize !== 0}
<ActionMenu.Item.Button
trailingIcon={IconRefresh}
on:click={() => {
selectedDeployment = deployment;
showRedeploy = true;
toggle();
trackEvent(Click.FunctionsRedeployClick);
}}>
Redeploy
</ActionMenu.Item.Button>
{/if}
{#if deployment.status === 'ready' && deployment.$id !== $func.deploymentId}
<ActionMenu.Item.Button
trailingIcon={IconLightningBolt}
@@ -63,14 +63,13 @@
{ id: 'deliveredAt', title: 'Delivered at', type: 'datetime', width: { min: 120 } }
]);
const region = page.params.region;
const project = page.params.project;
async function handleDelete() {
showDelete = false;
const promises = selected.map((id) =>
sdk.forProject(page.params.region, page.params.project).messaging.delete(id)
);
const promises = selected.map((id) => sdk.forProject(region, project).messaging.delete(id));
try {
await Promise.all(promises);
@@ -134,7 +133,7 @@
<Table.Row.Link
{root}
id={message.$id}
href={`${base}/project-${project}/messaging/message-${message.$id}`}>
href={`${base}/project-${region}-${project}/messaging/message-${message.$id}`}>
{#each $columns as column (column.id)}
<Table.Cell column={column.id} {root}>
{#if column.id === '$id'}
@@ -213,9 +212,7 @@
<Button external href="https://appwrite.io/docs/products/messaging/messages" text>
Documentation
</Button>
<Button
secondary
href={`${base}/project-${page.params.region}-${page.params.project}/messaging`}>
<Button secondary href={`${base}/project-${region}-${project}/messaging`}>
Clear search
</Button>
</div>
@@ -1,24 +1,25 @@
<script lang="ts" context="module">
import { get } from 'svelte/store';
import { page } from '$app/state';
import { base } from '$app/paths';
import { goto } from '$app/navigation';
import { writable } from 'svelte/store';
let showDelete = writable(false);
export const promptDeleteUser = (id: string) => {
export const promptDeleteProvider = (id: string) => {
showDelete.set(true);
goto(`${base}/project-${get(project).$id}/auth/user-${id}`);
goto(
`${base}/project-${page.params.region}-${page.params.$id}/messaging/providers/provider-${id}`
);
};
</script>
<script lang="ts">
import { CardGrid, BoxAvatar } from '$lib/components';
import { Button } from '$lib/elements/forms';
import { writable } from 'svelte/store';
import { provider } from './store';
import { goto } from '$app/navigation';
import { toLocaleDateTime } from '$lib/helpers/date';
import { project } from '$routes/(console)/project-[region]-[project]/store';
import DeleteProvider from './deleteProvider.svelte';
import { base } from '$app/paths';
</script>
<CardGrid>
@@ -124,6 +124,7 @@
on:invalid={handleInvalid}
extensions={['json']}
bind:files={files[input.name]}
maxSize={10000000}
required={!input.optional}>
<Layout.Stack alignItems="center" gap="s">
<Layout.Stack alignItems="center" gap="s">
@@ -28,6 +28,7 @@
export let data: PageData;
const region = page.params.region;
const project = page.params.project;
const columns = writable<Column[]>([
{ id: '$id', title: 'Topic ID', type: 'string', width: 200 },
@@ -64,7 +65,9 @@
]);
const topicCreated = async (event: CustomEvent<Models.Team<Record<string, unknown>>>) => {
await goto(`${base}/project-${project}/messaging/topics/topic-${event.detail.$id}`);
await goto(
`${base}/project-${region}-${project}/messaging/topics/topic-${event.detail.$id}`
);
};
</script>
@@ -1,35 +0,0 @@
<script lang="ts">
import { goto, invalidate } from '$app/navigation';
import { base } from '$app/paths';
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
import Confirm from '$lib/components/confirm.svelte';
import { Dependencies } from '$lib/constants';
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
import { project } from '../../../store';
import { key } from './store';
export let showDelete = false;
let error: string;
async function handleDelete() {
try {
await sdk.forConsole.projects.deleteKey($project.$id, $key.$id);
await invalidate(Dependencies.KEYS);
showDelete = false;
addNotification({
type: 'success',
message: `${$key.name} has been deleted`
});
trackEvent(Submit.KeyDelete);
await goto(`${base}/project-${$project.region}-${$project.$id}/overview/keys`);
} catch (e) {
error = e.message;
trackError(e, Submit.KeyDelete);
}
}
</script>
<Confirm onSubmit={handleDelete} title="Delete API key" bind:open={showDelete} bind:error>
Are you sure you want to delete this execution?
</Confirm>
@@ -0,0 +1,165 @@
<script lang="ts">
import { provider } from '.';
import { Button, Layout, Typography } from '@appwrite.io/pink-svelte';
import { InputNumber, InputPassword, InputText, InputTextarea } from '$lib/elements/forms';
export let formSubmitted = false;
$: disableButton = (() => {
if (!$provider) return true;
switch ($provider.provider) {
case 'appwrite':
return !$provider.endpoint || !$provider.projectID || !$provider.apiKey;
case 'firebase':
return !$provider.serviceAccount;
case 'supabase':
return (
!$provider.host ||
!$provider.port ||
!$provider.password ||
!$provider.endpoint ||
!$provider.apiKey
);
case 'nhost':
return (
!$provider.region ||
!$provider.subdomain ||
!$provider.password ||
!$provider.adminSecret
);
default:
return true;
}
})();
</script>
<Layout.Stack gap="xl">
{#if $provider.provider === 'appwrite'}
<Layout.Stack gap="l">
<InputText
id="endpoint"
label="Endpoint"
required
placeholder="Enter endpoint"
bind:value={$provider.endpoint} />
<InputText
id="project-id"
label="Project ID"
required
placeholder="Enter project ID"
bind:value={$provider.projectID} />
<InputPassword
id="api-key"
label="API Key"
required
placeholder="Enter API Key"
bind:value={$provider.apiKey} />
</Layout.Stack>
{:else if $provider.provider === 'firebase'}
<Layout.Stack gap="l">
<InputTextarea
id="credentials"
label="Service Account JSON credentials"
required
bind:value={$provider.serviceAccount}
placeholder="Enter Service Account JSON credentials" />
</Layout.Stack>
{:else if $provider.provider === 'supabase'}
<Layout.Stack gap="l">
<Typography.Text variant="m-400" color="--fgcolor-neutral-primary"
>Postgres credentials</Typography.Text>
<InputText
id="host"
label="Host"
required
placeholder="Enter host"
bind:value={$provider.host} />
<InputNumber id="port" label="Port" placeholder="5432" bind:value={$provider.port} />
<InputText
id="username"
label="Username"
placeholder="postgres"
bind:value={$provider.username} />
<InputPassword
id="password"
label="Password"
required
placeholder="Enter password"
bind:value={$provider.password} />
<Typography.Text variant="m-400" color="--fgcolor-neutral-primary"
>Supabase credentials</Typography.Text>
<InputText
id="endpoint"
label="Endpoint"
required
placeholder="Enter endpoint"
bind:value={$provider.endpoint} />
<InputPassword
id="api-key"
label="API Key"
required
placeholder="Enter API Key"
bind:value={$provider.apiKey} />
</Layout.Stack>
{:else if $provider.provider === 'nhost'}
<Layout.Stack gap="l">
<InputText
id="region"
label="Region"
required
placeholder="Enter region"
bind:value={$provider.region} />
<InputText
id="subdomain"
label="Subdomain"
required
placeholder="Enter subdomain"
bind:value={$provider.subdomain} />
<InputText
id="database"
label="Database"
placeholder={$provider.subdomain
? `Default: ${$provider.subdomain}`
: 'Enter database'}
bind:value={$provider.database} />
<InputText
id="username"
label="Username"
placeholder="postgres"
bind:value={$provider.username} />
<InputPassword
id="password"
label="Password"
required
placeholder="Enter password"
bind:value={$provider.password} />
<InputPassword
id="adminSecret"
label="Admin secret"
required
placeholder="Enter admin secret"
bind:value={$provider.adminSecret} />
</Layout.Stack>
{/if}
<Layout.Stack direction="row" justifyContent="flex-end">
<Button.Button
size="s"
disabled={disableButton}
on:click={() => {
formSubmitted = true;
}}>Next</Button.Button>
</Layout.Stack>
</Layout.Stack>
@@ -0,0 +1,112 @@
<script lang="ts">
import {
Accordion,
Badge,
Layout,
Selector,
Spinner,
Typography
} from '@appwrite.io/pink-svelte';
import type { MigrationFormData } from '$lib/stores/migration';
import { Button } from '$lib/elements/forms';
export let error: boolean = false;
export let groupKey: keyof MigrationFormData;
export let formGroup: MigrationFormData[typeof groupKey];
export let reportValue: string | number | undefined = undefined;
const labelMap = {
users: { root: 'Users', teams: 'Include teams' },
databases: { root: 'Databases', documents: 'Include documents' },
functions: {
root: 'Functions',
env: 'Include environment variables',
inactive: 'Include inactive deployments'
},
storage: { root: 'Storage' }
};
const descriptionMap = {
users: {
root: 'Import all users',
teams: 'Import all teams and the team memberships of your users'
},
databases: {
root: 'Import all databases, including collections, indexes and attributes',
documents: 'Import all documents inside collections'
},
functions: {
root: 'Import all functions and their active deployment',
env: 'Import all environment variables',
inactive: 'Import all deployments that are not currently active'
}
};
$: formGroupChildren = Object.keys(formGroup).filter((key) => key !== 'root');
let parentState: boolean | 'indeterminate';
$: {
const total = formGroupChildren.length;
const checked = formGroupChildren.filter((key) => formGroup[key]).length;
if (checked === 0) {
parentState = false;
} else if (checked === total) {
parentState = true;
} else {
parentState = 'indeterminate';
}
}
function onParentChange(event: CustomEvent<boolean | 'indeterminate'>) {
if (event.detail === 'indeterminate') return;
for (const key of Object.keys(formGroup)) {
formGroup[key] = event.detail;
}
}
$: isLoading = !error;
</script>
{#if !formGroupChildren.length}
<div style:padding-inline-start="0.5rem">
<Button extraCompact on:click={() => (formGroup.root = !formGroup.root)}>
<Layout.Stack direction="row" gap="s" alignItems="center">
<Layout.Stack inline direction="row" gap="l" alignItems="flex-start">
<Selector.Checkbox size="s" bind:checked={formGroup.root} />
<Typography.Text variant="m-500" color="--fgcolor-neutral-primary">
{labelMap[groupKey]?.root ?? groupKey}
</Typography.Text>
</Layout.Stack>
{#if reportValue !== undefined}
<Badge size="xs" variant="secondary" content={String(reportValue)} />
{:else if isLoading}
<Spinner size="s" />
{/if}
</Layout.Stack>
</Button>
</div>
{:else}
<Accordion
selectable
bind:checked={parentState}
on:change={onParentChange}
title={labelMap[groupKey]?.root ?? groupKey}
badge={error ? undefined : isLoading && reportValue ? reportValue.toString() : '...'}>
<Layout.Stack gap="m">
{#each formGroupChildren as key}
<div style:margin-inline-start="6px" style:margin-block-start="6px">
<Selector.Checkbox
size="s"
id={key}
bind:checked={formGroup[key]}
label={labelMap[groupKey]?.[key] ?? key}
description={descriptionMap[groupKey]?.[key]} />
</div>
{/each}
</Layout.Stack>
</Accordion>
{/if}
@@ -1,141 +0,0 @@
<script lang="ts">
import { InputNumber, InputPassword, InputText, InputTextarea } from '$lib/elements/forms';
import { WizardStep } from '$lib/layout';
import type { Provider } from '$lib/stores/migration';
import { provider } from '.';
const providers: Record<Provider, string> = {
appwrite: 'Appwrite',
firebase: 'Firebase',
supabase: 'Supabase',
nhost: 'NHost'
};
</script>
<WizardStep>
<svelte:fragment slot="title">Source</svelte:fragment>
<svelte:fragment slot="subtitle">
Select a source platform with the project you want to migrate. <a
class="link"
href="https://appwrite.io/docs/advanced/migrations"
target="_blank"
rel="noopener noreferrer">
Learn about which platforms are supported</a
>.
</svelte:fragment>
<div class="u-flex u-flex-vertical u-gap-8">
{#each Object.entries(providers) as [key, value]}
<label class="u-flex u-cross-center u-cursor-pointer u-gap-8">
<input
class="is-small"
type="radio"
name="provider"
bind:group={$provider.provider}
value={key} />
<div class="content">
<p>{value}</p>
</div>
</label>
{/each}
</div>
{#if $provider.provider === 'appwrite'}
<InputText
id="endpoint"
label="Endpoint"
required
placeholder="Enter endpoint"
bind:value={$provider.endpoint} />
<InputText
id="project-id"
label="Project ID"
required
placeholder="Enter project ID"
bind:value={$provider.projectID} />
<InputPassword
id="api-key"
label="API Key"
required
placeholder="Enter API Key"
bind:value={$provider.apiKey} />
{:else if $provider.provider === 'firebase'}
<div class="u-margin-block-start-16">
<InputTextarea
id="credentials"
label="Account credentials"
required
bind:value={$provider.serviceAccount}
placeholder="Enter account credentials" />
</div>
{:else if $provider.provider === 'supabase'}
<p class="body-text-1 u-bold">Postgres credentials</p>
<InputText
id="host"
label="Host"
required
placeholder="Enter host"
bind:value={$provider.host} />
<InputNumber id="port" label="Port" placeholder="5432" bind:value={$provider.port} />
<InputText
id="username"
label="Username"
placeholder="postgres"
bind:value={$provider.username} />
<InputPassword
id="password"
label="Password"
required
placeholder="Enter password"
bind:value={$provider.password} />
<p class="body-text-1 u-bold">Supabase credentials</p>
<InputText
id="endpoint"
label="Endpoint"
required
placeholder="Enter endpoint"
bind:value={$provider.endpoint} />
<InputPassword
id="api-key"
label="API Key"
required
placeholder="Enter API Key"
bind:value={$provider.apiKey} />
{:else if $provider.provider === 'nhost'}
<InputText
id="region"
label="Region"
required
placeholder="Enter region"
bind:value={$provider.region} />
<InputText
id="subdomain"
label="Subdomain"
required
placeholder="Enter subdomain"
bind:value={$provider.subdomain} />
<InputText
id="database"
label="Database"
placeholder={$provider.subdomain ? `Default: ${$provider.subdomain}` : 'Enter database'}
bind:value={$provider.database} />
<InputText
id="username"
label="Username"
placeholder="postgres"
bind:value={$provider.username} />
<InputPassword
id="password"
label="Password"
required
placeholder="Enter password"
bind:value={$provider.password} />
<InputPassword
id="adminSecret"
label="Admin secret"
required
placeholder="Enter admin secret"
bind:value={$provider.adminSecret} />
{/if}
</WizardStep>
@@ -1,25 +0,0 @@
<script lang="ts">
import { WizardStep } from '$lib/layout';
import { sdk } from '$lib/stores/sdk';
import { page } from '$app/state';
import ResourceForm from '$routes/(console)/(migration-wizard)/resource-form.svelte';
import { formData, provider } from '.';
</script>
<WizardStep>
<svelte:fragment slot="title">Resources</svelte:fragment>
<svelte:fragment slot="subtitle">
Select the resources you need to migrate to Appwrite. Some resources can be migrated, but
with limitations. <a
class="link"
href="https://appwrite.io/docs/advanced/migrations"
target="_blank"
rel="noopener noreferrer">
Learn about which resources are supported</a
>.
</svelte:fragment>
<ResourceForm
{formData}
{provider}
projectSdk={sdk.forProject(page.params.region, page.params.project)} />
</WizardStep>
@@ -1,18 +1,35 @@
<script lang="ts">
import type { WizardStepsType } from '$lib/layout/wizardWithSteps.svelte';
import Wizard from '$lib/layout/wizardWithSteps.svelte';
import { Wizard } from '$lib/layout';
import { sdk } from '$lib/stores/sdk';
import { wizard } from '$lib/stores/wizard';
import { onDestroy } from 'svelte';
import { formData, provider, resetImportStores } from '.';
import Step1 from './step1.svelte';
import Step2 from './step2.svelte';
import { invalidate } from '$app/navigation';
import { Dependencies } from '$lib/constants';
import { migrationFormToResources } from '$lib/stores/migration';
import { migrationFormToResources, type Provider } from '$lib/stores/migration';
import { started } from '../stores';
import { showMigrationBox } from '$lib/components/migrationBox.svelte';
import { addNotification } from '$lib/stores/notifications';
import {
Button,
Card,
Divider,
Fieldset,
Icon,
Layout,
Typography
} from '@appwrite.io/pink-svelte';
import { Link } from '$lib/elements';
import { EyebrowHeading } from '$lib/components';
import Credentials from './credentials.svelte';
import ResourceForm from '$routes/(console)/(migration-wizard)/resource-form.svelte';
import {
IconCog,
IconCurrencyDollar,
IconExclamation,
IconTrendingUp
} from '@appwrite.io/pink-icons-svelte';
import { capitalize } from '$lib/helpers/string';
import { page } from '$app/state';
const onExit = () => {
@@ -93,15 +110,153 @@
onDestroy(onExit);
const steps: WizardStepsType = new Map();
steps.set(1, {
label: 'Source',
component: Step1
});
steps.set(2, {
label: 'Resources',
component: Step2
});
const providers: Record<Provider, string> = {
appwrite: 'Appwrite self-hosted',
firebase: 'Firebase',
supabase: 'Supabase',
nhost: 'NHost'
};
let showResources = false;
let showExitModal = false;
let errorInResources = false;
$: isFinalsButtonEnabled =
!errorInResources &&
showResources &&
Object.values($formData).some((category) =>
Object.values(category).some((value) => value === true)
);
</script>
<Wizard title="Create Migration" {steps} on:exit={onExit} on:finish={onFinish} />
<Wizard title="Create migration" bind:showExitModal confirmExit {onExit}>
<Layout.Stack gap="l">
<Layout.Stack gap="xxl">
<Fieldset legend="Source">
<Layout.Stack gap="xl">
{#if !showResources}
<Typography.Text variant="m-400">
Select a source platform with the project you want to migrate.
<Link href="https://appwrite.io/docs/advanced/migrations" external>
Learn more.
</Link>
</Typography.Text>
<Layout.Grid rowGap="l" columns={3} columnsS={2} columnsXS={1}>
{#each Object.entries(providers) as [key, platform]}
<Card.Selector
bind:group={$provider.provider}
name={key}
id={key}
value={key}
title={platform}
imageRadius="s"
disabled={showResources} />
{/each}
</Layout.Grid>
<Divider />
<Credentials bind:formSubmitted={showResources} />
{:else}
<Layout.Stack
direction="row"
justifyContent="space-between"
alignItems="center"
gap="xs">
<Typography.Text variant="m-500">
{capitalize($provider.provider)}
</Typography.Text>
<Button.Button
size="s"
variant="secondary"
on:click={() => (showResources = !showResources)}>
Update
</Button.Button>
</Layout.Stack>
{/if}
</Layout.Stack>
</Fieldset>
{#if showResources}
<Fieldset legend="Resources">
<Layout.Stack gap="xl">
<ResourceForm
{formData}
{provider}
bind:errorInResources
projectSdk={sdk.forProject(page.params.region, page.params.project)} />
</Layout.Stack>
</Fieldset>
{/if}
</Layout.Stack>
</Layout.Stack>
<svelte:fragment slot="aside">
<Card.Base variant="primary" radius="s" padding="m">
<Layout.Stack gap="l">
<EyebrowHeading class="eyebrow" tag="h3" size={3}>Good to know</EyebrowHeading>
<!-- tip 1 -->
<Layout.Stack gap="l" direction="row" wrap="normal">
<span style:padding-block="2px">
<Icon icon={IconCog} size="s" />
</span>
<Layout.Stack gap="none">
<Typography.Text variant="m-600"
>Project settings are not imported</Typography.Text>
You will need to set service and project settings manually.
</Layout.Stack>
</Layout.Stack>
<!-- tip 2 -->
<Layout.Stack gap="l" direction="row" wrap="normal">
<span style:padding-block="2px">
<Icon icon={IconTrendingUp} size="s" />
</span>
<Layout.Stack gap="none">
<Typography.Text variant="m-600"
>Keep your organization plan's limits in mind</Typography.Text>
Make sure to have enough storage in your organization plan when importing files.
</Layout.Stack>
</Layout.Stack>
<!-- tip 3 -->
<Layout.Stack gap="l" direction="row" wrap="normal">
{@const isFirebase = $provider.provider === 'firebase'}
<span style:padding-block="3px">
<Icon
icon={isFirebase ? IconExclamation : IconCurrencyDollar}
size="s"
color={isFirebase ? '--fgcolor-warning' : undefined} />
</span>
<Layout.Stack gap="none">
<Typography.Text variant="m-600">
{isFirebase
? 'Possible charges by Firebase'
: 'Transfer is free of charge'}
</Typography.Text>
{#if isFirebase}
Appwrite does not impose charges for importing data, but please note
that Firebase may have its own pricing for this service
{:else}
You won't be charged for Appwrite bandwidth usage for importing data
{/if}
</Layout.Stack>
</Layout.Stack>
</Layout.Stack>
</Card.Base>
</svelte:fragment>
<svelte:fragment slot="footer">
<Button.Button
variant="secondary"
on:click={() => {
showExitModal = true;
}}>Cancel</Button.Button>
<Button.Button variant="primary" disabled={!isFinalsButtonEnabled} on:click={onFinish}
>Create</Button.Button>
</svelte:fragment>
</Wizard>
@@ -33,21 +33,9 @@
import { page } from '$app/state';
export let data;
let migration: Models.Migration = null;
let showExport = false;
let showMigration = false;
const getStatus = (status: string) => {
if (status === 'failed') {
return 'failed';
} else if (status === 'completed') {
return 'complete';
} else if (status === 'processing') {
return 'processing';
}
return 'pending';
};
let migration: Models.Migration = null;
onMount(() => {
return realtime
@@ -134,8 +122,25 @@
};
function showDetails(m: Models.Migration) {
showMigration = true;
migration = m;
showMigration = true;
}
function getTypedStatus(entry: Models.Migration) {
// migration > pending, processing, failed, completed
// status component = waiting, ready, processing, pending, failed, complete
switch (entry.status) {
case 'completed':
return 'complete';
case 'processing':
return 'processing';
case 'failed':
return 'failed';
case 'pending':
return 'pending';
default:
return 'waiting';
}
}
</script>
@@ -165,7 +170,7 @@
</svelte:fragment>
{#each data.migrations as entry}
<Table.Row.Base {root}>
{@const status = getStatus(entry.status)}
{@const status = getTypedStatus(entry)}
<Table.Cell {root}>
{#if isSameDay(new Date(), new Date(entry.$createdAt))}
Today
@@ -1,13 +1,24 @@
<script lang="ts">
import { Alert, Modal, Tab } from '$lib/components';
import Tabs from '$lib/components/tabs.svelte';
import { total } from '$lib/helpers/array';
import { toLocaleDateTime } from '$lib/helpers/date';
import { parseIfString } from '$lib/helpers/object';
import { Modal, Tab } from '$lib/components';
import Tabs from '$lib/components/tabs.svelte';
import { formatNum } from '$lib/helpers/string';
import type { Models } from '@appwrite.io/console';
import { parseIfString } from '$lib/helpers/object';
import { toLocaleDateTime } from '$lib/helpers/date';
import { ResourcesFriendly } from '$lib/stores/migration';
import { Card, Layout, Typography, Code } from '@appwrite.io/pink-svelte';
import { IconCheck, IconClock, IconExclamation } from '@appwrite.io/pink-icons-svelte';
import {
Alert,
Card,
Layout,
Typography,
Code,
Spinner,
Tag,
Icon
} from '@appwrite.io/pink-svelte';
import { Button } from '$lib/elements/forms';
export let migration: Models.Migration = null;
export let show = false;
@@ -44,6 +55,7 @@
};
let tab = 'details' as 'details' | 'logs';
let logs = JSON.stringify(migration, null, 2);
</script>
<Modal
@@ -56,7 +68,7 @@
</Tabs>
{#if tab === 'logs'}
<Code code={JSON.stringify(migration, null, 2)} lang="json" hideHeader />
<Code code={logs} lang="json" hideHeader />
{:else if tab === 'details'}
<Card.Base variant="secondary" padding="s">
<Layout.Stack>
@@ -76,82 +88,64 @@
</Card.Base>
{#if Object.values(statusCounters).some(hasError)}
<Alert
type="error"
buttons={[
{
slot: 'View logs',
onClick() {
tab = 'logs';
}
}
]}>
<Alert.Inline status="error">
There was an error migrating some of the project's entities.
</Alert>
<Button slot="actions" extraCompact on:click={() => (tab = 'logs')}
>View logs</Button>
</Alert.Inline>
{/if}
{#if Object.keys(statusCounters).length}
<div class="box">
{#each Object.keys(statusCounters) as entity}
{@const entityCounter = statusCounters[entity]}
<div class="u-flex u-cross-center u-gap-16">
<div class="icon-wrapper">
{#if hasError(entityCounter)}
<i class="icon-exclamation"></i>
{:else if isLoading(entityCounter)}
<div class="u-flex">
<span class="loader"></span>
</div>
{:else if hasSucceeded(entityCounter)}
<i class="icon-check"></i>
{:else}
<i class="icon-clock"></i>
{/if}
</div>
<Card.Base padding="s" variant="secondary">
<Layout.Stack gap="l">
{#each Object.keys(statusCounters) as entity}
{@const entityCounter = statusCounters[entity]}
<Layout.Stack
direction="row"
gap="l"
alignItems="center"
alignContent="center">
<div class="icon-wrapper">
{#if hasError(entityCounter)}
<Icon icon={IconExclamation} color="--bgcolor-warning" />
{:else if isLoading(entityCounter)}
<Spinner size="s" />
{:else if hasSucceeded(entityCounter)}
<Icon icon={IconCheck} />
{:else}
<Icon icon={IconClock} />
{/if}
</div>
<div>
<span class="u-capitalize"
>{total(Object.values(entityCounter)) > 1
? ResourcesFriendly[entity].plural
: ResourcesFriendly[entity].singular}</span>
<span class="inline-tag">{totalItems(entityCounter)}</span>
</div>
</div>
{/each}
</div>
<div>
<span class="u-capitalize"
>{total(Object.values(entityCounter)) > 1
? ResourcesFriendly[entity].plural
: ResourcesFriendly[entity].singular}</span>
<Tag size="xs" selected>{totalItems(entityCounter)}</Tag>
</div>
</Layout.Stack>
{/each}
</Layout.Stack>
</Card.Base>
{/if}
{/if}
</Modal>
<style lang="scss">
.box {
padding: 0;
> div {
padding: 1.25rem;
&:not(:last-child) {
border-bottom: 1px solid hsl(var(--border));
}
}
}
.icon-wrapper {
position: relative;
width: 1.5rem;
height: 1.5rem;
> * {
position: absolute;
> :global(*) {
top: 50%;
left: 50%;
position: absolute;
transform: translate(-50%, -50%);
}
}
.icon-exclamation {
color: hsl(var(--color-danger-100));
}
</style>
@@ -1,16 +1,20 @@
<script lang="ts">
import { page } from '$app/state';
import { Alert, Box, Modal } from '$lib/components';
import { Button, InputText, InputTextarea } from '$lib/elements/forms';
import { project } from '../../store';
import { Box, Modal } from '$lib/components';
import { getFormData } from '$lib/helpers/form';
import { feedback, feedbackData } from '$lib/stores/feedback';
import { organization } from '$lib/stores/organization';
import { Alert, Layout, Typography } from '@appwrite.io/pink-svelte';
import { Button, InputText, InputTextarea } from '$lib/elements/forms';
import { sdk } from '$lib/stores/sdk';
import { user } from '$lib/stores/user';
import { project } from '../../store';
import { organization } from '$lib/stores/organization';
export let show = false;
let submitted = false;
let endpointUrl = '';
let redirecting = false;
const isValidEndpoint = (endpoint: string) => {
try {
@@ -47,7 +51,7 @@
const onSubmit = async (e: SubmitEvent) => {
e.preventDefault();
submitted = true;
redirecting = true;
const formData = getFormData<{ endpoint: string; feedback: string }>(e);
@@ -114,56 +118,58 @@
const dest = `${removeTrailingSlash(endpoint)}/?migrate=${encodeURIComponent(
JSON.stringify(migrationData)
)}`;
redirecting = false;
window.location.href = dest;
};
function handleInvalid(event: Event) {
const input = event.target as HTMLInputElement;
const value = input.value;
if (!isValidEndpoint(value)) {
input.setCustomValidity('Please enter a valid endpoint');
} else {
input.setCustomValidity('');
}
input.reportValidity();
}
</script>
<Modal title="Export to self-hosted instance" bind:show {onSubmit}>
<Alert isStandalone>
<svelte:fragment slot="title">API key creation</svelte:fragment>
By initiating the transfer, an API key will be automatically generated in the background, which
you can delete after completion
</Alert>
<Layout.Stack gap="l">
<Alert.Inline status="info">
<svelte:fragment slot="title">API key creation</svelte:fragment>
By initiating the transfer, an API key will be automatically generated in the background,
which you can delete after completion
</Alert.Inline>
<InputText
label="Endpoint self-hosted instance"
required
id="endpoint"
placeholder="https://[YOUR_APPWRITE_HOSTNAME]"
autofocus
on:input={(e) => {
if (!submitted) return;
handleInvalid(e);
}} />
<InputText
required
id="endpoint"
autofocus
bind:value={endpointUrl}
label="Endpoint self-hosted instance"
placeholder="https://<YOUR_APPWRITE_HOSTNAME>" />
<Box>
<p class="u-bold">Share your feedback: why our self-hosted solution works better for you</p>
<p class="u-margin-block-start-8">
We appreciate your continued support and we understand that our self-hosted solution
might better fit your needs. To help us improve our Cloud solution, please share why it
works better for you. Your feedback is important to us and we'll use it to make our
services better.
</p>
<div class="u-margin-block-start-24">
<InputTextarea id="feedback" label="Your feedback" placeholder="Type here..." />
</div>
</Box>
<Box>
<Layout.Stack gap="xl">
<Typography.Text variant="m-600">
Share your feedback: why our self-hosted solution works better for you
</Typography.Text>
<div class="u-flex u-gap-16 u-cross-center" slot="footer">
<span> You will be redirected to your self-hosted instance </span>
We appreciate your continued support and we understand that our self-hosted solution
might better fit your needs. To help us improve our Cloud solution, please share why
it works better for you. Your feedback is important to us and we'll use it to make
our services better.
<Button submit>Continue</Button>
</div>
<InputTextarea id="feedback" label="Your feedback" placeholder="Type here..." />
</Layout.Stack>
</Box>
</Layout.Stack>
<Layout.Stack
direction="row"
gap="l"
alignItems="center"
justifyContent="flex-end"
slot="footer">
You will be redirected to your self-hosted instance
<Button
submit
forceShowLoader
submissionLoader={redirecting}
disabled={!isValidEndpoint(endpointUrl)}>
Continue
</Button>
</Layout.Stack>
</Modal>
@@ -55,7 +55,7 @@
</Button>
<svelte:fragment slot="menu" let:toggle>
<ActionMenu.Root>
{#if !inCard}
{#if !inCard && selectedDeployment?.sourceSize !== 0}
<ActionMenu.Item.Button
leadingIcon={IconRefresh}
on:click={(e) => {
@@ -0,0 +1,119 @@
<svg width="523" height="235" viewBox="0 0 523 235" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_6988_20924)">
<foreignObject x="491.889" y="-75.2851" width="170.845" height="170.845"><div xmlns="http://www.w3.org/1999/xhtml" style="backdrop-filter:blur(16.19px);clip-path:url(#bgblur_1_6988_20924_clip_path);height:100%;width:100%"></div></foreignObject><g data-figma-bg-blur-radius="32.3741">
<path d="M532.101 -21.6038L597.254 -41.3771C598.884 -41.872 600.093 -42.2385 601.073 -42.4506C601.928 -42.6356 602.593 -42.6994 603.195 -42.6322L603.45 -42.5954C604.584 -42.393 605.63 -41.8587 606.458 -41.0631L606.621 -40.9011C607.094 -40.4096 607.455 -39.7741 607.823 -38.8442C608.191 -37.9115 608.557 -36.7031 609.052 -35.0728L628.825 30.0797C629.32 31.7102 629.688 32.9189 629.9 33.8991C630.085 34.7539 630.148 35.4186 630.08 36.021L630.044 36.2762C629.841 37.4098 629.308 38.4561 628.512 39.2841L628.349 39.4468C627.858 39.9195 627.223 40.2812 626.293 40.6484C625.361 41.0169 624.151 41.3832 622.521 41.878L557.368 61.6513C555.738 62.1461 554.53 62.5133 553.55 62.7255C552.695 62.9105 552.03 62.9737 551.427 62.9064L551.172 62.8695C550.038 62.6672 548.992 62.1336 548.164 61.3382L548.001 61.1753C547.529 60.6839 547.168 60.0487 546.801 59.119C546.432 58.1863 546.065 56.9775 545.57 55.347L525.797 -9.8055C525.302 -11.4358 524.936 -12.6441 524.724 -13.6242C524.539 -14.4794 524.474 -15.1443 524.542 -15.7469L524.579 -16.002C524.781 -17.1357 525.315 -18.1822 526.111 -19.0102L526.273 -19.1726C526.764 -19.6454 527.4 -20.0063 528.33 -20.3736C529.263 -20.742 530.471 -21.109 532.101 -21.6038Z" stroke="#414146" stroke-width="0.505845"/>
<path d="M532.101 -21.6038L597.254 -41.3771C598.884 -41.872 600.093 -42.2385 601.073 -42.4506C601.928 -42.6356 602.593 -42.6994 603.195 -42.6322L603.45 -42.5954C604.584 -42.393 605.63 -41.8587 606.458 -41.0631L606.621 -40.9011C607.094 -40.4096 607.455 -39.7741 607.823 -38.8442C608.191 -37.9115 608.557 -36.7031 609.052 -35.0728L628.825 30.0797C629.32 31.7102 629.688 32.9189 629.9 33.8991C630.085 34.7539 630.148 35.4186 630.08 36.021L630.044 36.2762C629.841 37.4098 629.308 38.4561 628.512 39.2841L628.349 39.4468C627.858 39.9195 627.223 40.2812 626.293 40.6484C625.361 41.0169 624.151 41.3832 622.521 41.878L557.368 61.6513C555.738 62.1461 554.53 62.5133 553.55 62.7255C552.695 62.9105 552.03 62.9737 551.427 62.9064L551.172 62.8695C550.038 62.6672 548.992 62.1336 548.164 61.3382L548.001 61.1753C547.529 60.6839 547.168 60.0487 546.801 59.119C546.432 58.1863 546.065 56.9775 545.57 55.347L525.797 -9.8055C525.302 -11.4358 524.936 -12.6441 524.724 -13.6242C524.539 -14.4794 524.474 -15.1443 524.542 -15.7469L524.579 -16.002C524.781 -17.1357 525.315 -18.1822 526.111 -19.0102L526.273 -19.1726C526.764 -19.6454 527.4 -20.0063 528.33 -20.3736C529.263 -20.742 530.471 -21.109 532.101 -21.6038Z" stroke="url(#paint0_linear_6988_20924)" stroke-width="0.505845"/>
</g>
<foreignObject x="194.272" y="15.0401" width="170.845" height="170.845"><div xmlns="http://www.w3.org/1999/xhtml" style="backdrop-filter:blur(16.19px);clip-path:url(#bgblur_2_6988_20924_clip_path);height:100%;width:100%"></div></foreignObject><path data-figma-bg-blur-radius="32.3741" d="M234.484 68.7214L299.636 48.9481C301.267 48.4532 302.476 48.0867 303.456 47.8746C304.311 47.6896 304.975 47.6258 305.578 47.693L305.833 47.7298C306.967 47.9322 308.013 48.4665 308.841 49.2621L309.004 49.4241C309.476 49.9156 309.838 50.5511 310.206 51.481C310.574 52.4137 310.94 53.6221 311.435 55.2524L331.208 120.405C331.703 122.035 332.07 123.244 332.283 124.224C332.468 125.079 332.53 125.744 332.463 126.346L332.426 126.601C332.224 127.735 331.69 128.781 330.895 129.609L330.732 129.772C330.241 130.245 329.606 130.606 328.676 130.974C327.743 131.342 326.534 131.708 324.904 132.203L259.751 151.976C258.121 152.471 256.913 152.839 255.933 153.051C255.078 153.236 254.413 153.299 253.81 153.232L253.555 153.195C252.421 152.992 251.375 152.459 250.547 151.663L250.384 151.501C249.912 151.009 249.551 150.374 249.183 149.444C248.815 148.511 248.448 147.303 247.953 145.672L228.18 80.5197C227.685 78.8894 227.319 77.6811 227.106 76.701C226.921 75.8458 226.857 75.1809 226.925 74.5783L226.961 74.3232C227.164 73.1895 227.698 72.143 228.494 71.315L228.656 71.1526C229.147 70.6798 229.783 70.3189 230.713 69.9516C231.646 69.5832 232.854 69.2162 234.484 68.7214Z" fill="url(#paint1_linear_6988_20924)" fill-opacity="0.16" stroke="url(#paint2_linear_6988_20924)" stroke-width="0.505845"/>
<g opacity="0.32">
<rect width="44.5144" height="44.5144" transform="translate(351.328 55.1978) rotate(-16.8826)" fill="url(#paint3_linear_6988_20924)" fill-opacity="0.16"/>
<path d="M390.946 79.7962C387.834 83.0956 382.723 84.8887 376.842 84.2565C372.084 83.7576 367.639 81.7059 364.316 78.8289C365.663 79.3915 367.135 79.7221 368.67 79.9359C374.809 80.8028 380.136 78.9992 383.216 75.5283C376.59 73.0702 370.385 69.1169 365.652 65.7933C364.718 65.2995 363.91 64.5733 363.128 63.9361C368.39 66.4195 376.373 69.1457 379.111 69.7728C372.609 66.1104 366.268 61.0404 366.501 61.1636C376.685 66.6225 385.304 68.8644 385.304 68.8644C385.585 68.9259 385.805 68.9815 385.985 69.0362C386.022 68.5917 386.035 68.1379 386.021 67.6755C385.901 62.7572 382.899 58.0328 378.462 54.7163C387.053 56.8693 393.845 63.7459 394.129 71.141C394.138 71.34 394.143 71.5378 394.142 71.7337C399.082 75.0925 399.612 80.1643 398.893 79.411C395.8 76.2248 392.546 78.4738 390.946 79.7962Z" fill="white"/>
</g>
<g filter="url(#filter2_f_6988_20924)">
<path d="M213.667 215.095C164.688 214.939 124.983 199.71 124.983 181.08C124.983 162.45 164.688 147.474 213.667 147.63C262.646 147.786 302.351 163.015 302.351 181.645C302.351 200.275 262.646 215.251 213.667 215.095Z" fill="url(#paint4_radial_6988_20924)" fill-opacity="0.24"/>
</g>
<g filter="url(#filter3_f_6988_20924)">
<path d="M106.058 212.058C82.8072 197.152 81.3892 157.687 102.891 123.911C124.393 90.1352 160.673 74.8383 183.924 89.7446C207.175 104.651 208.593 144.116 187.091 177.892C165.589 211.668 129.31 226.964 106.058 212.058Z" fill="url(#paint5_radial_6988_20924)" fill-opacity="0.08"/>
</g>
<g filter="url(#filter4_f_6988_20924)">
<path d="M262.991 246.144C175.765 245.866 105.054 206.646 105.054 158.543C105.054 110.441 175.765 71.6717 262.991 71.9497C350.218 72.2276 420.929 111.448 420.929 159.55C420.929 207.652 350.218 246.422 262.991 246.144Z" fill="url(#paint6_radial_6988_20924)" fill-opacity="0.16"/>
</g>
<g filter="url(#filter5_f_6988_20924)">
<path d="M125.504 148.666C126.352 169.51 238.278 173.853 234 180.5C229.722 187.147 121.758 211.883 112.714 196.039C103.67 180.195 110.006 99.6661 114.284 93.0193C118.562 86.3725 116.46 132.822 125.504 148.666Z" fill="url(#paint7_linear_6988_20924)" fill-opacity="0.24"/>
</g>
<foreignObject x="109" y="2" width="305" height="194"><div xmlns="http://www.w3.org/1999/xhtml" style="backdrop-filter:blur(2px);clip-path:url(#bgblur_3_6988_20924_clip_path);height:100%;width:100%"></div></foreignObject><g data-figma-bg-blur-radius="4">
<rect x="113" y="6" width="297" height="186" rx="16" fill="#2D2D31" fill-opacity="0.24"/>
<rect x="113.5" y="6.5" width="296" height="185" rx="15.5" stroke="white" stroke-opacity="0.04"/>
</g>
<g filter="url(#filter7_d_6988_20924)">
<mask id="path-10-inside-1_6988_20924" fill="white">
<path d="M125 26C125 21.5817 128.582 18 133 18H390C394.418 18 398 21.5817 398 26V172C398 176.418 394.418 180 390 180H133C128.582 180 125 176.418 125 172V26Z"/>
</mask>
<path d="M125 26C125 21.5817 128.582 18 133 18H390C394.418 18 398 21.5817 398 26V172C398 176.418 394.418 180 390 180H133C128.582 180 125 176.418 125 172V26Z" fill="#1D1D21"/>
<path d="M133 18V18.8H390V18V17.2H133V18ZM398 26H397.2V172H398H398.8V26H398ZM390 180V179.2H133V180V180.8H390V180ZM125 172H125.8V26H125H124.2V172H125ZM133 180V179.2C129.024 179.2 125.8 175.976 125.8 172H125H124.2C124.2 176.86 128.14 180.8 133 180.8V180ZM398 172H397.2C397.2 175.976 393.976 179.2 390 179.2V180V180.8C394.86 180.8 398.8 176.86 398.8 172H398ZM390 18V18.8C393.976 18.8 397.2 22.0236 397.2 26H398H398.8C398.8 21.1399 394.86 17.2 390 17.2V18ZM133 18V17.2C128.14 17.2 124.2 21.1399 124.2 26H125H125.8C125.8 22.0235 129.024 18.8 133 18.8V18Z" fill="#2D2D31" mask="url(#path-10-inside-1_6988_20924)"/>
<path d="M133 18V18.8H390V18V17.2H133V18ZM398 26H397.2V172H398H398.8V26H398ZM390 180V179.2H133V180V180.8H390V180ZM125 172H125.8V26H125H124.2V172H125ZM133 180V179.2C129.024 179.2 125.8 175.976 125.8 172H125H124.2C124.2 176.86 128.14 180.8 133 180.8V180ZM398 172H397.2C397.2 175.976 393.976 179.2 390 179.2V180V180.8C394.86 180.8 398.8 176.86 398.8 172H398ZM390 18V18.8C393.976 18.8 397.2 22.0236 397.2 26H398H398.8C398.8 21.1399 394.86 17.2 390 17.2V18ZM133 18V17.2C128.14 17.2 124.2 21.1399 124.2 26H125H125.8C125.8 22.0235 129.024 18.8 133 18.8V18Z" fill="url(#paint8_linear_6988_20924)" fill-opacity="0.62" mask="url(#path-10-inside-1_6988_20924)"/>
</g>
<circle cx="140" cy="33" r="3" fill="#414146"/>
<circle cx="152" cy="33" r="3" fill="#414146"/>
<circle cx="164" cy="33" r="3" fill="#414146"/>
<rect opacity="0.5" x="179" y="30" width="207" height="6" rx="3" fill="#414146"/>
<rect opacity="0.5" x="217" y="60" width="91" height="11" rx="4" fill="#414146"/>
<rect opacity="0.5" x="179" y="79" width="164" height="11" rx="4" fill="#414146"/>
<rect x="241" y="102" width="42" height="14" rx="7" fill="#414146"/>
</g>
<defs>
<clipPath id="bgblur_1_6988_20924_clip_path" transform="translate(-491.889 75.2851)"><path d="M532.101 -21.6038L597.254 -41.3771C598.884 -41.872 600.093 -42.2385 601.073 -42.4506C601.928 -42.6356 602.593 -42.6994 603.195 -42.6322L603.45 -42.5954C604.584 -42.393 605.63 -41.8587 606.458 -41.0631L606.621 -40.9011C607.094 -40.4096 607.455 -39.7741 607.823 -38.8442C608.191 -37.9115 608.557 -36.7031 609.052 -35.0728L628.825 30.0797C629.32 31.7102 629.688 32.9189 629.9 33.8991C630.085 34.7539 630.148 35.4186 630.08 36.021L630.044 36.2762C629.841 37.4098 629.308 38.4561 628.512 39.2841L628.349 39.4468C627.858 39.9195 627.223 40.2812 626.293 40.6484C625.361 41.0169 624.151 41.3832 622.521 41.878L557.368 61.6513C555.738 62.1461 554.53 62.5133 553.55 62.7255C552.695 62.9105 552.03 62.9737 551.427 62.9064L551.172 62.8695C550.038 62.6672 548.992 62.1336 548.164 61.3382L548.001 61.1753C547.529 60.6839 547.168 60.0487 546.801 59.119C546.432 58.1863 546.065 56.9775 545.57 55.347L525.797 -9.8055C525.302 -11.4358 524.936 -12.6441 524.724 -13.6242C524.539 -14.4794 524.474 -15.1443 524.542 -15.7469L524.579 -16.002C524.781 -17.1357 525.315 -18.1822 526.111 -19.0102L526.273 -19.1726C526.764 -19.6454 527.4 -20.0063 528.33 -20.3736C529.263 -20.742 530.471 -21.109 532.101 -21.6038Z"/>
</clipPath><clipPath id="bgblur_2_6988_20924_clip_path" transform="translate(-194.272 -15.0401)"><path d="M234.484 68.7214L299.636 48.9481C301.267 48.4532 302.476 48.0867 303.456 47.8746C304.311 47.6896 304.975 47.6258 305.578 47.693L305.833 47.7298C306.967 47.9322 308.013 48.4665 308.841 49.2621L309.004 49.4241C309.476 49.9156 309.838 50.5511 310.206 51.481C310.574 52.4137 310.94 53.6221 311.435 55.2524L331.208 120.405C331.703 122.035 332.07 123.244 332.283 124.224C332.468 125.079 332.53 125.744 332.463 126.346L332.426 126.601C332.224 127.735 331.69 128.781 330.895 129.609L330.732 129.772C330.241 130.245 329.606 130.606 328.676 130.974C327.743 131.342 326.534 131.708 324.904 132.203L259.751 151.976C258.121 152.471 256.913 152.839 255.933 153.051C255.078 153.236 254.413 153.299 253.81 153.232L253.555 153.195C252.421 152.992 251.375 152.459 250.547 151.663L250.384 151.501C249.912 151.009 249.551 150.374 249.183 149.444C248.815 148.511 248.448 147.303 247.953 145.672L228.18 80.5197C227.685 78.8894 227.319 77.6811 227.106 76.701C226.921 75.8458 226.857 75.1809 226.925 74.5783L226.961 74.3232C227.164 73.1895 227.698 72.143 228.494 71.315L228.656 71.1526C229.147 70.6798 229.783 70.3189 230.713 69.9516C231.646 69.5832 232.854 69.2162 234.484 68.7214Z"/>
</clipPath><filter id="filter2_f_6988_20924" x="84.9826" y="107.629" width="257.368" height="147.467" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="20" result="effect1_foregroundBlur_6988_20924"/>
</filter>
<filter id="filter3_f_6988_20924" x="47.6483" y="44.0369" width="194.686" height="213.729" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="20" result="effect1_foregroundBlur_6988_20924"/>
</filter>
<filter id="filter4_f_6988_20924" x="97.5536" y="64.4481" width="330.875" height="189.197" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="3.75" result="effect1_foregroundBlur_6988_20924"/>
</filter>
<filter id="filter5_f_6988_20924" x="76.1082" y="60.373" width="190.011" height="172.875" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="16" result="effect1_foregroundBlur_6988_20924"/>
</filter>
<clipPath id="bgblur_3_6988_20924_clip_path" transform="translate(-109 -2)"><rect x="113" y="6" width="297" height="186" rx="16"/>
</clipPath><filter id="filter7_d_6988_20924" x="121" y="18" width="281" height="170" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="4"/>
<feGaussianBlur stdDeviation="2"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.04 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_6988_20924"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_6988_20924" result="shape"/>
</filter>
<linearGradient id="paint0_linear_6988_20924" x1="564.604" y1="-31.7326" x2="590.019" y2="52.0071" gradientUnits="userSpaceOnUse">
<stop stop-color="#1D1D21"/>
<stop offset="1" stop-color="#1D1D21" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint1_linear_6988_20924" x1="334.271" y1="129.625" x2="225.117" y2="71.2998" gradientUnits="userSpaceOnUse">
<stop stop-color="#FAFAFB"/>
<stop offset="1" stop-color="white"/>
</linearGradient>
<linearGradient id="paint2_linear_6988_20924" x1="262.385" y1="53.6668" x2="282.689" y2="156.696" gradientUnits="userSpaceOnUse">
<stop stop-color="white" stop-opacity="0.16"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint3_linear_6988_20924" x1="44.5144" y1="44.5144" x2="1.32663e-06" y2="-1.32663e-06" gradientUnits="userSpaceOnUse">
<stop stop-color="white"/>
<stop offset="1" stop-color="white" stop-opacity="0.02"/>
</linearGradient>
<radialGradient id="paint4_radial_6988_20924" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(168.328 181.263) rotate(11.4697) scale(83.3742 30.958)">
<stop stop-color="#F02E65"/>
<stop offset="1" stop-color="#F02E65" stop-opacity="0"/>
</radialGradient>
<radialGradient id="paint5_radial_6988_20924" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(123.416 137.185) rotate(69.8743) scale(57.994 53.9384)">
<stop stop-color="#F02E65"/>
<stop offset="1" stop-color="#F02E65" stop-opacity="0"/>
</radialGradient>
<radialGradient id="paint6_radial_6988_20924" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(262.991 159.047) rotate(-179.817) scale(157.939 87.0966)">
<stop stop-color="#F02E65"/>
<stop offset="1" stop-color="#F02E65" stop-opacity="0"/>
</radialGradient>
<linearGradient id="paint7_linear_6988_20924" x1="66.9998" y1="195.5" x2="131.437" y2="50.8892" gradientUnits="userSpaceOnUse">
<stop stop-color="#FE9567"/>
<stop offset="1" stop-color="#F97066" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint8_linear_6988_20924" x1="137" y1="197" x2="236.229" y2="129.971" gradientUnits="userSpaceOnUse">
<stop stop-color="#FE6054" stop-opacity="0.880574"/>
<stop offset="0.159063" stop-color="#FD366E"/>
<stop offset="1" stop-color="#414146"/>
</linearGradient>
<clipPath id="clip0_6988_20924">
<rect width="523" height="235" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 17 KiB

@@ -0,0 +1,157 @@
<svg width="527" height="235" viewBox="0 0 527 235" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_6988_1408)">
<foreignObject x="493.889" y="-75.2851" width="170.845" height="170.845"><div xmlns="http://www.w3.org/1999/xhtml" style="backdrop-filter:blur(16.19px);clip-path:url(#bgblur_1_6988_1408_clip_path);height:100%;width:100%"></div></foreignObject><g data-figma-bg-blur-radius="32.3741">
<path d="M534.101 -21.6038L599.254 -41.3771C600.884 -41.872 602.093 -42.2385 603.073 -42.4506C603.928 -42.6356 604.593 -42.6994 605.195 -42.6322L605.45 -42.5954C606.584 -42.393 607.63 -41.8587 608.458 -41.0631L608.621 -40.9011C609.094 -40.4096 609.455 -39.7741 609.823 -38.8442C610.191 -37.9115 610.557 -36.7031 611.052 -35.0728L630.825 30.0797C631.32 31.7102 631.688 32.9189 631.9 33.8991C632.085 34.7539 632.148 35.4186 632.08 36.021L632.044 36.2762C631.841 37.4098 631.308 38.4561 630.512 39.2841L630.349 39.4468C629.858 39.9195 629.223 40.2812 628.293 40.6484C627.361 41.0169 626.151 41.3832 624.521 41.878L559.368 61.6513C557.738 62.1461 556.53 62.5133 555.55 62.7255C554.695 62.9105 554.03 62.9737 553.427 62.9064L553.172 62.8695C552.038 62.6672 550.992 62.1336 550.164 61.3382L550.001 61.1753C549.529 60.6839 549.168 60.0487 548.801 59.119C548.432 58.1863 548.065 56.9775 547.57 55.347L527.797 -9.8055C527.302 -11.4358 526.936 -12.6441 526.724 -13.6242C526.539 -14.4794 526.474 -15.1443 526.542 -15.7469L526.579 -16.002C526.781 -17.1357 527.315 -18.1822 528.111 -19.0102L528.273 -19.1726C528.764 -19.6454 529.4 -20.0063 530.33 -20.3736C531.263 -20.742 532.471 -21.109 534.101 -21.6038Z" fill="url(#paint0_linear_6988_1408)" fill-opacity="0.4"/>
<path d="M534.101 -21.6038L599.254 -41.3771C600.884 -41.872 602.093 -42.2385 603.073 -42.4506C603.928 -42.6356 604.593 -42.6994 605.195 -42.6322L605.45 -42.5954C606.584 -42.393 607.63 -41.8587 608.458 -41.0631L608.621 -40.9011C609.094 -40.4096 609.455 -39.7741 609.823 -38.8442C610.191 -37.9115 610.557 -36.7031 611.052 -35.0728L630.825 30.0797C631.32 31.7102 631.688 32.9189 631.9 33.8991C632.085 34.7539 632.148 35.4186 632.08 36.021L632.044 36.2762C631.841 37.4098 631.308 38.4561 630.512 39.2841L630.349 39.4468C629.858 39.9195 629.223 40.2812 628.293 40.6484C627.361 41.0169 626.151 41.3832 624.521 41.878L559.368 61.6513C557.738 62.1461 556.53 62.5133 555.55 62.7255C554.695 62.9105 554.03 62.9737 553.427 62.9064L553.172 62.8695C552.038 62.6672 550.992 62.1336 550.164 61.3382L550.001 61.1753C549.529 60.6839 549.168 60.0487 548.801 59.119C548.432 58.1863 548.065 56.9775 547.57 55.347L527.797 -9.8055C527.302 -11.4358 526.936 -12.6441 526.724 -13.6242C526.539 -14.4794 526.474 -15.1443 526.542 -15.7469L526.579 -16.002C526.781 -17.1357 527.315 -18.1822 528.111 -19.0102L528.273 -19.1726C528.764 -19.6454 529.4 -20.0063 530.33 -20.3736C531.263 -20.742 532.471 -21.109 534.101 -21.6038Z" stroke="black" stroke-opacity="0.08" stroke-width="0.505845"/>
<path d="M534.101 -21.6038L599.254 -41.3771C600.884 -41.872 602.093 -42.2385 603.073 -42.4506C603.928 -42.6356 604.593 -42.6994 605.195 -42.6322L605.45 -42.5954C606.584 -42.393 607.63 -41.8587 608.458 -41.0631L608.621 -40.9011C609.094 -40.4096 609.455 -39.7741 609.823 -38.8442C610.191 -37.9115 610.557 -36.7031 611.052 -35.0728L630.825 30.0797C631.32 31.7102 631.688 32.9189 631.9 33.8991C632.085 34.7539 632.148 35.4186 632.08 36.021L632.044 36.2762C631.841 37.4098 631.308 38.4561 630.512 39.2841L630.349 39.4468C629.858 39.9195 629.223 40.2812 628.293 40.6484C627.361 41.0169 626.151 41.3832 624.521 41.878L559.368 61.6513C557.738 62.1461 556.53 62.5133 555.55 62.7255C554.695 62.9105 554.03 62.9737 553.427 62.9064L553.172 62.8695C552.038 62.6672 550.992 62.1336 550.164 61.3382L550.001 61.1753C549.529 60.6839 549.168 60.0487 548.801 59.119C548.432 58.1863 548.065 56.9775 547.57 55.347L527.797 -9.8055C527.302 -11.4358 526.936 -12.6441 526.724 -13.6242C526.539 -14.4794 526.474 -15.1443 526.542 -15.7469L526.579 -16.002C526.781 -17.1357 527.315 -18.1822 528.111 -19.0102L528.273 -19.1726C528.764 -19.6454 529.4 -20.0063 530.33 -20.3736C531.263 -20.742 532.471 -21.109 534.101 -21.6038Z" stroke="url(#paint1_linear_6988_1408)" stroke-width="0.505845"/>
</g>
<foreignObject x="265.385" y="-114.304" width="170.845" height="170.845"><div xmlns="http://www.w3.org/1999/xhtml" style="backdrop-filter:blur(16.19px);clip-path:url(#bgblur_2_6988_1408_clip_path);height:100%;width:100%"></div></foreignObject><path data-figma-bg-blur-radius="32.3741" d="M305.597 -60.6229L370.75 -80.3961C372.38 -80.891 373.589 -81.2575 374.569 -81.4697C375.424 -81.6546 376.089 -81.7185 376.691 -81.6513L376.946 -81.6144C378.08 -81.4121 379.126 -80.8777 379.954 -80.0821L380.117 -79.9202C380.59 -79.4286 380.951 -78.7932 381.319 -77.8632C381.687 -76.9305 382.053 -75.7221 382.548 -74.0918L402.321 -8.93936C402.816 -7.30883 403.184 -6.10015 403.396 -5.11999C403.581 -4.26514 403.644 -3.60046 403.576 -2.998L403.54 -2.74289C403.337 -1.60927 402.804 -0.56292 402.008 0.265053L401.845 0.42777C401.354 0.900446 400.719 1.26211 399.789 1.6294C398.857 1.99781 397.648 2.36411 396.017 2.85896L330.865 22.6322C329.234 23.127 328.026 23.4943 327.046 23.7064C326.191 23.8915 325.526 23.9547 324.923 23.8874L324.668 23.8505C323.535 23.6481 322.488 23.1145 321.66 22.3192L321.497 22.1563C321.025 21.6648 320.664 21.0297 320.297 20.0999C319.928 19.1672 319.561 17.9585 319.066 16.3279L299.293 -48.8245C298.798 -50.4549 298.432 -51.6632 298.22 -52.6433C298.035 -53.4984 297.971 -54.1633 298.038 -54.7659L298.075 -55.021C298.277 -56.1548 298.811 -57.2012 299.607 -58.0292L299.769 -58.1917C300.26 -58.6644 300.896 -59.0253 301.826 -59.3926C302.759 -59.761 303.967 -60.1281 305.597 -60.6229Z" fill="url(#paint2_linear_6988_1408)" fill-opacity="0.16" stroke="url(#paint3_linear_6988_1408)" stroke-width="0.505845"/>
<foreignObject x="226.385" y="114.269" width="170.845" height="170.845"><div xmlns="http://www.w3.org/1999/xhtml" style="backdrop-filter:blur(16.19px);clip-path:url(#bgblur_3_6988_1408_clip_path);height:100%;width:100%"></div></foreignObject><g data-figma-bg-blur-radius="32.3741">
<path d="M266.597 167.95L331.75 148.177C333.38 147.682 334.589 147.316 335.569 147.104C336.424 146.919 337.089 146.855 337.691 146.922L337.946 146.959C339.08 147.161 340.126 147.696 340.955 148.491L341.117 148.653C341.59 149.145 341.952 149.78 342.319 150.71C342.687 151.643 343.053 152.851 343.548 154.481L363.321 219.634C363.816 221.264 364.184 222.473 364.396 223.453C364.581 224.308 364.644 224.973 364.577 225.575L364.54 225.83C364.337 226.964 363.804 228.01 363.008 228.838L362.845 229.001C362.354 229.474 361.719 229.835 360.789 230.203C359.857 230.571 358.648 230.937 357.017 231.432L291.865 251.205C290.234 251.7 289.026 252.068 288.046 252.28C287.191 252.465 286.526 252.528 285.923 252.461L285.668 252.424C284.535 252.221 283.488 251.688 282.66 250.892L282.497 250.73C282.025 250.238 281.664 249.603 281.297 248.673C280.928 247.74 280.561 246.532 280.066 244.901L260.293 179.749C259.798 178.118 259.432 176.91 259.22 175.93C259.035 175.075 258.971 174.41 259.038 173.807L259.075 173.552C259.277 172.418 259.811 171.372 260.607 170.544L260.769 170.382C261.261 169.909 261.896 169.548 262.826 169.181C263.759 168.812 264.967 168.445 266.597 167.95Z" fill="url(#paint4_linear_6988_1408)" fill-opacity="0.16"/>
<path d="M266.597 167.95L331.75 148.177C333.38 147.682 334.589 147.316 335.569 147.104C336.424 146.919 337.089 146.855 337.691 146.922L337.946 146.959C339.08 147.161 340.126 147.696 340.955 148.491L341.117 148.653C341.59 149.145 341.952 149.78 342.319 150.71C342.687 151.643 343.053 152.851 343.548 154.481L363.321 219.634C363.816 221.264 364.184 222.473 364.396 223.453C364.581 224.308 364.644 224.973 364.577 225.575L364.54 225.83C364.337 226.964 363.804 228.01 363.008 228.838L362.845 229.001C362.354 229.474 361.719 229.835 360.789 230.203C359.857 230.571 358.648 230.937 357.017 231.432L291.865 251.205C290.234 251.7 289.026 252.068 288.046 252.28C287.191 252.465 286.526 252.528 285.923 252.461L285.668 252.424C284.535 252.221 283.488 251.688 282.66 250.892L282.497 250.73C282.025 250.238 281.664 249.603 281.297 248.673C280.928 247.74 280.561 246.532 280.066 244.901L260.293 179.749C259.798 178.118 259.432 176.91 259.22 175.93C259.035 175.075 258.971 174.41 259.038 173.807L259.075 173.552C259.277 172.418 259.811 171.372 260.607 170.544L260.769 170.382C261.261 169.909 261.896 169.548 262.826 169.181C263.759 168.812 264.967 168.445 266.597 167.95Z" stroke="black" stroke-opacity="0.08" stroke-width="0.505845"/>
<path d="M266.597 167.95L331.75 148.177C333.38 147.682 334.589 147.316 335.569 147.104C336.424 146.919 337.089 146.855 337.691 146.922L337.946 146.959C339.08 147.161 340.126 147.696 340.955 148.491L341.117 148.653C341.59 149.145 341.952 149.78 342.319 150.71C342.687 151.643 343.053 152.851 343.548 154.481L363.321 219.634C363.816 221.264 364.184 222.473 364.396 223.453C364.581 224.308 364.644 224.973 364.577 225.575L364.54 225.83C364.337 226.964 363.804 228.01 363.008 228.838L362.845 229.001C362.354 229.474 361.719 229.835 360.789 230.203C359.857 230.571 358.648 230.937 357.017 231.432L291.865 251.205C290.234 251.7 289.026 252.068 288.046 252.28C287.191 252.465 286.526 252.528 285.923 252.461L285.668 252.424C284.535 252.221 283.488 251.688 282.66 250.892L282.497 250.73C282.025 250.238 281.664 249.603 281.297 248.673C280.928 247.74 280.561 246.532 280.066 244.901L260.293 179.749C259.798 178.118 259.432 176.91 259.22 175.93C259.035 175.075 258.971 174.41 259.038 173.807L259.075 173.552C259.277 172.418 259.811 171.372 260.607 170.544L260.769 170.382C261.261 169.909 261.896 169.548 262.826 169.181C263.759 168.812 264.967 168.445 266.597 167.95Z" stroke="url(#paint5_linear_6988_1408)" stroke-width="0.505845"/>
</g>
<foreignObject x="196.272" y="15.0401" width="170.845" height="170.845"><div xmlns="http://www.w3.org/1999/xhtml" style="backdrop-filter:blur(16.19px);clip-path:url(#bgblur_4_6988_1408_clip_path);height:100%;width:100%"></div></foreignObject><path data-figma-bg-blur-radius="32.3741" d="M236.484 68.7214L301.636 48.9481C303.267 48.4532 304.476 48.0867 305.456 47.8746C306.311 47.6896 306.975 47.6258 307.578 47.693L307.833 47.7298C308.967 47.9322 310.013 48.4665 310.841 49.2621L311.004 49.4241C311.476 49.9156 311.838 50.5511 312.206 51.481C312.574 52.4137 312.94 53.6221 313.435 55.2524L333.208 120.405C333.703 122.035 334.07 123.244 334.283 124.224C334.468 125.079 334.53 125.744 334.463 126.346L334.426 126.601C334.224 127.735 333.69 128.781 332.895 129.609L332.732 129.772C332.241 130.245 331.606 130.606 330.676 130.974C329.743 131.342 328.534 131.708 326.904 132.203L261.751 151.976C260.121 152.471 258.913 152.839 257.933 153.051C257.078 153.236 256.413 153.299 255.81 153.232L255.555 153.195C254.421 152.992 253.375 152.459 252.547 151.663L252.384 151.501C251.912 151.009 251.551 150.374 251.183 149.444C250.815 148.511 250.448 147.303 249.953 145.672L230.18 80.5197C229.685 78.8894 229.319 77.6811 229.106 76.701C228.921 75.8458 228.857 75.1809 228.925 74.5783L228.961 74.3232C229.164 73.1895 229.698 72.143 230.494 71.315L230.656 71.1526C231.147 70.6798 231.783 70.3189 232.713 69.9516C233.646 69.5832 234.854 69.2162 236.484 68.7214Z" fill="url(#paint6_linear_6988_1408)" fill-opacity="0.16" stroke="url(#paint7_linear_6988_1408)" stroke-width="0.505845"/>
<foreignObject x="166.155" y="-84.1889" width="170.845" height="170.845"><div xmlns="http://www.w3.org/1999/xhtml" style="backdrop-filter:blur(16.19px);clip-path:url(#bgblur_5_6988_1408_clip_path);height:100%;width:100%"></div></foreignObject><g data-figma-bg-blur-radius="32.3741">
<path d="M199.821 -18.6356C198.833 -21.8886 198.34 -23.5152 198.596 -24.9498C198.821 -26.2118 199.44 -27.3701 200.364 -28.2586C201.414 -29.2688 203.041 -29.7625 206.294 -30.7498L271.446 -50.523C274.699 -51.5103 276.326 -52.0039 277.76 -51.7479C279.022 -51.5227 280.18 -50.9038 281.069 -49.9799C282.079 -48.9295 282.573 -47.303 283.56 -44.0499L303.333 21.1024C304.321 24.3555 304.814 25.9821 304.558 27.4167C304.333 28.6787 303.714 29.8369 302.79 30.7255C301.74 31.7357 300.113 32.2293 296.86 33.2166L231.708 52.9899C228.455 53.9772 226.828 54.4708 225.394 54.2148C224.132 53.9896 222.973 53.3707 222.085 52.4468C221.075 51.3964 220.581 49.7699 219.594 46.5168L199.821 -18.6356Z" fill="url(#paint8_linear_6988_1408)" fill-opacity="0.16"/>
<path d="M206.367 -30.5076L271.519 -50.2809C273.15 -50.7758 274.359 -51.1423 275.339 -51.3544C276.194 -51.5394 276.858 -51.6032 277.461 -51.536L277.716 -51.4992C278.85 -51.2968 279.896 -50.7625 280.724 -49.9669L280.886 -49.805C281.359 -49.3134 281.721 -48.6779 282.088 -47.748C282.457 -46.8153 282.823 -45.6069 283.318 -43.9766L303.091 21.1759C303.586 22.8064 303.953 24.0151 304.165 24.9952C304.35 25.8501 304.413 26.5148 304.346 27.1172L304.309 27.3723C304.107 28.506 303.573 29.5523 302.778 30.3803L302.615 30.543C302.123 31.0157 301.489 31.3773 300.559 31.7446C299.626 32.113 298.417 32.4793 296.787 32.9742L231.634 52.7475C230.004 53.2423 228.796 53.6095 227.816 53.8216C226.96 54.0067 226.295 54.0699 225.693 54.0026L225.438 53.9657C224.304 53.7634 223.258 53.2298 222.43 52.4344L222.267 52.2715C221.794 51.7801 221.434 51.1449 221.066 50.2152C220.698 49.2825 220.331 48.0737 219.836 46.4432L200.062 -18.7093C199.568 -20.3396 199.201 -21.548 198.989 -22.528C198.804 -23.3832 198.74 -24.0481 198.807 -24.6507L198.844 -24.9058C199.047 -26.0395 199.581 -27.086 200.376 -27.914L200.538 -28.0764C201.03 -28.5492 201.666 -28.9101 202.596 -29.2774C203.528 -29.6458 204.736 -30.0128 206.367 -30.5076Z" stroke="black" stroke-opacity="0.08" stroke-width="0.505845"/>
</g>
<g opacity="0.32">
<rect width="44.5144" height="44.5144" transform="translate(353.328 55.1978) rotate(-16.8826)" fill="url(#paint9_linear_6988_1408)" fill-opacity="0.16"/>
<path d="M392.946 79.7962C389.834 83.0956 384.723 84.8887 378.842 84.2565C374.084 83.7576 369.639 81.7059 366.316 78.8289C367.663 79.3915 369.135 79.7221 370.67 79.9359C376.809 80.8028 382.136 78.9992 385.216 75.5283C378.59 73.0702 372.385 69.1169 367.652 65.7933C366.718 65.2995 365.91 64.5733 365.128 63.9361C370.39 66.4195 378.373 69.1457 381.111 69.7728C374.609 66.1104 368.268 61.0404 368.501 61.1636C378.685 66.6225 387.304 68.8644 387.304 68.8644C387.585 68.9259 387.805 68.9815 387.985 69.0362C388.022 68.5917 388.035 68.1379 388.021 67.6755C387.901 62.7572 384.899 58.0328 380.462 54.7163C389.054 56.8693 395.846 63.7459 396.129 71.141C396.138 71.34 396.143 71.5378 396.142 71.7337C401.082 75.0925 401.612 80.1643 400.893 79.411C397.8 76.2248 394.546 78.4738 392.946 79.7962Z" fill="white"/>
</g>
<g filter="url(#filter5_f_6988_1408)">
<path d="M215.667 215.095C166.688 214.939 126.983 199.71 126.983 181.08C126.983 162.45 166.688 147.474 215.667 147.63C264.646 147.786 304.351 163.015 304.351 181.645C304.351 200.275 264.646 215.251 215.667 215.095Z" fill="url(#paint10_radial_6988_1408)" fill-opacity="0.16"/>
</g>
<g filter="url(#filter6_f_6988_1408)">
<path d="M264.991 246.144C177.765 245.866 107.054 206.646 107.054 158.543C107.054 110.441 177.765 71.6717 264.991 71.9497C352.218 72.2276 422.929 111.448 422.929 159.55C422.929 207.652 352.218 246.422 264.991 246.144Z" fill="url(#paint11_radial_6988_1408)" fill-opacity="0.1"/>
</g>
<g filter="url(#filter7_f_6988_1408)">
<path d="M119.504 148.666C120.352 169.51 184.5 182.511 180.222 189.158C175.944 195.805 115.758 211.884 106.714 196.04C97.6703 180.196 104.006 99.6665 108.284 93.0198C112.562 86.373 110.461 132.822 119.504 148.666Z" fill="url(#paint12_radial_6988_1408)" fill-opacity="0.2"/>
</g>
<g filter="url(#filter8_f_6988_1408)">
<path d="M118.058 218.059C94.8066 203.153 93.3885 163.688 114.891 129.912C136.393 96.1364 172.672 80.8395 195.924 95.7458C219.175 110.652 220.593 150.117 199.091 183.893C177.589 217.669 141.309 232.966 118.058 218.059Z" fill="url(#paint13_radial_6988_1408)" fill-opacity="0.08"/>
</g>
<foreignObject x="111" y="2" width="305" height="194"><div xmlns="http://www.w3.org/1999/xhtml" style="backdrop-filter:blur(2px);clip-path:url(#bgblur_6_6988_1408_clip_path);height:100%;width:100%"></div></foreignObject><g data-figma-bg-blur-radius="4">
<rect x="115" y="6" width="297" height="186" rx="16" fill="white" fill-opacity="0.32"/>
<rect x="115.5" y="6.5" width="296" height="185" rx="15.5" stroke="#19191C" stroke-opacity="0.03"/>
</g>
<g filter="url(#filter10_d_6988_1408)">
<mask id="path-14-inside-1_6988_1408" fill="white">
<path d="M127 26C127 21.5817 130.582 18 135 18H392C396.418 18 400 21.5817 400 26V172C400 176.418 396.418 180 392 180H135C130.582 180 127 176.418 127 172V26Z"/>
</mask>
<path d="M127 26C127 21.5817 130.582 18 135 18H392C396.418 18 400 21.5817 400 26V172C400 176.418 396.418 180 392 180H135C130.582 180 127 176.418 127 172V26Z" fill="white"/>
<path d="M135 18V18.8H392V18V17.2H135V18ZM400 26H399.2V172H400H400.8V26H400ZM392 180V179.2H135V180V180.8H392V180ZM127 172H127.8V26H127H126.2V172H127ZM135 180V179.2C131.024 179.2 127.8 175.976 127.8 172H127H126.2C126.2 176.86 130.14 180.8 135 180.8V180ZM400 172H399.2C399.2 175.976 395.977 179.2 392 179.2V180V180.8C396.86 180.8 400.8 176.86 400.8 172H400ZM392 18V18.8C395.977 18.8 399.2 22.0236 399.2 26H400H400.8C400.8 21.1399 396.86 17.2 392 17.2V18ZM135 18V17.2C130.14 17.2 126.2 21.1399 126.2 26H127H127.8C127.8 22.0235 131.024 18.8 135 18.8V18Z" fill="#EDEDF0" mask="url(#path-14-inside-1_6988_1408)"/>
<path d="M135 18V18.8H392V18V17.2H135V18ZM400 26H399.2V172H400H400.8V26H400ZM392 180V179.2H135V180V180.8H392V180ZM127 172H127.8V26H127H126.2V172H127ZM135 180V179.2C131.024 179.2 127.8 175.976 127.8 172H127H126.2C126.2 176.86 130.14 180.8 135 180.8V180ZM400 172H399.2C399.2 175.976 395.977 179.2 392 179.2V180V180.8C396.86 180.8 400.8 176.86 400.8 172H400ZM392 18V18.8C395.977 18.8 399.2 22.0236 399.2 26H400H400.8C400.8 21.1399 396.86 17.2 392 17.2V18ZM135 18V17.2C130.14 17.2 126.2 21.1399 126.2 26H127H127.8C127.8 22.0235 131.024 18.8 135 18.8V18Z" fill="url(#paint14_linear_6988_1408)" fill-opacity="0.4" mask="url(#path-14-inside-1_6988_1408)"/>
</g>
<circle cx="142" cy="33" r="3" fill="#E4E4E7"/>
<circle cx="154" cy="33" r="3" fill="#E4E4E7"/>
<circle cx="166" cy="33" r="3" fill="#E4E4E7"/>
<rect opacity="0.5" x="181" y="30" width="207" height="6" rx="3" fill="#EDEDF0"/>
<rect opacity="0.5" x="219" y="60" width="91" height="11" rx="4" fill="#EDEDF0"/>
<rect opacity="0.5" x="181" y="79" width="164" height="11" rx="4" fill="#EDEDF0"/>
<rect x="243" y="102" width="42" height="14" rx="7" fill="#E4E4E7"/>
</g>
<defs>
<clipPath id="bgblur_1_6988_1408_clip_path" transform="translate(-493.889 75.2851)"><path d="M534.101 -21.6038L599.254 -41.3771C600.884 -41.872 602.093 -42.2385 603.073 -42.4506C603.928 -42.6356 604.593 -42.6994 605.195 -42.6322L605.45 -42.5954C606.584 -42.393 607.63 -41.8587 608.458 -41.0631L608.621 -40.9011C609.094 -40.4096 609.455 -39.7741 609.823 -38.8442C610.191 -37.9115 610.557 -36.7031 611.052 -35.0728L630.825 30.0797C631.32 31.7102 631.688 32.9189 631.9 33.8991C632.085 34.7539 632.148 35.4186 632.08 36.021L632.044 36.2762C631.841 37.4098 631.308 38.4561 630.512 39.2841L630.349 39.4468C629.858 39.9195 629.223 40.2812 628.293 40.6484C627.361 41.0169 626.151 41.3832 624.521 41.878L559.368 61.6513C557.738 62.1461 556.53 62.5133 555.55 62.7255C554.695 62.9105 554.03 62.9737 553.427 62.9064L553.172 62.8695C552.038 62.6672 550.992 62.1336 550.164 61.3382L550.001 61.1753C549.529 60.6839 549.168 60.0487 548.801 59.119C548.432 58.1863 548.065 56.9775 547.57 55.347L527.797 -9.8055C527.302 -11.4358 526.936 -12.6441 526.724 -13.6242C526.539 -14.4794 526.474 -15.1443 526.542 -15.7469L526.579 -16.002C526.781 -17.1357 527.315 -18.1822 528.111 -19.0102L528.273 -19.1726C528.764 -19.6454 529.4 -20.0063 530.33 -20.3736C531.263 -20.742 532.471 -21.109 534.101 -21.6038Z"/>
</clipPath><clipPath id="bgblur_2_6988_1408_clip_path" transform="translate(-265.385 114.304)"><path d="M305.597 -60.6229L370.75 -80.3961C372.38 -80.891 373.589 -81.2575 374.569 -81.4697C375.424 -81.6546 376.089 -81.7185 376.691 -81.6513L376.946 -81.6144C378.08 -81.4121 379.126 -80.8777 379.954 -80.0821L380.117 -79.9202C380.59 -79.4286 380.951 -78.7932 381.319 -77.8632C381.687 -76.9305 382.053 -75.7221 382.548 -74.0918L402.321 -8.93936C402.816 -7.30883 403.184 -6.10015 403.396 -5.11999C403.581 -4.26514 403.644 -3.60046 403.576 -2.998L403.54 -2.74289C403.337 -1.60927 402.804 -0.56292 402.008 0.265053L401.845 0.42777C401.354 0.900446 400.719 1.26211 399.789 1.6294C398.857 1.99781 397.648 2.36411 396.017 2.85896L330.865 22.6322C329.234 23.127 328.026 23.4943 327.046 23.7064C326.191 23.8915 325.526 23.9547 324.923 23.8874L324.668 23.8505C323.535 23.6481 322.488 23.1145 321.66 22.3192L321.497 22.1563C321.025 21.6648 320.664 21.0297 320.297 20.0999C319.928 19.1672 319.561 17.9585 319.066 16.3279L299.293 -48.8245C298.798 -50.4549 298.432 -51.6632 298.22 -52.6433C298.035 -53.4984 297.971 -54.1633 298.038 -54.7659L298.075 -55.021C298.277 -56.1548 298.811 -57.2012 299.607 -58.0292L299.769 -58.1917C300.26 -58.6644 300.896 -59.0253 301.826 -59.3926C302.759 -59.761 303.967 -60.1281 305.597 -60.6229Z"/>
</clipPath><clipPath id="bgblur_3_6988_1408_clip_path" transform="translate(-226.385 -114.269)"><path d="M266.597 167.95L331.75 148.177C333.38 147.682 334.589 147.316 335.569 147.104C336.424 146.919 337.089 146.855 337.691 146.922L337.946 146.959C339.08 147.161 340.126 147.696 340.955 148.491L341.117 148.653C341.59 149.145 341.952 149.78 342.319 150.71C342.687 151.643 343.053 152.851 343.548 154.481L363.321 219.634C363.816 221.264 364.184 222.473 364.396 223.453C364.581 224.308 364.644 224.973 364.577 225.575L364.54 225.83C364.337 226.964 363.804 228.01 363.008 228.838L362.845 229.001C362.354 229.474 361.719 229.835 360.789 230.203C359.857 230.571 358.648 230.937 357.017 231.432L291.865 251.205C290.234 251.7 289.026 252.068 288.046 252.28C287.191 252.465 286.526 252.528 285.923 252.461L285.668 252.424C284.535 252.221 283.488 251.688 282.66 250.892L282.497 250.73C282.025 250.238 281.664 249.603 281.297 248.673C280.928 247.74 280.561 246.532 280.066 244.901L260.293 179.749C259.798 178.118 259.432 176.91 259.22 175.93C259.035 175.075 258.971 174.41 259.038 173.807L259.075 173.552C259.277 172.418 259.811 171.372 260.607 170.544L260.769 170.382C261.261 169.909 261.896 169.548 262.826 169.181C263.759 168.812 264.967 168.445 266.597 167.95Z"/>
</clipPath><clipPath id="bgblur_4_6988_1408_clip_path" transform="translate(-196.272 -15.0401)"><path d="M236.484 68.7214L301.636 48.9481C303.267 48.4532 304.476 48.0867 305.456 47.8746C306.311 47.6896 306.975 47.6258 307.578 47.693L307.833 47.7298C308.967 47.9322 310.013 48.4665 310.841 49.2621L311.004 49.4241C311.476 49.9156 311.838 50.5511 312.206 51.481C312.574 52.4137 312.94 53.6221 313.435 55.2524L333.208 120.405C333.703 122.035 334.07 123.244 334.283 124.224C334.468 125.079 334.53 125.744 334.463 126.346L334.426 126.601C334.224 127.735 333.69 128.781 332.895 129.609L332.732 129.772C332.241 130.245 331.606 130.606 330.676 130.974C329.743 131.342 328.534 131.708 326.904 132.203L261.751 151.976C260.121 152.471 258.913 152.839 257.933 153.051C257.078 153.236 256.413 153.299 255.81 153.232L255.555 153.195C254.421 152.992 253.375 152.459 252.547 151.663L252.384 151.501C251.912 151.009 251.551 150.374 251.183 149.444C250.815 148.511 250.448 147.303 249.953 145.672L230.18 80.5197C229.685 78.8894 229.319 77.6811 229.106 76.701C228.921 75.8458 228.857 75.1809 228.925 74.5783L228.961 74.3232C229.164 73.1895 229.698 72.143 230.494 71.315L230.656 71.1526C231.147 70.6798 231.783 70.3189 232.713 69.9516C233.646 69.5832 234.854 69.2162 236.484 68.7214Z"/>
</clipPath><clipPath id="bgblur_5_6988_1408_clip_path" transform="translate(-166.155 84.1889)"><path d="M199.821 -18.6356C198.833 -21.8886 198.34 -23.5152 198.596 -24.9498C198.821 -26.2118 199.44 -27.3701 200.364 -28.2586C201.414 -29.2688 203.041 -29.7625 206.294 -30.7498L271.446 -50.523C274.699 -51.5103 276.326 -52.0039 277.76 -51.7479C279.022 -51.5227 280.18 -50.9038 281.069 -49.9799C282.079 -48.9295 282.573 -47.303 283.56 -44.0499L303.333 21.1024C304.321 24.3555 304.814 25.9821 304.558 27.4167C304.333 28.6787 303.714 29.8369 302.79 30.7255C301.74 31.7357 300.113 32.2293 296.86 33.2166L231.708 52.9899C228.455 53.9772 226.828 54.4708 225.394 54.2148C224.132 53.9896 222.973 53.3707 222.085 52.4468C221.075 51.3964 220.581 49.7699 219.594 46.5168L199.821 -18.6356Z"/>
</clipPath><filter id="filter5_f_6988_1408" x="119.483" y="140.129" width="192.368" height="82.4673" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="3.75" result="effect1_foregroundBlur_6988_1408"/>
</filter>
<filter id="filter6_f_6988_1408" x="99.5536" y="64.4481" width="330.875" height="189.197" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="3.75" result="effect1_foregroundBlur_6988_1408"/>
</filter>
<filter id="filter7_f_6988_1408" x="87.1083" y="77.3735" width="108.318" height="140.43" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="7.5" result="effect1_foregroundBlur_6988_1408"/>
</filter>
<filter id="filter8_f_6988_1408" x="59.6477" y="50.0381" width="194.686" height="213.729" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="20" result="effect1_foregroundBlur_6988_1408"/>
</filter>
<clipPath id="bgblur_6_6988_1408_clip_path" transform="translate(-111 -2)"><rect x="115" y="6" width="297" height="186" rx="16"/>
</clipPath><filter id="filter10_d_6988_1408" x="123" y="18" width="281" height="170" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="4"/>
<feGaussianBlur stdDeviation="2"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.04 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_6988_1408"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_6988_1408" result="shape"/>
</filter>
<linearGradient id="paint0_linear_6988_1408" x1="545.5" y1="65" x2="570.5" y2="-12.5" gradientUnits="userSpaceOnUse">
<stop stop-color="#FAFAFB"/>
<stop offset="1" stop-color="white"/>
</linearGradient>
<linearGradient id="paint1_linear_6988_1408" x1="566.604" y1="-31.7326" x2="592.019" y2="52.0071" gradientUnits="userSpaceOnUse">
<stop stop-color="white"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint2_linear_6988_1408" x1="405.384" y1="0.280849" x2="296.23" y2="-58.0444" gradientUnits="userSpaceOnUse">
<stop stop-color="#FAFAFB"/>
<stop offset="1" stop-color="white"/>
</linearGradient>
<linearGradient id="paint3_linear_6988_1408" x1="333.498" y1="-75.6775" x2="353.802" y2="27.3519" gradientUnits="userSpaceOnUse">
<stop stop-color="white" stop-opacity="0.16"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint4_linear_6988_1408" x1="366.384" y1="228.854" x2="257.231" y2="170.529" gradientUnits="userSpaceOnUse">
<stop stop-color="#FAFAFB"/>
<stop offset="1" stop-color="white"/>
</linearGradient>
<linearGradient id="paint5_linear_6988_1408" x1="299.1" y1="157.822" x2="324.515" y2="241.561" gradientUnits="userSpaceOnUse">
<stop stop-color="white" stop-opacity="0"/>
<stop offset="1" stop-color="white"/>
</linearGradient>
<linearGradient id="paint6_linear_6988_1408" x1="336.271" y1="129.625" x2="227.117" y2="71.2998" gradientUnits="userSpaceOnUse">
<stop stop-color="#FAFAFB"/>
<stop offset="1" stop-color="white"/>
</linearGradient>
<linearGradient id="paint7_linear_6988_1408" x1="264.385" y1="53.6668" x2="284.689" y2="156.696" gradientUnits="userSpaceOnUse">
<stop stop-color="white" stop-opacity="0.16"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint8_linear_6988_1408" x1="306.154" y1="30.3961" x2="197" y2="-27.9292" gradientUnits="userSpaceOnUse">
<stop stop-color="#FAFAFB"/>
<stop offset="1" stop-color="white"/>
</linearGradient>
<linearGradient id="paint9_linear_6988_1408" x1="44.5144" y1="44.5144" x2="1.32663e-06" y2="-1.32663e-06" gradientUnits="userSpaceOnUse">
<stop stop-color="white"/>
<stop offset="1" stop-color="white" stop-opacity="0.02"/>
</linearGradient>
<radialGradient id="paint10_radial_6988_1408" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(170.328 181.263) rotate(11.4697) scale(83.3742 30.958)">
<stop stop-color="#F02E65"/>
<stop offset="1" stop-color="#F02E65" stop-opacity="0"/>
</radialGradient>
<radialGradient id="paint11_radial_6988_1408" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(264.991 159.047) rotate(-179.817) scale(157.939 87.0966)">
<stop stop-color="#F02E65"/>
<stop offset="1" stop-color="#F02E65" stop-opacity="0"/>
</radialGradient>
<radialGradient id="paint12_radial_6988_1408" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(128.406 186.79) rotate(-127.788) scale(34.964 15.3555)">
<stop stop-color="#FE9567"/>
<stop offset="1" stop-color="#F97066" stop-opacity="0"/>
</radialGradient>
<radialGradient id="paint13_radial_6988_1408" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(135.415 143.186) rotate(69.8743) scale(57.994 53.9384)">
<stop stop-color="#F02E65"/>
<stop offset="1" stop-color="#F02E65" stop-opacity="0"/>
</radialGradient>
<linearGradient id="paint14_linear_6988_1408" x1="147.5" y1="199" x2="243.737" y2="136.165" gradientUnits="userSpaceOnUse">
<stop stop-color="#FE6054"/>
<stop offset="0.342973" stop-color="#FD366E"/>
<stop offset="1" stop-color="#EDEDF0"/>
</linearGradient>
<clipPath id="clip0_6988_1408">
<rect width="527" height="235" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 31 KiB

@@ -11,12 +11,14 @@
import { isServiceLimited } from '$lib/stores/billing';
import { organization } from '$lib/stores/organization';
import { canWriteSites } from '$lib/stores/roles.js';
import { Icon, Layout } from '@appwrite.io/pink-svelte';
import { Card, Icon, Layout, Typography } from '@appwrite.io/pink-svelte';
import { Button } from '$lib/elements/forms';
import { app } from '$lib/stores/app';
import CreateSiteModal from './createSiteModal.svelte';
import EmptyLight from './(images)/empty-sites-light.svg';
import EmptyDark from './(images)/empty-sites-dark.svg';
import EmptyLightMobile from './(images)/empty-sites-light-mobile.svg';
import EmptyDarkMobile from './(images)/empty-sites-dark-mobile.svg';
import Grid from './grid.svelte';
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
import { columns } from './store';
@@ -26,8 +28,15 @@
import { invalidate } from '$app/navigation';
import { Dependencies } from '$lib/constants';
import { sdk } from '$lib/stores/sdk';
import { APPWRITE_OFFICIALS_ORG, isCloud } from '$lib/system';
import { addNotification } from '$lib/stores/notifications';
import { isOnWaitlistSites, joinWaitlistSites } from '$lib/helpers/waitlist';
import { isSmallViewport } from '$lib/stores/viewport';
export let data;
let show = false;
let isOnWaitlist = isOnWaitlistSites();
$: $registerCommands([
{
@@ -37,6 +46,7 @@
},
keys: ['c'],
disabled:
!showSites ||
isServiceLimited('sites', $organization?.billingPlan, data.siteList?.total) ||
!$canWriteSites,
icon: IconPlus,
@@ -53,46 +63,123 @@
}
});
});
/**
* Controls visibility of Sites feature:
* - Shown if running on self-hosted
* - Shown on cloud only if the organization is Appwrite's.
* - Hidden on cloud for any non-Appwrite organization.
*/
$: showSites = !isCloud || $organization.$id === APPWRITE_OFFICIALS_ORG;
$: isDark = $app.themeInUse === 'dark';
$: imgSrc = isDark
? $isSmallViewport
? EmptyDarkMobile
: EmptyDark
: $isSmallViewport
? EmptyLightMobile
: EmptyLight;
$: imgClass = $isSmallViewport ? 'mobile' : 'desktop';
function addToWaitlist() {
joinWaitlistSites();
addNotification({
type: 'success',
title: 'Waitlist joined',
message: "We'll let you know as soon as Appwrite Sites is ready for you."
});
isOnWaitlist = true;
}
</script>
<Container>
<Layout.Stack direction="row" justifyContent="space-between">
<Layout.Stack direction="row" alignItems="center">
<SearchQuery placeholder="Search by name" />
{#if showSites}
<Layout.Stack direction="row" justifyContent="space-between">
<Layout.Stack direction="row" alignItems="center">
<SearchQuery placeholder="Search by name" />
</Layout.Stack>
<Layout.Stack direction="row" alignItems="center" justifyContent="flex-end">
<ViewSelector
{columns}
view={data.view}
hideColumns
hideView={!data.siteList.total} />
{#if $canWriteSites}
<Button on:mousedown={() => (show = true)} event="create_site" size="s">
<Icon icon={IconPlus} slot="start" size="s" />
Create site
</Button>
{/if}
</Layout.Stack>
</Layout.Stack>
<Layout.Stack direction="row" alignItems="center" justifyContent="flex-end">
<ViewSelector {columns} view={data.view} hideColumns hideView={!data.siteList.total} />
{#if $canWriteSites}
<Button on:mousedown={() => (show = true)} event="create_site" size="s">
<Icon icon={IconPlus} slot="start" size="s" />
Create site
</Button>
{#if data.siteList.total}
{#if data.view === View.Grid}
<Grid siteList={data.siteList} />
{:else}
<Table siteList={data.siteList} />
{/if}
</Layout.Stack>
</Layout.Stack>
{#if data.siteList.total}
{#if data.view === View.Grid}
<Grid siteList={data.siteList} />
<PaginationWithLimit
name="Sites"
limit={data.limit}
offset={data.offset}
total={data.siteList.total} />
{:else if data.search}
<EmptySearch target="sites" />
{:else}
<Table siteList={data.siteList} />
<Empty
single
allowCreate={$canWriteSites}
href="https://appwrite.io/docs/products/sites"
description="Deploy and manage your web your web applications with Sites. "
target="site"
src={$app.themeInUse === 'dark' ? EmptyDark : EmptyLight}
on:click={() => (show = true)}>
</Empty>
{/if}
<PaginationWithLimit
name="Sites"
limit={data.limit}
offset={data.offset}
total={data.siteList.total} />
{:else if data.search}
<EmptySearch target="sites" />
{:else}
<Empty
single
allowCreate={$canWriteSites}
href="https://appwrite.io/docs/products/sites"
description="Deploy and manage your web your web applications with Sites. "
target="site"
src={$app.themeInUse === 'dark' ? EmptyDark : EmptyLight}
on:click={() => (show = true)}>
</Empty>
<Card.Base padding="m">
<Layout.Stack gap="xxl">
<img src={imgSrc} alt="create" aria-hidden="true" height="242" class={imgClass} />
<Layout.Stack>
{#if isOnWaitlist}
<Typography.Title size="s" align="center" color="--fgcolor-neutral-primary">
You've successfully joined the Sites waitlist
</Typography.Title>
<Typography.Text align="center" color="--fgcolor-neutral-secondary">
We can't wait for you to try out Sites on Cloud. You will get access
soon.
</Typography.Text>
{:else}
<Layout.Stack gap="m" alignItems="center">
<Typography.Title
size="s"
align="center"
color="--fgcolor-neutral-primary">
Appwrite Sites is in high demand
</Typography.Title>
<div style:max-width="600px">
<Typography.Text align="center" color="--fgcolor-neutral-secondary">
To ensure a smooth experience for everyone, we’re rolling out
access gradually. Join the waitlist and be one of the first to
deploy with Sites.
</Typography.Text>
</div>
<div style:margin-block-start="1rem">
<Button on:click={addToWaitlist}>Join waitlist</Button>
</div>
</Layout.Stack>
{/if}
</Layout.Stack>
</Layout.Stack>
</Card.Base>
{/if}
</Container>
@@ -2,8 +2,15 @@ import { Query } from '@appwrite.io/console';
import { sdk } from '$lib/stores/sdk';
import { getLimit, getPage, getSearch, getView, pageToOffset, View } from '$lib/helpers/load';
import { CARD_LIMIT, Dependencies } from '$lib/constants';
import { APPWRITE_OFFICIALS_ORG, isCloud } from '$lib/system';
export const load = async ({ url, depends, route, params, parent }) => {
// don't load anything on cloud unless org is appwrite atm!
const { organization } = await parent();
if (isCloud && organization?.$id !== APPWRITE_OFFICIALS_ORG) {
return;
}
export const load = async ({ url, depends, route, params }) => {
depends(Dependencies.SITES);
const page = getPage(url);
@@ -20,6 +20,9 @@
import { consoleVariables } from '$routes/(console)/store';
import { IconInfo } from '@appwrite.io/pink-icons-svelte';
import { InvalidFileType, removeFile } from '$lib/helpers/files';
import { humanFileSize } from '$lib/helpers/sizeConvertion';
import { isCloud } from '$lib/system';
import { currentPlan } from '$lib/stores/organization';
export let data;
let showExitModal = false;
@@ -29,7 +32,7 @@
let name = 'My website';
let id = ID.unique();
let domain = id;
let domain = `${id}.${$consoleVariables._APP_DOMAIN_SITES}`;
let framework: Models.Framework =
data.frameworks.frameworks?.find((f) => f.key === 'other') ??
data.frameworks.frameworks?.[0];
@@ -40,9 +43,22 @@
let variables: Partial<Models.Variable>[] = [];
let files: FileList;
$: maxSize =
isCloud && $currentPlan
? $currentPlan.deploymentSize * 1000000
: $consoleVariables._APP_COMPUTE_SIZE_LIMIT; // already in MB
$: readableMaxSize = humanFileSize(maxSize);
async function create() {
try {
domain = await buildVerboseDomain(name, $organization.name, $project.name, id);
domain = await buildVerboseDomain(
$consoleVariables._APP_DOMAIN_SITES,
name,
$organization.name,
$project.name,
id
);
const fr = Object.values(Framework).find((f) => f === framework.key);
const buildRuntime = Object.values(BuildRuntime).find(
@@ -167,7 +183,7 @@
<Upload.Dropzone
extensions={['gz', 'tar']}
bind:files
maxSize={10000000}
{maxSize}
required
on:invalid={handleInvalid}>
<Layout.Stack alignItems="center" gap="s">
@@ -192,7 +208,8 @@
</Tooltip>
</Layout.Stack>
<Typography.Caption variant="400"
>Max file size 10MB</Typography.Caption>
>Max file size: {readableMaxSize.value +
readableMaxSize.unit}</Typography.Caption>
</Layout.Stack>
</Layout.Stack>
</Upload.Dropzone>
@@ -45,4 +45,6 @@ export async function buildVerboseDomain(
if (await checkDomain(domain, apex)) {
return domain;
}
return ID.unique();
}
@@ -2,9 +2,17 @@ import Breadcrumbs from './breadcrumbs.svelte';
import Header from './header.svelte';
import { Dependencies } from '$lib/constants';
import { sdk } from '$lib/stores/sdk';
import { error } from '@sveltejs/kit';
import { error, redirect } from '@sveltejs/kit';
import { APPWRITE_OFFICIALS_ORG, isCloud } from '$lib/system';
import { base } from '$app/paths';
export const load = async ({ depends, params, parent }) => {
// don't load anything on cloud unless org is appwrite atm!
const { organization } = await parent();
if (isCloud && organization?.$id !== APPWRITE_OFFICIALS_ORG) {
redirect(307, `${base}/project-${params.region}-${params.project}/sites`);
}
export const load = async ({ depends, params }) => {
depends(Dependencies.SITE);
try {
const [site] = await Promise.all([
@@ -12,6 +12,7 @@
import { isCloud } from '$lib/system';
import { currentPlan } from '$lib/stores/organization';
import { consoleVariables } from '$routes/(console)/store';
import { humanFileSize } from '$lib/helpers/sizeConvertion';
export let show = false;
export let site: Models.Site;
@@ -21,11 +22,11 @@
$: maxSize =
isCloud && $currentPlan
? $currentPlan?.deploymentSize === 0
? 0
: $currentPlan?.deploymentSize * 1000000
? $currentPlan.deploymentSize * 1000000
: $consoleVariables._APP_COMPUTE_SIZE_LIMIT; // already in MB
$: readableMaxSize = humanFileSize(maxSize);
async function createDeployment() {
try {
uploader.uploadSiteDeployment(site.$id, files[0]);
@@ -94,7 +95,9 @@
>Only .tar.gz files allowed</svelte:fragment>
</Tooltip>
</Layout.Stack>
<Typography.Caption variant="400">Max file size 10MB</Typography.Caption>
<Typography.Caption variant="400"
>Max file size: {readableMaxSize.value +
readableMaxSize.unit}</Typography.Caption>
</Layout.Stack>
</Layout.Stack>
</Upload.Dropzone>
@@ -43,12 +43,16 @@
</Button>
</Layout.Stack>
{#if deploymentList?.total}
{@const hasAuthor = deploymentList.deployments.some((d) => d?.providerCommitAuthor)}
<Table.Root
columns={[
{ id: '$id', width: 200 },
{ id: 'status', width: { min: 120 } },
{ id: 'source', width: { min: 120 } },
{ id: '$updatedAt', width: { min: 120 } },
{ id: 'status', width: { min: 80, max: 100 } },
{ id: 'source', width: { min: 80, max: 100 } },
{
id: '$updatedAt',
width: { min: hasAuthor ? 280 : 100 }
},
{ id: 'actions', width: 50 }
]}
let:root>
@@ -29,14 +29,14 @@
</Button>
</Layout.Stack>
{#if data.domains.total}
<Table domains={data.domains} />
{#if data.proxyRules.total}
<Table proxyRules={data.proxyRules} />
<PaginationWithLimit
name="Domains"
limit={data.limit}
offset={data.offset}
total={data.domains.total} />
total={data.proxyRules.total} />
{:else if data?.search}
<EmptySearch hidePages bind:search={data.search} target="domains" hidePagination>
<Button
@@ -22,7 +22,7 @@ export const load = async ({ params, depends, url, route }) => {
limit,
query,
search,
domains: await sdk
proxyRules: await sdk
.forProject(params.region, params.project)
.proxy.listRules(
[
@@ -11,10 +11,10 @@
let {
show = $bindable(),
selectedDomain
selectedProxyRule
}: {
show: boolean;
selectedDomain: Models.ProxyRule;
selectedProxyRule: Models.ProxyRule;
} = $props();
let error: string = $state(null);
@@ -23,7 +23,7 @@
try {
await sdk
.forProject(page.params.region, page.params.project)
.proxy.deleteRule(selectedDomain.$id);
.proxy.deleteRule(selectedProxyRule.$id);
await invalidate(Dependencies.SITES_DOMAINS);
show = false;
addNotification({
@@ -39,13 +39,13 @@
</script>
<Confirm title="Delete domain" bind:open={show} onSubmit={deleteDomain} bind:error confirmDeletion>
{#if selectedDomain}
{#if selectedProxyRule}
<Typography.Text variant="m-400">
Are you sure you want to delete this domain? You will no longer be able to view your
site by visiting:
</Typography.Text>
<Typography.Text variant="m-500">
{selectedDomain.domain}
{selectedProxyRule.domain}
</Typography.Text>
{/if}
</Confirm>
@@ -17,13 +17,13 @@
let {
show = $bindable(false),
selectedDomain
selectedProxyRule
}: {
show: boolean;
selectedDomain: Models.ProxyRule;
selectedProxyRule: Models.ProxyRule;
} = $props();
const isSubDomain = $derived.by(() => isASubdomain(selectedDomain?.domain));
const isSubDomain = $derived.by(() => isASubdomain(selectedProxyRule?.domain));
let selectedTab = $state<'cname' | 'nameserver' | 'a' | 'aaaa'>('nameserver');
$effect(() => {
@@ -43,13 +43,13 @@
try {
const domain = await sdk
.forProject(page.params.region, page.params.project)
.proxy.updateRuleVerification(selectedDomain.$id);
.proxy.updateRuleVerification(selectedProxyRule.$id);
await invalidate(Dependencies.SITES_DOMAINS);
verified = domain.status === 'verified';
show = false;
addNotification({
type: 'success',
message: `${selectedDomain.domain} has been verified`
message: `${selectedProxyRule.domain} has been verified`
});
trackEvent(Submit.DomainUpdateVerification);
} catch (e) {
@@ -104,13 +104,13 @@
<Divider />
</div>
{#if selectedTab === 'nameserver'}
<NameserverTable domain={selectedDomain.domain} {verified} />
<NameserverTable domain={selectedProxyRule.domain} {verified} />
{:else}
<RecordTable
{verified}
service="sites"
variant={selectedTab}
domain={selectedDomain.domain} />
domain={selectedProxyRule.domain} />
{/if}
<svelte:fragment slot="footer">
@@ -19,14 +19,22 @@
import { columns } from './store';
let {
domains
proxyRules
}: {
domains: Models.ProxyRuleList;
proxyRules: Models.ProxyRuleList;
} = $props();
let showDelete = $state(false);
let showRetry = $state(false);
let selectedDomain: Models.ProxyRule = $state(null);
let selectedProxyRule: Models.ProxyRule = $state(null);
const proxyTarget = (proxy: Models.ProxyRule) => {
return proxy?.redirectUrl
? 'Redirect to ' + proxy.redirectUrl
: proxy?.deploymentVcsProviderBranch
? 'Deployed from' + proxy.deploymentVcsProviderBranch
: 'Active deployment';
};
</script>
<Table.Root columns={[...$columns, { id: 'actions', width: 40 }]} let:root>
@@ -38,25 +46,25 @@
{/each}
<Table.Header.Cell column="actions" {root} />
</svelte:fragment>
{#each domains.rules as domain}
{#each proxyRules.rules as rule}
<Table.Row.Base {root}>
{#each $columns as column}
<Table.Cell column={column.id} {root}>
{#if column.id === 'domain'}
<Layout.Stack direction="row" gap="xs">
<Link external href={`${$protocol}${domain.domain}`} variant="quiet">
<Link external href={`${$protocol}${rule.domain}`} variant="quiet">
<Typography.Text truncate>
{domain.domain}
{rule.domain}
</Typography.Text>
</Link>
{#if domain.status === 'verifying'}
{#if rule.status === 'verifying'}
<Badge
variant="secondary"
type="warning"
content="Verifying"
size="s" />
{:else if domain.status !== 'verified'}
{:else if rule.status !== 'verified'}
<Badge
variant="secondary"
type="error"
@@ -65,11 +73,7 @@
{/if}
</Layout.Stack>
{:else if column.id === 'target'}
{domain?.redirectUrl
? 'Redirect to ' + domain.redirectUrl
: domain?.deploymentVcsProviderBranch
? 'Deployed from' + domain.deploymentVcsProviderBranch
: '-'}
{proxyTarget(rule)}
{/if}
</Table.Cell>
{/each}
@@ -88,11 +92,11 @@
<svelte:fragment slot="tooltip" let:toggle>
<ActionMenu.Root>
{#if domain.status !== 'verified'}
{#if rule.status !== 'verified'}
<ActionMenu.Item.Button
leadingIcon={IconRefresh}
on:click={(e) => {
selectedDomain = domain;
selectedProxyRule = rule;
showRetry = true;
toggle(e);
}}>
@@ -103,7 +107,7 @@
status="danger"
leadingIcon={IconTrash}
on:click={(e) => {
selectedDomain = domain;
selectedProxyRule = rule;
showDelete = true;
toggle(e);
trackEvent(Click.DomainDeleteClick, {
@@ -122,9 +126,9 @@
</Table.Root>
{#if showDelete}
<DeleteDomainModal bind:show={showDelete} {selectedDomain} />
<DeleteDomainModal bind:show={showDelete} {selectedProxyRule} />
{/if}
{#if showRetry}
<RetryDomainModal bind:show={showRetry} {selectedDomain} />
<RetryDomainModal bind:show={showRetry} {selectedProxyRule} />
{/if}

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