Merge branch 'main' of https://github.com/appwrite/console into feat-cookie-consent
@@ -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=
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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'
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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} />
|
||||
@@ -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>
|
||||
@@ -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));
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
@@ -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'}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
<ul class="sessions">
|
||||
<!-- TODO: remove inline styling -->
|
||||
<ul class="list" style:gap="0.25rem">
|
||||
<slot />
|
||||
</ul>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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,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"
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
export let steps: {
|
||||
text: string;
|
||||
optional: boolean;
|
||||
disabled?: boolean;
|
||||
substeps?: {
|
||||
text: string;
|
||||
}[];
|
||||
|
||||
@@ -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>
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
@@ -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} />
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,2 +1,3 @@
|
||||
export { default as Pill } from './pill.svelte';
|
||||
export { default as SelectSearchItem } from './selectSearchItem.svelte';
|
||||
export { default as Flag } from './flag.svelte';
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
<InputCheckbox
|
||||
bind:element={el}
|
||||
id="select-{id}"
|
||||
wrapperTag="div"
|
||||
checked={selectedIds.includes(id)}
|
||||
on:click={handleClick} />
|
||||
</TableCell>
|
||||
|
||||
@@ -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,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,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 />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,5 +1,6 @@
|
||||
export function getProjectId() {
|
||||
const pathname = window.location.pathname + '/';
|
||||
const projectMatch = pathname.match(/\/project-(.*?)\//);
|
||||
|
||||
return projectMatch?.[1] || null;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 4.8 MiB |
|
After Width: | Height: | Size: 4.8 MiB |
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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" />
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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}>
|
||||
|
||||