Migrate UI components to Pink design library

The commit updates various UI components to use the new Pink design library, replacing older custom components. Key changes include:

- Replace custom Alert components with Pink's Alert.Inline
- Replace custom Table components with Pink's Table.Root/Cell structure
- Update form inputs and selectors to use Pink variants
- Remove unnecessary wrapper elements and classes
- Adjust layout and typography to match Pink patterns
- Clean up redundant styles and markup

Focus is on maintaining functionality while adopting the new design system components.
This commit is contained in:
Torsten Dittmann
2025-03-13 17:24:48 +04:00
parent 84c116505e
commit ecbf8b2488
38 changed files with 848 additions and 1075 deletions
+5 -9
View File
@@ -1,11 +1,11 @@
<script lang="ts">
import { Remarkable } from 'remarkable';
import Template from './template.svelte';
import { Keyboard, Layout } from '@appwrite.io/pink-svelte';
import { Alert, Keyboard, Layout } from '@appwrite.io/pink-svelte';
const markdownInstance = new Remarkable();
import { Alert, AvatarInitials, Code, LoadingDots, SvgIcon } from '$lib/components';
import { AvatarInitials, Code, LoadingDots, SvgIcon } from '$lib/components';
import { user } from '$lib/stores/user';
import { useCompletion } from 'ai/svelte';
import { subPanels } from '../subPanels';
@@ -218,13 +218,9 @@
{#if $error}
<div style="padding: 1rem; padding-block-end: 0;">
<Alert type="error">
<span slot="title">Something went wrong</span>
<p>
An unexpected error occurred while handling your request. Please try again
later.
</p>
</Alert>
<Alert.Inline status="error" title="Something went wrong">
An unexpected error occurred while handling your request. Please try again later.
</Alert.Inline>
</div>
{/if}
+29 -37
View File
@@ -36,42 +36,34 @@
}
</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'}
<InputText
placeholder="Coupon code"
id="code"
label="Add credits"
{required}
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.toUpperCase()} 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-exclamation-circle u-color-text-danger" />
<span>
{couponData.code.toUpperCase()} is not a valid promo code
</span>
<span class="icon-tag u-color-text-success" />
<slot data={couponData}>
<span>
{couponData.code.toUpperCase()} applied (-{formatCurrency(couponData.credits)})
</span>
</slot>
</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.toUpperCase()} applied (-{formatCurrency(
couponData.credits
)})
</span>
</slot>
</div>
<Button icon text on:click={removeCoupon}><span class="icon-x"></span></Button>
</div>
{/if}
</FormList>
<Button icon text on:click={removeCoupon}><span class="icon-x"></span></Button>
</div>
{/if}
@@ -3,6 +3,7 @@
import { BillingPlan } from '$lib/constants';
import { Button } from '$lib/elements/forms';
import { tierToPlan, upgradeURL } from '$lib/stores/billing';
import { Layout, Typography } from '@appwrite.io/pink-svelte';
import { Card } from '..';
export let service: string;
@@ -11,14 +12,13 @@
<Card>
<slot>
<div class="u-flex u-flex-vertical u-main-center u-cross-center u-gap-8">
<h6 class="body-text-1 u-bold u-trim-1">Upgrade to add {service}</h6>
<p class="text u-text-center">
<Layout.Stack alignItems="center">
<Typography.Text variant="m-600">Upgrade to add {service}</Typography.Text>
<Typography.Text>
Upgrade to a {tierToPlan(BillingPlan.PRO).name} plan to add {service} to your organization
</p>
</Typography.Text>
<Button
class="u-margin-block-start-16"
secondary
fullWidthMobile
href={$upgradeURL}
@@ -30,6 +30,6 @@
}}>
Upgrade
</Button>
</div>
</Layout.Stack>
</slot>
</Card>
@@ -94,12 +94,10 @@
<div bind:this={element}></div>
</div>
{#if showSetAsDefault}
<ul>
<InputChoice
bind:value={setAsDefault}
id="default"
label="Set as default payment method for this organization" />
</ul>
<InputChoice
bind:value={setAsDefault}
id="default"
label="Set as default payment method for this organization" />
{/if}
{/if}
</Layout.Stack>
+17 -21
View File
@@ -66,29 +66,25 @@
</script>
<FakeModal bind:show title="Add payment method" bind:error onSubmit={handleSubmit}>
<FormList>
<slot />
<InputText
id="name"
required
autofocus={true}
bind:value={name}
label="Cardholder name"
placeholder="Cardholder name" />
<slot />
<InputText
id="name"
required
autofocus={true}
bind:value={name}
label="Cardholder name"
placeholder="Cardholder name" />
<div class="aw-stripe-container" data-private>
<Layout.Stack gap="l" alignItems="center" justifyContent="center">
{#if isLoading}
<Spinner />
{/if}
<div class="aw-stripe-container" data-private>
{#if isLoading}
<Spinner />
{/if}
<div class="stripe-element" bind:this={element}>
<!-- Stripe will create form elements here -->
</div>
</Layout.Stack>
<div class="stripe-element" bind:this={element}>
<!-- Stripe will create form elements here -->
</div>
<slot name="end"></slot>
</FormList>
</div>
<slot name="end"></slot>
<svelte:fragment slot="footer">
<Button secondary on:click={() => (show = false)}>Cancel</Button>
<Button submit disabled={!name}>Add</Button>
@@ -98,7 +94,7 @@
<style lang="scss">
.aw-stripe-container {
display: flex;
min-height: 295px;
min-height: 245px;
.stripe-element {
width: 100%;
@@ -2,38 +2,40 @@
import { BillingPlan } from '$lib/constants';
import { formatNum } from '$lib/helpers/string';
import { plansInfo, tierFree, tierPro, tierScale, type Tier } from '$lib/stores/billing';
import { Card, SecondaryTabs, SecondaryTabsItem } from '..';
import { Card, Layout, Tabs, Typography } from '@appwrite.io/pink-svelte';
export let downgrade = false;
let selectedTab: Tier = BillingPlan.FREE;
export let downgrade = false;
$: plan = $plansInfo.get(selectedTab);
</script>
<Card style="--card-padding: 1.5rem">
<div class="comparison-box">
<SecondaryTabs stretch>
<SecondaryTabsItem
disabled={selectedTab === BillingPlan.FREE}
<Card.Base>
<Layout.Stack>
<Tabs.Root stretch let:root>
<Tabs.Item.Button
{root}
active={selectedTab === BillingPlan.FREE}
on:click={() => (selectedTab = BillingPlan.FREE)}>
{tierFree.name}
</SecondaryTabsItem>
<SecondaryTabsItem
disabled={selectedTab === BillingPlan.PRO}
</Tabs.Item.Button>
<Tabs.Item.Button
{root}
active={selectedTab === BillingPlan.PRO}
on:click={() => (selectedTab = BillingPlan.PRO)}>
{tierPro.name}
</SecondaryTabsItem>
<SecondaryTabsItem
disabled={selectedTab === BillingPlan.SCALE}
</Tabs.Item.Button>
<Tabs.Item.Button
{root}
active={selectedTab === BillingPlan.SCALE}
on:click={() => (selectedTab = BillingPlan.SCALE)}>
{tierScale.name}
</SecondaryTabsItem>
</SecondaryTabs>
</div>
</Tabs.Item.Button>
</Tabs.Root>
<div class="u-margin-block-start-24">
<Typography.Text variant="m-600">{plan.name} plan</Typography.Text>
{#if selectedTab === BillingPlan.FREE}
<h3 class="u-bold body-text-1">{plan.name} plan</h3>
{#if downgrade}
<ul class="u-margin-block-start-8 list u-gap-4 u-small">
<li class="list-item u-gap-4 u-cross-center">
@@ -67,37 +69,26 @@
</li>
</ul>
{:else}
<ul class="u-margin-block-start-8 un-order-list">
<ul class="un-order-list">
<li>
<span class="text">
Limited to {plan.databases} Database, {plan.buckets} Buckets, {plan.functions}
Functions per project
</span>
Limited to {plan.databases} Database, {plan.buckets} Buckets, {plan.functions}
Functions per project
</li>
<li>Limited to 1 organization member</li>
<li>
Limited to {plan.bandwidth}GB bandwidth
</li>
<li>
<span class="text"> Limited to 1 organization member </span>
Limited to {plan.storage}GB storage
</li>
<li>
<span class="text">
{plan.bandwidth}GB bandwidth
</span>
</li>
<li>
<span class="text">
{plan.storage}GB storage
</span>
</li>
<li>
<span class="text">
{formatNum(plan.executions)} executions
</span>
Limited to {formatNum(plan.executions)} executions
</li>
</ul>
{/if}
{:else if selectedTab === BillingPlan.PRO}
<h3 class="u-bold body-text-1">{plan.name} plan</h3>
<p class="u-margin-block-start-8">Everything in the Free plan, plus:</p>
<ul class="un-order-list u-margin-inline-start-4">
<Typography.Text>Everything in the Free plan, plus:</Typography.Text>
<ul class="un-order-list">
<li>Unlimited databases, buckets, functions</li>
<li>{plan.bandwidth}GB bandwidth</li>
<li>{plan.storage}GB storage</li>
@@ -105,9 +96,8 @@
<li>Email support</li>
</ul>
{:else if selectedTab === BillingPlan.SCALE}
<h3 class="u-bold body-text-1">{plan.name} plan</h3>
<p class="u-margin-block-start-8">Everything in the Pro plan, plus:</p>
<ul class="un-order-list u-margin-inline-start-4">
<Typography.Text>Everything in the Pro plan, plus:</Typography.Text>
<ul class="un-order-list">
<li>Unlimited seats</li>
<li>Organization roles</li>
<li>SOC-2, HIPAA compliance</li>
@@ -115,29 +105,5 @@
<li>Priority support</li>
</ul>
{/if}
</div>
</Card>
<style lang="scss">
.comparison-box {
border-radius: var(--border-radius-small);
background: hsl(var(--color-neutral-5));
}
:global(.theme-dark) .comparison-box {
background: hsl(var(--color-neutral-85));
}
.comparison-box :global(.secondary-tabs-button:where(:disabled)) {
background: hsl(var(--color-neutral-0));
border: 1px solid hsl(var(--color-neutral-10));
}
:global(.theme-dark) .comparison-box :global(.secondary-tabs-button:where(:disabled)) {
background: hsl(var(--color-neutral-80));
border: 1px solid hsl(var(--color-neutral-85));
}
.inline-tag {
line-height: 140%;
font-weight: 500;
}
</style>
</Layout.Stack>
</Card.Base>
+78 -89
View File
@@ -1,14 +1,4 @@
<script lang="ts">
import {
TableBody,
TableCell,
TableCellHead,
TableCellText,
TableHeader,
TableRow,
TableScroll
} from '$lib/elements/table';
import { Alert } from '$lib/components';
import { calculateExcess, plansInfo, tierToPlan, type Tier } from '$lib/stores/billing';
import { organization } from '$lib/stores/organization';
import { toLocaleDate } from '$lib/helpers/date';
@@ -20,7 +10,9 @@
import type { OrganizationUsage } from '$lib/sdk/billing';
import { sdk } from '$lib/stores/sdk';
import { BillingPlan } from '$lib/constants';
import { Tooltip } from '@appwrite.io/pink-svelte';
import { Alert, Icon, Table, Tooltip } from '@appwrite.io/pink-svelte';
import Cell from '$lib/elements/table/cell.svelte';
import { IconInfo } from '@appwrite.io/pink-icons-svelte';
export let tier: Tier;
export let members: number;
@@ -48,12 +40,11 @@
</script>
{#if showExcess}
<Alert type="error" {...$$restProps}>
<svelte:fragment slot="title">
Your {tierToPlan($organization.billingPlan).name} plan subscription will end on {toLocaleDate(
$organization.billingNextInvoiceDate
)}
</svelte:fragment>
<Alert.Inline
status="error"
title={`Your ${tierToPlan($organization.billingPlan).name} plan subscription will end on ${toLocaleDate(
$organization.billingNextInvoiceDate
)}`}>
Following payment of your final invoice, your organization will switch to the {tierToPlan(
BillingPlan.FREE
).name} plan. {#if excess?.members > 0}All team members except the owner will be removed on
@@ -67,80 +58,78 @@
Learn more
</Button>
</svelte:fragment>
</Alert>
</Alert.Inline>
<TableScroll noMargin dense class="u-margin-block-start-16">
<TableHeader>
<TableCellHead>Resource</TableCellHead>
<TableCellHead>Free limit</TableCellHead>
<TableCellHead>
<Table.Root>
<svelte:fragment slot="header">
<Table.Header.Cell>Resource</Table.Header.Cell>
<Table.Header.Cell>Free limit</Table.Header.Cell>
<Table.Header.Cell>
Excess usage <Tooltip
><span class="icon-info"></span>
><Icon icon={IconInfo} />
<span slot="tooltip">Metrics are estimates updated every 24 hours</span>
</Tooltip>
</TableCellHead>
</TableHeader>
<TableBody>
{#if excess?.members}
<TableRow>
<TableCellText title="members">Organization members</TableCellText>
<TableCellText title="limit">{plan.members} members</TableCellText>
<TableCell title="excess">
<p class="u-color-text-danger u-flex u-cross-center u-gap-4">
<span class="icon-arrow-up" />
{excess?.members} members
</p>
</TableCell>
</TableRow>
{/if}
{#if excess?.storage}
<TableRow>
<TableCellText title="storage">Storage</TableCellText>
<TableCellText title="limit">{plan.storage} GB</TableCellText>
<TableCell title="excess">
<p class="u-color-text-danger">
<span class="icon-arrow-up" />
{humanFileSize(excess?.storage).value}
{humanFileSize(excess?.storage).unit}
</p>
</TableCell>
</TableRow>
{/if}
{#if excess?.executions}
<TableRow>
<TableCellText title="executions">Function executions</TableCellText>
<TableCellText title="limit">
{abbreviateNumber(plan.executions)} executions
</TableCellText>
<TableCell title="excess">
<p class="u-color-text-danger">
<span class="icon-arrow-up" />
<span
title={excess?.executions
? excess.executions.toString()
: 'executions'}>
{formatNum(excess?.executions)} executions
</span>
</p>
</TableCell>
</TableRow>
{/if}
{#if excess?.users}
<TableRow>
<TableCellText title="users">Users</TableCellText>
<TableCellText title="limit">
{abbreviateNumber(plan.users)} users
</TableCellText>
<TableCell title="excess">
<p class="u-color-text-danger">
<span class="icon-arrow-up" />
<span title={excess?.users ? excess.users.toString() : 'users'}>
{formatNum(excess?.users)} users
</span>
</p>
</TableCell>
</TableRow>
{/if}
</TableBody>
</TableScroll>
</Table.Header.Cell>
</svelte:fragment>
{#if excess?.members}
<Table.Row>
<Table.Cell>Organization members</Table.Cell>
<Table.Cell>{plan.members} members</Table.Cell>
<Table.Cell>
<p class="u-color-text-danger u-flex u-cross-center u-gap-4">
<span class="icon-arrow-up" />
{excess?.members} members
</p>
</Table.Cell>
</Table.Row>
{/if}
{#if excess?.storage}
<Table.Row>
<Table.Cell>Storage</Table.Cell>
<Table.Cell>{plan.storage} GB</Table.Cell>
<Table.Cell>
<p class="u-color-text-danger">
<span class="icon-arrow-up" />
{humanFileSize(excess?.storage).value}
{humanFileSize(excess?.storage).unit}
</p>
</Table.Cell>
</Table.Row>
{/if}
{#if excess?.executions}
<Table.Row>
<Table.Cell>Function executions</Table.Cell>
<Table.Cell>
{abbreviateNumber(plan.executions)} executions
</Table.Cell>
<Table.Cell>
<p class="u-color-text-danger">
<span class="icon-arrow-up" />
<span
title={excess?.executions
? excess.executions.toString()
: 'executions'}>
{formatNum(excess?.executions)} executions
</span>
</p>
</Table.Cell>
</Table.Row>
{/if}
{#if excess?.users}
<Table.Row>
<Table.Cell>Users</Table.Cell>
<Table.Cell>
{abbreviateNumber(plan.users)} users
</Table.Cell>
<Table.Cell>
<p class="u-color-text-danger">
<span class="icon-arrow-up" />
<span title={excess?.users ? excess.users.toString() : 'users'}>
{formatNum(excess?.users)} users
</span>
</p>
</Table.Cell>
</Table.Row>
{/if}
</Table.Root>
{/if}
+58 -80
View File
@@ -3,6 +3,7 @@
import { formatCurrency } from '$lib/helpers/numbers';
import { plansInfo, type Tier, tierFree, tierPro, tierScale } from '$lib/stores/billing';
import { organization } from '$lib/stores/organization';
import { Badge, Layout, Typography } from '@appwrite.io/pink-svelte';
import { LabelCard } from '..';
export let billingPlan: Tier;
@@ -17,83 +18,60 @@
$: scalePlan = $plansInfo.get(BillingPlan.SCALE);
</script>
{#if billingPlan}
<ul class="u-flex u-flex-vertical u-gap-16 u-margin-block-start-8 {classes}">
<li>
<LabelCard
name="plan"
bind:group={billingPlan}
disabled={anyOrgFree || !selfService}
value={BillingPlan.FREE}
tooltipShow={anyOrgFree}
title={tierFree.name}
tooltipText="You are limited to 1 Free organization per account."
padding="m">
<div
class="u-flex u-flex-vertical u-gap-4 u-width-full-line"
class:u-opacity-50={anyOrgFree || !selfService}>
<h4 class="body-text-2 u-bold">
{tierFree.name}
{#if $organization?.billingPlan === BillingPlan.FREE && !isNewOrg}
<span class="inline-tag">Current plan</span>
{/if}
</h4>
<p class="u-color-text-offline u-small">
{tierFree.description}
</p>
<p>
{formatCurrency(freePlan?.price ?? 0)}
</p>
</div>
</LabelCard>
</li>
<li>
<LabelCard
name="plan"
disabled={!selfService}
bind:group={billingPlan}
value={BillingPlan.PRO}
title={tierPro.name}
padding="m">
<div
class="u-flex u-flex-vertical u-gap-4 u-width-full-line"
class:u-opacity-50={!selfService}>
<h4 class="body-text-2 u-bold">
{#if $organization?.billingPlan === BillingPlan.PRO && !isNewOrg}
<span class="inline-tag">Current plan</span>
{/if}
</h4>
<p class="u-color-text-offline u-small">
{tierPro.description}
</p>
<p>
{formatCurrency(proPlan?.price ?? 0)} per member/month + usage
</p>
</div>
</LabelCard>
</li>
<li>
<LabelCard
name="plan"
bind:group={billingPlan}
value={BillingPlan.SCALE}
title={tierScale.name}
padding="m">
<div class="u-flex u-flex-vertical u-gap-4 u-width-full-line">
<h4 class="body-text-2 u-bold">
{#if $organization?.billingPlan === BillingPlan.SCALE && !isNewOrg}
<span class="inline-tag">Current plan</span>
{/if}
</h4>
<p class="u-color-text-offline u-small">
{tierScale.description}
</p>
<p>
{formatCurrency(scalePlan?.price ?? 0)} per month + usage
</p>
</div>
</LabelCard>
</li>
</ul>
{/if}
<Layout.Stack>
<LabelCard
name="plan"
bind:group={billingPlan}
disabled={anyOrgFree || !selfService}
value={BillingPlan.FREE}
tooltipShow={anyOrgFree}
title={tierFree.name}
tooltipText="You are limited to 1 Free organization per account.">
<svelte:fragment slot="action">
{#if $organization?.billingPlan === BillingPlan.FREE && !isNewOrg}
<Badge variant="secondary" size="xs" content="Current plan" />
{/if}
</svelte:fragment>
<Typography.Caption variant="400">
{tierFree.description}
</Typography.Caption>
<Typography.Text>
{formatCurrency(freePlan?.price ?? 0)}
</Typography.Text>
</LabelCard>
<LabelCard
name="plan"
disabled={!selfService}
bind:group={billingPlan}
value={BillingPlan.PRO}
title={tierPro.name}>
<svelte:fragment slot="action">
{#if $organization?.billingPlan === BillingPlan.PRO && !isNewOrg}
<Badge variant="secondary" size="xs" content="Current plan" />
{/if}
</svelte:fragment>
<Typography.Caption variant="400">
{tierPro.description}
</Typography.Caption>
<Typography.Text>
{formatCurrency(proPlan?.price ?? 0)} per month + usage
</Typography.Text>
</LabelCard>
<LabelCard
name="plan"
bind:group={billingPlan}
value={BillingPlan.SCALE}
title={tierScale.name}>
<svelte:fragment slot="action">
{#if $organization?.billingPlan === BillingPlan.SCALE && !isNewOrg}
<Badge variant="secondary" size="xs" content="Current plan" />
{/if}
</svelte:fragment>
<Typography.Caption variant="400">
{tierScale.description}
</Typography.Caption>
<Typography.Text>
{formatCurrency(scalePlan?.price ?? 0)} per month + usage
</Typography.Text>
</LabelCard>
</Layout.Stack>
@@ -1,14 +1,15 @@
<script lang="ts">
import { Button, Helper, InputChoice, InputSelectSearch, InputText } from '$lib/elements/forms';
import { Button, InputChoice, InputText } from '$lib/elements/forms';
import type { PaymentList, PaymentMethodData } from '$lib/sdk/billing';
import { sdk } from '$lib/stores/sdk';
import { hasStripePublicKey, isCloud } from '$lib/system';
import { onMount } from 'svelte';
import { Alert, Card, CreditCardBrandImage } from '..';
import PaymentModal from './paymentModal.svelte';
import { capitalize } from '$lib/helpers/string';
import { Icon } from '@appwrite.io/pink-svelte';
import { Alert, Fieldset, Icon, Layout, Selector } from '@appwrite.io/pink-svelte';
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
import InputSelect from '$lib/elements/forms/inputSelect.svelte';
import { invalidate } from '$app/navigation';
import { Dependencies } from '$lib/constants';
export let methods: PaymentList;
export let value: string;
@@ -16,22 +17,10 @@
let showTaxId = false;
let showPaymentModal = false;
let input: HTMLInputElement;
let error: string;
function handleInvalid(event: Event) {
event.preventDefault();
if (input.validity.valueMissing) {
error = 'This field is required';
return;
}
error = input.validationMessage;
}
async function cardSaved(event: CustomEvent<PaymentMethodData>) {
value = event.detail.$id;
methods = await sdk.forConsole.billing.listPaymentMethods();
invalidate(Dependencies.UPGRADE_PLAN);
}
onMount(() => {
@@ -44,110 +33,64 @@
$: selectedPaymentMethod = methods?.paymentMethods?.find((method) => method.$id === value);
</script>
{#if filteredMethods?.length}
{#if selectedPaymentMethod?.country?.toLowerCase() === 'in'}
<Alert type="warning">
<svelte:fragment slot="title">Indian credit or debit card-holders</svelte:fragment>
To comply with RBI regulations in India, Appwrite will ask for verification to charge up
to $150 USD on your payment method. We will never charge more than the cost of your plan
and the resources you use, or your budget cap limit. For higher usage limits, please contact
us.
</Alert>
{/if}
<InputSelectSearch
id="method"
required
label="Payment method"
placeholder="Select payment method"
bind:value
options={filteredMethods.map((method) => {
return {
value: method.$id,
label: `${capitalize(method.brand)} ending in ${method.last4}`,
data: [method.brand]
};
})}
interactiveOutput
let:option={o}>
<svelte:fragment slot="output" let:option={o}>
<output class="input-text u-cursor-pointer">
<span class="u-flex u-gap-16 u-flex-vertical">
<span class="u-flex u-gap-16">
<span class="u-flex u-cross-center u-gap-8" style="padding-inline:0.25rem">
<span>{o.label}</span>
<CreditCardBrandImage brand={o.data?.toString()} />
</span>
</span>
</span>
</output>
</svelte:fragment>
<span class="u-flex u-gap-16 u-flex-vertical">
<span class="u-flex u-gap-16">
<span class="u-flex u-cross-center u-gap-8" style="padding-inline:0.25rem">
<span>{o.label}</span>
<CreditCardBrandImage brand={o.data?.toString()} />
</span>
</span>
</span>
<svelte:fragment slot="listEnd">
<Button text on:click={() => (showPaymentModal = true)}>
<Icon icon={IconPlus} slot="start" size="s" />
Add new payment method
</Button>
</svelte:fragment>
</InputSelectSearch>
{:else}
<div>
<input
bind:this={input}
on:invalid={handleInvalid}
required
class="u-hide"
type="text"
name="method"
id="method" />
<Card
isDashed
style="--p-card-padding:0.75rem; --p-card-bg-color: transparent; --p-card-border-radius: 0.5rem"
isTile>
<div class="u-flex u-main-space-between u-cross-center">
<p>
<span class="icon-exclamation-circle"></span>
<span class="text">No saved payment methods</span>
</p>
<Fieldset legend="Payment">
<Layout.Stack>
{#if filteredMethods?.length}
{#if selectedPaymentMethod?.country?.toLowerCase() === 'in'}
<Alert.Inline status="warning">
<svelte:fragment slot="title"
>Indian credit or debit card-holders</svelte:fragment>
To comply with RBI regulations in India, Appwrite will ask for verification to charge
up to $150 USD on your payment method. We will never charge more than the cost of
your plan and the resources you use, or your budget cap limit. For higher usage limits,
please contact us.
</Alert.Inline>
{/if}
<InputSelect
id="method"
required
label="Payment method"
placeholder="Select payment method"
bind:value
options={filteredMethods.map((method) => {
return {
value: method.$id,
label: `${capitalize(method.brand)} ending in ${method.last4}`,
data: [method.brand]
};
})} />
<div>
<Button secondary on:click={() => (showPaymentModal = true)}>
<Icon icon={IconPlus} slot="start" size="s" />
Add new payment method
</Button>
</div>
{:else}
<Alert.Inline title="No saved payment methods">
<Button slot="actions" secondary on:click={() => (showPaymentModal = true)}>
<Icon icon={IconPlus} slot="start" size="s" />
Add
</Button>
</div>
</Card>
{#if error}
<Helper class="u-position-relative" type="warning">{error}</Helper>
</Alert.Inline>
{/if}
</div>
{/if}
</Layout.Stack>
</Fieldset>
{#if showPaymentModal && isCloud && hasStripePublicKey}
<PaymentModal bind:show={showPaymentModal} on:submit={cardSaved}>
<svelte:fragment slot="end">
<InputChoice
type="checkbox"
<Selector.Checkbox
id="taxIdCheck"
label="I'm purchasing as a business"
fullWidth
bind:value={showTaxId}>
{#if showTaxId}
<div class="u-margin-block-start-8">
<InputText
id="taxId"
label="Tax ID"
autofocus
placeholder="Tax ID"
bind:value={taxId} />
</div>
{/if}
</InputChoice>
bind:checked={showTaxId} />
{#if showTaxId}
<InputText
id="taxId"
label="Tax ID"
autofocus
placeholder="Tax ID"
bind:value={taxId} />
{/if}
</svelte:fragment>
</PaymentModal>
{/if}
+49 -58
View File
@@ -1,19 +1,12 @@
<script lang="ts">
import { Modal } from '$lib/components';
import { Button } from '$lib/elements/forms';
import {
Table,
TableBody,
TableCellHead,
TableCellText,
TableHeader,
TableRow
} from '$lib/elements/table';
import { toLocaleDate } from '$lib/helpers/date';
import { organization, type Organization } from '$lib/stores/organization';
import { type Organization } from '$lib/stores/organization';
import { plansInfo } from '$lib/stores/billing';
import { abbreviateNumber, formatCurrency } from '$lib/helpers/numbers';
import { BillingPlan } from '$lib/constants';
import { Table, Typography } from '@appwrite.io/pink-svelte';
export let show = false;
export let org: Organization;
@@ -22,7 +15,7 @@
$: nextDate = org?.name
? new Date(new Date().getFullYear(), new Date().getMonth() + 1, 1).toString()
: $organization?.billingNextInvoiceDate;
: org?.billingNextInvoiceDate;
const planData = [
{
@@ -52,64 +45,62 @@
$: isFree = org.billingPlan === BillingPlan.FREE;
</script>
<Modal bind:show size="big" title="Usage rates">
<Modal bind:show title="Usage rates">
{#if isFree}
Usage on the {$plansInfo?.get(BillingPlan.FREE).name} plan is limited for the following resources.
Next billing period: {toLocaleDate(nextDate)}.
<Typography.Text>
Usage on the {$plansInfo?.get(BillingPlan.FREE).name} plan is limited for the following resources.
Next billing period: {toLocaleDate(nextDate)}.
</Typography.Text>
{:else if org.billingPlan === BillingPlan.PRO}
<p>
<Typography.Text>
Usage on the Pro plan will be charged at the end of each billing period at the following
rates. Next billing period: {toLocaleDate(nextDate)}.
</p>
</Typography.Text>
{:else if org.billingPlan === BillingPlan.SCALE}
<p>
<Typography.Text>
Usage on the Scale plan will be charged at the end of each billing period at the
following rates. Next billing period: {toLocaleDate(nextDate)}.
</p>
</Typography.Text>
{/if}
<Table noStyles noMargin>
<TableHeader>
<TableCellHead>Resource</TableCellHead>
<TableCellHead>Limit</TableCellHead>
<Table.Root>
<svelte:fragment slot="header">
<Table.Header.Cell>Resource</Table.Header.Cell>
<Table.Header.Cell>Limit</Table.Header.Cell>
{#if !isFree}
<TableCellHead>Rate</TableCellHead>
<Table.Header.Cell>Rate</Table.Header.Cell>
{/if}
</TableHeader>
<TableBody>
{#each planData as usage}
{#if usage['id'] === 'members'}
<TableRow>
<TableCellText title="resource">{usage.resource}</TableCellText>
<TableCellText title="limit">
{plan[usage.id] || 'Unlimited'}
</TableCellText>
{#if !isFree}
<TableCellText title="rate">
{formatCurrency(plan.addons?.member?.price)}/{usage?.unit}
</TableCellText>
{/if}
</TableRow>
{:else}
{@const addon = plan.addons[usage.id]}
<TableRow>
<TableCellText title="resource">{usage.resource}</TableCellText>
<TableCellText title="limit">
{abbreviateNumber(plan[usage.id])}{usage?.unit}
</TableCellText>
{#if !isFree}
<TableCellText title="rate">
{formatCurrency(addon?.price)}/{['MB', 'GB', 'TB'].includes(
addon?.unit
)
? addon?.value
: abbreviateNumber(addon?.value, 0)}{usage?.unit}
</TableCellText>
{/if}
</TableRow>
{/if}
{/each}
</TableBody>
</Table>
</svelte:fragment>
{#each planData as usage}
{#if usage['id'] === 'members'}
<Table.Row>
<Table.Cell>{usage.resource}</Table.Cell>
<Table.Cell>
{plan[usage.id] || 'Unlimited'}
</Table.Cell>
{#if !isFree}
<Table.Cell>
{formatCurrency(plan.addons?.member?.price)}/{usage?.unit}
</Table.Cell>
{/if}
</Table.Row>
{:else}
{@const addon = plan.addons[usage.id]}
<Table.Row>
<Table.Cell>{usage.resource}</Table.Cell>
<Table.Cell>
{abbreviateNumber(plan[usage.id])}{usage?.unit}
</Table.Cell>
{#if !isFree}
<Table.Cell>
{formatCurrency(addon?.price)}/{['MB', 'GB', 'TB'].includes(addon?.unit)
? addon?.value
: abbreviateNumber(addon?.value, 0)}{usage?.unit}
</Table.Cell>
{/if}
</Table.Row>
{/if}
{/each}
</Table.Root>
<svelte:fragment slot="footer">
<Button text on:click={() => (show = false)}>Close</Button>
</svelte:fragment>
@@ -1,20 +1,20 @@
<script lang="ts">
import { Modal } from '$lib/components';
import { Button, FormList, InputText } from '$lib/elements/forms';
import { Button, InputText } from '$lib/elements/forms';
import type { Coupon } from '$lib/sdk/billing';
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
export let show = false;
let error: string = null;
let coupon: string = '';
export let couponData: Partial<Coupon> = {
code: null,
status: null,
credits: null
};
let error: string = null;
let coupon: string = '';
const dispatch = createEventDispatcher();
async function addCoupon() {
try {
@@ -37,15 +37,20 @@
}
</script>
<Modal bind:show title="Add credits" onSubmit={addCoupon} size="big" bind:error>
Credits will be applied automatically to your next invoice.
<Modal bind:show title="Add credits" onSubmit={addCoupon} bind:error>
<svelte:fragment slot="description">
Credits will be applied automatically to your next invoice.
</svelte:fragment>
<FormList>
<InputText placeholder="Promo code" id="code" label="Add promo code" bind:value={coupon} />
</FormList>
<InputText
required
placeholder="Promo code"
id="code"
label="Add promo code"
bind:value={coupon} />
<svelte:fragment slot="footer">
<Button text on:click={() => (show = false)}>Cancel</Button>
<Button submit>Add</Button>
<Button submit disabled={coupon === ''}>Add</Button>
</svelte:fragment>
</Modal>
+5 -4
View File
@@ -6,7 +6,7 @@
type Props = ComponentProps<Selector>;
export let group: string;
export let value: string | number | boolean;
export let value: string;
export let tooltipText: string = null;
export let tooltipShow = false;
@@ -15,6 +15,7 @@
export let imageRadius: Props['imageRadius'] = 'xxs';
export let padding: Props['padding'] = 's';
export let variant: Props['variant'] = 'primary';
export let name: Props['name'] = undefined;
//temporarily unefined
export let title: Props['title'] = undefined;
export let disabled = false;
@@ -27,6 +28,7 @@
<Tooltip disabled={!tooltipText || !tooltipShow}>
<Card.Selector
{name}
{src}
{alt}
{padding}
@@ -38,10 +40,9 @@
title={title ?? slotTitle?.innerText}
bind:group>
{#if $$slots.default}
<p>
<slot />
</p>
<slot />
{/if}
<slot name="action" slot="action" />
</Card.Selector>
<span slot="tooltip">{tooltipText}</span>
</Tooltip>
+1
View File
@@ -8,6 +8,7 @@ export enum Dependencies {
CREDIT = 'dependency:credit',
INVOICES = 'dependency:invoices',
ADDRESS = 'dependency:address',
UPGRADE_PLAN = 'dependency:upgrade_plan',
PAYMENT_METHODS = 'dependency:paymentMethods',
ORGANIZATION = 'dependency:organization',
MEMBERS = 'dependency:members',
+49 -66
View File
@@ -3,14 +3,6 @@
import { log } from '$lib/stores/logs';
import { Alert, Card, Code, Copy, Id, SvgIcon, Tab, Tabs } from '../components';
import { calculateTime } from '$lib/helpers/timeConversion';
import {
TableBody,
TableCellHead,
TableCellText,
TableHeader,
TableRow,
TableScroll
} from '$lib/elements/table';
import { beforeNavigate } from '$app/navigation';
import { Pill } from '$lib/elements';
import { isCloud } from '$lib/system';
@@ -18,7 +10,7 @@
import { organization } from '$lib/stores/organization';
import { Button } from '$lib/elements/forms';
import { BillingPlan } from '$lib/constants';
import { Tooltip, Typography } from '@appwrite.io/pink-svelte';
import { Table, Tooltip, Typography } from '@appwrite.io/pink-svelte';
let selectedRequest = 'parameters';
let selectedResponse = 'logs';
@@ -223,26 +215,22 @@
</div>
{#if selectedRequest === 'parameters'}
{#if parameters?.length}
<div class="u-margin-block-start-24">
<TableScroll noMargin>
<TableHeader>
<TableCellHead>Name</TableCellHead>
<TableCellHead>Value</TableCellHead>
</TableHeader>
<TableBody>
{#each parameters as param}
<TableRow>
<TableCellText title="Key">
{param.key}
</TableCellText>
<TableCellText title="Value">
{param.value}
</TableCellText>
</TableRow>
{/each}
</TableBody>
</TableScroll>
</div>
<Table.Root>
<svelte:fragment slot="header">
<Table.Header.Cell>Name</Table.Header.Cell>
<Table.Header.Cell>Value</Table.Header.Cell>
</svelte:fragment>
{#each parameters as param}
<Table.Row>
<Table.Cell>
{param.key}
</Table.Cell>
<Table.Cell>
{param.value}
</Table.Cell>
</Table.Row>
{/each}
</Table.Root>
{/if}
<p class="text u-text-center u-padding-24">
@@ -261,26 +249,22 @@
</p>
{:else if selectedRequest === 'headers'}
{#if execution.requestHeaders.length}
<div class="u-margin-block-start-24">
<TableScroll noMargin>
<TableHeader>
<TableCellHead>Name</TableCellHead>
<TableCellHead>Value</TableCellHead>
</TableHeader>
<TableBody>
{#each execution.requestHeaders as header}
<TableRow>
<TableCellText title="Name">
{header.name}
</TableCellText>
<TableCellText title="Value">
{header.value}
</TableCellText>
</TableRow>
{/each}
</TableBody>
</TableScroll>
</div>
<Table.Root>
<svelte:fragment slot="header">
<Table.Header.Cell>Name</Table.Header.Cell>
<Table.Header.Cell>Value</Table.Header.Cell>
</svelte:fragment>
{#each execution.requestHeaders as header}
<Table.Row>
<Table.Cell>
{header.key}
</Table.Cell>
<Table.Cell>
{header.value}
</Table.Cell>
</Table.Row>
{/each}
</Table.Root>
{/if}
<p class="text u-text-center u-padding-16">
@@ -379,23 +363,22 @@
{/if}
{:else if selectedResponse === 'headers'}
{#if execution.responseHeaders.length}
<TableScroll noMargin>
<TableHeader>
<TableCellHead>Name</TableCellHead>
<TableCellHead>Value</TableCellHead>
</TableHeader>
<TableBody>
{#each execution.responseHeaders as header}
<TableRow>
<TableCellText title="Name">
{header.name}
</TableCellText>
<TableCellText title="Value"
>{header.value}</TableCellText>
</TableRow>
{/each}
</TableBody>
</TableScroll>
<Table.Root>
<svelte:fragment slot="header">
<Table.Header.Cell>Name</Table.Header.Cell>
<Table.Header.Cell>Value</Table.Header.Cell>
</svelte:fragment>
{#each execution.responseHeaders as header}
<Table.Row>
<Table.Cell>
{header.key}
</Table.Cell>
<Table.Cell>
{header.value}
</Table.Cell>
</Table.Row>
{/each}
</Table.Root>
{/if}
<p class="text u-text-center u-padding-16">
{execution.responseHeaders?.length
+6 -9
View File
@@ -1,6 +1,5 @@
<script lang="ts">
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { Alert } from '$lib/components';
import { Button, FormList, InputDomain } from '$lib/elements/forms';
import { WizardStep } from '$lib/layout';
import { sdk } from '$lib/stores/sdk';
@@ -8,6 +7,7 @@
import { func } from '$routes/(console)/project-[project]/functions/function-[function]/store';
import { domain } from './store';
import { consoleVariables } from '$routes/(console)/store';
import { Alert } from '@appwrite.io/pink-svelte';
let error = null;
const isDomainsEnabled = $consoleVariables?._APP_DOMAIN_ENABLED === true;
@@ -64,13 +64,10 @@
</div>
{/if}
{#if isSelfHosted && !isDomainsEnabled}
<Alert class="common-section" type="info">
<svelte:fragment slot="title">
Adding a domain to a self-hosted instance
</svelte:fragment>
To add a domain to a locally hosted Appwrite project, you must first configure your server
domain.
<svelte:fragment slot="buttons">
<Alert.Inline status="info" title="Adding a domain to a self-hosted instance">
To add a domain to a locally hosted Appwrite project, you must first configure your
server domain.
<svelte:fragment slot="actions">
<Button
href="https://appwrite.io/docs/advanced/self-hosting/functions#git"
external
@@ -78,6 +75,6 @@
Learn more
</Button>
</svelte:fragment>
</Alert>
</Alert.Inline>
{/if}
</WizardStep>
@@ -1,5 +1,5 @@
<script lang="ts">
import { Alert, Modal } from '$lib/components';
import { Modal } from '$lib/components';
import { Button, FormList, InputNumber, InputSelect } from '$lib/elements/forms';
import { invalidate } from '$app/navigation';
import { Dependencies } from '$lib/constants';
@@ -7,6 +7,7 @@
import { sdk } from '$lib/stores/sdk';
import type { PaymentMethodData } from '$lib/sdk/billing';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { Alert } from '@appwrite.io/pink-svelte';
export let show = false;
export let selectedPaymentMethod: PaymentMethodData;
@@ -65,9 +66,7 @@
<Modal bind:error onSubmit={handleSubmit} bind:show title="Update payment method">
{#if selectedPaymentMethod?.expired}
<Alert type="error">
<svelte:fragment slot="title">This payment method has expired</svelte:fragment>
</Alert>
<Alert.Inline status="error" title="This payment method has expired" />
{/if}
<svelte:fragment slot="description">
{#if isLinked}
@@ -13,14 +13,14 @@
import { BillingPlan, Dependencies } from '$lib/constants';
import { Button, Form, InputTags, InputText } from '$lib/elements/forms';
import { Wizard } from '$lib/layout';
import type { Coupon, PaymentList } from '$lib/sdk/billing';
import type { Coupon } from '$lib/sdk/billing';
import { tierToPlan } from '$lib/stores/billing';
import { addNotification } from '$lib/stores/notifications';
import type { Organization } from '$lib/stores/organization';
import { sdk } from '$lib/stores/sdk';
import { ID } from '@appwrite.io/console';
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
import { Fieldset, Icon, Link, Typography } from '@appwrite.io/pink-svelte';
import { Fieldset, Icon, Layout, Link, Typography } from '@appwrite.io/pink-svelte';
import { writable } from 'svelte/store';
export let data;
@@ -31,9 +31,9 @@
let showExitModal = false;
let formComponent: Form;
let isSubmitting = writable(false);
let methods: PaymentList;
let name: string;
let paymentMethodId: string;
let paymentMethodId: string =
data.paymentMethods.paymentMethods.find((method) => !!method?.last4)?.$id ?? null;
let collaborators: string[] = [];
let taxId: string;
let billingBudget: number;
@@ -43,11 +43,6 @@
previousPage = from?.url?.pathname || previousPage;
});
async function loadPaymentMethods() {
methods = await sdk.forConsole.billing.listPaymentMethods();
paymentMethodId = methods.paymentMethods.find((method) => !!method?.last4)?.$id ?? null;
}
async function create() {
try {
let org: Organization;
@@ -121,10 +116,6 @@
trackError(e, Submit.OrganizationCreate);
}
}
$: if (selectedPlan !== BillingPlan.FREE) {
loadPaymentMethods();
}
</script>
<svelte:head>
@@ -133,44 +124,49 @@
<Wizard title="Create organization" href={previousPage} bind:showExitModal confirmExit>
<Form bind:this={formComponent} onSubmit={create} bind:isSubmitting>
<Fieldset legend="Options">
<InputText
bind:value={name}
label="Organization name"
placeholder="Enter organization name"
id="name"
required />
</Fieldset>
<Fieldset legend="Select plan">
<Typography.Text>
For more details on our plans, visit our
<Link.Anchor
href="https://appwrite.io/pricing"
target="_blank"
rel="noopener noreferrer">pricing page</Link.Anchor
>.
</Typography.Text>
<PlanSelection
bind:billingPlan={selectedPlan}
anyOrgFree={data.hasFreeOrganizations}
isNewOrg />
</Fieldset>
{#if selectedPlan !== BillingPlan.FREE}
<Fieldset legend="Invite members">
<InputTags
bind:tags={collaborators}
label="Invite members by email"
placeholder="Enter email address(es)"
id="members" />
<Layout.Stack gap="xxl">
<Fieldset legend="Options">
<InputText
bind:value={name}
label="Organization name"
placeholder="Enter organization name"
id="name"
required />
</Fieldset>
<SelectPaymentMethod bind:methods bind:value={paymentMethodId} bind:taxId />
{#if !selectedCoupon?.code}
<Button text on:click={() => (showCreditModal = true)}>
<Icon icon={IconPlus} slot="start" size="s" />
Add credits
</Button>
<Fieldset legend="Select plan">
<Typography.Text>
For more details on our plans, visit our
<Link.Anchor
href="https://appwrite.io/pricing"
target="_blank"
rel="noopener noreferrer">pricing page</Link.Anchor
>.
</Typography.Text>
<PlanSelection
bind:billingPlan={selectedPlan}
anyOrgFree={data.hasFreeOrganizations}
isNewOrg />
</Fieldset>
{#if selectedPlan !== BillingPlan.FREE}
<SelectPaymentMethod
methods={data.paymentMethods}
bind:value={paymentMethodId}
bind:taxId />
<Fieldset legend="Invite members">
<InputTags
bind:tags={collaborators}
label="Invite members by email"
placeholder="Enter email address(es)"
id="members" />
</Fieldset>
{#if !selectedCoupon?.code}
<Button text on:click={() => (showCreditModal = true)}>
<Icon icon={IconPlus} slot="start" size="s" />
Add credits
</Button>
{/if}
{/if}
{/if}
</Layout.Stack>
</Form>
<svelte:fragment slot="aside">
{#if selectedPlan !== BillingPlan.FREE}
@@ -6,7 +6,10 @@ import type { Organization } from '$lib/stores/organization';
export const load: PageLoad = async ({ url, parent }) => {
const { organizations } = await parent();
const coupon = await getCoupon(url);
const [coupon, paymentMethods] = await Promise.all([
getCoupon(url),
sdk.forConsole.billing.listPaymentMethods()
]);
let plan = getPlanFromUrl(url);
const hasFreeOrganizations = organizations.teams?.some(
(org) => (org as Organization)?.billingPlan === BillingPlan.FREE
@@ -19,6 +22,7 @@ export const load: PageLoad = async ({ url, parent }) => {
plan,
coupon,
hasFreeOrganizations,
paymentMethods,
name: url.searchParams.get('name') ?? ''
};
};
@@ -8,9 +8,9 @@
import { Dependencies } from '$lib/constants';
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
import { ID } from '@appwrite.io/console';
import Alert from '$lib/components/alert.svelte';
import { isCloud } from '$lib/system';
import { base } from '$app/paths';
import { Alert } from '@appwrite.io/pink-svelte';
export let show = false;
@@ -39,18 +39,17 @@
}
</script>
<Modal title="Create new organization" {error} onSubmit={create} size="big" bind:show>
<Modal title="Create new organization" {error} onSubmit={create} bind:show>
{#if isCloud}
<Alert type="info">
<svelte:fragment slot="title">Get ready for Appwrite Pro</svelte:fragment>
<Alert.Inline status="info" title="Get ready for Appwrite Pro">
We will soon introduce the much-anticipated Pro plan. Your account will continue to have
access to <b>one free organization</b>. If you manage more than one organization, you
will need to either upgrade to the Pro plan, transfer your projects to a Pro
organization, or migrate to self-hosting.
<svelte:fragment slot="buttons">
<svelte:fragment slot="actions">
<Button href="https://appwrite.io/pricing" external text>Learn more</Button>
</svelte:fragment>
</Alert>
</Alert.Inline>
{/if}
<FormList>
<InputText
@@ -9,7 +9,6 @@
import AvailableCredit from './availableCredit.svelte';
import PaymentHistory from './paymentHistory.svelte';
import TaxId from './taxId.svelte';
import { Alert } from '$lib/components';
import { failedInvoice, paymentMethods, tierToPlan, upgradeURL } from '$lib/stores/billing';
import type { PaymentMethodData } from '$lib/sdk/billing';
import { onMount } from 'svelte';
@@ -21,7 +20,7 @@
import { selectedInvoice, showRetryModal } from './store';
import { Button } from '$lib/elements/forms';
import { goto } from '$app/navigation';
import { Typography } from '@appwrite.io/pink-svelte';
import { Alert, Typography } from '@appwrite.io/pink-svelte';
export let data;
@@ -79,9 +78,9 @@
<Container>
{#if $failedInvoice}
{#if $failedInvoice?.lastError}
<Alert type="error" class="common-section">
<Alert.Inline status="error">
The scheduled payment for {$organization.name} failed due to following error: {$failedInvoice.lastError}
<svelte:fragment slot="buttons">
<svelte:fragment slot="actions">
<Button
text
on:click={() => {
@@ -89,37 +88,33 @@
$showRetryModal = true;
}}>Try again</Button>
</svelte:fragment>
</Alert>
</Alert.Inline>
{:else}
<Alert type="error" class="common-section">
<svelte:fragment slot="title">
The scheduled payment for {$organization.name} failed
</svelte:fragment>
To avoid service disruptions in organization's your projects, please verify your payment
details and update them if necessary.
</Alert>
<Alert.Inline
status="error"
title={`The scheduled payment for ${$organization.name} failed`}>
To avoid service disruptions in organization's your projects, please verify your
payment details and update them if necessary.
</Alert.Inline>
{/if}
{/if}
{#if defaultPaymentMethod?.failed && !backupPaymentMethod}
<Alert type="error" class="common-section">
<svelte:fragment slot="title">
The default payment method for {$organization.name} has expired
</svelte:fragment>
<Alert.Inline
status="error"
title={`The default payment method for ${$organization.name} has expired`}>
To avoid service disruptions in your organization's projects, please update your payment
details.
</Alert>
</Alert.Inline>
{/if}
{#if $organization?.billingPlanDowngrade}
<Alert type="info" class="common-section">
<Alert.Inline status="info">
Your organization will change to a {tierToPlan($organization?.billingPlanDowngrade)
.name} plan once your current billing cycle ends and your invoice is paid on {toLocaleDate(
$organization.billingNextInvoiceDate
)}.
</Alert>
</Alert.Inline>
{/if}
<div class="common-section">
<Typography.Title>Billing</Typography.Title>
</div>
<Typography.Title>Billing</Typography.Title>
<PlanSummary
creditList={data?.creditList}
members={data?.members}
@@ -3,7 +3,7 @@
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { Modal } from '$lib/components';
import { Dependencies } from '$lib/constants';
import { Button, FormList, InputText } from '$lib/elements/forms';
import { Button, InputText } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { organization } from '$lib/stores/organization';
import { sdk } from '$lib/stores/sdk';
@@ -51,6 +51,6 @@
<svelte:fragment slot="footer">
<Button text on:click={() => (show = false)}>Cancel</Button>
<Button disabled={!coupon} submit>Add credits</Button>
<Button disabled={coupon === ''} submit>Add credits</Button>
</svelte:fragment>
</Modal>
@@ -34,8 +34,9 @@
let showExitModal = false;
let formComponent: Form;
let isSubmitting = writable(false);
let methods: PaymentList;
let paymentMethodId: string;
let paymentMethodId: string =
data.organization.paymentMethodId ??
data.paymentMethods.paymentMethods.find((method) => !!method?.last4)?.$id;
let collaborators: string[] =
data?.members?.memberships
?.map((m) => {
@@ -52,15 +53,6 @@
previousPage = from?.url?.pathname || previousPage;
});
async function loadPaymentMethods() {
methods = await sdk.forConsole.billing.listPaymentMethods();
paymentMethodId =
data.organization.paymentMethodId ??
methods.paymentMethods.find((method) => !!method?.last4)?.$id ??
null;
}
async function handleSubmit() {
if (isDowngrade) {
await downgrade();
@@ -184,10 +176,9 @@
$: isUpgrade = data.plan > data.organization.billingPlan;
$: isDowngrade = data.plan < data.organization.billingPlan;
$: if (selectedPlan !== BillingPlan.FREE) {
loadPaymentMethods();
}
$: isButtonDisabled = $organization.billingPlan === selectedPlan;
$: console.log(data.paymentMethods);
</script>
<svelte:head>
@@ -248,6 +239,10 @@
<!-- Show email input if upgrading from free plan -->
{#if selectedPlan !== BillingPlan.FREE && data.organization.billingPlan === BillingPlan.FREE}
<SelectPaymentMethod
methods={data.paymentMethods}
bind:value={paymentMethodId}
bind:taxId />
<Fieldset legend="Invite members">
<InputTags
bind:tags={collaborators}
@@ -255,7 +250,6 @@
placeholder="Enter email address(es)"
id="members" />
</Fieldset>
<SelectPaymentMethod bind:methods bind:value={paymentMethodId} bind:taxId />
{#if !selectedCoupon?.code}
<Button text on:click={() => (showCreditModal = true)}>
<Icon icon={IconPlus} slot="start" size="s" />
@@ -6,9 +6,13 @@ import type { Organization } from '$lib/stores/organization';
export const load: PageLoad = async ({ depends, parent, url }) => {
const { members, organization, currentPlan, organizations } = await parent();
depends(Dependencies.ORGANIZATION);
depends(Dependencies.UPGRADE_PLAN);
const [coupon, paymentMethods] = await Promise.all([
getCoupon(url),
sdk.forConsole.billing.listPaymentMethods()
]);
const coupon = await getCoupon(url);
let plan = getPlanFromUrl(url);
if (organization?.billingPlan === BillingPlan.SCALE) {
@@ -18,7 +22,6 @@ export const load: PageLoad = async ({ depends, parent, url }) => {
}
const selfService = currentPlan?.selfService ?? true;
const hasFreeOrgs = organizations.teams?.some(
(org) => (org as Organization)?.billingPlan === BillingPlan.FREE
);
@@ -28,7 +31,8 @@ export const load: PageLoad = async ({ depends, parent, url }) => {
plan,
coupon,
selfService,
hasFreeOrgs
hasFreeOrgs,
paymentMethods
};
};
@@ -12,14 +12,6 @@
import { toLocaleDate } from '$lib/helpers/date';
import { isCloud } from '$lib/system';
import type { InvoiceList } from '$lib/sdk/billing';
import {
TableBody,
TableCell,
TableCellHead,
TableHeader,
TableRow,
TableScroll
} from '$lib/elements/table';
import { formatCurrency } from '$lib/helpers/numbers';
import { tierToPlan } from '$lib/stores/billing';
import { Table, Tabs, Alert } from '@appwrite.io/pink-svelte';
@@ -163,24 +155,22 @@
{/each}
</SecondaryTabs>
<TableScroll dense noMargin>
<TableHeader>
<Table.Root>
<svelte:fragment slot="header">
{#each tabData.headers as header, index}
<TableCellHead width={index === 1 ? columnWidthSmall : columnWidth}
>{header}</TableCellHead>
<Table.Header.Cell
width={(index === 1 ? columnWidthSmall : columnWidth) + 'px'}
>{header}</Table.Header.Cell>
{/each}
</TableHeader>
<TableBody>
{#each tabData.rows as row}
<TableRow>
{#each row.cells as cell, index}
<TableCell width={index === 1 ? columnWidthSmall : columnWidth}
>{cell}</TableCell>
{/each}
</TableRow>
{/each}
</TableBody>
</TableScroll>
</svelte:fragment>
{#each tabData.rows as row}
<Table.Row>
{#each row.cells as cell}
<Table.Cell>{cell}</Table.Cell>
{/each}
</Table.Row>
{/each}
</Table.Root>
</div>
{/if}
<InputText
@@ -1,12 +1,13 @@
<script lang="ts">
import { page } from '$app/stores';
import { Alert, CopyInput, Modal } from '$lib/components';
import { CopyInput, Modal } from '$lib/components';
import { Button, FormList, InputPassword, InputSwitch, InputText } from '$lib/elements/forms';
import { oAuthProviders, type Provider } from '$lib/stores/oauth-providers';
import { sdk } from '$lib/stores/sdk';
import { onMount } from 'svelte';
import { updateOAuth } from '../updateOAuth';
import type { Models } from '@appwrite.io/console';
import { Alert } from '@appwrite.io/pink-svelte';
const projectId = $page.params.project;
@@ -59,12 +60,11 @@
label="App Secret"
placeholder="Enter App Secret"
minlength={0}
showPasswordButton
bind:value={secret} />
<Alert type="info">
<Alert.Inline status="info">
To complete the setup, create an OAuth2 client ID with "Web application" as the
application type, then add this redirect URI to your {provider.name} configuration.
</Alert>
</Alert.Inline>
<div>
<p>URI</p>
<CopyInput
@@ -1,12 +1,13 @@
<script lang="ts">
import { page } from '$app/stores';
import { Alert, CopyInput, Modal } from '$lib/components';
import { CopyInput, Modal } from '$lib/components';
import { Button, FormList, InputPassword, InputSwitch, InputText } from '$lib/elements/forms';
import { oAuthProviders, type Provider } from '$lib/stores/oauth-providers';
import { sdk } from '$lib/stores/sdk';
import { onMount } from 'svelte';
import { updateOAuth } from '../updateOAuth';
import type { Models } from '@appwrite.io/console';
import { Alert } from '@appwrite.io/pink-svelte';
const projectId = $page.params.project;
@@ -41,44 +42,41 @@
clientSecret && tenantID ? JSON.stringify({ clientSecret, tenantID }) : provider.secret;
</script>
<Modal {error} onSubmit={update} size="big" bind:show on:close>
<Modal {error} onSubmit={update} bind:show on:close>
<svelte:fragment slot="title">{provider.name} OAuth2 settings</svelte:fragment>
<FormList>
<p>
To use {provider.name} authentication in your application, first fill in this form. For more
info you can
<a class="link" href={oAuthProvider?.docs} target="_blank" rel="noopener noreferrer">
visit the docs.
</a>
</p>
<InputSwitch id="state" bind:value={enabled} label={enabled ? 'Enabled' : 'Disabled'} />
<InputText
id="appID"
label="Application (client) ID"
autofocus={true}
placeholder="Enter ID"
bind:value={appId} />
<InputPassword
id="secret"
label="Client Secret"
placeholder="Enter Client Secret"
showPasswordButton
minlength={0}
bind:value={clientSecret} />
<InputText
id="tenant"
label="Target Tenant"
placeholder="'common','organizations','consumers' or your TenantID"
bind:value={tenantID} />
<Alert type="info">
To complete set up, add this OAuth2 redirect URI to your {provider.name} app configuration.
</Alert>
<div>
<p>URI</p>
<CopyInput
value={`${sdk.forConsole.client.config.endpoint}/account/sessions/oauth2/callback/${provider.key}/${projectId}`} />
</div>
</FormList>
<p>
To use {provider.name} authentication in your application, first fill in this form. For more
info you can
<a class="link" href={oAuthProvider?.docs} target="_blank" rel="noopener noreferrer">
visit the docs.
</a>
</p>
<InputSwitch id="state" bind:value={enabled} label={enabled ? 'Enabled' : 'Disabled'} />
<InputText
id="appID"
label="Application (client) ID"
autofocus={true}
placeholder="Enter ID"
bind:value={appId} />
<InputPassword
id="secret"
label="Client Secret"
placeholder="Enter Client Secret"
minlength={0}
bind:value={clientSecret} />
<InputText
id="tenant"
label="Target Tenant"
placeholder="'common','organizations','consumers' or your TenantID"
bind:value={tenantID} />
<Alert.Inline status="info">
To complete set up, add this OAuth2 redirect URI to your {provider.name} app configuration.
</Alert.Inline>
<div>
<p>URI</p>
<CopyInput
value={`${sdk.forConsole.client.config.endpoint}/account/sessions/oauth2/callback/${provider.key}/${projectId}`} />
</div>
<svelte:fragment slot="footer">
<Button secondary on:click={() => (provider = null)}>Cancel</Button>
<Button
@@ -1,12 +1,13 @@
<script lang="ts">
import { page } from '$app/stores';
import { Alert, CopyInput, Modal } from '$lib/components';
import { CopyInput, Modal } from '$lib/components';
import { Button, FormList, InputPassword, InputSwitch, InputText } from '$lib/elements/forms';
import { oAuthProviders, type Provider } from '$lib/stores/oauth-providers';
import { sdk } from '$lib/stores/sdk';
import { onMount } from 'svelte';
import { updateOAuth } from '../updateOAuth';
import type { Models } from '@appwrite.io/console';
import { Alert } from '@appwrite.io/pink-svelte';
const projectId = $page.params.project;
@@ -66,57 +67,54 @@
<Modal {error} onSubmit={update} size="big" bind:show on:close>
<svelte:fragment slot="title">{provider.name} OAuth2 settings</svelte:fragment>
<FormList>
<p>
To use {provider.name} authentication in your application, first fill in this form. For more
info you can
<a class="link" href={oAuthProvider?.docs} target="_blank" rel="noopener noreferrer"
>visit the docs.</a>
</p>
<InputSwitch id="state" bind:value={enabled} label={enabled ? 'Enabled' : 'Disabled'} />
<InputText
id="appID"
label="Client ID"
autofocus={true}
placeholder="Enter ID"
bind:value={appId} />
<InputPassword
id="secret"
label="Client Secret"
placeholder="Enter Client Secret"
minlength={0}
showPasswordButton
bind:value={clientSecret} />
<InputText
id="well-known-endpoint"
label="Well-Known Endpoint"
placeholder="https://example.com/.well-known/openid-configuration"
bind:value={wellKnownEndpoint} />
<InputText
id="authorization-endpoint"
label="Authorization Endpoint"
placeholder="https://example.com/authorize"
bind:value={authorizationEndpoint} />
<InputText
id="token-endpoint"
label="Token Endpoint"
placeholder="https://example.com/token"
bind:value={tokenEndpoint} />
<InputText
id="userinfo-endpoint"
label="User Info Endpoint"
placeholder="https://example.com/userinfo"
bind:value={userinfoEndpoint} />
<p>
To use {provider.name} authentication in your application, first fill in this form. For more
info you can
<a class="link" href={oAuthProvider?.docs} target="_blank" rel="noopener noreferrer"
>visit the docs.</a>
</p>
<InputSwitch id="state" bind:value={enabled} label={enabled ? 'Enabled' : 'Disabled'} />
<InputText
id="appID"
label="Client ID"
autofocus={true}
placeholder="Enter ID"
bind:value={appId} />
<InputPassword
id="secret"
label="Client Secret"
placeholder="Enter Client Secret"
minlength={0}
bind:value={clientSecret} />
<InputText
id="well-known-endpoint"
label="Well-Known Endpoint"
placeholder="https://example.com/.well-known/openid-configuration"
bind:value={wellKnownEndpoint} />
<InputText
id="authorization-endpoint"
label="Authorization Endpoint"
placeholder="https://example.com/authorize"
bind:value={authorizationEndpoint} />
<InputText
id="token-endpoint"
label="Token Endpoint"
placeholder="https://example.com/token"
bind:value={tokenEndpoint} />
<InputText
id="userinfo-endpoint"
label="User Info Endpoint"
placeholder="https://example.com/userinfo"
bind:value={userinfoEndpoint} />
<Alert type="info">
To complete set up, add this OAuth2 redirect URI to your {provider.name} app configuration.
</Alert>
<div>
<p>URI</p>
<CopyInput
value={`${sdk.forConsole.client.config.endpoint}/account/sessions/oauth2/callback/${provider.key}/${projectId}`} />
</div>
</FormList>
<Alert.Inline status="info">
To complete set up, add this OAuth2 redirect URI to your {provider.name} app configuration.
</Alert.Inline>
<div>
<p>URI</p>
<CopyInput
value={`${sdk.forConsole.client.config.endpoint}/account/sessions/oauth2/callback/${provider.key}/${projectId}`} />
</div>
<svelte:fragment slot="footer">
<Button secondary on:click={() => (provider = null)}>Cancel</Button>
<Button
@@ -1,12 +1,13 @@
<script lang="ts">
import { page } from '$app/stores';
import { Alert, CopyInput, Modal } from '$lib/components';
import { Button, FormList, InputPassword, InputSwitch, InputText } from '$lib/elements/forms';
import { CopyInput, Modal } from '$lib/components';
import { Button, InputPassword, InputSwitch, InputText } from '$lib/elements/forms';
import { oAuthProviders, type Provider } from '$lib/stores/oauth-providers';
import { sdk } from '$lib/stores/sdk';
import { onMount } from 'svelte';
import { updateOAuth } from '../updateOAuth';
import type { Models } from '@appwrite.io/console';
import { Alert } from '@appwrite.io/pink-svelte';
const projectId = $page.params.project;
@@ -48,47 +49,44 @@
<Modal {error} onSubmit={update} size="big" bind:show on:close>
<svelte:fragment slot="title">{provider.name} OAuth2 settings</svelte:fragment>
<FormList>
<p>
To use {provider.name} authentication in your application, first fill in this form. For more
info you can
<a class="link" href={oAuthProvider?.docs} target="_blank" rel="noopener noreferrer"
>visit the docs.</a>
</p>
<InputSwitch id="state" bind:value={enabled} label={enabled ? 'Enabled' : 'Disabled'} />
<InputText
id="appID"
label="Client ID"
autofocus={true}
placeholder="Enter ID"
bind:value={appId} />
<InputPassword
id="secret"
label="Client Secret"
placeholder="Enter Client Secret"
minlength={0}
showPasswordButton
bind:value={clientSecret} />
<InputText
id="domain"
label="Okta Domain"
placeholder="dev-1337.okta.com"
bind:value={oktaDomain} />
<InputText
id="serverID"
label="Authorization Server ID"
placeholder="default"
bind:value={authorizationServerId} />
<p>
To use {provider.name} authentication in your application, first fill in this form. For more
info you can
<a class="link" href={oAuthProvider?.docs} target="_blank" rel="noopener noreferrer"
>visit the docs.</a>
</p>
<InputSwitch id="state" bind:value={enabled} label={enabled ? 'Enabled' : 'Disabled'} />
<InputText
id="appID"
label="Client ID"
autofocus={true}
placeholder="Enter ID"
bind:value={appId} />
<InputPassword
id="secret"
label="Client Secret"
placeholder="Enter Client Secret"
minlength={0}
bind:value={clientSecret} />
<InputText
id="domain"
label="Okta Domain"
placeholder="dev-1337.okta.com"
bind:value={oktaDomain} />
<InputText
id="serverID"
label="Authorization Server ID"
placeholder="default"
bind:value={authorizationServerId} />
<Alert type="info">
To complete set up, add this OAuth2 redirect URI to your {provider.name} app configuration.
</Alert>
<div>
<p>URI</p>
<CopyInput
value={`${sdk.forConsole.client.config.endpoint}/account/sessions/oauth2/callback/${provider.key}/${projectId}`} />
</div>
</FormList>
<Alert.Inline status="info">
To complete set up, add this OAuth2 redirect URI to your {provider.name} app configuration.
</Alert.Inline>
<div>
<p>URI</p>
<CopyInput
value={`${sdk.forConsole.client.config.endpoint}/account/sessions/oauth2/callback/${provider.key}/${projectId}`} />
</div>
<svelte:fragment slot="footer">
<Button secondary on:click={() => (provider = null)}>Cancel</Button>
<Button
@@ -277,35 +277,33 @@
</p>
{#if relAttributes?.length}
<TableScroll noMargin>
<TableHeader>
<TableCellHead width={50}>Relation</TableCellHead>
<TableCellHead width={50}>Setting</TableCellHead>
<TableCellHead width={200} />
</TableHeader>
<TableBody>
{#each relAttributes as attr}
<TableRow>
<TableCell title="relation">
<span class="u-flex u-cross-center u-gap-8">
{#if attr.twoWay}
<span class="icon-switch-horizontal" />
{:else}
<span class="icon-arrow-sm-right" />
{/if}
<span data-private>{attr.key}</span>
</span>
</TableCell>
<TableCellText title="Settings">
{attr.onDelete}
</TableCellText>
<TableCellText title="description">
{Deletion[attr.onDelete]}
</TableCellText>
</TableRow>
{/each}
</TableBody>
</TableScroll>
<Table.Root>
<svelte:fragment slot="header">
<Table.Header.Cell width="50px">Relation</Table.Header.Cell>
<Table.Header.Cell width="50px">Setting</Table.Header.Cell>
<Table.Header.Cell />
</svelte:fragment>
{#each relAttributes as attr}
<Table.Row>
<Table.Cell>
<span class="u-flex u-cross-center u-gap-8">
{#if attr.twoWay}
<span class="icon-switch-horizontal" />
{:else}
<span class="icon-arrow-sm-right" />
{/if}
<span data-private>{attr.key}</span>
</span>
</Table.Cell>
<Table.Cell>
{attr.onDelete}
</Table.Cell>
<Table.Cell>
{Deletion[attr.onDelete]}
</Table.Cell>
</Table.Row>
{/each}
</Table.Root>
<div class="u-flex u-flex-vertical u-gap-16">
<Alert>To change the selection edit the relationship settings.</Alert>
@@ -1,11 +1,12 @@
<script lang="ts">
import { Button } from '$lib/elements/forms';
import { Modal, Code, Alert } from '$lib/components';
import { Modal, Code } from '$lib/components';
import { onMount } from 'svelte';
import { page } from '$app/stores';
import { func } from '../store';
import SecondaryTabs from '$lib/components/secondaryTabs.svelte';
import SecondaryTabsItem from '$lib/components/secondaryTabsItem.svelte';
import { Alert } from '@appwrite.io/pink-svelte';
export let show = false;
@@ -89,14 +90,14 @@
function's folder.
</p>
<Alert dismissible={false} type="warning">
<Alert.Inline dismissible={false} status="warning">
If you did not create your function using the CLI, initialize your function by following our <a
href="https://appwrite.io/docs/tooling/command-line/installation"
target="_blank"
rel="noopener noreferrer"
class="link">documentation</a
>.
</Alert>
</Alert.Inline>
<div class="editor-border box">
<SecondaryTabs large class="u-sep-block-end u-padding-8">
@@ -7,7 +7,7 @@
InputText,
InputTextarea
} from '$lib/elements/forms';
import { Alert, Collapsible, CollapsibleItem, Modal } from '$lib/components';
import { Collapsible, CollapsibleItem, Modal } from '$lib/components';
import { sdk } from '$lib/stores/sdk';
import { createEventDispatcher } from 'svelte';
import { page } from '$app/stores';
@@ -15,6 +15,7 @@
import { Dependencies } from '$lib/constants';
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
import { func } from '../store';
import { Alert } from '@appwrite.io/pink-svelte';
export let show = false;
@@ -79,18 +80,15 @@
required={true} />
{#if $func.version === 'v2'}
<Alert type="info">
<svelte:fragment slot="title">
Build commands now available for functions v3.0
</svelte:fragment>
<Alert.Inline status="info" title="Build commands now available for functions v3.0">
Update your function version to make use of new features including build commands.
<svelte:fragment slot="buttons">
<svelte:fragment slot="actions">
<Button
href="https://appwrite.io/docs/products/functions/development"
external
text>Learn more</Button>
</svelte:fragment>
</Alert>
</Alert.Inline>
{:else}
<Collapsible>
<CollapsibleItem>
@@ -1,6 +1,6 @@
<script lang="ts">
import { goto, invalidate } from '$app/navigation';
import { Alert, Empty, EmptyFilter, PaginationWithLimit, ViewSelector } from '$lib/components';
import { Empty, EmptyFilter, PaginationWithLimit, ViewSelector } from '$lib/components';
import { Dependencies } from '$lib/constants';
import { Button } from '$lib/elements/forms';
import { Container } from '$lib/layout';
@@ -9,7 +9,7 @@
import { project } from '$routes/(console)/project-[project]/store';
import { base } from '$app/paths';
import { View } from '$lib/helpers/load';
import { Icon, Layout } from '@appwrite.io/pink-svelte';
import { Alert, Icon, Layout, Link } from '@appwrite.io/pink-svelte';
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
import Table from './table.svelte';
import { columns } from './store';
@@ -17,9 +17,7 @@
export let data;
let showMobileFilters = false;
onMount(() => {
data?.query ? (showMobileFilters = true) : (showMobileFilters = false);
return sdk.forConsole.client.subscribe('console', (response) => {
if (response.events.includes('functions.*.executions.*')) {
invalidate(Dependencies.EXECUTIONS);
@@ -51,15 +49,13 @@
{#if !data.func.logging}
<div class="common-section">
<Alert type="info" isStandalone>
<svelte:fragment slot="title">Your execution logs are disabled</svelte:fragment>
<Alert.Inline status="info" title="Your execution logs are disabled">
To view execution logs and errors, enable them in your
<a
href={`${base}/project-${$project.$id}/functions/function-${data.func.$id}/settings`}
class="link">
Function settings</a
<Link.Anchor
href={`${base}/project-${$project.$id}/functions/function-${data.func.$id}/settings`}>
Function settings</Link.Anchor
>.
</Alert>
</Alert.Inline>
</div>
{/if}
{#if data?.executions?.total}
@@ -1,6 +1,6 @@
<script lang="ts">
import { MessagingProviderType, type Models } from '@appwrite.io/console';
import { CardGrid, Empty, PaginationInline, EmptySearch, Alert } from '$lib/components';
import { CardGrid, Empty, PaginationInline, EmptySearch } from '$lib/components';
import { onMount } from 'svelte';
import { sdk } from '$lib/stores/sdk';
import { invalidate } from '$app/navigation';
@@ -12,7 +12,7 @@
import UserTargetsModal from '../userTargetsModal.svelte';
import { isValueOfStringEnum } from '$lib/helpers/types';
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
import { Icon, Layout, Table, Typography } from '@appwrite.io/pink-svelte';
import { Alert, Icon, Layout, Table, Typography } from '@appwrite.io/pink-svelte';
export let message: Models.Message & { data: Record<string, unknown> };
export let selectedTargetsById: Record<string, Models.Target>;
@@ -130,20 +130,18 @@
{#if hasDeletedUsers}
<div class:u-padding-block-end-16={dataSource.length}>
{#if hasDeletedUsers && !dataSource.length}
<Alert type="info">
<svelte:fragment slot="title"
>There are no targets to show</svelte:fragment>
This message was sent to users who are no longer available, so their information
cannot be displayed.
</Alert>
<Alert.Inline status="info" title="There are no targets to show">
This message was sent to users who are no longer available, so their
information cannot be displayed.
</Alert.Inline>
{:else}
<Alert
type="info"
<Alert.Inline
status="info"
dismissible={dataSource.length > 0}
on:dismiss={() => (hasDeletedUsers = false)}>
This message was sent to users who are no longer available, so their
information cannot be displayed.
</Alert>
</Alert.Inline>
{/if}
</div>
{/if}
@@ -26,6 +26,7 @@
import { targetsById } from '../../store';
import { MessagingProviderType, type Models } from '@appwrite.io/console';
import type { Column } from '$lib/helpers/types';
import { Selector, Table } from '@appwrite.io/pink-svelte';
export let columns: Column[];
export let data: PageData;
@@ -80,67 +81,54 @@
});
</script>
<TableScroll>
<TableHeader>
<TableCellHeadCheck
bind:selected={selectedIds}
pageItemsIds={data.subscribers.subscribers.map((d) => d.$id)} />
<Table.Root>
<svelte:fragment slot="header">
<Table.Header.Selector width="40px" />
{#each columns as column}
{#if column.show}
<TableCellHead width={column.width}>{column.title}</TableCellHead>
<Table.Header.Cell width={column.width + 'px'}>{column.title}</Table.Header.Cell>
{/if}
{/each}
</TableHeader>
<TableBody>
{#each data.subscribers.subscribers as subscriber (subscriber.$id)}
{@const target = subscriber.target}
<TableRowLink
href={`${base}/project-${$project.$id}/auth/user-${subscriber.target.userId}`}>
<TableCellCheck bind:selectedIds id={subscriber.$id} />
</svelte:fragment>
{#each data.subscribers.subscribers as subscriber (subscriber.$id)}
{@const target = subscriber.target}
<Table.Link href={`${base}/project-${$project.$id}/auth/user-${subscriber.target.userId}`}>
<Table.Cell>
<Selector.Checkbox size="s" />
</Table.Cell>
{#each columns as column}
{#if column.show}
{#each columns as column}
{#if column.show}
<Table.Cell>
{#if column.id === '$id'}
{#key column.id}
<TableCell title="Subscriber ID">
<Id value={subscriber.$id}>
{subscriber.$id}
</Id>
</TableCell>
<Id value={subscriber.$id}>
{subscriber.$id}
</Id>
{/key}
{:else if column.id === 'targetId'}
<TableCell title={column.title}>
<Id value={subscriber[column.id]}>
{subscriber[column.id]}
</Id>
</TableCell>
{:else if column.id === 'target'}
<TableCell title={column.title}>
{#if target.providerType === MessagingProviderType.Push}
{target.name}
{:else}
{target.identifier}
{/if}
</TableCell>
{:else if column.id === 'type'}
<TableCellText title={column.title} width={column.width}>
<ProviderType type={subscriber.target.providerType} size="s" />
</TableCellText>
{:else if column.id === '$createdAt'}
<TableCellText title={column.title} width={column.width}>
{toLocaleDateTime(subscriber[column.id])}
</TableCellText>
{:else}
<TableCellText title={column.title} width={column.width}>
<Id value={subscriber[column.id]}>
{subscriber[column.id]}
</TableCellText>
</Id>
{:else if column.id === 'target'}
{#if target.providerType === MessagingProviderType.Push}
{target.name}
{:else}
{target.identifier}
{/if}
{:else if column.id === 'type'}
<ProviderType type={subscriber.target.providerType} size="s" />
{:else if column.id === '$createdAt'}
{toLocaleDateTime(subscriber[column.id])}
{:else}
{subscriber[column.id]}
{/if}
{/if}
{/each}
</TableRowLink>
{/each}
</TableBody>
</TableScroll>
</Table.Cell>
{/if}
{/each}
</Table.Link>
{/each}
</Table.Root>
<FloatingActionBar show={selectedIds.length > 0}>
<div class="u-flex u-cross-center u-main-space-between actions">
@@ -1,21 +1,13 @@
<script lang="ts">
import { Card } from '$lib/components';
import { Button } from '$lib/elements/forms';
import {
Table,
TableBody,
TableCell,
TableCellHead,
TableHeader,
TableRow
} from '$lib/elements/table';
import { WizardStep } from '$lib/layout';
import { messageParams, providerType, targetsById, getTotal } from './store';
import Actions from '../actions.svelte';
import { topicsById } from '../store';
import type { Models } from '@appwrite.io/console';
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
import { Icon } from '@appwrite.io/pink-svelte';
import { Icon, Table } from '@appwrite.io/pink-svelte';
let showTopics = false;
let showUserTargets = false;
@@ -24,7 +16,6 @@
function addTopics(event: CustomEvent<Record<string, Models.Topic>>) {
$topicsById = event.detail;
showDropdown = false;
}
function removeTopic(topicId: string) {
@@ -34,7 +25,6 @@
function addTargets(event: CustomEvent<Record<string, Models.Target>>) {
$targetsById = event.detail;
showDropdown = false;
}
function removeTarget(targetId: string) {
@@ -80,65 +70,63 @@
</Card>
{:else}
<div class="table-wrapper">
<Table noMargin noStyles>
<TableHeader>
<TableCellHead width={140}>Target</TableCellHead>
<TableCellHead width={32} />
</TableHeader>
<TableBody>
{#each Object.entries($topicsById) as [topicId, topic] (topicId)}
<TableRow>
<TableCell title="Target">
<div class="u-flex u-cross-center">
<span class="title">
<span class="u-line-height-1-5">
<span class="body-text-2 u-bold" data-private>
{topic.name}
</span>
<span class="collapsible-button-optional">
({getTotal(topic)} targets)
</span>
</span></span>
</div>
</TableCell>
<TableCell title="Remove" width={40}>
<div class="u-flex u-main-end">
<button
class="button is-text is-only-icon"
type="button"
aria-label="delete"
on:click={() => removeTopic(topicId)}>
<span class="icon-x u-font-size-20" aria-hidden="true" />
</button>
</div>
</TableCell>
</TableRow>
{/each}
{#each Object.entries($targetsById) as [targetId, target] (targetId)}
<TableRow>
<TableCell title="Target">
<div class="u-flex u-cross-center">
<span class="text">
{target.name ? target.name : target.identifier}
</span>
</div>
</TableCell>
<Table.Root>
<svelte:fragment slot="header">
<Table.Header.Cell>Target</Table.Header.Cell>
<Table.Header.Cell width="32px" />
</svelte:fragment>
{#each Object.entries($topicsById) as [topicId, topic] (topicId)}
<Table.Row>
<Table.Cell>
<div class="u-flex u-cross-center">
<span class="title">
<span class="u-line-height-1-5">
<span class="body-text-2 u-bold" data-private>
{topic.name}
</span>
<span class="collapsible-button-optional">
({getTotal(topic)} targets)
</span>
</span></span>
</div>
</Table.Cell>
<Table.Cell>
<div class="u-flex u-main-end">
<button
class="button is-text is-only-icon"
type="button"
aria-label="delete"
on:click={() => removeTopic(topicId)}>
<span class="icon-x u-font-size-20" aria-hidden="true" />
</button>
</div>
</Table.Cell>
</Table.Row>
{/each}
{#each Object.entries($targetsById) as [targetId, target] (targetId)}
<Table.Row>
<Table.Cell>
<div class="u-flex u-cross-center">
<span class="text">
{target.name ? target.name : target.identifier}
</span>
</div>
</Table.Cell>
<TableCell title="Remove" width={40}>
<div class="u-flex u-main-end">
<button
class="button is-text is-only-icon"
type="button"
aria-label="delete"
on:click={() => removeTarget(targetId)}>
<span class="icon-x u-font-size-20" aria-hidden="true" />
</button>
</div>
</TableCell>
</TableRow>
{/each}
</TableBody>
</Table>
<Table.Cell>
<div class="u-flex u-main-end">
<button
class="button is-text is-only-icon"
type="button"
aria-label="delete"
on:click={() => removeTarget(targetId)}>
<span class="icon-x u-font-size-20" aria-hidden="true" />
</button>
</div>
</Table.Cell>
</Table.Row>
{/each}
</Table.Root>
</div>
<Actions
providerType={$providerType}
@@ -1,12 +1,13 @@
<script lang="ts">
import { invalidate } from '$app/navigation';
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
import { Alert, CardGrid } from '$lib/components';
import { CardGrid } from '$lib/components';
import { Dependencies } from '$lib/constants';
import { Button, Form, FormList } from '$lib/elements/forms';
import { Button, Form } from '$lib/elements/forms';
import { diffDays } from '$lib/helpers/date';
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
import { Alert } from '@appwrite.io/pink-svelte';
import { project } from '../../../store';
import ExpirationInput from '../expirationInput.svelte';
import { key } from './store';
@@ -49,18 +50,20 @@
Set a date after which your API key will expire.
<svelte:fragment slot="aside">
{#if isExpired}
<Alert type="error" dismissible on:dismiss={() => (alertsDismissed = true)}>
<span slot="title">Your API key has expired</span>
<p>
For security reasons, we recommend you delete your expired key and create a
new one.
</p>
</Alert>
<Alert.Inline
status="error"
on:dismiss={() => (alertsDismissed = true)}
title="Your API key has expired">
For security reasons, we recommend you delete your expired key and create a new
one.
</Alert.Inline>
{:else if isExpiring}
<Alert type="warning" dismissible on:dismiss={() => (alertsDismissed = true)}>
<span slot="title">Your API key is about to expire</span>
<p>Update the expiration date to keep the key active</p>
</Alert>
<Alert.Inline
status="warning"
on:dismiss={() => (alertsDismissed = true)}
title="Your API key is about to expire">
Update the expiration date to keep the key active
</Alert.Inline>
{/if}
<ExpirationInput bind:value={expiration} />
</svelte:fragment>
@@ -106,47 +106,43 @@
</script>
<Modal title="Export to self-hosted instance" bind:show {onSubmit}>
<FormList>
<Alert isStandalone>
<svelte:fragment slot="title">API key creation</svelte:fragment>
By initiating the transfer, an API key will be automatically generated in the background,
which you can delete after completion
</Alert>
<Alert isStandalone>
<svelte:fragment slot="title">API key creation</svelte:fragment>
By initiating the transfer, an API key will be automatically generated in the background, which
you can delete after completion
</Alert>
<InputText
label="Endpoint self-hosted instance"
required
id="endpoint"
placeholder="https://[YOUR_APPWRITE_HOSTNAME]"
autofocus
on:input={(e) => {
if (!submitted) return;
const input = e.target;
const value = input.value;
<InputText
label="Endpoint self-hosted instance"
required
id="endpoint"
placeholder="https://[YOUR_APPWRITE_HOSTNAME]"
autofocus
on:input={(e) => {
if (!submitted) return;
const input = e.target;
const value = input.value;
if (!isValidEndpoint(value)) {
input.setCustomValidity('Please enter a valid endpoint');
} else {
input.setCustomValidity('');
}
input.reportValidity();
}} />
if (!isValidEndpoint(value)) {
input.setCustomValidity('Please enter a valid endpoint');
} else {
input.setCustomValidity('');
}
input.reportValidity();
}} />
<Box>
<p class="u-bold">
Share your feedback: why our self-hosted solution works better for you
</p>
<p class="u-margin-block-start-8">
We appreciate your continued support and we understand that our self-hosted solution
might better fit your needs. To help us improve our Cloud solution, please share why
it works better for you. Your feedback is important to us and we'll use it to make
our services better.
</p>
<div class="u-margin-block-start-24">
<InputTextarea id="feedback" label="Your feedback" placeholder="Type here..." />
</div>
</Box>
</FormList>
<Box>
<p class="u-bold">Share your feedback: why our self-hosted solution works better for you</p>
<p class="u-margin-block-start-8">
We appreciate your continued support and we understand that our self-hosted solution
might better fit your needs. To help us improve our Cloud solution, please share why it
works better for you. Your feedback is important to us and we'll use it to make our
services better.
</p>
<div class="u-margin-block-start-24">
<InputTextarea id="feedback" label="Your feedback" placeholder="Type here..." />
</div>
</Box>
<div class="u-flex u-gap-16 u-cross-center" slot="footer">
<span> You will be redirected to your self-hosted instance </span>
+4 -8
View File
@@ -8,8 +8,7 @@
import { page } from '$app/stores';
import { onMount } from 'svelte';
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
import { Alert } from '$lib/components';
import { Layout, Link, Typography } from '@appwrite.io/pink-svelte';
import { Layout, Link, Typography, Alert } from '@appwrite.io/pink-svelte';
let teamId: string, membershipId: string, userId: string, secret: string;
let terms = false;
@@ -54,13 +53,10 @@
</svelte:fragment>
<svelte:fragment>
{#if !userId || !secret || !membershipId || !teamId}
<Alert type="warning">
<svelte:fragment slot="title">The invite link is not valid</svelte:fragment>
<Alert.Inline status="warning" title="The invite link is not valid">
Please ask the project owner to send you a new invite.
</Alert>
<div class="u-flex u-main-end u-margin-block-start-40">
<Button href={`${base}/register`}>Sign up to Appwrite</Button>
</div>
</Alert.Inline>
<Button href={`${base}/register`}>Sign up to Appwrite</Button>
{:else}
<Layout.Stack>
<Typography.Text>