Merge remote-tracking branch 'origin/main' into feat-1.5-auth

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

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 4.8 MiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 4.8 MiB

+26 -4
View File
@@ -15,17 +15,28 @@
TableCellText,
TableScroll
} from '$lib/elements/table';
import { Container } from '$lib/layout';
import { toLocaleDateTime } from '$lib/helpers/date';
import { Container, ContainerHeader } from '$lib/layout';
import { hoursToDays, toLocaleDateTime } from '$lib/helpers/date';
import type { Models } from '@appwrite.io/console';
import { tierToPlan, type PlanServices } from '$lib/stores/billing';
import { isCloud } from '$lib/system';
import { organization } from '$lib/stores/organization';
import { Button } from '$lib/elements/forms';
import { BillingPlan } from '$lib/constants';
export let logs: Models.LogList;
export let offset = 0;
export let limit = 0;
export let service: PlanServices = null;
</script>
<Container>
<Heading tag="h2" size="5">Activity</Heading>
{#if service}
<ContainerHeader title="Activity" />
{:else}
<Heading tag="h2" size="5">Activity</Heading>
{/if}
{#if logs.total}
<TableScroll>
<TableHeader>
@@ -35,7 +46,18 @@
<TableCellHead width={90}>IP</TableCellHead>
<TableCellHead width={140}>Date</TableCellHead>
</TableHeader>
<TableBody>
<TableBody service="logs" total={isCloud ? Infinity : 0}>
<svelte:fragment slot="limit" let:limit let:upgradeMethod>
<p class="text">
Logs are retained in rolling {hoursToDays(limit)} intervals with the
{tierToPlan($organization.billingPlan).name}
plan.
{#if $organization?.billingPlan === BillingPlan.STARTER}
<Button link on:click={upgradeMethod}>Upgrade</Button> to increase your log
retention for a longer period.
{/if}
</p>
</svelte:fragment>
{#each logs.logs as log}
<TableRow>
<TableCell title="Client">
+40
View File
@@ -0,0 +1,40 @@
<script lang="ts">
import { tooltip } from '$lib/actions/tooltip';
import { BillingPlan } from '$lib/constants';
import { Button } from '$lib/elements/forms';
import { tierToPlan } from '$lib/stores/billing';
import { organization } from '$lib/stores/organization';
export let title: string;
export let tooltipContent =
$organization.billingPlan === BillingPlan.STARTER
? `Upgrade to add more ${title.toLocaleLowerCase()}`
: `You've reached the ${title.toLocaleLowerCase()} limit for the ${
tierToPlan($organization.billingPlan).name
} plan`;
export let disabled: boolean;
export let buttonText: string;
export let buttonMethod: () => void | Promise<void>;
export let buttonEvent: string = buttonText?.toLocaleLowerCase();
export let icon = 'plus';
export let showIcon = true;
export let buttonType: 'primary' | 'secondary' | 'text' = 'primary';
</script>
<div
use:tooltip={{
content: tooltipContent,
disabled: !disabled
}}>
<Button
text={buttonType === 'text'}
secondary={buttonType === 'secondary'}
on:click={buttonMethod}
event={buttonEvent}
{disabled}>
{#if showIcon}
<span class={`icon-${icon}`} aria-hidden="true" />
{/if}
<span class="text">{buttonText}</span>
</Button>
</div>
+165
View File
@@ -0,0 +1,165 @@
<script lang="ts">
import {
tierToPlan,
getServiceLimit,
type PlanServices,
showUsageRatesModal,
checkForUsageFees,
readOnly,
checkForProjectLimitation
} from '$lib/stores/billing';
import { Alert, DropList, Heading } from '$lib/components';
import { Pill } from '$lib/elements';
import { organization } from '$lib/stores/organization';
import { GRACE_PERIOD_OVERRIDE, isCloud } from '$lib/system';
import { createEventDispatcher, onMount } from 'svelte';
import { wizard } from '$lib/stores/wizard';
import ChangeOrganizationTierCloud from '$routes/console/changeOrganizationTierCloud.svelte';
import { ContainerButton } from '.';
import { Button } from '$lib/elements/forms';
import { BillingPlan } from '$lib/constants';
export let isFlex = true;
export let title: string;
export let titleTag: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' = 'h2';
export let titleSize: '1' | '2' | '3' | '4' | '5' | '6' | '7' = '5';
export let serviceId = title.toLocaleLowerCase() as PlanServices;
export let total: number = null;
export let alertType: 'info' | 'success' | 'warning' | 'error' | 'default' = 'warning';
export let showAlert = true;
export let buttonText: string = null;
export let buttonMethod: () => void = null;
export let buttonEvent: string = buttonText?.toLocaleLowerCase();
export let buttonDisabled = false;
let showDropdown = false;
const { bandwidth, documents, storage, users, executions } = $organization?.billingLimits ?? {};
const limitedServices = [
{ name: 'bandwidth', value: bandwidth },
{ name: 'documents', value: documents },
{ name: 'storage', value: storage },
{ name: 'users', value: users },
{ name: 'executions', value: executions }
];
const limit = getServiceLimit(serviceId) || Infinity;
const upgradeMethod = () => {
showDropdown = false;
wizard.start(ChangeOrganizationTierCloud);
};
const dispatch = createEventDispatcher();
$: tier = tierToPlan($organization?.billingPlan)?.name;
$: hasProjectLimitation =
checkForProjectLimitation(serviceId) && $organization?.billingPlan === BillingPlan.STARTER;
$: hasUsageFees = hasProjectLimitation
? checkForUsageFees($organization?.billingPlan, serviceId)
: false;
$: isLimited = limit !== 0 && limit < Infinity;
$: overflowingServices = limitedServices.filter((service) => service.value >= 0);
$: isButtonDisabled =
buttonDisabled ||
($readOnly && !GRACE_PERIOD_OVERRIDE) ||
(isLimited && total >= limit && !hasUsageFees);
onMount(() => {
dispatch('data', { isButtonDisabled, limit, tier });
});
</script>
<!-- Show only if on Cloud, alerts are enabled, and it isn't a project limited service -->
{#if isCloud && showAlert}
{#if $readOnly}
{@const services = overflowingServices
.map((s) => {
return s.name.toLocaleLowerCase();
})
.join(', ')}
<slot name="alert" {limit} {tier} {title} {upgradeMethod} {hasUsageFees} {services}>
{#if $organization?.billingPlan !== BillingPlan.STARTER && hasUsageFees}
<Alert type="info" isStandalone>
<span class="text">
You've reached the {services} limit for the {tier} plan.
<Button link on:click={() => ($showUsageRatesModal = true)}
>Excess usage fees will apply</Button
>.
</span>
</Alert>
{:else}
<Alert type={alertType} isStandalone>
<span class="text">
You've reached the {services} limit for the {tier} plan. <Button
link
on:click={upgradeMethod}>Upgrade</Button> your organization for additional
resources.
</span>
</Alert>
{/if}
</slot>
{/if}
{/if}
<header class:u-flex={isFlex} class="u-gap-12 common-section u-main-space-between u-flex-wrap">
<div class="u-flex u-cross-child-center u-cross-center u-gap-16 u-flex-wrap">
<Heading tag={titleTag} size={titleSize}>{title}</Heading>
{#if isCloud && isLimited}
<DropList bind:show={showDropdown} width="16">
{#if hasProjectLimitation}
<Pill button on:click={() => (showDropdown = !showDropdown)}>
<span class="icon-info" />{total}/{limit}
{title} used
</Pill>
{:else}
<Pill button on:click={() => (showDropdown = !showDropdown)}>
<span class="icon-info" />{title} limited
</Pill>
{/if}
<svelte:fragment slot="list">
<slot name="tooltip" {limit} {tier} {title} {upgradeMethod} {hasUsageFees}>
{#if hasProjectLimitation}
<p class="text">
Your are limited to {limit}
{title.toLocaleLowerCase()} per project on the {tier} plan.
{#if $organization?.billingPlan === BillingPlan.STARTER}<Button
link
on:click={upgradeMethod}>Upgrade</Button>
for addtional {title.toLocaleLowerCase()}.
{/if}
</p>
{:else if hasUsageFees}
<p class="text">
You are limited to {limit}
{title.toLocaleLowerCase()} per organization on the {tier} plan.
<Button link on:click={() => ($showUsageRatesModal = true)}
>Excess usage fees will apply</Button
>.
</p>
{:else}
<p class="text">
You are limited to {limit}
{title.toLocaleLowerCase()} per organization on the {tier} plan.
{#if $organization?.billingPlan === BillingPlan.STARTER}
<Button link on:click={upgradeMethod}>Upgrade</Button>
for additional {title.toLocaleLowerCase()}.
{/if}
</p>
{/if}
</slot>
</svelte:fragment>
</DropList>
{/if}
</div>
<slot {isButtonDisabled}>
{#if buttonText}
<ContainerButton
{title}
disabled={isButtonDisabled}
{buttonText}
{buttonEvent}
{buttonMethod} />
{/if}
</slot>
</header>
+15 -1
View File
@@ -1,4 +1,6 @@
<script>
import { settings } from '$lib/components/consent.svelte';
import { clickOnEnter } from '$lib/helpers/a11y';
import { isCloud } from '$lib/system';
import { version } from '$routes/console/store';
@@ -8,7 +10,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" />
@@ -39,6 +41,18 @@
<span class="text">Privacy</span>
</a>
</li>
{#if isCloud}
<li class="inline-links-item">
<span
style:cursor="pointer"
role="button"
tabindex="0"
on:keyup={clickOnEnter}
on:click={() => settings.set(true)}>
<span class="text">Cookies</span>
</span>
</li>
{/if}
</ul>
</div>
<div class="main-footer-end">
+54 -21
View File
@@ -1,12 +1,14 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { base } from '$app/paths';
import { AvatarInitials, DropList, DropListItem, DropListLink, Support } from '$lib/components';
import { app } from '$lib/stores/app';
import { user } from '$lib/stores/user';
import { organizationList, organization, newOrgModal } from '$lib/stores/organization';
import { page } from '$app/stores';
import { Submit, trackEvent } from '$lib/actions/analytics';
import { tooltip } from '$lib/actions/tooltip';
import { toggleCommandCenter } from '$lib/commandCenter/commandCenter.svelte';
import { AvatarInitials, DropList, DropListItem, DropListLink } from '$lib/components';
import { Feedback } from '$lib/components/feedback';
import Button from '$lib/elements/forms/button.svelte';
import { isMac } from '$lib/helpers/platform';
import AppwriteLogoDark from '$lib/images/appwrite-logo-dark.svg';
@@ -14,15 +16,21 @@
import DarkMode from '$lib/images/mode/dark-mode.svg';
import LightMode from '$lib/images/mode/light-mode.svg';
import SystemMode from '$lib/images/mode/system-mode.svg';
import { app } from '$lib/stores/app';
import { feedback } from '$lib/stores/feedback';
import { newOrgModal, organization, organizationList } from '$lib/stores/organization';
import { sdk } from '$lib/stores/sdk';
import { user } from '$lib/stores/user';
import { isCloud } from '$lib/system';
import { slide } from 'svelte/transition';
import { sdk } from '$lib/stores/sdk';
import { isCloud } from '$lib/system';
import { wizard } from '$lib/stores/wizard';
import CreateOrganizationCloud from '$routes/console/createOrganizationCloud.svelte';
import { Feedback } from '$lib/components/feedback';
import ChangeOrganizationTierCloud from '$routes/console/changeOrganizationTierCloud.svelte';
import { Pill } from '$lib/elements';
import { showExcess } from '$routes/console/organization-[organization]/store';
import { readOnly } from '$lib/stores/billing';
import { BillingPlan } from '$lib/constants';
let showDropdown = false;
let showSupport = false;
let droplistElement: HTMLDivElement;
function toggleFeedback() {
@@ -48,6 +56,13 @@
}
}
function createOrg() {
showDropdown = false;
if (isCloud) {
wizard.start(CreateOrganizationCloud);
} else newOrgModal.set(true);
}
$: if (showDropdown) {
trackEvent('click_menu_dropdown');
}
@@ -90,8 +105,27 @@
<svelte:component this={$page.data.breadcrumbs} />
{/if}
{#if !$page.url.pathname.includes('/console/account') && $readOnly}
<div style="min-inline-size: fit-content">
<Pill danger button on:click={() => ($showExcess = true)}>
<div>
<span class="icon-exclamation-circle" aria-hidden="true" />
<span>limit reached</span>
</div>
</Pill>
</div>
{/if}
<div class="main-header-end">
<nav class="u-flex is-only-desktop">
<nav class="u-flex is-only-desktop u-cross-center">
{#if isCloud && $organization?.billingPlan === BillingPlan.STARTER && !$page.url.pathname.startsWith('/console/account')}
<Button
disabled={$organization?.markedForDeletion}
on:click={() => wizard.start(ChangeOrganizationTierCloud)}>
Upgrade
</Button>
{/if}
{#if $feedback.notification}
<div class="u-flex u-cross-center">
<div class="pulse-notification" />
@@ -105,13 +139,17 @@
<Feedback />
</svelte:fragment>
</DropList>
<a
href="https://appwrite.io/discord"
target="_blank"
rel="noopener noreferrer"
class="button is-small is-text">
<span class="text">Support</span>
</a>
{#if isCloud}
<DropList width="18.5" bind:show={showSupport} scrollable={true}>
<Button text on:click={() => (showSupport = !showSupport)}>
<span class="text">Support</span>
</Button>
<svelte:fragment slot="other">
<Support bind:show={showSupport} />
</svelte:fragment>
</DropList>
{/if}
<Button
actions={[
(node) => {
@@ -163,12 +201,7 @@
{/if}
<section class="drop-section">
<ul class="drop-list">
<DropListItem
icon="plus"
on:click={() => {
showDropdown = false;
newOrgModal.set(true);
}}>
<DropListItem icon="plus" on:click={createOrg}>
New organization
</DropListItem>
<DropListLink
+32
View File
@@ -0,0 +1,32 @@
<script lang="ts">
export let title: string;
export let type: 'info' | 'success' | 'warning' | 'error' | 'default' = 'info';
</script>
<section
class="alert is-action is-action-and-top-sticky u-sep-block-end"
class:is-success={type === 'success'}
class:is-warning={type === 'warning'}
class:is-danger={type === 'error'}
class:is-info={type === 'info'}>
<div class="alert-grid">
<span class="icon-info" aria-hidden="true" />
<div class="alert-content">
{#if title || $$slots.title}
<h6 class="alert-title">
<slot name="title">
{title}
</slot>
</h6>
{/if}
<p class="alert-message">
<slot />
</p>
</div>
{#if $$slots.buttons}
<div class="alert-buttons u-flex u-gap-16 u-cross-child-center">
<slot name="buttons" />
</div>
{/if}
</div>
</section>
+3
View File
@@ -16,3 +16,6 @@ export { default as Usage, type UsagePeriods } from './usage.svelte';
export { default as Activity } from './activity.svelte';
export { default as Progress } from './progress.svelte';
export { default as GridHeader } from './gridHeader.svelte';
export { default as ContainerHeader } from './containerHeader.svelte';
export { default as HeaderAlert } from './headerAlert.svelte';
export { default as ContainerButton } from './containerButton.svelte';
+33 -4
View File
@@ -1,7 +1,7 @@
<script lang="ts">
import { toLocaleDateTime } from '$lib/helpers/date';
import { hoursToDays, toLocaleDateTime } from '$lib/helpers/date';
import { log } from '$lib/stores/logs';
import { Card, Code, Heading, Id, SvgIcon, Tab, Tabs } from '../components';
import { Alert, Card, Code, Heading, Id, SvgIcon, Tab, Tabs } from '../components';
import { calculateTime } from '$lib/helpers/timeConversion';
import {
TableBody,
@@ -13,10 +13,21 @@
} from '$lib/elements/table';
import { beforeNavigate } from '$app/navigation';
import { Pill } from '$lib/elements';
import { isCloud } from '$lib/system';
import ChangeOrganizationTierCloud from '$routes/console/changeOrganizationTierCloud.svelte';
import { wizard } from '$lib/stores/wizard';
import { getServiceLimit, tierToPlan } from '$lib/stores/billing';
import { organization } from '$lib/stores/organization';
import { app } from '$lib/stores/app';
import { Button } from '$lib/elements/forms';
import { BillingPlan } from '$lib/constants';
let selectedRequest = 'parameters';
let selectedResponse = 'logs';
const limit = getServiceLimit('logs');
const tier = tierToPlan($organization?.billingPlan)?.name;
function handleKeydown(event: KeyboardEvent) {
if (event.key === 'Escape') {
event.preventDefault();
@@ -154,7 +165,9 @@
</div>
</header>
<div class="code-panel-content grid-1-2" style="u-grid">
<div class="grid-1-2-col-1 u-flex u-flex-vertical u-gap-16">
<div
class="grid-1-2-col-1 u-flex u-flex-vertical u-gap-16"
class:theme-dark={$app.themeInUse === 'light'}>
<Heading tag="h3" size="6">Request</Heading>
<div class="u-sep-block-end">
<Tabs>
@@ -265,7 +278,9 @@
</p>
{/if}
</div>
<div class="grid-1-2-col-2 u-flex u-flex-vertical u-gap-16 u-min-width-0">
<div
class="grid-1-2-col-2 u-flex u-flex-vertical u-gap-16 u-min-width-0"
class:theme-dark={$app.themeInUse === 'light'}>
<Heading tag="h3" size="6">Response</Heading>
<div class="u-sep-block-end">
<Tabs>
@@ -293,6 +308,20 @@
</div>
{#if selectedResponse === 'logs'}
{#if execution?.logs}
{#if isCloud && limit !== 0 && limit < Infinity}
<Alert>
Logs are retained in rolling {hoursToDays(limit)} intervals
with the {tier} plan.
{#if $organization.billingPlan === BillingPlan.STARTER}
<Button
link
on:click={() =>
wizard.start(ChangeOrganizationTierCloud)}
>Upgrade</Button> to increase your log retention
for a longer period.
{/if}
</Alert>
{/if}
<Code withCopy noMargin code={execution.logs} language="sh" />
{:else}
<Card isDashed isTile>
+27 -2
View File
@@ -3,10 +3,15 @@
import { page } from '$app/stores';
import { trackEvent } from '$lib/actions/analytics';
import { tooltip } from '$lib/actions/tooltip';
import { BillingPlan } from '$lib/constants';
import { isMac } from '$lib/helpers/platform';
import { slide } from '$lib/helpers/transition';
import { organization } from '$lib/stores/organization';
import { wizard } from '$lib/stores/wizard';
import { isCloud } from '$lib/system';
import ChangeOrganizationTierCloud from '$routes/console/changeOrganizationTierCloud.svelte';
import Create from '$routes/console/feedbackWizard.svelte';
import { showSupportModal } from '$routes/console/wizard/support/store';
export let isOpen = false;
@@ -32,10 +37,15 @@
}
}
function openWizard() {
function openFeedback() {
isOpen = false;
wizard.start(Create);
}
function openSupport() {
isOpen = false;
$showSupportModal = true;
}
</script>
<svelte:window on:keydown={handleKeyDown} />
@@ -169,9 +179,24 @@
<span class="icon-cog" aria-hidden="true" />
<span class="text">Settings</span>
</a>
<ul class="drop-list is-only-mobile">
{#if isCloud && $organization?.billingPlan !== BillingPlan.SCALE}
<li class="drop-list-item">
<button
class="drop-button"
on:click={() => wizard.start(ChangeOrganizationTierCloud)}>
<span class="text">Upgrade</span>
</button>
</li>
{/if}
<li class="drop-list-item">
<button class="drop-button" on:click={openWizard}>
<button class="drop-button" on:click={openSupport}>
<span class="text">Support</span>
</button>
</li>
<li class="drop-list-item">
<button class="drop-button" on:click={openFeedback}>
<span class="text">Feedback</span>
</button>
</li>
+7 -1
View File
@@ -7,6 +7,8 @@
export let icon: Notification['icon'] = null;
export let title: Notification['title'];
export let buttons: Notification['buttons'];
export let message: Notification['message'];
export let html: Notification['isHtml'] = false;
const dispatch = createEventDispatcher();
</script>
@@ -38,7 +40,11 @@
{#if title}
<h4 class="alert-sticky-title">{title}</h4>
{/if}
<p><slot /></p>
{#if html}
{@html message}
{:else}
<p>{message}</p>
{/if}
</div>
{#if buttons}
<div class="alert-sticky-buttons u-flex">
+3 -3
View File
@@ -14,9 +14,9 @@
title={notification.title}
icon={notification.icon}
on:dismiss={() => dismissNotification(notification.id)}
buttons={notification?.buttons}>
{notification.message}
</Notification>
buttons={notification?.buttons}
message={notification.message}
html={notification?.isHtml} />
</li>
{/each}
</ul>
+5 -1
View File
@@ -46,7 +46,11 @@
<main
class:grid-with-side={showSideNavigation}
class:is-open={isOpen}
class:u-hide={$wizard.show || $log.show || $wizard.cover}>
class:u-hide={$wizard.show || $log.show || $wizard.cover}
class:is-fixed-layout={$$slots.alert}>
{#if $$slots.alert}
<slot name="alert" />
{/if}
<header class="main-header u-padding-inline-end-0">
<button
class:u-hide={!showSideNavigation}

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