Merge branch 'main' of https://github.com/appwrite/console into feat-cookie-consent

This commit is contained in:
Torsten Dittmann
2024-01-02 12:26:04 +01:00
308 changed files with 14483 additions and 2143 deletions
+2 -1
View File
@@ -1,4 +1,5 @@
VITE_APPWRITE_ENDPOINT=http://localhost/v1
VITE_APPWRITE_GROWTH_ENDPOINT=
VITE_GA_PROJECT=
VITE_CONSOLE_MODE=self-hosted
VITE_CONSOLE_MODE=self-hosted
VITE_STRIPE_PUBLIC_KEY=
+583 -438
View File
File diff suppressed because it is too large Load Diff
+7 -3
View File
@@ -18,12 +18,13 @@
"e2e": "playwright test tests/e2e"
},
"dependencies": {
"@appwrite.io/console": "^0.3.0",
"@appwrite.io/pink": "0.1.0-next.9",
"@appwrite.io/pink-icons": "^0.1.0-next.9",
"@appwrite.io/console": "^0.4.2",
"@appwrite.io/pink": "0.2.0",
"@appwrite.io/pink-icons": "0.2.0",
"@popperjs/core": "^2.11.8",
"@sentry/svelte": "^7.66.0",
"@sentry/tracing": "^7.66.0",
"@stripe/stripe-js": "^2.2.0",
"ai": "^2.2.11",
"analytics": "^0.8.9",
"dayjs": "^1.11.9",
@@ -40,6 +41,8 @@
"web-vitals": "^3.4.0"
},
"devDependencies": {
"@melt-ui/pp": "^0.1.4",
"@melt-ui/svelte": "^0.61.2",
"@playwright/test": "^1.37.1",
"@sveltejs/adapter-static": "^2.0.3",
"@sveltejs/kit": "^1.24.0",
@@ -65,6 +68,7 @@
"svelte-check": "^3.5.1",
"svelte-jester": "^2.3.2",
"svelte-preprocess": "^5.0.4",
"svelte-sequential-preprocessor": "^2.0.1",
"tslib": "^2.6.2",
"typescript": "^5.2.2",
"vite": "^4.4.9",
+19
View File
@@ -256,6 +256,25 @@ export enum Submit {
FileCreate = 'submit_file_create',
FileDelete = 'submit_file_delete',
FileUpdatePermissions = 'submit_file_update_permissions',
BudgetCapUpdate = 'submit_budget_cap_update',
BudgetAlertsUpdate = 'submit_budget_alert_conditions_update',
CreditRedeem = 'submit_credit_redeem',
PaymentMethodCreate = 'submit_payment_method_create',
PaymentMethodUpdate = 'submit_payment_method_update',
PaymentMethodDelete = 'submit_payment_method_delete',
BillingAddressCreate = 'submit_billing_address_create',
BillingAddressUpdate = 'submit_billing_address_update',
BillingAddressDelete = 'submit_billing_address_delete',
OrganizationPaymentUpdate = 'submit_organization_payment_update',
OrganizationPaymentDelete = 'submit_organization_payment_delete',
OrganizationBackupPaymentUpdate = 'submit_organization_backup_payment_update',
OrganizationBackupPaymentDelete = 'submit_organization_backup_payment_delete',
OrganizationBillingAddressUpdate = 'submit_organization_billing_address_update',
OrganizationBillingAddressDelete = 'submit_organization_billing_address_delete',
OrganizationUpgrade = 'submit_organization_upgrade',
OrganizationDowngrade = 'submit_organization_downgrade',
OrganizationBillingTaxIdUpdate = 'submit_organization_billing_tax_id_update',
SupportTicket = 'submit_support_ticket',
InstallationCreate = 'submit_installation_create',
InstallationDelete = 'submit_installation_delete',
EmailChangeLocale = 'submit_email_change_locale',
-1
View File
@@ -15,7 +15,6 @@
const { input, handleSubmit, completion, isLoading, complete, error } = useCompletion({
api: endpoint + '/console/assistant',
headers: {
'content-type': 'application/json',
'x-appwrite-project': 'console'
},
credentials: 'include'
+14 -11
View File
@@ -369,37 +369,39 @@
--cmd-center-border: hsl(var(--color-neutral-10));
--cmd-center-shadow: 0px 16px 32px 0px rgba(55, 59, 77, 0.04);
--kbd-bg: hsl(var(--color-neutral-30));
--kbd-bg: hsl(var(--color-neutral-15));
--kbd-color: hsl(var(--color-neutral-60));
--crumb-bg: hsl(var(--color-neutral-10));
--crumb-color: hsl(var(--color-neutral-100));
--result-bg: hsl(var(--color-neutral-10));
--footer-bg: linear-gradient(180deg, #fff 49.38%, #e8e9f0 100%);
--footer-bg: linear-gradient(180deg, #fff 0%, #e8e9f0 100%);
--icon-color: hsl(var(--color-neutral-50));
--label-color: hsl(var(--color-neutral-100));
--label-color: hsl(var(--color-neutral-60));
}
:global(.theme-dark) .card {
--cmd-center-bg: hsl(var(--color-neutral-100-new));
--cmd-center-border: hsl(var(--color-neutral-150));
--cmd-center-shadow: 0px 16px 32px 0px hsl(var(--color-neutral-110-new));
--cmd-center-bg: hsl(var(--color-neutral-90));
--cmd-center-border: hsl(var(--color-neutral-80));
--cmd-center-shadow: 0px 16px 32px 0px #14141f;
--kbd-bg: hsl(var(--color-neutral-150));
--kbd-bg: hsl(var(--color-neutral-80));
--kbd-color: hsl(var(--color-neutral-15));
--crumb-bg: hsl(var(--color-neutral-150));
--crumb-color: hsl(var(--color-neutral-30));
--result-bg: hsl(var(--color-neutral-200));
--result-bg: hsl(var(--color-neutral-85));
--footer-bg: linear-gradient(
180deg,
hsl(var(--color-neutral-100-new)) 0%,
hsl(var(--color-neutral-85-new)) 100%
hsl(var(--color-neutral-100)) 0%,
hsl(var(--color-neutral-85)) 100%
);
--icon-color: hsl(var(--color-neutral-70));
--label-color: hsl(var(--color-neutral-30));
--label-color: hsl(var(--color-neutral-15));
}
// Elements
@@ -430,6 +432,7 @@
}
:global(.kbd) {
color: var(--kbd-color);
background-color: var(--kbd-bg);
padding-inline: 0.25rem;
}
+15 -11
View File
@@ -1,6 +1,7 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import type { Buttons } from '../stores/notifications';
import { Button } from '$lib/elements/forms';
export let dismissible = false;
export let type: 'info' | 'success' | 'warning' | 'error' | 'default' = 'info';
@@ -44,20 +45,23 @@
<slot name="title" />
</h6>
{/if}
{#if $$slots.default}
<p class="alert-message"><slot /></p>
{/if}
{#if ($$slots.buttons || buttons?.length) && !isAction}
<div class="alert-buttons u-flex">
<slot name="buttons">
{#each buttons as button}
<button type="button" class="button is-text" on:click={button.method}>
<span class="text">{button.name}</span>
</button>
{/each}
</slot>
</div>
{/if}
<slot name="action">
{#if ($$slots.buttons || buttons?.length) && !isAction}
<div class="alert-buttons u-flex">
<slot name="buttons">
{#each buttons as button}
<Button text on:click={button.method}>
<span class="text">{button.name}</span>
</Button>
{/each}
</slot>
</div>
{/if}
</slot>
</div>
{#if ($$slots.buttons || buttons?.length) && isAction}
<div class="alert-buttons u-flex u-gap-16 u-cross-child-center">
@@ -0,0 +1,14 @@
<script lang="ts">
import { page } from '$app/stores';
import { HeaderAlert } from '$lib/layout';
import { organization } from '$lib/stores/organization';
</script>
{#if $organization?.markedForDeletion && !$page.url.pathname.includes('/console/account')}
<HeaderAlert title="Organization flagged for deletion">
<svelte:fragment>
All existing projects in the {$organization.name} organization have been paused. This organization
will be deleted once your upcoming invoice is processed successfully.
</svelte:fragment>
</HeaderAlert>
{/if}
@@ -0,0 +1,29 @@
<script lang="ts">
import { page } from '$app/stores';
import { Button } from '$lib/elements/forms';
import { HeaderAlert } from '$lib/layout';
import { actionRequiredInvoices } from '$lib/stores/billing';
import { organization } from '$lib/stores/organization';
import { VARS } from '$lib/system';
const endpoint = VARS.APPWRITE_ENDPOINT ?? `${$page.url.origin}/v1`;
</script>
{#if $actionRequiredInvoices && $actionRequiredInvoices?.invoices?.length && !$page.url.pathname.includes('/console/account')}
<HeaderAlert title="Authorization required" type="error">
Please authorize your upcoming payment for {$organization.name}. Your bank requires this
security measure to proceed with payment.
<svelte:fragment slot="buttons">
<Button
text
href={`${endpoint}/organizations/${$organization.$id}/invoices/${$actionRequiredInvoices.invoices[0].$id}/view`}>
View invoice
</Button>
<Button
secondary
href={`/console/organization-${$organization.$id}/billing?type=confirmation&invoice=${$actionRequiredInvoices.invoices[0].$id}`}>
Authorize payment
</Button>
</svelte:fragment>
</HeaderAlert>
{/if}
@@ -0,0 +1,32 @@
<script lang="ts">
import { base } from '$app/paths';
import { page } from '$app/stores';
import { Button } from '$lib/elements/forms';
import { diffDays, toLocaleDate } from '$lib/helpers/date';
import { HeaderAlert } from '$lib/layout';
import { failedInvoice } from '$lib/stores/billing';
</script>
{#if $failedInvoice && !$page.url.pathname.includes('/console/account')}
{@const daysPassed = diffDays(new Date($failedInvoice.dueAt), new Date())}
<HeaderAlert title="Your projects are at risk">
<svelte:fragment>
{#if daysPassed > 30}
Your scheduled payment on <b>{toLocaleDate($failedInvoice?.dueAt)}</b> failed. To resume
write access of your organization, please update your billing details.
{:else}
Your scheduled payment on <b>{toLocaleDate($failedInvoice?.dueAt)}</b> failed. Access
to paid projects within this organization will be disabled if no action is taken within
30 days.
{/if}
</svelte:fragment>
<svelte:fragment slot="buttons">
<Button
href={`${base}/console/organization-${$failedInvoice?.teamId}/billing#paymentMethods`}
secondary
fullWidthMobile>
<span class="text">Update billing details</span>
</Button>
</svelte:fragment>
</HeaderAlert>
{/if}
@@ -0,0 +1,13 @@
<script lang="ts">
import { Button } from '$lib/elements/forms';
import { HeaderAlert } from '$lib/layout';
</script>
<HeaderAlert title="You are limited to one free organization per account">
All but one organization will be automatically upgraded to a Pro plan on <b>31 January 2024</b>.
You can add a payment method or transfer projects from your settings.
<svelte:fragment slot="buttons">
<Button secondary href="/console/account/organizations">View organizations</Button>
</svelte:fragment>
</HeaderAlert>
@@ -0,0 +1,74 @@
<script lang="ts">
import { Button, FormList, InputText } from '$lib/elements/forms';
import type { Coupon } from '$lib/sdk/billing';
import { sdk } from '$lib/stores/sdk';
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
export let required = false;
export let coupon: string = '';
export let couponData: Partial<Coupon> = {
code: null,
status: null,
credits: null
};
async function addCoupon() {
try {
const response = await sdk.forConsole.billing.getCoupon(coupon);
couponData = response;
dispatch('validation', couponData);
coupon = null;
} catch (error) {
couponData.code = coupon;
couponData.status = 'error';
}
}
function removeCoupon() {
couponData = {
code: null,
status: null,
credits: null
};
}
</script>
<FormList gap={8}>
<InputText
placeholder="Coupon code"
id="code"
label="Add credits"
{required}
hideRequired
disabled={couponData?.status === 'active'}
bind:value={coupon}>
<Button
secondary
disabled={couponData?.status === 'active' || !coupon}
on:click={addCoupon}>
Apply
</Button>
</InputText>
{#if couponData?.status === 'error'}
<div>
<span class="icon-exclamation-circle u-color-text-danger" />
<span>
{couponData.code} is not a valid promo code
</span>
</div>
{:else if couponData?.status === 'active'}
<div class="u-flex u-main-space-between u-cross-center">
<div>
<span class="icon-tag u-color-text-success" />
<slot data={couponData}>
<span>
{couponData.code} applied (-${couponData.credits})
</span>
</slot>
</div>
<Button text on:click={removeCoupon}>Remove</Button>
</div>
{/if}
</FormList>
+2
View File
@@ -0,0 +1,2 @@
export { default as PaymentBoxes } from './paymentBoxes.svelte';
export { default as CouponInput } from './couponInput.svelte';
@@ -0,0 +1,91 @@
<script lang="ts">
import { FormList, InputText } from '$lib/elements/forms';
import { onDestroy, onMount } from 'svelte';
import { CreditCardBrandImage, RadioBoxes } from '..';
import { unmountPaymentElement } from '$lib/stores/stripe';
export let methods: Record<string, unknown>[];
export let group: string;
export let name: string;
let element: HTMLDivElement;
let loader: HTMLDivElement;
let observer: MutationObserver;
onMount(() => {
observer = new MutationObserver((mutationsList) => {
for (let mutation of mutationsList) {
if (mutation.type === 'childList') {
if (mutation.addedNodes.length > 0) {
for (let node of Array.from(mutation.addedNodes)) {
if (
node instanceof Element &&
node.className.toLowerCase().includes('__privatestripeelement')
) {
loader.style.display = 'none';
}
}
}
}
}
});
});
onDestroy(() => {
observer.disconnect();
unmountPaymentElement();
});
$: if (element) {
observer.observe(element, { childList: true });
}
</script>
<RadioBoxes elements={methods} total={methods?.length} variableName="$id" name="payment" bind:group>
<svelte:fragment slot="element" let:element>
<slot {element}>
<span class="u-flex u-cross-center u-gap-8" style="padding-inline:0.25rem">
<span>
<span class="u-capitalize">{element.brand}</span> ending in {element.last4}</span>
<CreditCardBrandImage brand={element.brand?.toString()} />
</span>
</slot>
</svelte:fragment>
<svelte:fragment slot="new">
<span style="padding-inline:0.25rem">Add new payment method</span>
</svelte:fragment>
<FormList class="u-margin-block-start-8" gap={16}>
<InputText
id="name"
label="Cardholder name"
placeholder="Cardholder name"
bind:value={name}
required
autofocus={true} />
<div class="aw-stripe-container" data-private>
<div class="loader-container" bind:this={loader}>
<div class="loader"></div>
</div>
<div id="payment-element" bind:this={element}>
<!-- Stripe will create form elements here -->
</div>
</div>
</FormList>
</RadioBoxes>
<style lang="scss">
.aw-stripe-container {
min-height: 295px;
position: relative;
.loader-container {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 0;
}
}
</style>
+1 -1
View File
@@ -5,7 +5,7 @@
export { classes as class };
enum radiuses {
xsmall = '--border-radius-extra-large',
xsmall = '--border-radius-xsmall',
small = '--border-radius-small',
medium = '--border-radius-medium',
large = '--border-radius-large'
+3 -1
View File
@@ -24,6 +24,8 @@
export let isButton = false;
export let danger = false;
export let href: string = null;
let classes = '';
export { classes as class };
function getElement() {
switch (true) {
@@ -39,7 +41,7 @@
<svelte:element
this={getElement()}
class="card"
class="card {classes}"
class:common-section={!isTile}
class:is-border-dashed={isDashed}
class:is-danger={danger}
+14 -3
View File
@@ -2,11 +2,18 @@
import { page } from '$app/stores';
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';
export let offset = 0;
export let total = 0;
export let event: string = null;
export let service = '';
export let serviceId: PlanServices = service as PlanServices;
$: planLimit = getServiceLimit(serviceId) || Infinity;
$: limit = preferences.get($page.route)?.limit ?? CARD_LIMIT;
</script>
@@ -18,8 +25,12 @@
<slot />
{#if total > 3 ? total < limit + offset : total % 2 !== 0}
<Empty on:click target={event}>
<slot name="empty" />
</Empty>
{#if isCloud && serviceId && total >= planLimit}
<CardPlanLimit {service} />
{:else}
<Empty on:click target={event}>
<slot name="empty" />
</Empty>
{/if}
{/if}
</ul>
+1 -1
View File
@@ -10,7 +10,7 @@
<div class="grid-1-2-col-1 u-flex u-flex-vertical u-gap-16">
<slot />
</div>
<div class="grid-1-2-col-2 u-flex u-flex-vertical u-gap-24">
<div class="grid-1-2-col-2 u-flex u-flex-vertical u-gap-16 u-min-width-0">
<slot name="aside" />
</div>
</div>
+16
View File
@@ -0,0 +1,16 @@
<script lang="ts">
import { Button } from '$lib/elements/forms';
import { wizard } from '$lib/stores/wizard';
import ChangeOrganizationTierCloud from '$routes/console/changeOrganizationTierCloud.svelte';
export let service: string;
</script>
<article class="card u-grid u-cross-center u-width-full-line">
<div class="u-flex u-flex-vertical u-gap-24 u-main-center u-cross-center">
<p class="text u-text-center">Upgrade your plan to add more {service}</p>
<Button secondary on:click={() => wizard.start(ChangeOrganizationTierCloud)}>
Change plan
</Button>
</div>
</article>
+1 -1
View File
@@ -9,7 +9,7 @@
<details class="collapsible-wrapper" {open}>
<!-- svelte-ignore a11y-no-redundant-roles -->
<summary
class="collapsible-button"
class="collapsible-button u-position-relative"
on:keyup={clickOnEnter}
on:click
role="button"
@@ -0,0 +1,14 @@
<script lang="ts">
import { sdk } from '$lib/stores/sdk';
export let brand: string;
export let width = 23;
export let height = 16;
function getCreditCardImage(brand: string, width = 46, height = 32) {
if (!brand) return '';
return sdk.forConsole.avatars.getCreditCard(brand, width, height).toString();
}
</script>
<img style="border-radius: 2.5px" {width} {height} src={getCreditCardImage(brand)} alt={brand} />
+36
View File
@@ -0,0 +1,36 @@
<script lang="ts">
import type { PaymentMethodData } from '$lib/sdk/billing';
import { Alert } from '.';
import CreditCardBrandImage from './creditCardBrandImage.svelte';
export let isBox = false;
export let paymentMethod: PaymentMethodData;
</script>
<div class:box={isBox}>
<div class="u-flex u-main-space-between u-cross-start">
<div class="u-line-height-1-5 u-flex u-flex-vertical u-gap-2">
<span class="u-flex u-cross-center u-gap-8">
<p class="text u-bold">
<span class="u-capitalize">{paymentMethod?.brand}</span> ending in {paymentMethod?.last4}
</p>
<CreditCardBrandImage brand={paymentMethod?.brand} />
</span>
<p class="text">
Expires {paymentMethod?.expiryMonth}/{paymentMethod?.expiryYear}
</p>
{#if paymentMethod?.name}
<p class="text">
{paymentMethod.name}
</p>
{/if}
</div>
<slot />
</div>
{#if paymentMethod?.expired}
<Alert type="error" class="u-margin-block-start-16 u-width-full-line">
<svelte:fragment slot="title">This payment method has expired</svelte:fragment>
</Alert>
{/if}
</div>
+1 -1
View File
@@ -143,7 +143,7 @@
.drop-arrow::before {
content: '';
transform: rotate(45deg);
background: hsl(var(--color-neutral-200));
background: hsl(var(--color-neutral-85));
body.theme-light & {
background: hsl(var(--color-neutral-10));
+9 -1
View File
@@ -4,6 +4,7 @@
export let disabled = false;
export let icon: string = null;
export let event: string = null;
export let loading = false;
function track() {
if (!event) {
@@ -17,10 +18,17 @@
</script>
<li class="drop-list-item">
<button class="drop-button" on:click={track} on:click|preventDefault {disabled}>
<button
class="drop-button u-flex u-cross-center u-main-space-between"
on:click={track}
on:click|preventDefault
{disabled}>
<span class="text"><slot /></span>
{#if icon}
<span class={`icon-${icon}`} aria-hidden="true" />
{/if}
{#if loading}
<span class="loader is-small u-line-height-1-5" aria-hidden="true" />
{/if}
</button>
</li>
+14
View File
@@ -1,13 +1,27 @@
<script lang="ts">
import { trackEvent } from '$lib/actions/analytics';
export let href: string;
export let icon: string = null;
export let disabled = false;
export let external = false;
export let event: string = null;
function track() {
if (!event) {
return;
}
trackEvent(`click_${event}`, {
from: 'drop-list-link'
});
}
</script>
<li class="drop-list-item">
<a
on:click
on:click={track}
{href}
class="drop-button"
class:is-disabled={disabled}
+17 -12
View File
@@ -7,8 +7,10 @@
import { trackEvent } from '$lib/actions/analytics';
export let single = false;
export let noMedia = false;
export let target: string = null;
export let href: string = null;
export let marginTop = false;
function track() {
if (target) {
@@ -23,17 +25,19 @@
<article class="card u-grid u-cross-center u-width-full-line common-section">
<div
class="u-flex u-flex-vertical u-cross-center u-gap-24 u-width-full-line u-overflow-hidden">
<button
type="button"
on:click|preventDefault
on:click={track}
aria-label="create {target}">
{#if $app.themeInUse === 'dark'}
<img src={EmptyDark} alt="create" aria-hidden="true" width="376" />
{:else}
<img src={EmptyLight} alt="create" aria-hidden="true" width="376" />
{/if}
</button>
{#if !noMedia}
<button
type="button"
on:click|preventDefault
on:click={track}
aria-label="create {target}">
{#if $app.themeInUse === 'dark'}
<img src={EmptyDark} alt="create" aria-hidden="true" width="376" />
{:else}
<img src={EmptyLight} alt="create" aria-hidden="true" width="376" />
{/if}
</button>
{/if}
<slot>
<div class="u-text-center">
<Heading size="7" tag="h2" trimmed={false}>
@@ -63,7 +67,8 @@
on:click={track}
aria-label="create"
type="button"
class="card u-grid u-cross-center u-width-full-line dashed">
class="card u-grid u-cross-center u-width-full-line dashed"
class:common-section={marginTop}>
<div class="u-flex u-cross-center u-flex-vertical u-main-center u-flex">
<div class="common-section">
<div class="button is-secondary is-only-icon">
+154
View File
@@ -0,0 +1,154 @@
<script lang="ts">
import { Alert } from '$lib/components';
import { onMount } from 'svelte';
import Form from '$lib/elements/forms/form.svelte';
import { trackEvent } from '$lib/actions/analytics';
import { clickOnEnter } from '$lib/helpers/a11y';
export let show = false;
export let size: 'small' | 'big' = 'big';
export let icon: string = null;
export let state: 'success' | 'warning' | 'error' | 'info' = null;
export let error: string = null;
export let closable = true;
export let headerDivider = true;
export let onSubmit: (e: SubmitEvent) => Promise<void> | void = function () {
return;
};
export let title = '';
let backdrop: HTMLDivElement;
onMount(async () => {});
function handleBLur(event: MouseEvent) {
if (event.target === backdrop) {
trackEvent('click_close_modal', {
from: 'backdrop'
});
closeModal();
}
}
function closeModal() {
document.documentElement.classList.remove('u-overflow-hidden');
show = false;
}
function handleKeydown(event: KeyboardEvent) {
if (event.key === 'Escape') {
event.preventDefault();
trackEvent('click_close_modal', {
from: 'escape'
});
closeModal();
}
}
$: if (backdrop) {
document.body.appendChild(backdrop);
}
$: if (show) {
document.documentElement.classList.add('u-overflow-hidden');
} else {
closeModal();
error = null;
}
</script>
<svelte:window on:keydown={handleKeydown} />
{#if show}
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="payment-modal-backdrop"
on:keyup={clickOnEnter}
on:click={handleBLur}
bind:this={backdrop}>
<div
class="modal"
class:is-small={size === 'small'}
class:is-big={size === 'big'}
class:is-separate-header={headerDivider}>
<Form isModal {onSubmit}>
<header class="modal-header">
<div class="u-flex u-main-space-between u-cross-center u-gap-16">
<div class="u-flex u-cross-center u-gap-16">
{#if icon}
<div
class="avatar is-medium"
class:is-success={state === 'success'}
class:is-warning={state === 'warning'}
class:is-danger={state === 'error'}
class:is-info={state === 'info'}>
<span class={`icon-${icon}`} aria-hidden="true" />
</div>
{/if}
<h4 class="modal-title heading-level-5">
<slot name="title">
{title}
</slot>
</h4>
</div>
{#if closable}
<button
type="button"
class="button is-text is-only-icon"
style="--button-size:1.5rem;"
aria-label="Close Modal"
title="Close Modal"
on:click={() =>
trackEvent('click_close_modal', {
from: 'button'
})}
on:click={closeModal}>
<span class="icon-x" aria-hidden="true" />
</button>
{/if}
</div>
</header>
<div class="modal-content">
{#if error}
<Alert
dismissible
type="warning"
on:dismiss={() => {
error = null;
}}>
{error}
</Alert>
{/if}
<slot />
</div>
{#if $$slots.footer}
<div class="modal-footer">
<div class="u-flex u-main-end u-gap-16">
<slot name="footer" />
</div>
</div>
{/if}
</Form>
</div>
</div>
{/if}
<style lang="scss" global>
.payment-modal-backdrop {
position: fixed;
top: 0;
left: 0;
z-index: 100000;
width: 100%;
height: 100%;
background-color: hsl(240 22% 10% / 0.3);
display: flex;
align-items: center;
justify-content: center;
:global() {
background-color: hsl(240 5% 8% / 0.6);
}
}
</style>
+2 -2
View File
@@ -115,7 +115,7 @@
<div>
<form on:submit|preventDefault={addFilter}>
<div class="selects u-flex u-gap-8 u-margin-block-start-16">
<ul class="selects u-flex u-gap-8 u-margin-block-start-16">
<InputSelect
id="column"
options={$columns.map((c) => ({
@@ -130,7 +130,7 @@
options={operatorsForColumn}
placeholder="Select operator"
bind:value={operatorKey} />
</div>
</ul>
{#if column && operator && !operator?.hideInput}
<div class="u-margin-block-start-8">
{#if column.type === 'integer' || column.type === 'double'}
+3 -3
View File
@@ -26,13 +26,13 @@
}
:global(.theme-dark) .floating-action-bar {
border: 1px solid hsl(var(--color-neutral-200));
background: hsl(var(--color-neutral-300));
border: 1px solid hsl(var(--color-neutral-85));
background: hsl(var(--color-neutral-90));
box-shadow: 0px 6px 16px 8px #14141f;
}
:global(.theme-light) .floating-action-bar {
border: 1px solid hsl(var(--color-neutral-30));
border: 1px solid hsl(var(--color-neutral-15));
background: hsl(var(--color-neutral-0));
box-shadow: 0px 6px 16px 0px rgba(55, 59, 77, 0.14);
}
+1 -1
View File
@@ -7,7 +7,7 @@
<div class="grid-item-1">
<div class="grid-item-1-start-start">
<div class="eyebrow-heading-3"><slot name="eyebrow" /></div>
<Heading tag="h2" size="6"><slot name="title" /></Heading>
<Heading tag="h2" size="7"><slot name="title" /></Heading>
</div>
<div class="grid-item-1-start-end">
<slot name="status" />
+3 -1
View File
@@ -6,8 +6,10 @@
export let size: Size;
export let trimmed = true;
export let id: string = null;
let classes = '';
export { classes as class };
</script>
<svelte:element this={tag} class={`heading-level-${size}`} class:u-trim-1={trimmed} {id}>
<svelte:element this={tag} class={`heading-level-${size} ${classes}`} class:u-trim-1={trimmed} {id}>
<slot />
</svelte:element>
+11
View File
@@ -46,13 +46,20 @@ export { default as EventModal } from './eventModal.svelte';
export { default as Status } from './status.svelte';
export { default as Heading } from './heading.svelte';
export { default as Output } from './output.svelte';
export { default as Support } from './support.svelte';
export { default as ViewSelector } from './viewSelector.svelte';
export { default as LabelCard } from './labelCard.svelte';
export { default as RegionCard } from './regionCard.svelte';
export { default as Limit } from './limit.svelte';
export { default as PaginationWithLimit } from './paginationWithLimit.svelte';
export { default as ClickableList } from './clickableList.svelte';
export { default as ClickableListItem } from './clickableListItem.svelte';
export { default as Id } from './id.svelte';
export { default as ProgressBar } from './progressBar.svelte';
export { default as ProgressBarBig } from './progressBarBig.svelte';
export { default as CreditCardInfo } from './creditCardInfo.svelte';
export { default as CreditCardBrandImage } from './creditCardBrandImage.svelte';
export { default as CardPlanLimit } from './cardPlanLimit.svelte';
export { default as NumericList } from './numericList.svelte';
export { default as NumericListItem } from './numericListItem.svelte';
export { default as EyebrowHeading } from './eyebrowHeading.svelte';
@@ -60,3 +67,7 @@ export { default as SvgIcon } from './svgIcon.svelte';
export { default as MigrationBox } from './migrationBox.svelte';
export { default as FloatingActionBar } from './floatingActionBar.svelte';
export { default as LoadingDots } from './loadingDots.svelte';
export { default as FakeModal } from './fakeModal.svelte';
export { default as RadioBoxes } from './radioBoxes.svelte';
export { default as ModalWrapper } from './modalWrapper.svelte';
export { default as ModalSideCol } from './modalSideCol.svelte';
+24 -13
View File
@@ -1,4 +1,6 @@
<script lang="ts">
import { tooltip } from '$lib/actions/tooltip';
export let name: string;
export let group: string;
export let value: string | number | boolean;
@@ -9,6 +11,8 @@
export let borderRadius: 'xsmall' | 'small' | 'medium' | 'large' = 'small';
export let backgroundColor: string = null;
export let backgroundColorHover: string = null;
export let tooltipText: string = null;
export let tooltipShow = false;
enum Radius {
xsmall = '--border-radius-xsmall',
@@ -19,12 +23,15 @@
</script>
<label
class="card is-allow-focus u-cursor-pointer"
class="card u-cursor-pointer"
class:is-allow-focus={!disabled}
class:is-disabled={disabled}
class:u-height-100-percent={fullHeight}
style:--card-padding={`${padding}rem`}
style:--card-border-radius={`var(${Radius[borderRadius]})`}
style:--p-card-bg-color-default={backgroundColor}
style:--p-card-bg-color-hover={backgroundColorHover}>
style:--p-card-bg-color-hover={backgroundColorHover}
use:tooltip={{ content: tooltipText, disabled: !tooltipText || !tooltipShow }}>
<div class="u-flex u-gap-8">
<input
class="is-small u-margin-block-start-2"
@@ -34,18 +41,22 @@
{value}
bind:group
on:click />
<div class="u-flex u-flex-vertical u-gap-4">
{#if $$slots.title}
<h4 class="body-text-2 u-bold"><slot name="title" /></h4>
{#if $$slots.custom}
<slot name="custom" {disabled} />
{:else}
<div class="u-flex u-flex-vertical u-gap-4" class:u-opacity-50={disabled}>
{#if $$slots.title}
<h4 class="body-text-2 u-bold"><slot name="title" /></h4>
{/if}
{#if $$slots.default}
<p class="u-color-text-gray u-small">
<slot />
</p>
{/if}
</div>
{#if icon}
<span class={`icon-${icon} u-margin-inline-start-auto`} aria-hidden="true" />
{/if}
{#if $$slots.default}
<p class="u-color-text-gray u-small">
<slot />
</p>
{/if}
</div>
{#if icon}
<span class={`icon-${icon} u-margin-inline-start-auto`} aria-hidden="true" />
{/if}
</div>
</label>
+2 -1
View File
@@ -1,3 +1,4 @@
<ul class="sessions">
<!-- TODO: remove inline styling -->
<ul class="list" style:gap="0.25rem">
<slot />
</ul>
+4 -12
View File
@@ -1,16 +1,8 @@
<script lang="ts">
export let avatar: string;
export let icon: string = null;
</script>
<li class="sessions-item">
<article class="card">
<img class="sessions-item-image avatar" src={avatar} alt="" />
<header class="sessions-item-header">
<slot name="header" />
</header>
<p class="sessions-item-info">
<slot name="info" />
</p>
<slot name="action" />
</article>
<li class="list-item">
<span class={icon ? `icon-${icon}` : 'icon-check-circle'} aria-hidden="true" />
<slot />
</li>
+66 -130
View File
@@ -1,6 +1,5 @@
<script lang="ts">
import { createEventDispatcher, onDestroy, onMount } from 'svelte';
import { Alert } from '$lib/components';
import { Alert, ModalWrapper } from '$lib/components';
import { trackEvent } from '$lib/actions/analytics';
import { Form } from '$lib/elements/forms';
import { disableCommands } from '$lib/commandCenter';
@@ -18,61 +17,8 @@
export let title = '';
export let description = '';
let dialog: HTMLDialogElement;
let alert: HTMLElement;
const dispatch = createEventDispatcher();
onMount(() => {
if (show) openModal();
});
onDestroy(() => {
if (show) closeModal();
});
function handleBLur(event: MouseEvent) {
if (event.target === dialog) {
trackEvent('click_close_modal', {
from: 'backdrop'
});
closeModal();
}
}
function openModal() {
if (dialog && !dialog.open) {
dialog.showModal();
document.documentElement.classList.add('u-overflow-hidden');
}
}
function closeModal() {
if (closable) {
if (dialog && dialog.open) {
dispatch('close');
dialog.close();
show = false;
document.documentElement.classList.remove('u-overflow-hidden');
}
}
}
function handleKeydown(event: KeyboardEvent) {
if (event.key === 'Escape') {
event.preventDefault();
trackEvent('click_close_modal', {
from: 'escape'
});
closeModal();
}
}
$: if (show) {
openModal();
} else {
closeModal();
}
$: $disableCommands(show);
$: if (error) {
@@ -80,82 +26,72 @@
}
</script>
<svelte:window on:mousedown={handleBLur} on:keydown={handleKeydown} />
<dialog
class="modal"
class:is-small={size === 'small'}
class:is-big={size === 'big'}
class:is-separate-header={headerDivider}
bind:this={dialog}
on:cancel|preventDefault>
{#if show}
<Form isModal {onSubmit}>
<header class="modal-header">
<div class="u-flex u-main-space-between u-cross-center u-gap-16">
<div class="u-flex u-cross-center u-gap-16">
{#if icon}
<div
class="avatar is-medium"
class:is-success={state === 'success'}
class:is-warning={state === 'warning'}
class:is-danger={state === 'error'}
class:is-info={state === 'info'}>
<span class={`icon-${icon}`} aria-hidden="true" />
</div>
{/if}
<h4 class="modal-title heading-level-5">
<slot name="title">
{title}
</slot>
</h4>
</div>
{#if closable}
<button
type="button"
class="button is-text is-only-icon"
style="--button-size:1.5rem;"
aria-label="Close Modal"
title="Close Modal"
on:click={() =>
trackEvent('click_close_modal', {
from: 'button'
})}
on:click={closeModal}>
<span class="icon-x" aria-hidden="true" />
</button>
<ModalWrapper bind:show {size} {headerDivider} let:close>
<Form isModal {onSubmit}>
<header class="modal-header">
<div class="u-flex u-main-space-between u-cross-center u-gap-16">
<div class="u-flex u-cross-center u-gap-16">
{#if icon}
<div
class="avatar is-medium"
class:is-success={state === 'success'}
class:is-warning={state === 'warning'}
class:is-danger={state === 'error'}
class:is-info={state === 'info'}>
<span class={`icon-${icon}`} aria-hidden="true" />
</div>
{/if}
</div>
<p>
<slot name="description">
{description}
</slot>
</p>
</header>
<div class="modal-content">
{#if error}
<div bind:this={alert}>
<Alert
dismissible
type="warning"
on:dismiss={() => {
error = null;
}}>
{error}
</Alert>
</div>
{/if}
<slot />
</div>
{#if $$slots.footer}
<div class="modal-footer">
<div class="u-flex u-main-end u-gap-16">
<slot name="footer" />
</div>
<h4 class="modal-title heading-level-5">
<slot name="title">
{title}
</slot>
</h4>
</div>
{#if closable}
<button
type="button"
class="button is-text is-only-icon"
style="--button-size:1.5rem;"
aria-label="Close Modal"
title="Close Modal"
on:click={() =>
trackEvent('click_close_modal', {
from: 'button'
})}
on:click={close}>
<span class="icon-x" aria-hidden="true" />
</button>
{/if}
</div>
<p class="u-margin-block-start-4">
<slot name="description">
{description}
</slot>
</p>
</header>
<div class="modal-content">
{#if error}
<div bind:this={alert}>
<Alert
dismissible
type="warning"
on:dismiss={() => {
error = null;
}}>
{error}
</Alert>
</div>
{/if}
</Form>
{/if}
</dialog>
<slot />
</div>
{#if $$slots.footer}
<div class="modal-footer">
<div class="u-flex u-main-end u-gap-16">
<slot name="footer" />
</div>
</div>
{/if}
</Form>
</ModalWrapper>
+77
View File
@@ -0,0 +1,77 @@
<script lang="ts">
import { ModalWrapper } from '$lib/components';
import { trackEvent } from '$lib/actions/analytics';
export let show = false;
export let title = '';
export let description = '';
export let style = '';
</script>
<ModalWrapper bind:show let:close {style}>
<div class="grid-1-1 u-width-full-line mk-grid">
<div class="mk-grid-item-1 u-width-full-line is-not-mobile">
<slot name="side" />
</div>
<div class="u-padding-32 mk-grid-item-2">
<header class="modal-header">
<div class="u-flex u-main-space-between u-gap-16">
<h4 class="modal-title heading-level-5">
<slot name="title">
{title}
</slot>
</h4>
<button
type="button"
class="button is-text is-only-icon"
style="--button-size:1.5rem;"
aria-label="Close Modal"
title="Close Modal"
on:click={() =>
trackEvent('click_close_modal', {
from: 'button'
})}
on:click={close}>
<span class="icon-x" aria-hidden="true" />
</button>
</div>
<p class="u-margin-block-start-4">
<slot name="description">
{description}
</slot>
</p>
</header>
<div class="modal-content mk-content">
<slot />
</div>
</div>
</div>
</ModalWrapper>
<style lang="scss">
@import '@appwrite.io/pink/src/abstract/mixins/_scroll.scss';
.mk-grid {
overflow: hidden;
&-item-1 {
overflow: hidden;
}
&-item-2 {
overflow: auto;
@include scroll;
}
}
@media screen and (max-width: 768px) {
.mk-grid {
&-item-1 {
order: 2;
max-height: 16rem;
}
&-item-2 {
order: 1;
}
}
}
</style>
+82
View File
@@ -0,0 +1,82 @@
<script lang="ts">
import { createEventDispatcher, onDestroy, onMount } from 'svelte';
import { trackEvent } from '$lib/actions/analytics';
import { disableCommands } from '$lib/commandCenter';
export let show = false;
export let size: 'small' | 'big' = null;
export let closable = true;
export let headerDivider = true;
export let style = '';
let dialog: HTMLDialogElement;
const dispatch = createEventDispatcher();
onMount(() => {
if (show) openModal();
});
onDestroy(() => {
if (show) closeModal();
});
function handleBLur(event: MouseEvent) {
if (event.target === dialog) {
trackEvent('click_close_modal', {
from: 'backdrop'
});
closeModal();
}
}
function openModal() {
if (dialog && !dialog.open) {
dialog.showModal();
document.documentElement.classList.add('u-overflow-hidden');
}
}
function closeModal() {
if (closable) {
if (dialog && dialog.open) {
dispatch('close');
dialog.close();
show = false;
document.documentElement.classList.remove('u-overflow-hidden');
}
}
}
function handleKeydown(event: KeyboardEvent) {
if (event.key === 'Escape') {
event.preventDefault();
trackEvent('click_close_modal', {
from: 'escape'
});
closeModal();
}
}
$: if (show) {
openModal();
} else {
closeModal();
}
$: $disableCommands(show);
</script>
<svelte:window on:mousedown={handleBLur} on:keydown={handleKeydown} />
<dialog
class="modal"
class:is-small={size === 'small'}
class:is-big={size === 'big'}
class:is-separate-header={headerDivider}
{style}
bind:this={dialog}
on:cancel|preventDefault>
{#if show}
<slot close={closeModal} />
{/if}
</dialog>
+18 -15
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import { Button, FormList, Helper, InputText } from '$lib/elements/forms';
import { Button, FormItem, FormList, Helper, InputText } from '$lib/elements/forms';
import { createEventDispatcher } from 'svelte';
import { Modal } from '..';
import type { Writable } from 'svelte/store';
@@ -25,21 +25,24 @@
$: disabled = !value || $groups.has(value);
</script>
<Modal title="Custom permission" bind:show on:close={reset} onSubmit={create}>
<p class="text">
Custom permissions allow you to grant access to specific users or teams using their ID and
role.
</p>
<Modal
title="Custom permission"
description="Custom permissions allow you to grant access to specific users or teams using their ID and
role."
bind:show
on:close={reset}
onSubmit={create}>
<FormList>
<InputText
showLabel={false}
id="custom-permission"
label="Custom permission"
placeholder="user:[USER_ID] or team:[TEAM_ID]/[ROLE]"
bind:value />
<Helper type="neutral">
A permission should be formatted as: user:[USER_ID] or team:[TEAM_ID]/[ROLE]¸
</Helper>
<FormItem>
<InputText
id="custom-permission"
label="Role"
placeholder="user:[USER_ID] or team:[TEAM_ID]/[ROLE]"
bind:value />
<Helper type="neutral">
A permission should be formatted as: user:[USER_ID] or team:[TEAM_ID]/[ROLE]¸
</Helper>
</FormItem>
</FormList>
<svelte:fragment slot="footer">
+12 -6
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import { Button, FormList, Helper, InputText } from '$lib/elements/forms';
import { Button, FormItem, FormList, Helper, InputText } from '$lib/elements/forms';
import { createEventDispatcher } from 'svelte';
import { Modal } from '..';
import type { Writable } from 'svelte/store';
@@ -32,12 +32,18 @@
$: disabled = !value || $groups.has(value);
</script>
<Modal title="Label" bind:show on:close={reset} onSubmit={create}>
<p class="text">Labels allow you to grant access to users with the specfied label.</p>
<Modal
title="Label"
description="Labels allow you to grant access to users with the specfied label."
bind:show
on:close={reset}
onSubmit={create}>
<FormList>
<InputText showLabel={false} id="label" label="Label" placeholder="admin" bind:value />
<Helper type={isError ? 'warning' : 'neutral'}
>Only alphanumeric characters are allowed.</Helper>
<FormItem>
<InputText id="label" label="Label" placeholder="Enter label" bind:value />
<Helper type={isError ? 'warning' : 'neutral'}
>Only alphanumeric characters are allowed.</Helper>
</FormItem>
</FormList>
<svelte:fragment slot="footer">
+38
View File
@@ -0,0 +1,38 @@
<script lang="ts">
export let name: string;
export let icon: string = null;
export let unit: string;
export let used: number;
export let max: number;
export let status: null | 'warning' | 'error' = null;
$: progress = Math.round((used / max) * 100);
//TODO: depending on the data received, we could set the status
</script>
<section class="progress-bar">
<div class="progress-bar-top-line u-flex u-gap-8 u-main-space-between">
<h3 class="body-text-1 u-flex u-cross-baseline u-gap-4">
{#if icon}
<span class={`icon-${icon}`} aria-hidden="true" />
{/if}
<span class="text">{name}</span>
</h3>
<p
class="u-medium u-bold"
class:u-color-text-warning={status === 'warning'}
class:u-color-text-danger={status === 'error'}>
{progress}%
</p>
</div>
<div
class="progress-bar-container"
class:is-warning={status === 'warning'}
class:is-danger={status === 'error'}
style:--graph-size={progress + '%'} />
<div class="u-flex u-gap-8 u-main-space-between u-cross-baseline">
<span class="progress-bar-text-used">{used}{unit}</span>
<span class="progress-bar-text-max">{max}{unit}</span>
</div>
</section>
+36
View File
@@ -0,0 +1,36 @@
<script lang="ts">
export let currentValue: string;
export let currentUnit: string;
export let maxValue: string;
export let maxUnit: string;
export let progressValue: number;
export let progressMax: number;
export let showBar = true;
export let minimum = 0;
export let maximum = 100;
$: progress = Math.round((progressValue / progressMax) * 100);
</script>
<section class="progress-bar">
<div class="u-flex u-flex-vertical">
<div class="u-flex u-main-space-between">
<p>
<span class="heading-level-4">{currentValue}</span>
<span class="body-text-1 u-bold">{currentUnit}</span>
</p>
<p class="heading-level-4">{progress}%</p>
</div>
<p class="body-text-2">
{maxValue}
{maxUnit}
</p>
</div>
{#if showBar}
<div
class="progress-bar-container u-margin-block-start-16"
class:is-warning={progress >= 75 && progress < 100}
class:is-danger={progress >= 100}
style:--graph-size={Math.max(Math.min(progress, maximum), minimum) + '%'} />
{/if}
</section>
+41
View File
@@ -0,0 +1,41 @@
<script lang="ts">
import { InputRadio } from '$lib/elements/forms';
export let total: number;
export let variableName: string = '$id';
export let elements: Record<string, unknown>[];
export let group: string;
export let name: string;
export let disabledCondition: string = null;
</script>
<div class:boxes-wrapper={total} data-private>
{#if total}
{#each elements as element}
{@const value = element[variableName]?.toString()}
<div class="box">
<InputRadio
id={`${name}-${value}`}
{value}
{name}
bind:group
disabled={disabledCondition ? value === disabledCondition : false}>
<slot name="element" {element} />
</InputRadio>
</div>
{/each}
{/if}
<div class="box">
{#if total}
<InputRadio id="payment-method" value={null} {name} bind:group>
<slot name="new">
<span style="padding-inline:0.25rem">Add new {name}</span>
</slot>
</InputRadio>
{/if}
{#if group === null}
<slot />
{/if}
</div>
</div>
+34
View File
@@ -0,0 +1,34 @@
<script lang="ts">
export let name: string;
export let group: string;
export let value: string | number | boolean;
export let disabled = false;
export let padding = 1;
export let fullHeight = true;
export let borderRadius: 'xsmall' | 'small' | 'medium' | 'large' = 'small';
enum Radius {
xsmall = '--border-radius-xsmall',
small = '--border-radius-small',
medium = '--border-radius-medium',
large = '--border-radius-large'
}
</script>
<label
class="box u-cursor-pointer u-flex u-flex-vertical u-gap-16"
class:is-allow-focus={!disabled}
class:is-disabled={disabled}
class:u-height-100-percent={fullHeight}
style:--box-padding={`${padding}rem`}
style:--box-border-radius={`var(${Radius[borderRadius]})`}>
<input
class="is-small u-margin-block-start-2"
type="radio"
{name}
{disabled}
{value}
bind:group
on:click />
<slot />
</label>
+3 -1
View File
@@ -3,12 +3,14 @@
export let current = false;
export let currentSub = 0;
export let isSub = false;
export let step: { text: string; substeps?: { text: string }[] };
export let step: { text: string; disabled?: boolean; substeps?: { text: string }[] };
</script>
<li
class:steps-item={!isSub}
class:steps-sub-item={isSub}
class:u-opacity-50={step.disabled}
style:cursor={completed ? 'pointer' : 'default'}
aria-label={`${completed ? 'done' : current ? 'current' : ''} step`}>
<button
type="button"
+1
View File
@@ -5,6 +5,7 @@
export let steps: {
text: string;
optional: boolean;
disabled?: boolean;
substeps?: {
text: string;
}[];
+125
View File
@@ -0,0 +1,125 @@
<script lang="ts">
import { Button } from '$lib/elements/forms';
import SupportOfflineLight from '$lib/images/support/support-offline-light.png';
import SupportOfflineDark from '$lib/images/support/support-offline-dark.png';
import SupportOnlineLight from '$lib/images/support/support-online-light.png';
import SupportOnlineDark from '$lib/images/support/support-online-dark.png';
import { app } from '$lib/stores/app';
import { wizard } from '$lib/stores/wizard';
import SupportWizard from '../../routes/console/supportWizard.svelte';
import { isSupportOnline, showSupportModal } from '../../routes/console/wizard/support/store';
import { isCloud } from '$lib/system';
import { organization } from '$lib/stores/organization';
import { BillingPlan } from '$lib/constants';
export let show = false;
$: isPaid =
$organization?.billingPlan === BillingPlan.PRO ||
$organization?.billingPlan === BillingPlan.SCALE;
</script>
{#if isCloud}
<section class="drop-section u-grid u-gap-24 u-padding-24">
<div class="u-flex u-main-center">
{#if isSupportOnline()}
{#if $app.themeInUse === 'light'}
<img src={SupportOnlineLight} alt="Support" height="87" width="230" />
{:else}
<img src={SupportOnlineDark} alt="Support" height="87" width="230" />
{/if}
{:else if $app.themeInUse === 'light'}
<img src={SupportOfflineLight} alt="Support" height="87" width="230" />
{:else}
<img src={SupportOfflineDark} alt="Support" height="87" width="230" />
{/if}
</div>
<div>
<h4 class="eyebrow-heading-3">Premium support</h4>
{#if isPaid}
<p class="u-line-height-1-5 u-margin-block-start-8">
Get personalized support from the Appwrite team from <b>09:00 - 17:00 UTC</b>
</p>
{/if}
</div>
{#if $organization?.billingPlan === BillingPlan.STARTER}
<Button fullWidth href="https://appwrite.io/pricing" external>
<span class="text">Get Premium support</span>
</Button>
{:else}
<Button
secondary
fullWidth
on:click={() => {
show = false;
$showSupportModal = false;
wizard.start(SupportWizard);
}}>
<span class="text">Contact our Support Team</span>
</Button>
{/if}
</section>
{/if}
<section class="drop-section u-grid u-gap-24 u-padding-24">
<div>
<h4 class="eyebrow-heading-3">Troubleshooting</h4>
<div class="u-margin-block-start-8 u-width-full-line">
{#key $app.themeInUse}
<iframe
title="Appwrite Status"
src={`https://status.appwrite.online/badge?theme=${
$app.themeInUse === 'dark' ? 'dark' : 'light'
}`}
width="250"
height="30"
frameborder="0"
scrolling="no">
</iframe>
{/key}
</div>
</div>
<div class="u-flex u-gap-16">
<a
href="https://appwrite.io/docs"
target="_blank"
rel="noopener noreferrer"
class="button is-secondary u-padding-inline-12 u-stretch u-main-center u-gap-4 u-flex-basis-auto">
<span class="icon-book-open" aria-hidden="true" />
<span class="text">Docs</span>
</a>
<a
href="https://github.com/appwrite/appwrite/issues"
target="_blank"
rel="noopener noreferrer"
class="button is-secondary u-padding-inline-12 u-stretch u-main-center u-gap-4 u-flex-basis-auto">
<span class="icon-github" aria-hidden="true" />
<span class="text">Open issue</span>
</a>
</div>
</section>
<section class="drop-section u-grid u-gap-8 u-padding-24">
<div>
<h4 class="eyebrow-heading-3">Community support</h4>
<p class="text u-margin-block-start-8">Get help from our community</p>
</div>
<ul class="u-flex u-gap-8">
<li>
<Button href="https://github.com/appwrite" text noMargin external ariaLabel="Github">
<span class="icon-github" aria-hidden="true" />
</Button>
</li>
<li>
<Button
href="https://appwrite.io/discord"
round
text
noMargin
external
ariaLabel="Discord">
<span class="icon-discord" aria-hidden="true" />
</Button>
</li>
</ul>
</section>
+350
View File
@@ -3,7 +3,12 @@ export const CARD_LIMIT = 6; // default card limit
export const INTERVAL = 5 * 60000; // default interval to check for feedback
export enum Dependencies {
CREDIT = 'dependency:credit',
INVOICES = 'dependency:invoices',
ADDRESS = 'dependency:address',
PAYMENT_METHODS = 'dependency:paymentMethods',
ORGANIZATION = 'dependency:organization',
MEMBERS = 'dependency:members',
PROJECT = 'dependency:project',
PROJECT_VARIABLES = 'dependency:project_variables',
PROJECT_INSTALLATIONS = 'dependency:project_installations',
@@ -273,3 +278,348 @@ export const eventServices: Array<EventService> = [
]
}
];
export const usageRates = {
'tier-0': [
{
id: 'members',
resource: 'Organization members',
amount: 1,
unit: '',
rate: '$20/member'
},
{ id: 'bandwith', resource: 'Bandwidth', amount: 10, unit: 'GB', rate: '$0.04/GB' },
{ id: 'storage', resource: 'Storage', amount: 2, unit: 'GB', rate: '$0.025/GB' },
{
id: 'executions',
resource: 'Function executions',
amount: 750000,
unit: 'executions',
rate: '$2/1M executions'
},
{
id: 'users',
resource: 'Active users',
amount: 200000,
unit: 'AU',
rate: '$0.0012/user'
},
{
id: 'connections',
resource: 'Concurrent connections',
amount: 750,
unit: 'connections',
rate: '$5/1K connections'
}
],
'tier-1': [
{
id: 'members',
resource: 'Organization members',
amount: 'Unlimited',
unit: '',
rate: '$20/member'
},
{ id: 'bandwith', resource: 'Bandwidth', amount: 1, unit: 'TB', rate: '$0.04/GB' },
{ id: 'storage', resource: 'Storage', amount: 150, unit: 'GB', rate: '$0.025/GB' },
{
id: 'executions',
resource: 'Function executions',
amount: 3500000,
unit: 'executions',
rate: '$2/1M executions'
},
{
id: 'users',
resource: 'Active users',
amount: 200000,
unit: 'AU',
rate: '$0.0012/user'
},
{
id: 'connections',
resource: 'Concurrent connections',
amount: 750,
unit: 'connections',
rate: '$5/1K connections'
}
],
'tier-2': [
{
id: 'members',
resource: 'Organization members',
amount: 'Unlimited',
unit: '',
rate: '$20/member'
},
{ id: 'bandwith', resource: 'Bandwidth', amount: 1, unit: 'TB', rate: '$0.04/GB' },
{ id: 'storage', resource: 'Storage', amount: 150, unit: 'GB', rate: '$0.025/GB' },
{
id: 'executions',
resource: 'Function executions',
amount: 3500000,
unit: 'executions',
rate: '$2/1M executions'
},
{
id: 'users',
resource: 'Active users',
amount: 200000,
unit: 'AU',
rate: '$0.0012/user'
},
{
id: 'connections',
resource: 'Concurrent connections',
amount: 750,
unit: 'connections',
rate: '$5/1K connections'
}
]
};
//resources: bandwidth, buckets, file size limit, functions, executions, users,teams,logs, members, platforms, webhooks, databases, connections, messages
export const limitRates = {
'tier-0': [
{
id: 'bandwith',
amount: 10,
unit: 'GB'
},
{
id: 'buckets',
amount: 3,
unit: ''
},
{
id: 'file-size-limit',
amount: 1,
unit: 'MB'
},
{
id: 'storage',
amount: 5,
unit: 'GB'
},
{
id: 'functions',
amount: 1,
unit: ''
},
{
id: 'executions',
amount: 750000,
unit: 'executions'
},
{
id: 'users',
amount: 200000,
unit: 'AU'
},
{
id: 'teams',
amount: 1,
unit: ''
},
{
id: 'logs',
amount: 1,
unit: ''
},
{
id: 'members',
amount: 1,
unit: ''
},
{
id: 'platforms',
amount: 1,
unit: ''
},
{
id: 'webhooks',
amount: 1,
unit: ''
},
{
id: 'databases',
amount: 1,
unit: ''
},
{
id: 'connections',
amount: 1,
unit: ''
},
{
id: 'messages',
amount: 1,
unit: ''
}
],
'tier-1': [
{
id: 'bandwith',
amount: 10,
unit: 'GB'
},
{
id: 'buckets',
amount: 1,
unit: ''
},
{
id: 'file-size-limit',
amount: 5,
unit: 'MB'
},
{
id: 'storage',
amount: 5,
unit: 'GB'
},
{
id: 'functions',
amount: 1,
unit: ''
},
{
id: 'executions',
amount: 750000,
unit: 'executions'
},
{
id: 'users',
amount: 200000,
unit: 'AU'
},
{
id: 'teams',
amount: 1,
unit: ''
},
{
id: 'logs',
amount: 1,
unit: ''
},
{
id: 'members',
amount: 1,
unit: ''
},
{
id: 'platforms',
amount: 1,
unit: ''
},
{
id: 'webhooks',
amount: 1,
unit: ''
},
{
id: 'databases',
amount: 1,
unit: ''
},
{
id: 'connections',
amount: 1,
unit: ''
},
{
id: 'messages',
amount: 1,
unit: ''
}
],
'tier-2': [
{
id: 'bandwith',
amount: 10,
unit: 'GB'
},
{
id: 'buckets',
amount: 1,
unit: ''
},
{
id: 'file-size-limit',
amount: 5,
unit: 'MB'
},
{
id: 'storage',
amount: 5,
unit: 'GB'
},
{
id: 'functions',
amount: 1,
unit: ''
},
{
id: 'executions',
amount: 750000,
unit: 'executions'
},
{
id: 'users',
amount: 200000,
unit: 'AU'
},
{
id: 'teams',
amount: 1,
unit: ''
},
{
id: 'logs',
amount: 1,
unit: ''
},
{
id: 'members',
amount: 1,
unit: ''
},
{
id: 'platforms',
amount: 1,
unit: ''
},
{
id: 'webhooks',
amount: 1,
unit: ''
},
{
id: 'databases',
amount: 1,
unit: ''
},
{
id: 'connections',
amount: 1,
unit: ''
},
{
id: 'messages',
amount: 1,
unit: ''
}
]
};
export enum BillingPlan {
STARTER = 'tier-0',
PRO = 'tier-1',
SCALE = 'tier-2'
}
+28
View File
@@ -0,0 +1,28 @@
<script lang="ts">
import { sdk } from '$lib/stores/sdk';
export let flag: string;
export let name: string = flag;
export let width = 40;
export let height = 30;
export let quality = 100;
let classes: string = undefined;
export { classes as class };
export function getFlag(country: string, width: number, height: number, quality: number) {
let flag = sdk.forProject.avatars
.getFlag(country, width * 2, height * 2, quality)
?.toString();
flag?.includes('&project=')
? (flag = flag.replace('&project=', '&mode=admin'))
: flag + '&mode=admin';
return flag;
}
</script>
<img
style="border-radius: 2.5px"
src={getFlag(flag, width, height, quality)}
alt={name}
class={classes}
{width}
{height} />
+13 -3
View File
@@ -10,17 +10,20 @@
export let github = false;
export let text = false;
export let danger = false;
export let disabled = false;
export let round = false;
export let link = false;
export let disabled = false;
export let external = false;
export let href: string = null;
export let fullWidth = false;
export let fullWidthMobile = false;
export let ariaLabel: string = null;
export let noMargin = false;
export let event: string = null;
let classes: string = undefined;
export { classes as class };
export let actions: MultiActionArray = [];
export let submissionLoader = false;
const isSubmitting = hasContext('form')
? getContext<FormContext>('form').isSubmitting
@@ -39,7 +42,7 @@
}
$: resolvedClasses = [
'button',
link ? 'link' : 'button',
disabled && 'is-disabled',
round && 'is-only-icon',
secondary && 'is-secondary',
@@ -47,6 +50,7 @@
text && 'is-text',
danger && 'is-danger',
fullWidth && 'is-full-width',
fullWidthMobile && 'is-full-width-mobile',
noMargin && 'u-padding-inline-0',
classes
]
@@ -74,6 +78,12 @@
aria-label={ariaLabel}
type={submit ? 'submit' : 'button'}
use:multiAction={actions}>
<slot />
{#if $isSubmitting && submissionLoader}
<span
class="loader is-small"
style:--p-loader-base-full-color="transparent"
aria-hidden="true" />
{/if}
<slot isSubmitting={$isSubmitting} />
</button>
{/if}
+3
View File
@@ -14,6 +14,8 @@
export let noStyle = false;
export let isModal = false;
export let onSubmit: (e: SubmitEvent) => Promise<void> | void;
let classes = '';
export { classes as class };
const { isSubmitting } = setContext<FormContext>('form', {
isSubmitting: writable(false)
@@ -27,6 +29,7 @@
</script>
<form
class={classes}
class:form={!noStyle}
class:common-section={!noMargin}
class:modal-form={isModal}
+5 -1
View File
@@ -6,11 +6,15 @@
export let tag: FormItemTag = 'li';
export let fullWidth = false;
export let isMultiple = false;
export let style: string = '';
let classes: string = '';
export { classes as class };
</script>
<svelte:element
this={tag}
class="form-item"
class="form-item {classes}"
{style}
class:is-multiple={isMultiple}
class:u-width-full-line={fullWidth}>
<slot />
+2
View File
@@ -27,3 +27,5 @@ export { default as InputSecret } from './inputSecret.svelte';
export { default as Helper } from './helper.svelte';
export { default as Label } from './label.svelte';
export { default as InputProjectId } from './inputProjectId.svelte';
export { default as InputDate } from './inputDate.svelte';
export { default as InputDateRange } from './inputDateRange.svelte';
+4 -1
View File
@@ -1,5 +1,6 @@
<script lang="ts">
import { FormItem, Helper, Label } from '.';
import type { FormItemTag } from './formItem.svelte';
interface $$Props extends Partial<HTMLLabelElement> {
id: string;
@@ -12,6 +13,7 @@
disabled?: boolean;
element?: HTMLInputElement | undefined;
indeterminate?: boolean;
wrapperTag?: FormItemTag;
}
export let id: string;
@@ -23,6 +25,7 @@
export let required = false;
export let disabled = false;
export let element: HTMLInputElement | undefined = undefined;
export let wrapperTag: FormItemTag = 'li';
let error: string;
@@ -40,7 +43,7 @@
}
</script>
<FormItem>
<FormItem tag={wrapperTag}>
{#if label}
<Label {required} {tooltip} {optionalText} hide={!showLabel} for={id}>
{label}
+3 -1
View File
@@ -9,6 +9,7 @@
export let required = false;
export let disabled = false;
export let tooltip: string = null;
export let fullWidth = false;
let element: HTMLInputElement;
let error: string;
@@ -35,11 +36,12 @@
{required}
type="checkbox"
class:switch={type === 'switchbox'}
aria-checked={value}
bind:this={element}
bind:checked={value}
on:invalid={handleInvalid} />
<div class="choice-item-content">
<div class="choice-item-content" class:u-width-full-line={fullWidth}>
{#if (label && showLabel) || tooltip}
<div class="u-flex u-gap-4">
{#if label}
+88
View File
@@ -0,0 +1,88 @@
<script lang="ts">
import { onMount } from 'svelte';
import { FormItem, Helper, Label } from '.';
import NullCheckbox from './nullCheckbox.svelte';
export let label: string;
export let showLabel = true;
export let optionalText: string | undefined = undefined;
export let id: string;
export let value = '';
export let required = false;
export let nullable = false;
export let disabled = false;
export let readonly = false;
export let autofocus = false;
export let autocomplete = false;
let element: HTMLInputElement;
let error: string;
onMount(() => {
if (element && autofocus) {
element.focus();
}
});
function handleInvalid(event: Event) {
event.preventDefault();
if (element.validity.valueMissing) {
error = 'This field is required';
return;
}
error = element.validationMessage;
}
let prevValue = '';
function handleNullChange(e: CustomEvent<boolean>) {
const isNull = e.detail;
if (isNull) {
prevValue = value;
value = null;
} else {
value = prevValue;
}
}
$: if (value) {
error = null;
}
$: isNullable = nullable && !required;
</script>
<FormItem>
<Label {required} {optionalText} hide={!showLabel} for={id}>
{label}
</Label>
<div class="input-text-wrapper">
<input
{id}
{disabled}
{readonly}
{required}
step=".001"
autocomplete={autocomplete ? 'on' : 'off'}
type="date"
class="input-text"
bind:value
bind:this={element}
on:invalid={handleInvalid}
style:--amount-of-buttons={isNullable ? 2.75 : 1}
style:--button-size={isNullable ? '2rem' : '1rem'} />
{#if isNullable}
<ul
class="buttons-list u-cross-center u-gap-8 u-position-absolute u-inset-block-start-8 u-inset-block-end-8 u-inset-inline-end-12">
<li class="buttons-list-item">
<NullCheckbox checked={value === null} on:change={handleNullChange} />
</li>
</ul>
{/if}
</div>
{#if error}
<Helper type="warning">{error}</Helper>
{/if}
</FormItem>
@@ -0,0 +1,175 @@
<script lang="ts">
import { createDateRangePicker, melt, type DateRange } from '@melt-ui/svelte';
import { Label } from '.';
const {
elements: {
calendar,
cell,
content,
grid,
heading,
nextButton,
prevButton,
startSegment,
endSegment,
trigger
},
states: { months, headingValue, daysOfWeek, segmentContents, value },
helpers: { isDateDisabled, isDateUnavailable }
} = createDateRangePicker();
export let data: DateRange;
export let showLabel = true;
export let label: string;
export let id: string;
export let required = false;
export let hideRequired = false;
export let tooltip: string = '';
export let optionalText: string = '';
export let showDates = true;
$: if ($value) {
data = $value;
}
</script>
{#if label}
<Label {required} {hideRequired} {tooltip} {optionalText} hide={!showLabel} for={id}>
{label}
</Label>
{/if}
<div class="u-flex u-gap-4">
{#if showDates}
{#each $segmentContents.start as seg}
<div use:melt={$startSegment(seg.part)}>
{seg.value}
</div>
{/each}
<div aria-hidden="true">-</div>
{#each $segmentContents.end as seg}
<div use:melt={$endSegment(seg.part)}>
{seg.value}
</div>
{/each}
{/if}
<div>
<button use:melt={$trigger} class="button is-secondary" type="button">
<slot>Open calendar</slot>
</button>
</div>
</div>
<div use:melt={$content} class="card dt-container">
<div use:melt={$calendar} class="u-flex u-flex-vertical">
<header class="u-flex u-main-space-between u-cross-center">
<button type="button" class="button is-only-icon is-text" use:melt={$prevButton}>
<span class="icon-cheveron-left" aria-label="previous month"></span>
</button>
<div {...$heading} use:melt={$heading}>
{$headingValue}
</div>
<button type="button" class="button is-only-icon is-text" use:melt={$nextButton}>
<span class="icon-cheveron-right" aria-label="next month"></span>
</button>
</header>
{#each $months as month}
<table use:melt={$grid}>
<thead aria-hidden="true">
<tr>
{#each $daysOfWeek as day}
<th class="dt-cell">
<span class="u-flex u-main-center u-cross-center">
{day}
</span>
</th>
{/each}
</tr>
</thead>
<tbody>
{#each month.weeks as days}
<tr>
{#each days as date}
<td
class="dt-cell"
role="gridcell"
aria-disabled={$isDateDisabled(date) ||
$isDateUnavailable(date)}>
<div
use:melt={$cell(date, month.value)}
class="u-flex u-main-center u-cross-center dt-button">
{date.day}
</div>
</td>
{/each}
</tr>
{/each}
</tbody>
</table>
{/each}
</div>
</div>
<style lang="scss">
:global(.theme-dark) .dt-button {
--dt-text-hover: var(--color-neutral-20);
--dt-button-hover: var(--color-neutral-200);
--dt-border-hover: var(--color-neutral-70);
--dt-text-active: var(--color-neutral-60);
--dt-button-active: var(--color-neutral-200);
--dt-border-active: var(--color-neutral-60);
}
:global(.theme-light) .dt-button {
--dt-text-hover: var(--color-neutral-100);
--dt-button-hover: var(--color-neutral-30);
--dt-border-hover: var(--color-neutral-15);
--dt-text-active: var(--color-neutral-100);
--dt-button-active: var(--color-neutral-30);
--dt-border-active: var(--color-neutral-15);
}
.dt-container {
min-width: 20rem;
}
.dt-cell {
height: 1.5rem;
width: 1.5rem;
padding: 0.1rem;
border-radius: 100%;
user-select: none;
// [aria-disabled='true'] {
// pointer-events: none;
// opacity: 0.4;
// }
}
.dt-button {
aspect-ratio: 1/1;
border-radius: 100%;
cursor: pointer;
&:hover,
&[data-highlighted],
&[data-range-highlighted] {
background-color: hsl(var(--dt-button-hover));
color: hsl(var(--dt-text-hover));
}
&[data-selected] {
background-color: hsl(var(--dt-button-active));
color: hsl(var(--dt-text-active));
}
}
// [data-melt-calendar-cell][data-unavailable] {
// @apply pointer-events-none text-red-400 line-through;
// }
// [data-melt-calendar-cell][data-selected] {
// @apply bg-magnum-400 text-neutral-950;
// }
// [data-melt-calendar-cell][data-outside-visible-months] {
// @apply pointer-events-none cursor-default opacity-40 hover:bg-transparent;
// }
// [data-melt-calendar-cell][data-outside-month] {
// @apply pointer-events-none cursor-default opacity-0 hover:bg-transparent;
// }
</style>
+6 -1
View File
@@ -60,7 +60,11 @@
{label}
</Label>
<div class="input-text-wrapper">
<div
class:input-text-wrapper={!$$slots.default}
class:u-flex={$$slots.default}
class:u-gap-16={$$slots.default}
class:u-cross-center={$$slots.default}>
<input
{id}
{placeholder}
@@ -81,6 +85,7 @@
</li>
</ul>
{/if}
<slot />
</div>
{#if error}
<Helper type="warning">{error}</Helper>
+7 -3
View File
@@ -1,6 +1,6 @@
<script lang="ts">
import { onMount } from 'svelte';
import { FormItem, Helper, Label } from '.';
import { FormItem, FormItemPart, Helper, Label } from '.';
import NullCheckbox from './nullCheckbox.svelte';
export let label: string | undefined = undefined;
@@ -17,6 +17,8 @@
export let min: number = null;
export let max: number = null;
export let step: number | 'any' = 1;
export let isMultiple = false;
export let fullWidth = false;
let element: HTMLInputElement;
let error: string;
@@ -62,9 +64,11 @@
value = prevValue;
}
}
$: wrapper = isMultiple ? FormItemPart : FormItem;
</script>
<FormItem>
<svelte:component this={wrapper} {fullWidth}>
{#if label}
<Label {required} {optionalText} hide={!showLabel} for={id}>
{label}
@@ -99,4 +103,4 @@
{#if error}
<Helper type="warning">{error}</Helper>
{/if}
</FormItem>
</svelte:component>
+6 -2
View File
@@ -1,7 +1,7 @@
<script lang="ts">
import { FormItem, Helper, Label } from '.';
export let label: string;
export let label: string = null;
export let showLabel = true;
export let id: string;
export let group: string;
@@ -41,7 +41,11 @@
bind:this={element}
on:invalid={handleInvalid} />
<Label {required} hide={!showLabel} for={id}>
{label}
{#if label}
{label}
{:else}
<slot />
{/if}
</Label>
</div>
{#if error}
+6 -3
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import { FormItem, Helper, Label } from '.';
import { FormItem, FormItemPart, Helper, Label } from '.';
import type { FormItemTag } from './formItem.svelte';
export let id: string;
@@ -16,6 +16,8 @@
value: string | boolean | number;
label: string;
}[];
export let isMultiple = false;
export let fullWidth = false;
let element: HTMLSelectElement;
let error: string;
@@ -45,9 +47,10 @@
}
$: hasNullOption = options.some((option) => option.value === null);
$: wrapper = isMultiple ? FormItemPart : FormItem;
</script>
<FormItem tag={wrapperTag}>
<svelte:component this={wrapper} {fullWidth} tag={wrapperTag}>
{#if label}
<Label {required} {hideRequired} {optionalText} hide={!showLabel} for={id}>
{label}
@@ -79,4 +82,4 @@
{:else}
<slot name="helper" />
{/if}
</FormItem>
</svelte:component>
@@ -24,6 +24,7 @@
export let hideRequired = false;
export let disabled = false;
export let fullWidth = false;
export let fullWidthDrop = true;
export let autofocus = false;
export let interactiveOutput = false;
// stretch is used inside of a flex container to give the element flex:1
@@ -119,7 +120,7 @@
scrollable
placement="bottom-end"
position="static"
fullWidth={true}
fullWidth={fullWidthDrop}
fixed>
<Label {required} {hideRequired} {optionalText} hide={!showLabel} for={id} {tooltip}>
{label}
+6 -1
View File
@@ -78,7 +78,11 @@
</Label>
{/if}
<div class="input-text-wrapper">
<div
class:input-text-wrapper={!$$slots.default}
class:u-flex={$$slots.default}
class:u-gap-16={$$slots.default}
class:u-cross-center={$$slots.default}>
<input
{id}
{name}
@@ -110,6 +114,7 @@
{/if}
</ul>
{/if}
<slot />
</div>
{#if error}
<Helper type="warning">{error}</Helper>
+1
View File
@@ -1,2 +1,3 @@
export { default as Pill } from './pill.svelte';
export { default as SelectSearchItem } from './selectSearchItem.svelte';
export { default as Flag } from './flag.svelte';
+14
View File
@@ -1,4 +1,6 @@
<script lang="ts">
import { trackEvent } from '$lib/actions/analytics';
export let disabled = false;
export let selected = false;
export let success = false;
@@ -9,6 +11,17 @@
export let submit = false;
export let external = false;
export let href: string = null;
export let event: string = null;
function track() {
if (!event) {
return;
}
trackEvent(`click_${event}`, {
from: 'tag'
});
}
</script>
{#if href}
@@ -28,6 +41,7 @@
{:else if button}
<button
on:click
on:click={track}
{disabled}
type={submit ? 'submit' : 'button'}
class="tag"
+40 -1
View File
@@ -1,3 +1,42 @@
<div class="table-tbody" role="rowgroup">
<script lang="ts">
import { getServiceLimit, type PlanServices } from '$lib/stores/billing';
import { wizard } from '$lib/stores/wizard';
import { isCloud } from '$lib/system';
import ChangeOrganizationTierCloud from '$routes/console/changeOrganizationTierCloud.svelte';
import { Button } from '../forms';
let tableBody: HTMLDivElement;
export let service: PlanServices = null;
export let name = service;
export let total: number = null;
let columns = 0;
const limit = getServiceLimit(service) || Infinity;
const upgradeMethod = () => {
wizard.start(ChangeOrganizationTierCloud);
};
$: limitReached = limit !== 0 && limit < Infinity && total >= limit;
$: if (tableBody) {
columns = tableBody?.parentNode?.querySelectorAll('.table-thead-col')?.length;
}
</script>
<div class="table-tbody" role="rowgroup" bind:this={tableBody}>
<slot />
</div>
{#if isCloud && limitReached && service}
<tr class="table-row">
<td class="table-col" width="100%" colspan={columns}>
<span class="u-flex u-gap-24 u-main-center u-cross-center">
<slot name="limit" {upgradeMethod} {limit}>
<span class="text">Upgrade your plan to add {name} to your organization</span>
<Button secondary on:click={upgradeMethod}>Upgrade plan</Button>
</slot>
</span>
</td>
</tr>
{/if}
+6 -1
View File
@@ -5,13 +5,18 @@
export let showOverflow = false;
let className = '';
export { className as class };
export let style = '';
export let right = false;
</script>
<div
style={width ? `--p-col-width:${width?.toString()}` : ''}
{style}
style:--p-col-width={width?.toString() ?? ''}
class="table-col {className}"
class:u-overflow-visible={showOverflow}
class:is-only-desktop={onlyDesktop}
class:u-flex={right}
class:u-main-end={right}
data-title={title}
role="cell">
<slot />
+1
View File
@@ -33,6 +33,7 @@
<InputCheckbox
bind:element={el}
id="select-{id}"
wrapperTag="div"
checked={selectedIds.includes(id)}
on:click={handleClick} />
</TableCell>
+2
View File
@@ -2,11 +2,13 @@
export let width: number = null;
export let onlyDesktop = false;
export let eyebrow = true;
export let style = '';
</script>
<div
style:--p-col-width={width?.toString()}
class:is-only-desktop={onlyDesktop}
{style}
class="table-thead-col"
role="columnheader">
<span class={eyebrow ? 'eyebrow-heading-3' : 'body-text-2 u-bold'}>
+1 -1
View File
@@ -1,6 +1,5 @@
<script lang="ts">
import { isHTMLInputElement } from '$lib/helpers/types';
import { TableCellHead } from '.';
import { InputCheckbox } from '../forms';
@@ -27,6 +26,7 @@
<TableCellHead width={10}>
<InputCheckbox
id="select-all"
wrapperTag="div"
indeterminate={someSelected && !allSelected}
checked={allSelected}
on:click={handleClick} />
+2
View File
@@ -2,6 +2,7 @@
export let noMargin = false;
export let noStyles = false;
export let style = '';
export let transparent = false;
</script>
<div
@@ -9,6 +10,7 @@
class:u-margin-block-start-32={!noMargin}
class:is-remove-outer-styles={noStyles}
{style}
style:--p-table-bg-color={transparent ? 'var(--transparent)' : ''}
role="table"
data-private>
<slot />
+7 -1
View File
@@ -3,6 +3,8 @@
export let isSticky = false;
export let noMargin = false;
export let style = '';
export let transparent = false;
let isOverflowing = false;
const hasOverflow: Action<HTMLDivElement, unknown> = (node) => {
@@ -40,7 +42,11 @@
<div class="table-with-scroll" class:u-margin-block-start-32={!noMargin} data-private>
<div class="table-wrapper" use:hasOverflow={(v) => (isOverflowing = v)}>
<table class="table" class:is-sticky-scroll={isSticky && isOverflowing}>
<table
class="table"
class:is-sticky-scroll={isSticky && isOverflowing}
{style}
style:--p-table-bg-color={transparent ? 'var(--transparent)' : ''}>
<slot />
</table>
</div>
+16
View File
@@ -61,3 +61,19 @@ export const diffDays = (date1: Date, date2: Date) => {
export function timeFromNow(datetime: string): string {
return dayjs().to(dayjs(datetime));
}
export function hoursToDays(hours: number) {
if (hours > 24) {
return `${Math.floor(hours / 24)} days`;
} else {
return `${hours} hour`;
}
}
export function getTomorrow(date: Date) {
const tomorrow = new Date(date);
tomorrow.setDate(tomorrow.getDate() + 2);
tomorrow.setHours(0, 0, 0, 0);
return tomorrow;
}
+22
View File
@@ -0,0 +1,22 @@
export function abbreviateNumber(num: number, decimals: number = 1): string {
if (isNaN(num)) return String(num);
if (num >= 1000000) {
const result = num / 1000000;
return result.toFixed(result % 1 !== 0 ? decimals : 0) + 'M';
} else if (num >= 1000) {
const result = num / 1000;
return result.toFixed(result % 1 !== 0 ? decimals : 0) + 'K';
} else {
return num.toString();
}
}
export function toDecimals(num: number, decimals: number = 1): number {
return parseFloat(num.toFixed(decimals));
}
export function formatNumberWithCommas(number: number): string {
if (isNaN(number)) return String(number);
const formatter = new Intl.NumberFormat('en');
return formatter.format(number);
}
+1
View File
@@ -1,5 +1,6 @@
export function getProjectId() {
const pathname = window.location.pathname + '/';
const projectMatch = pathname.match(/\/project-(.*?)\//);
return projectMatch?.[1] || null;
}
+4 -3
View File
@@ -14,9 +14,9 @@ export function calculateSize(bytes: number, decimals = 1) {
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
}
export function sizeToBytes(value: number, unit: Size) {
export function sizeToBytes(value: number, unit: Size, base = 1024) {
const index = sizes.indexOf(unit);
return value * Math.pow(1024, index);
return value * Math.pow(base, index);
}
export function bytesToSize(value: number, unit: Size) {
@@ -28,12 +28,13 @@ export function humanFileSize(bytes: number): {
value: string;
unit: Size;
} {
if (typeof bytes !== 'number') return { value: '0', unit: 'Bytes' };
const value = prettyBytes(bytes, {
locale: 'en'
}).split(' ');
return {
value: value[0],
unit: value[1] as Size
unit: value[1].toUpperCase() as Size
};
}
+10 -3
View File
@@ -44,11 +44,18 @@ export function createValueUnitPair<T = string>(initialValue = 0, units: Unit<T>
unit.subscribe((newValue, prev) => {
value.update((v) => {
const unitInBase = v * units.find((u) => u.name === prev).value;
return unitInBase / units.find((u) => u.name === newValue).value;
const prevUnit = units.find((u) => u.name === prev);
const newUnit = units.find((u) => u.name === newValue);
// If either unit is not found, just return the current value
if (!prevUnit || !newUnit) {
return v;
}
const unitInBase = v * prevUnit.value;
return unitInBase / newUnit.value;
});
});
return {
value,
unit,
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 4.8 MiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 4.8 MiB

+26 -4
View File
@@ -15,17 +15,28 @@
TableCellText,
TableScroll
} from '$lib/elements/table';
import { Container } from '$lib/layout';
import { toLocaleDateTime } from '$lib/helpers/date';
import { Container, ContainerHeader } from '$lib/layout';
import { hoursToDays, toLocaleDateTime } from '$lib/helpers/date';
import type { Models } from '@appwrite.io/console';
import { tierToPlan, type PlanServices } from '$lib/stores/billing';
import { isCloud } from '$lib/system';
import { organization } from '$lib/stores/organization';
import { Button } from '$lib/elements/forms';
import { BillingPlan } from '$lib/constants';
export let logs: Models.LogList;
export let offset = 0;
export let limit = 0;
export let service: PlanServices = null;
</script>
<Container>
<Heading tag="h2" size="5">Activity</Heading>
{#if service}
<ContainerHeader title="Activity" />
{:else}
<Heading tag="h2" size="5">Activity</Heading>
{/if}
{#if logs.total}
<TableScroll>
<TableHeader>
@@ -35,7 +46,18 @@
<TableCellHead width={90}>IP</TableCellHead>
<TableCellHead width={140}>Date</TableCellHead>
</TableHeader>
<TableBody>
<TableBody service="logs" total={isCloud ? Infinity : 0}>
<svelte:fragment slot="limit" let:limit let:upgradeMethod>
<p class="text">
Logs are retained in rolling {hoursToDays(limit)} intervals with the
{tierToPlan($organization.billingPlan).name}
plan.
{#if $organization?.billingPlan === BillingPlan.STARTER}
<Button link on:click={upgradeMethod}>Upgrade</Button> to increase your log
retention for a longer period.
{/if}
</p>
</svelte:fragment>
{#each logs.logs as log}
<TableRow>
<TableCell title="Client">
+40
View File
@@ -0,0 +1,40 @@
<script lang="ts">
import { tooltip } from '$lib/actions/tooltip';
import { BillingPlan } from '$lib/constants';
import { Button } from '$lib/elements/forms';
import { tierToPlan } from '$lib/stores/billing';
import { organization } from '$lib/stores/organization';
export let title: string;
export let tooltipContent =
$organization.billingPlan === BillingPlan.STARTER
? `Upgrade to add more ${title.toLocaleLowerCase()}`
: `You've reached the ${title.toLocaleLowerCase()} limit for the ${
tierToPlan($organization.billingPlan).name
} plan`;
export let disabled: boolean;
export let buttonText: string;
export let buttonMethod: () => void | Promise<void>;
export let buttonEvent: string = buttonText?.toLocaleLowerCase();
export let icon = 'plus';
export let showIcon = true;
export let buttonType: 'primary' | 'secondary' | 'text' = 'primary';
</script>
<div
use:tooltip={{
content: tooltipContent,
disabled: !disabled
}}>
<Button
text={buttonType === 'text'}
secondary={buttonType === 'secondary'}
on:click={buttonMethod}
event={buttonEvent}
{disabled}>
{#if showIcon}
<span class={`icon-${icon}`} aria-hidden="true" />
{/if}
<span class="text">{buttonText}</span>
</Button>
</div>
+165
View File
@@ -0,0 +1,165 @@
<script lang="ts">
import {
tierToPlan,
getServiceLimit,
type PlanServices,
showUsageRatesModal,
checkForUsageFees,
readOnly,
checkForProjectLimitation
} from '$lib/stores/billing';
import { Alert, DropList, Heading } from '$lib/components';
import { Pill } from '$lib/elements';
import { organization } from '$lib/stores/organization';
import { GRACE_PERIOD_OVERRIDE, isCloud } from '$lib/system';
import { createEventDispatcher, onMount } from 'svelte';
import { wizard } from '$lib/stores/wizard';
import ChangeOrganizationTierCloud from '$routes/console/changeOrganizationTierCloud.svelte';
import { ContainerButton } from '.';
import { Button } from '$lib/elements/forms';
import { BillingPlan } from '$lib/constants';
export let isFlex = true;
export let title: string;
export let titleTag: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' = 'h2';
export let titleSize: '1' | '2' | '3' | '4' | '5' | '6' | '7' = '5';
export let serviceId = title.toLocaleLowerCase() as PlanServices;
export let total: number = null;
export let alertType: 'info' | 'success' | 'warning' | 'error' | 'default' = 'warning';
export let showAlert = true;
export let buttonText: string = null;
export let buttonMethod: () => void = null;
export let buttonEvent: string = buttonText?.toLocaleLowerCase();
export let buttonDisabled = false;
let showDropdown = false;
const { bandwidth, documents, storage, users, executions } = $organization?.billingLimits ?? {};
const limitedServices = [
{ name: 'bandwidth', value: bandwidth },
{ name: 'documents', value: documents },
{ name: 'storage', value: storage },
{ name: 'users', value: users },
{ name: 'executions', value: executions }
];
const limit = getServiceLimit(serviceId) || Infinity;
const upgradeMethod = () => {
showDropdown = false;
wizard.start(ChangeOrganizationTierCloud);
};
const dispatch = createEventDispatcher();
$: tier = tierToPlan($organization?.billingPlan)?.name;
$: hasProjectLimitation =
checkForProjectLimitation(serviceId) && $organization?.billingPlan === BillingPlan.STARTER;
$: hasUsageFees = hasProjectLimitation
? checkForUsageFees($organization?.billingPlan, serviceId)
: false;
$: isLimited = limit !== 0 && limit < Infinity;
$: overflowingServices = limitedServices.filter((service) => service.value >= 0);
$: isButtonDisabled =
buttonDisabled ||
($readOnly && !GRACE_PERIOD_OVERRIDE) ||
(isLimited && total >= limit && !hasUsageFees);
onMount(() => {
dispatch('data', { isButtonDisabled, limit, tier });
});
</script>
<!-- Show only if on Cloud, alerts are enabled, and it isn't a project limited service -->
{#if isCloud && showAlert}
{#if $readOnly}
{@const services = overflowingServices
.map((s) => {
return s.name.toLocaleLowerCase();
})
.join(', ')}
<slot name="alert" {limit} {tier} {title} {upgradeMethod} {hasUsageFees} {services}>
{#if $organization?.billingPlan !== BillingPlan.STARTER && hasUsageFees}
<Alert type="info" isStandalone>
<span class="text">
You've reached the {services} limit for the {tier} plan.
<Button link on:click={() => ($showUsageRatesModal = true)}
>Excess usage fees will apply</Button
>.
</span>
</Alert>
{:else}
<Alert type={alertType} isStandalone>
<span class="text">
You've reached the {services} limit for the {tier} plan. <Button
link
on:click={upgradeMethod}>Upgrade</Button> your organization for additional
resources.
</span>
</Alert>
{/if}
</slot>
{/if}
{/if}
<header class:u-flex={isFlex} class="u-gap-12 common-section u-main-space-between u-flex-wrap">
<div class="u-flex u-cross-child-center u-cross-center u-gap-16 u-flex-wrap">
<Heading tag={titleTag} size={titleSize}>{title}</Heading>
{#if isCloud && isLimited}
<DropList bind:show={showDropdown} width="16">
{#if hasProjectLimitation}
<Pill button on:click={() => (showDropdown = !showDropdown)}>
<span class="icon-info" />{total}/{limit}
{title} used
</Pill>
{:else}
<Pill button on:click={() => (showDropdown = !showDropdown)}>
<span class="icon-info" />{title} limited
</Pill>
{/if}
<svelte:fragment slot="list">
<slot name="tooltip" {limit} {tier} {title} {upgradeMethod} {hasUsageFees}>
{#if hasProjectLimitation}
<p class="text">
Your are limited to {limit}
{title.toLocaleLowerCase()} per project on the {tier} plan.
{#if $organization?.billingPlan === BillingPlan.STARTER}<Button
link
on:click={upgradeMethod}>Upgrade</Button>
for addtional {title.toLocaleLowerCase()}.
{/if}
</p>
{:else if hasUsageFees}
<p class="text">
You are limited to {limit}
{title.toLocaleLowerCase()} per organization on the {tier} plan.
<Button link on:click={() => ($showUsageRatesModal = true)}
>Excess usage fees will apply</Button
>.
</p>
{:else}
<p class="text">
You are limited to {limit}
{title.toLocaleLowerCase()} per organization on the {tier} plan.
{#if $organization?.billingPlan === BillingPlan.STARTER}
<Button link on:click={upgradeMethod}>Upgrade</Button>
for additional {title.toLocaleLowerCase()}.
{/if}
</p>
{/if}
</slot>
</svelte:fragment>
</DropList>
{/if}
</div>
<slot {isButtonDisabled}>
{#if buttonText}
<ContainerButton
{title}
disabled={isButtonDisabled}
{buttonText}
{buttonEvent}
{buttonMethod} />
{/if}
</slot>
</header>
+1 -1
View File
@@ -8,7 +8,7 @@
<footer class="main-footer u-cross-center">
<div class="main-footer-start">
<ul class="inline-links is-no-padding-first-and-last u-x-small">
<li class="inline-links-item">
<li class="inline-links-item" style="line-height: 1.02;">
<div class="u-flex u-cross-center u-gap-8">
{#if isCloud}
<span class="icon-cloud" />
+54 -21
View File
@@ -1,12 +1,14 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { base } from '$app/paths';
import { AvatarInitials, DropList, DropListItem, DropListLink, Support } from '$lib/components';
import { app } from '$lib/stores/app';
import { user } from '$lib/stores/user';
import { organizationList, organization, newOrgModal } from '$lib/stores/organization';
import { page } from '$app/stores';
import { Submit, trackEvent } from '$lib/actions/analytics';
import { tooltip } from '$lib/actions/tooltip';
import { toggleCommandCenter } from '$lib/commandCenter/commandCenter.svelte';
import { AvatarInitials, DropList, DropListItem, DropListLink } from '$lib/components';
import { Feedback } from '$lib/components/feedback';
import Button from '$lib/elements/forms/button.svelte';
import { isMac } from '$lib/helpers/platform';
import AppwriteLogoDark from '$lib/images/appwrite-logo-dark.svg';
@@ -14,15 +16,21 @@
import DarkMode from '$lib/images/mode/dark-mode.svg';
import LightMode from '$lib/images/mode/light-mode.svg';
import SystemMode from '$lib/images/mode/system-mode.svg';
import { app } from '$lib/stores/app';
import { feedback } from '$lib/stores/feedback';
import { newOrgModal, organization, organizationList } from '$lib/stores/organization';
import { sdk } from '$lib/stores/sdk';
import { user } from '$lib/stores/user';
import { isCloud } from '$lib/system';
import { slide } from 'svelte/transition';
import { sdk } from '$lib/stores/sdk';
import { isCloud } from '$lib/system';
import { wizard } from '$lib/stores/wizard';
import CreateOrganizationCloud from '$routes/console/createOrganizationCloud.svelte';
import { Feedback } from '$lib/components/feedback';
import ChangeOrganizationTierCloud from '$routes/console/changeOrganizationTierCloud.svelte';
import { Pill } from '$lib/elements';
import { showExcess } from '$routes/console/organization-[organization]/store';
import { readOnly } from '$lib/stores/billing';
import { BillingPlan } from '$lib/constants';
let showDropdown = false;
let showSupport = false;
let droplistElement: HTMLDivElement;
function toggleFeedback() {
@@ -48,6 +56,13 @@
}
}
function createOrg() {
showDropdown = false;
if (isCloud) {
wizard.start(CreateOrganizationCloud);
} else newOrgModal.set(true);
}
$: if (showDropdown) {
trackEvent('click_menu_dropdown');
}
@@ -90,8 +105,27 @@
<svelte:component this={$page.data.breadcrumbs} />
{/if}
{#if !$page.url.pathname.includes('/console/account') && $readOnly}
<div style="min-inline-size: fit-content">
<Pill danger button on:click={() => ($showExcess = true)}>
<div>
<span class="icon-exclamation-circle" aria-hidden="true" />
<span>limit reached</span>
</div>
</Pill>
</div>
{/if}
<div class="main-header-end">
<nav class="u-flex is-only-desktop">
<nav class="u-flex is-only-desktop u-cross-center">
{#if isCloud && $organization?.billingPlan === BillingPlan.STARTER && !$page.url.pathname.startsWith('/console/account')}
<Button
disabled={$organization?.markedForDeletion}
on:click={() => wizard.start(ChangeOrganizationTierCloud)}>
Upgrade
</Button>
{/if}
{#if $feedback.notification}
<div class="u-flex u-cross-center">
<div class="pulse-notification" />
@@ -105,13 +139,17 @@
<Feedback />
</svelte:fragment>
</DropList>
<a
href="https://appwrite.io/discord"
target="_blank"
rel="noopener noreferrer"
class="button is-small is-text">
<span class="text">Support</span>
</a>
{#if isCloud}
<DropList width="18.5" bind:show={showSupport} scrollable={true}>
<Button text on:click={() => (showSupport = !showSupport)}>
<span class="text">Support</span>
</Button>
<svelte:fragment slot="other">
<Support bind:show={showSupport} />
</svelte:fragment>
</DropList>
{/if}
<Button
actions={[
(node) => {
@@ -163,12 +201,7 @@
{/if}
<section class="drop-section">
<ul class="drop-list">
<DropListItem
icon="plus"
on:click={() => {
showDropdown = false;
newOrgModal.set(true);
}}>
<DropListItem icon="plus" on:click={createOrg}>
New organization
</DropListItem>
<DropListLink
+32
View File
@@ -0,0 +1,32 @@
<script lang="ts">
export let title: string;
export let type: 'info' | 'success' | 'warning' | 'error' | 'default' = 'info';
</script>
<section
class="alert is-action is-action-and-top-sticky u-sep-block-end"
class:is-success={type === 'success'}
class:is-warning={type === 'warning'}
class:is-danger={type === 'error'}
class:is-info={type === 'info'}>
<div class="alert-grid">
<span class="icon-info" aria-hidden="true" />
<div class="alert-content">
{#if title || $$slots.title}
<h6 class="alert-title">
<slot name="title">
{title}
</slot>
</h6>
{/if}
<p class="alert-message">
<slot />
</p>
</div>
{#if $$slots.buttons}
<div class="alert-buttons u-flex u-gap-16 u-cross-child-center">
<slot name="buttons" />
</div>
{/if}
</div>
</section>
+3
View File
@@ -16,3 +16,6 @@ export { default as Usage, type UsagePeriods } from './usage.svelte';
export { default as Activity } from './activity.svelte';
export { default as Progress } from './progress.svelte';
export { default as GridHeader } from './gridHeader.svelte';
export { default as ContainerHeader } from './containerHeader.svelte';
export { default as HeaderAlert } from './headerAlert.svelte';
export { default as ContainerButton } from './containerButton.svelte';
+33 -4
View File
@@ -1,7 +1,7 @@
<script lang="ts">
import { toLocaleDateTime } from '$lib/helpers/date';
import { hoursToDays, toLocaleDateTime } from '$lib/helpers/date';
import { log } from '$lib/stores/logs';
import { Card, Code, Heading, Id, SvgIcon, Tab, Tabs } from '../components';
import { Alert, Card, Code, Heading, Id, SvgIcon, Tab, Tabs } from '../components';
import { calculateTime } from '$lib/helpers/timeConversion';
import {
TableBody,
@@ -13,10 +13,21 @@
} from '$lib/elements/table';
import { beforeNavigate } from '$app/navigation';
import { Pill } from '$lib/elements';
import { isCloud } from '$lib/system';
import ChangeOrganizationTierCloud from '$routes/console/changeOrganizationTierCloud.svelte';
import { wizard } from '$lib/stores/wizard';
import { getServiceLimit, tierToPlan } from '$lib/stores/billing';
import { organization } from '$lib/stores/organization';
import { app } from '$lib/stores/app';
import { Button } from '$lib/elements/forms';
import { BillingPlan } from '$lib/constants';
let selectedRequest = 'parameters';
let selectedResponse = 'logs';
const limit = getServiceLimit('logs');
const tier = tierToPlan($organization?.billingPlan)?.name;
function handleKeydown(event: KeyboardEvent) {
if (event.key === 'Escape') {
event.preventDefault();
@@ -154,7 +165,9 @@
</div>
</header>
<div class="code-panel-content grid-1-2" style="u-grid">
<div class="grid-1-2-col-1 u-flex u-flex-vertical u-gap-16">
<div
class="grid-1-2-col-1 u-flex u-flex-vertical u-gap-16"
class:theme-dark={$app.themeInUse === 'light'}>
<Heading tag="h3" size="6">Request</Heading>
<div class="u-sep-block-end">
<Tabs>
@@ -265,7 +278,9 @@
</p>
{/if}
</div>
<div class="grid-1-2-col-2 u-flex u-flex-vertical u-gap-16 u-min-width-0">
<div
class="grid-1-2-col-2 u-flex u-flex-vertical u-gap-16 u-min-width-0"
class:theme-dark={$app.themeInUse === 'light'}>
<Heading tag="h3" size="6">Response</Heading>
<div class="u-sep-block-end">
<Tabs>
@@ -293,6 +308,20 @@
</div>
{#if selectedResponse === 'logs'}
{#if execution?.logs}
{#if isCloud && limit !== 0 && limit < Infinity}
<Alert>
Logs are retained in rolling {hoursToDays(limit)} intervals
with the {tier} plan.
{#if $organization.billingPlan === BillingPlan.STARTER}
<Button
link
on:click={() =>
wizard.start(ChangeOrganizationTierCloud)}
>Upgrade</Button> to increase your log retention
for a longer period.
{/if}
</Alert>
{/if}
<Code withCopy noMargin code={execution.logs} language="sh" />
{:else}
<Card isDashed isTile>
+27 -2
View File
@@ -3,10 +3,15 @@
import { page } from '$app/stores';
import { trackEvent } from '$lib/actions/analytics';
import { tooltip } from '$lib/actions/tooltip';
import { BillingPlan } from '$lib/constants';
import { isMac } from '$lib/helpers/platform';
import { slide } from '$lib/helpers/transition';
import { organization } from '$lib/stores/organization';
import { wizard } from '$lib/stores/wizard';
import { isCloud } from '$lib/system';
import ChangeOrganizationTierCloud from '$routes/console/changeOrganizationTierCloud.svelte';
import Create from '$routes/console/feedbackWizard.svelte';
import { showSupportModal } from '$routes/console/wizard/support/store';
export let isOpen = false;
@@ -32,10 +37,15 @@
}
}
function openWizard() {
function openFeedback() {
isOpen = false;
wizard.start(Create);
}
function openSupport() {
isOpen = false;
$showSupportModal = true;
}
</script>
<svelte:window on:keydown={handleKeyDown} />
@@ -169,9 +179,24 @@
<span class="icon-cog" aria-hidden="true" />
<span class="text">Settings</span>
</a>
<ul class="drop-list is-only-mobile">
{#if isCloud && $organization?.billingPlan !== BillingPlan.SCALE}
<li class="drop-list-item">
<button
class="drop-button"
on:click={() => wizard.start(ChangeOrganizationTierCloud)}>
<span class="text">Upgrade</span>
</button>
</li>
{/if}
<li class="drop-list-item">
<button class="drop-button" on:click={openWizard}>
<button class="drop-button" on:click={openSupport}>
<span class="text">Support</span>
</button>
</li>
<li class="drop-list-item">
<button class="drop-button" on:click={openFeedback}>
<span class="text">Feedback</span>
</button>
</li>
+7 -1
View File
@@ -7,6 +7,8 @@
export let icon: Notification['icon'] = null;
export let title: Notification['title'];
export let buttons: Notification['buttons'];
export let message: Notification['message'];
export let html: Notification['isHtml'] = false;
const dispatch = createEventDispatcher();
</script>
@@ -38,7 +40,11 @@
{#if title}
<h4 class="alert-sticky-title">{title}</h4>
{/if}
<p><slot /></p>
{#if html}
{@html message}
{:else}
<p>{message}</p>
{/if}
</div>
{#if buttons}
<div class="alert-sticky-buttons u-flex">
+3 -3
View File
@@ -14,9 +14,9 @@
title={notification.title}
icon={notification.icon}
on:dismiss={() => dismissNotification(notification.id)}
buttons={notification?.buttons}>
{notification.message}
</Notification>
buttons={notification?.buttons}
message={notification.message}
html={notification?.isHtml} />
</li>
{/each}
</ul>
+5 -1
View File
@@ -46,7 +46,11 @@
<main
class:grid-with-side={showSideNavigation}
class:is-open={isOpen}
class:u-hide={$wizard.show || $log.show || $wizard.cover}>
class:u-hide={$wizard.show || $log.show || $wizard.cover}
class:is-fixed-layout={$$slots.alert}>
{#if $$slots.alert}
<slot name="alert" />
{/if}
<header class="main-header u-padding-inline-end-0">
<button
class:u-hide={!showSideNavigation}
+336 -90
View File
@@ -5,32 +5,99 @@
import LoginLight from '$lib/images/login/login-light-mode.png';
import { app } from '$lib/stores/app';
import { user } from '$lib/stores/user';
import { isCloud } from '$lib/system';
export let imgLight = LoginLight;
export let imgDark = LoginDark;
export const imgLight = LoginLight;
export const imgDark = LoginDark;
</script>
<main class="grid-1-1 is-full-page" id="main">
<section
class="u-flex u-flex-vertical"
style:--url={`url(${$app.themeInUse === 'dark' ? imgDark : imgLight})`}>
<div class="logo u-flex u-gap-16">
<a href={user ? '/console' : '/'}>
<section class="u-flex u-flex-vertical side-bg">
<div class="logo u-flex u-gap-16 u-margin-inline-auto is-not-mobile">
<a href={$user ? '/console' : '/'}>
{#if $app.themeInUse === 'dark'}
<img
src={AppwriteLogoDark}
width="160"
width="123"
class="u-block u-only-dark"
alt="Appwrite Logo" />
{:else}
<img
src={AppwriteLogoLight}
width="160"
width="123"
class="u-block u-only-light"
alt="Appwrite Logo" />
{/if}
</a>
</div>
<div class="appwrite-pro">
<span class="text">APPWRITE</span>
<span class="appwrite-pro-text">
<span class="appwrite-pro-text-letter">P</span><span
class="appwrite-pro-text-letter">R</span
><span class="appwrite-pro-text-letter">O</span></span>
</div>
<div class="now-available">Now available</div>
</section>
<section class="grid-1-1-col-2 u-flex u-main-center u-cross-center _u-padding-16-mobile">
<div class="container u-flex u-flex-vertical u-cross-center u-main-center">
<div class="u-max-width-500 u-width-full-line">
<h1 class="heading-level-2 u-margin-block-start-auto">
<slot name="title" />
</h1>
<div class="u-margin-block-start-24">
<slot />
</div>
<ul class="inline-links is-center is-with-sep u-margin-block-start-32">
<slot name="links" />
</ul>
</div>
<div
class="logo u-flex u-gap-16 u-margin-inline-auto is-only-mobile u-margin-block-start-32">
<a href={user ? '/console' : '/'}>
{#if $app.themeInUse === 'dark'}
<img
src={AppwriteLogoDark}
width="93"
class="u-block u-only-dark"
alt="Appwrite Logo" />
{:else}
<img
src={AppwriteLogoLight}
width="93"
class="u-block u-only-light"
alt="Appwrite Logo" />
{/if}
</a>
</div>
</div>
</section>
</main>
<!-- OLD one -->
<!--
<main class="grid-1-1 is-full-page" id="main">
<section
class="u-flex u-flex-vertical"
style:--url={`url(${$app.themeInUse === 'dark' ? imgDark : imgLight})`}>
<div class="logo u-flex u-gap-16">
<a href={user ? '/console' : '/'}>
{#if $app.themeInUse === 'dark'}
<img
src={AppwriteLogoDark}
width="160"
class="u-block u-only-dark"
alt="Appwrite Logo" />
{:else}
<img
src={AppwriteLogoLight}
width="160"
class="u-block u-only-light"
alt="Appwrite Logo" />
{/if}
</a>
{#if isCloud}
<span class="aw-badges aw-eyebrow">Cloud Beta</span>
{/if}
@@ -59,10 +126,254 @@
</div>
</section>
</main>
<style lang="scss">
@import '@appwrite.io/pink/src/abstract/variables/_devices.scss';
/* Default (including mobile) */
#main section:first-child {
padding-block-start: 2.25rem;
padding-block-end: 2rem;
div {
padding-inline-start: 1rem;
padding-inline-end: 1rem;
}
.tag-line {
font-family: 'Aeonik Pro';
font-size: 4rem;
font-style: normal;
font-weight: 400;
line-height: 100%; /* 80px */
letter-spacing: -1.6px;
backdrop-filter: blur(0.5 rem);
.underscore {
-webkit-text-fill-color: #f02e65;
}
}
}
/* for smaller screens */
@media #{$break2open} {
#main section:first-child {
background: var(--url);
background-repeat: no-repeat;
background-position: top;
background-size: cover;
padding-block-start: 6.25rem;
padding-block-end: 6.875rem;
div {
padding-inline-start: 2.625rem;
padding-inline-end: 2rem;
}
}
}
/* for larger screens */
@media #{$break3open} {
#main section:first-child {
div {
padding-inline-start: 5.625rem;
padding-inline-end: 5rem;
}
.tag-line {
font-size: 5rem;
}
}
}
:global(.theme-dark) .tag-line {
background: linear-gradient(45deg, white, white 60%, #fd376f);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
:global(.theme-light) .tag-line {
background: linear-gradient(45deg, #19191d, #19191d 60%, #fd376f);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.aw-eyebrow {
font-family: 'Source Code Pro', monospace;
line-height: 0.9rem;
font-size: 0.78rem;
letter-spacing: 0.08em;
font-weight: 400;
text-transform: uppercase;
}
.aw-badges {
--p-badges-shadow-bg-color: #f2c8d6;
--p-badges-shadow-border-color: #f69db7;
--p-badges-shadowopacity: 0.4;
align-self: center;
color: hsl(var(--color-neutral-0));
padding-block: 0.375rem;
padding-inline: 0.75rem;
border-radius: 0.375rem;
background: radial-gradient(
98.13% 199.7% at 98.13% 100%,
#fe6947 0%,
#fd366e 41.6%,
#fd366e 100%
);
-webkit-backdrop-filter: blur(2.5rem);
backdrop-filter: blur(2.5rem);
box-shadow:
0.1875rem 0.1875rem var(--p-badges-shadow-bg-color),
0.25rem 0.1875rem var(--p-badges-shadow-border-color),
0.1875rem 0.25rem var(--p-badges-shadow-border-color),
0.125rem 0.1875rem var(--p-badges-shadow-border-color),
0.1875rem 0.125rem var(--p-badges-shadow-border-color);
:global(.theme-dark) & {
--p-badges-shadow-bg-color: #2c2c2f;
--p-badges-shadow-border-color: #39393c;
--p-badges-shadowopacity: 0.13;
}
:global(.theme-light) & {
--p-badges-shadow-bg-color: #f2c8d6;
--p-badges-shadow-border-color: #f69db7;
--p-badges-shadowopacity: 0.4;
}
}
</style>
-->
<style lang="scss">
@import '@appwrite.io/pink/src/abstract/variables/_common.scss';
@import '@appwrite.io/pink/src/abstract/variables/_devices.scss';
@import '@appwrite.io/pink/src/abstract/functions/_pxToRem.scss';
/* mobile utility class */
@media #{$break1} {
._u-padding-16-mobile {
padding: pxToRem(16);
}
}
.appwrite-pro {
position: relative;
z-index: 1;
display: flex;
justify-content: center;
align-items: baseline;
color: hsl(var(--color-neutral-100));
@media #{$break1} {
gap: pxToRem(10);
font-size: pxToRem(18);
letter-spacing: pxToRem(4);
}
@media #{$break2open} {
gap: pxToRem(24);
font-size: pxToRem(40);
letter-spacing: pxToRem(8);
line-height: 120%;
margin-block-start: pxToRem(160);
}
&-text {
padding: pxToRem(18) pxToRem(28);
border: pxToRem(2) solid hsl(343 98% 60% / 0.2);
border-radius: pxToRem(16);
background: rgba(253, 54, 110, 0.1);
box-shadow: 0 -12.173px 20.289px 0px rgba(253, 54, 110, 0.08) inset;
text-align: center;
display: flex;
justify-content: center;
align-items: center;
&-letter {
width: 2rem;
@media #{$break1} {
width: 1rem;
}
}
@media #{$break1} {
padding: pxToRem(8) pxToRem(12);
border-radius: pxToRem(8);
}
}
}
:global(.theme-dark) .appwrite-pro {
color: hsl(var(--color-neutral-10));
}
.now-available {
position: relative;
z-index: 1;
background: linear-gradient(70deg, #fb5491 -35.72%, #19191d 79.96%);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
margin-inline: auto;
@media #{$break1} {
font-size: pxToRem(18);
margin-block-start: pxToRem(12);
}
@media #{$break2open} {
font-size: pxToRem(30);
margin-block-start: pxToRem(36);
}
}
:global(.theme-dark) .now-available {
background: linear-gradient(89deg, #fb5491 -29.25%, #fff 43.27%);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.side-bg {
position: relative;
background-color: #ededf0;
overflow: hidden;
}
.side-bg::before {
position: absolute;
inset-block-start: -950px;
inset-inline-end: -650px;
content: '';
display: block;
inline-size: 100%;
block-size: 100%;
background: radial-gradient(49.55% 43.54% at 47% 50.69%, #e7f8f7 0%, #85dbd8 100%);
filter: blur(250px);
@media #{$break1} {
inset-block-start: -200px;
inset-inline-end: -400px;
filter: blur(100px);
}
}
.side-bg::after {
position: absolute;
inset-block-end: -850px;
inset-inline-start: -600px;
content: '';
display: block;
inline-size: 100%;
block-size: 100%;
background: radial-gradient(50% 46.73% at 50% 53.27%, #fe9567 28.17%, #fd366e 59.38%);
filter: blur(250px);
@media #{$break1} {
inset-block-end: -200px;
inset-inline-start: -400px;
filter: blur(100px);
}
}
:global(.theme-dark) .side-bg {
background-color: #19191d;
}
/****** OLD ******/
/* Default (including mobile) */
#main section:first-child {
padding-block-start: 2.25rem;
@@ -73,28 +384,23 @@
padding-inline-end: 1rem;
}
.tag-line {
font-family: 'Aeonik Pro';
font-size: 4rem;
font-style: normal;
font-weight: 400;
line-height: 100%; /* 80px */
letter-spacing: -1.6px;
backdrop-filter: blur(0.5 rem);
.underscore {
-webkit-text-fill-color: #f02e65;
}
}
// .tag-line {
// font-family: 'Aeonik Pro';
// font-size: 4rem;
// font-style: normal;
// font-weight: 400;
// line-height: 100%; /* 80px */
// letter-spacing: -1.6px;
// backdrop-filter: blur(0.5 rem);
// .underscore {
// -webkit-text-fill-color: #f02e65;
// }
// }
}
/* for smaller screens */
@media #{$break2open} {
#main section:first-child {
background: var(--url);
background-repeat: no-repeat;
background-position: top;
background-size: cover;
padding-block-start: 6.25rem;
padding-block-end: 6.875rem;
@@ -112,69 +418,9 @@
padding-inline-start: 5.625rem;
padding-inline-end: 5rem;
}
.tag-line {
font-size: 5rem;
}
}
}
:global(.theme-dark) .tag-line {
background: linear-gradient(45deg, white, white 60%, #fd376f);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
:global(.theme-light) .tag-line {
background: linear-gradient(45deg, #19191d, #19191d 60%, #fd376f);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.aw-eyebrow {
font-family: 'Source Code Pro', monospace;
line-height: 0.9rem;
font-size: 0.78rem;
letter-spacing: 0.08em;
font-weight: 400;
text-transform: uppercase;
}
.aw-badges {
--p-badges-shadow-bg-color: #f2c8d6;
--p-badges-shadow-border-color: #f69db7;
--p-badges-shadowopacity: 0.4;
align-self: center;
color: hsl(var(--color-neutral-0));
padding-block: 0.375rem;
padding-inline: 0.75rem;
border-radius: 0.375rem;
background: radial-gradient(
98.13% 199.7% at 98.13% 100%,
#fe6947 0%,
#fd366e 41.6%,
#fd366e 100%
);
-webkit-backdrop-filter: blur(2.5rem);
backdrop-filter: blur(2.5rem);
box-shadow:
0.1875rem 0.1875rem var(--p-badges-shadow-bg-color),
0.25rem 0.1875rem var(--p-badges-shadow-border-color),
0.1875rem 0.25rem var(--p-badges-shadow-border-color),
0.125rem 0.1875rem var(--p-badges-shadow-border-color),
0.1875rem 0.125rem var(--p-badges-shadow-border-color);
:global(.theme-dark) & {
--p-badges-shadow-bg-color: #2c2c2f;
--p-badges-shadow-border-color: #39393c;
--p-badges-shadowopacity: 0.13;
}
:global(.theme-light) & {
--p-badges-shadow-bg-color: #f2c8d6;
--p-badges-shadow-border-color: #f69db7;
--p-badges-shadowopacity: 0.4;
// .tag-line {
// font-size: 5rem;
// }
}
}
</style>
+52 -85
View File
@@ -1,24 +1,69 @@
<script context="module" lang="ts">
export type UsagePeriods = '24h' | '30d' | '90d';
export function periodToDates(period: UsagePeriods): {
start: string;
end: string;
period: '1h' | '1d';
} {
const start = new Date();
switch (period) {
case '24h':
start.setUTCHours(start.getHours() - 24);
break;
case '30d':
start.setUTCHours(0, 0, 0, 0);
start.setUTCDate(start.getDate() - 30);
break;
case '90d':
start.setUTCHours(0, 0, 0, 0);
start.setUTCDate(start.getDate() - 90);
break;
}
const end = new Date();
end.setUTCDate(end.getUTCDate() + 1);
end.setUTCHours(0, 0, 0, 0);
return {
start: start.toISOString(),
end: end.toISOString(),
period: period === '24h' ? '1h' : '1d'
};
}
export function last(set: Models.Metric[]): Models.Metric | null {
if (!set) return null;
return set.slice(-1)[0] ?? null;
}
export function total(set: Models.Metric[]): number {
return set.reduce((prev, curr) => prev + curr.value, 0);
return set?.reduce((prev, curr) => prev + curr.value, 0) ?? 0;
}
export function accumulateFromEndingTotal(
metrics: Models.Metric[],
endingTotal: number
): Array<[string, number]> {
return metrics.reduceRight(
(acc, curr) => {
acc.total -= curr.value;
acc.data.unshift([curr.date, acc.total]);
return acc;
},
{
total: endingTotal,
data: []
}
).data;
}
</script>
<script lang="ts">
import { Container } from '$lib/layout';
import { BarChart, LineChart } from '$lib/charts';
import { BarChart } from '$lib/charts';
import { formatNumberWithCommas } from '$lib/helpers/numbers';
import { Card, SecondaryTabs, SecondaryTabsItem, Heading } from '$lib/components';
import { Colors } from '$lib/charts/config';
import type { Models } from '@appwrite.io/console';
import { page } from '$app/stores';
import { Tiles } from '../components/index.js';
type MetricMetadata = {
title: string;
@@ -26,17 +71,9 @@
};
export let title: string;
export let total: number;
export let count: Models.Metric[];
export let created: Models.Metric[];
export let read: Models.Metric[];
export let updated: Models.Metric[];
export let deleted: Models.Metric[];
export let countMetadata: MetricMetadata;
export let createdMetadata: MetricMetadata;
export let readMetadata: MetricMetadata;
export let updatedMetadata: MetricMetadata;
export let deletedMetadata: MetricMetadata;
export let path: string = null;
</script>
@@ -59,7 +96,7 @@
</div>
<Card>
{#if count}
<Heading tag="h6" size="6">{total(count)}</Heading>
<Heading tag="h6" size="6">{formatNumberWithCommas(total)}</Heading>
<p>{countMetadata.title}</p>
<div class="u-margin-block-start-16" />
<div class="chart-container">
@@ -67,82 +104,12 @@
series={[
{
name: countMetadata.legend,
data: [...count.map((e) => [e.date, e.value])]
data: accumulateFromEndingTotal(count, total)
}
]} />
</div>
{/if}
</Card>
<Tiles>
<Card isTile>
{#if created}
<Heading tag="h6" size="6">{total(created)}</Heading>
<p>{createdMetadata.title}</p>
<div class="u-margin-block-start-16" />
<div class="chart-container">
<LineChart
series={[
{
name: createdMetadata.legend,
data: [...created.map((e) => [e.date, e.value])],
color: Colors.Secondary
}
]} />
</div>
{/if}
</Card>
<Card isTile>
{#if read}
<Heading tag="h6" size="6">{total(read)}</Heading>
<p>{readMetadata.title}</p>
<div class="u-margin-block-start-16" />
<div class="chart-container">
<LineChart
series={[
{
name: readMetadata.legend,
data: [...read.map((e) => [e.date, e.value])],
color: Colors.Tertiary
}
]} />
</div>
{/if}
</Card>
<Card isTile>
{#if updated}
<Heading tag="h6" size="6">{total(updated)}</Heading>
<p>{updatedMetadata.title}</p>
<div class="u-margin-block-start-16" />
<div class="chart-container">
<LineChart
series={[
{
name: updatedMetadata.legend,
data: [...updated.map((e) => [e.date, e.value])],
color: Colors.Quaternary
}
]} />
</div>
{/if}
</Card>
<Card isTile>
{#if deleted}
<Heading tag="h6" size="6">{total(deleted)}</Heading>
<p>{deletedMetadata.title}</p>
<div class="u-margin-block-start-16" />
<div class="chart-container">
<LineChart
series={[
{
name: deletedMetadata.legend,
data: [...deleted.map((e) => [e.date, e.value])],
color: Colors.Quinary
}
]} />
</div>
{/if}
</Card>
</Tiles>
</Container>
<style lang="scss">
+58 -19
View File
@@ -5,6 +5,7 @@
label: string;
component: typeof SvelteComponent<unknown>;
optional?: boolean;
disabled?: boolean;
}
>;
</script>
@@ -61,32 +62,70 @@
async function submit() {
if ($wizard.interceptor) {
$wizard.nextDisabled = true;
try {
await $wizard.interceptor();
} catch (error) {
if (!$wizard.interceptorNotificationEnabled) return;
addNotification({
message: error.message,
type: 'error'
});
return;
} finally {
$wizard.nextDisabled = false;
}
}
wizard.setInterceptor(null);
if (isLastStep) {
$wizard.nextDisabled = true;
trackEvent('wizard_finish');
dispatch('finish');
//Reactivate button in case there are errors
setTimeout(() => {
$wizard.nextDisabled = false;
}, 2000);
if ($wizard.finalAction) {
$wizard.nextDisabled = true;
try {
await $wizard.finalAction();
trackEvent('wizard_finish');
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
} finally {
$wizard.nextDisabled = false;
}
} else {
$wizard.nextDisabled = true;
trackEvent('wizard_finish');
dispatch('finish');
setTimeout(() => {
$wizard.nextDisabled = false;
}, 2000);
}
} else {
if (steps.get($wizard.step + 1)?.disabled) {
$wizard.step++;
while (steps.get($wizard.step)?.disabled) {
$wizard.step++;
}
} else {
$wizard.step++;
}
trackEvent('wizard_next');
$wizard.step++;
}
}
function previousStep() {
if (steps.get($wizard.step - 1)?.disabled) {
$wizard.step--;
while (steps.get($wizard.step)?.disabled) {
$wizard.step--;
}
} else {
$wizard.step--;
}
wizard.setInterceptor(null);
trackEvent('wizard_back');
}
$: sortedSteps = [...steps].sort(([a], [b]) => (a > b ? 1 : -1));
$: isLastStep = $wizard.step === steps.size;
</script>
@@ -112,13 +151,16 @@
</header>
<aside class="wizard-side">
<Steps
on:step={handleStepClick}
steps={sortedSteps.map(([, { label, optional }]) => ({
text: label,
optional
}))}
currentStep={$wizard.step} />
<slot name="aside">
<Steps
on:step={handleStepClick}
steps={sortedSteps.map(([, { label, optional, disabled }]) => ({
text: label,
optional,
disabled
}))}
currentStep={$wizard.step} />
</slot>
</aside>
<div class="wizard-media">
{#if $wizard.media}
@@ -143,10 +185,7 @@
{#if $wizard.step === 1}
<Button secondary on:click={handleExit}>Cancel</Button>
{:else}
<Button
secondary
on:click={() => $wizard.step--}
on:click={() => trackEvent('wizard_back')}>Back</Button>
<Button secondary on:click={previousStep}>Back</Button>
{/if}
<Button submit disabled={$wizard.nextDisabled}>

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