Merge branch 'feat-pink-v2' into file-tokens

This commit is contained in:
Darshan
2025-02-20 15:55:03 +05:30
committed by GitHub
27 changed files with 728 additions and 686 deletions
+1 -1
View File
@@ -21,7 +21,7 @@
"dependencies": {
"@appwrite.io/console": "https://pkg.pr.new/appwrite/appwrite/@appwrite.io/console@d13e864",
"@appwrite.io/pink-icons": "0.25.0",
"@appwrite.io/pink-icons-svelte": "https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@ffaca61",
"@appwrite.io/pink-icons-svelte": "https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@2ff4e332",
"@appwrite.io/pink-legacy": "^1.0.1",
"@appwrite.io/pink-svelte": "https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@ffaca61",
"@appwrite.io/pink-svelte": "https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@134aa5d",
+5 -5
View File
@@ -15,8 +15,8 @@ importers:
specifier: 0.25.0
version: 0.25.0
'@appwrite.io/pink-icons-svelte':
specifier: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@ffaca61
version: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@ffaca61(svelte@4.2.19)
specifier: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@2ff4e332
version: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@2ff4e332(svelte@4.2.19)
'@appwrite.io/pink-legacy':
specifier: ^1.0.1
version: 1.0.1
@@ -223,8 +223,8 @@ packages:
peerDependencies:
svelte: ^4.0.0
'@appwrite.io/pink-icons-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@ffaca61c9e94c624744b168517346fb0f5b04d84':
resolution: {tarball: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@ffaca61c9e94c624744b168517346fb0f5b04d84}
'@appwrite.io/pink-icons-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@2ff4e332':
resolution: {tarball: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@2ff4e332}
version: 1.0.0-next.7
peerDependencies:
svelte: ^4.0.0
@@ -4105,7 +4105,7 @@ snapshots:
dependencies:
svelte: 4.2.19
'@appwrite.io/pink-icons-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@ffaca61c9e94c624744b168517346fb0f5b04d84(svelte@4.2.19)':
'@appwrite.io/pink-icons-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@2ff4e332(svelte@4.2.19)':
dependencies:
svelte: 4.2.19
+3 -1
View File
@@ -24,7 +24,9 @@
{#if total > 2}
<Avatar {size}>
+{total - 2}
<span style:font-size="10px">
+{total - 2}
</span>
</Avatar>
{/if}
</AvatarGroup>
+25 -2
View File
@@ -44,6 +44,7 @@
import { isTabletViewport } from '$lib/stores/viewport';
import { isCloud } from '$lib/system.js';
import { user } from '$lib/stores/user';
import { trackEvent } from '$lib/actions/analytics';
let showSupport = false;
@@ -68,6 +69,10 @@
: 'light'
: theme;
trackEvent('select_theme', {
value: theme === 'auto' ? 'system' : theme
});
app.update(() => ({
themeInUse: themeInUse,
theme: theme
@@ -130,12 +135,24 @@
<Button.Anchor
size="s"
variant="primary"
on:click={() => {
trackEvent('click_organization_upgrade', {
from: 'button',
source: 'top_nav'
});
}}
href={`${base}/organization-${currentOrg.$id}/change-plan`}
>Upgrade</Button.Anchor>
{/if}
<DropList show={$feedback.show} class="extended-width">
<Button.Button type="button" variant="compact" on:click={() => toggleFeedback()}
<Button.Button
type="button"
variant="compact"
on:click={() => {
toggleFeedback();
trackEvent('click_menu_feedback', { source: 'top_nav' });
}}
>Feedback
</Button.Button>
<svelte:fragment slot="other">
@@ -152,7 +169,10 @@
<Button.Button
variant="compact"
type="button"
on:click={() => (showSupport = !showSupport)}>
on:click={() => {
showSupport = !showSupport;
trackEvent('click_menu_support', { source: 'top_nav' });
}}>
Support
</Button.Button>
@@ -174,6 +194,9 @@
<Link.Button
on:click={() => {
showAccountMenu = !showAccountMenu;
if (showAccountMenu) {
trackEvent('click_menu_dropdown');
}
}}>
<div style:user-select="none">
<Avatar size="s" src={avatar} />
+3 -14
View File
@@ -1,21 +1,10 @@
<div class="u-flex-vertical u-gap-16">
<div>
<slot />
</div>
<style>
div {
color: hsl(var(--color-neutral-50));
line-height: 1.25rem;
&:first-child {
color: hsl(var(--color-neutral-70));
}
}
:global(.theme-dark) div {
color: hsl(var(--color-neutral-20));
&:first-child {
color: hsl(var(--color-neutral-10));
}
max-width: 360px;
text-wrap: auto;
}
</style>
+49 -45
View File
@@ -5,58 +5,62 @@
import { organization } from '$lib/stores/organization';
import { BillingPlan } from '$lib/constants';
import Button from '$lib/elements/forms/button.svelte';
import { Badge, Layout, Link, Tag, Typography } from '@appwrite.io/pink-svelte';
</script>
<Base>
{#if isCloud}
{#if $organization?.billingPlan !== BillingPlan.FREE}
<div class="u-flex-vertical u-gap-8">
<p>
<span class="u-bold">Roles</span>
{#if $organization?.billingPlan === BillingPlan.FREE}
<span class="inline-tag u-normal u-x-small">Pro plan</span>
{/if}
</p>
<p>Owner, Developer, Editor, Analyst and Billing.</p>
</div>
<p>
<Button link external href="https://appwrite.io/docs/advanced/platform/roles"
>Learn more</Button> about roles.
</p>
{:else}
<div class="u-flex-vertical u-gap-8">
<p>
<span class="u-bold">Roles</span>
{#if $organization?.billingPlan === BillingPlan.FREE}
<span class="inline-tag u-normal u-x-small">Pro plan</span>
{/if}
</p>
<p>
<Layout.Stack>
{#if isCloud}
{#if $organization?.billingPlan !== BillingPlan.FREE}
<Typography.Text variant="m-600">Roles</Typography.Text>
<Typography.Text>Owner, Developer, Editor, Analyst and Billing.</Typography.Text>
<Typography.Text>
<Link.Anchor
target="_blank"
rel="noopener noreferrer"
href="https://appwrite.io/docs/advanced/platform/roles"
>Learn more</Link.Anchor>
</Typography.Text>
{:else}
<Layout.Stack direction="row" gap="s">
<Typography.Text variant="m-600">
<span class="u-bold">Roles</span>
</Typography.Text>
<Badge variant="secondary" size="xs" content="Pro plan" />
</Layout.Stack>
<Typography.Text>
Upgrade to Pro to assign new roles to members such as Owner, Developer, Editor
or Analyst.
</Typography.Text>
<p class="u-flex u-main-end u-cross-center u-gap-4">
<Button
size="s"
text
external
href="https://appwrite.io/docs/advanced/platform/roles">Learn more</Button>
<Button size="s" secondary external href={$upgradeURL}>Upgrade plan</Button>
</p>
</div>
{/if}
{:else}
<Layout.Stack direction="row" gap="s">
<Typography.Text variant="m-600">
<span class="u-bold">Roles</span>
</Typography.Text>
<Badge variant="secondary" size="xs" content="Cloud" />
</Layout.Stack>
<Typography.Text>
Upgrade to Cloud to assign new roles to members or ask us about our enterprise self
hosted offering.
</Typography.Text>
<p class="u-flex u-main-end u-cross-center u-gap-4">
<Button text external href="https://appwrite.io/docs/advanced/platform/roles"
>Learn more</Button>
<Button secondary external href={$upgradeURL}>Upgrade plan</Button>
<Button
size="s"
text
external
href="https://appwrite.io/docs/advanced/platform/roles">Learn more</Button>
<Button size="s" secondary external href={$upgradeURL}>Upgrade to Cloud</Button>
</p>
{/if}
{:else}
<div class="u-flex-vertical u-gap-8">
<p>
<span class="u-bold">Roles</span>
<span class="inline-tag u-normal u-x-small">Appwrite Cloud</span>
</p>
<p>
Upgrade to Cloud to assign new roles to members or ask us about our entriprise self
hosted offering.
</p>
</div>
<p class="u-flex u-main-end u-cross-center u-gap-4">
<Button text external href="https://appwrite.io/docs/advanced/platform/roles"
>Learn more</Button>
<Button secondary external href={$upgradeURL}>Upgrade to Cloud</Button>
</p>
{/if}
</Layout.Stack>
</Base>
+31 -6
View File
@@ -34,6 +34,7 @@
import MobileFeedbackModal from '$routes/(console)/wizard/feedback/mobileFeedbackModal.svelte';
import { getSidebarState, updateSidebarState } from '$lib/helpers/sidebar';
import { isTabletViewport } from '$lib/stores/viewport';
import { trackEvent } from '$lib/actions/analytics';
type $$Props = HTMLElement & {
state?: 'closed' | 'open' | 'icons';
@@ -66,6 +67,7 @@
$: state = $isTabletViewport ? 'closed' : getSidebarState();
$: pathname = $page.url.pathname;
$: isOnProjectSettings = /^\/console\/project-[a-zA-Z0-9-]+\/settings$/.test(pathname);
const projectOptions = [
{ name: 'Auth', icon: IconUserGroup, slug: 'auth' },
@@ -111,6 +113,7 @@
class="progress-card"
href={`/console/project-${project.$id}/get-started`}
on:click={() => {
trackEvent('click_menu_get_started');
sideBarIsOpen = false;
}}>
<div class="progressCircle">
@@ -138,6 +141,7 @@
class="link"
class:active={pathname.includes('overview')}
on:click={() => {
trackEvent('click_menu_overview');
sideBarIsOpen = false;
}}
><span class="link-icon"
@@ -165,6 +169,7 @@
class="link"
class:active={pathname.includes(projectOption.slug)}
on:click={() => {
trackEvent(`click_menu_${projectOption.slug}`);
sideBarIsOpen = false;
}}
><span class="link-icon"
@@ -182,7 +187,12 @@
</div>
<div class="only-mobile">
<Tooltip inline={false} placement="right" disabled={state !== 'icons'}>
<a href={`/console/project-${project.$id}/settings`} class="link"
<a
href={`/console/project-${project.$id}/settings`}
on:click={() => {
trackEvent('click_menu_settings');
}}
class="link"
><span class="link-icon"><Icon icon={IconCog} size="s" /></span
><span
class:no-text={state === 'icons'}
@@ -200,7 +210,10 @@
<Button.Button
variant="secondary"
size="s"
on:click={() => toggleFeedback()}
on:click={() => {
toggleFeedback();
trackEvent('click_menu_feedback', { source: 'side_nav' });
}}
>Feedback
</Button.Button>
<svelte:fragment slot="other">
@@ -212,7 +225,10 @@
<Button.Button
variant="secondary"
size="s"
on:click={() => ($showSupportModal = true)}>
on:click={() => {
$showSupportModal = true;
trackEvent('click_menu_support', { source: 'side_nav' });
}}>
<span>Support</span>
<svelte:fragment slot="other">
@@ -232,7 +248,10 @@
<a
href={`/console/project-${project.$id}/settings`}
class="link"
class:active={pathname.includes('settings')}
on:click={() => {
trackEvent('click_menu_settings');
}}
class:active={isOnProjectSettings}
><span class="link-icon"><Icon icon={IconCog} size="s" /></span><span
class:no-text={state === 'icons'}
class:has-text={state === 'open'}
@@ -251,7 +270,10 @@
<Button.Button
variant="secondary"
size="s"
on:click={() => toggleFeedback()}
on:click={() => {
toggleFeedback();
trackEvent('click_menu_feedback', { source: 'side_nav' });
}}
><span>Feedback</span>
</Button.Button>
<svelte:fragment slot="other">
@@ -263,7 +285,10 @@
<Button.Button
variant="secondary"
size="s"
on:click={() => ($showSupportModal = true)}>
on:click={() => {
$showSupportModal = true;
trackEvent('click_menu_support', { source: 'side_nav' });
}}>
<span>Support</span>
<svelte:fragment slot="other">
+2
View File
@@ -10,6 +10,7 @@
export let selected = false;
export let href: string = null;
export let event: string = null;
export let noscroll = false;
export let root: { variant: Variant; stretch: boolean } = {
variant: 'primary',
stretch: false
@@ -87,6 +88,7 @@
{#if href}
<Tabs.Item.Link
{noscroll}
{root}
{href}
bind:active={selected}
@@ -54,6 +54,7 @@
{disabled}
{isSearchable}
helper={error ?? helper}
{required}
state={error ? 'error' : 'default'}
on:invalid={handleInvalid}
on:input
@@ -3,7 +3,7 @@
import { goto, invalidate } from '$app/navigation';
import { base } from '$app/paths';
import { Submit, trackEvent } from '$lib/actions/analytics';
import { Heading, Trim } from '$lib/components';
import { Trim } from '$lib/components';
import { Dependencies } from '$lib/constants';
import { Button } from '$lib/elements/forms';
import { isValueOfStringEnum } from '$lib/helpers/types';
@@ -1,149 +0,0 @@
<script lang="ts">
import { goto, invalidate } from '$app/navigation';
import { page } from '$app/stores';
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
import { Card, Heading } from '$lib/components';
import { BillingPlan, Dependencies } from '$lib/constants';
import { Button, Form, InputSelect, InputText } from '$lib/elements/forms';
import FormList from '$lib/elements/forms/formList.svelte';
import { Container } from '$lib/layout';
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
import { isCloud } from '$lib/system';
import { ID } from '@appwrite.io/console';
import { onMount } from 'svelte';
import { tierToPlan, type Tier, plansInfo } from '$lib/stores/billing';
import { formatCurrency } from '$lib/helpers/numbers';
import { base } from '$app/paths';
import { checkPricingRefAndRedirect } from '$lib/helpers/pricingRedirect';
let name: string;
let plan: Tier;
const options = isCloud
? [
{
value: BillingPlan.FREE,
label: `${tierToPlan(BillingPlan.FREE).name} - ${formatCurrency($plansInfo.get(BillingPlan.FREE).price)} / month`
},
{
value: BillingPlan.PRO,
label: `${tierToPlan(BillingPlan.PRO).name} - ${formatCurrency($plansInfo.get(BillingPlan.PRO).price)} / month + add-ons`
},
{
value: BillingPlan.SCALE,
label: `${tierToPlan(BillingPlan.SCALE).name} - ${formatCurrency($plansInfo.get(BillingPlan.SCALE).price)}/month + usage`
}
]
: [];
onMount(() => {
if (isCloud) {
checkPricingRefAndRedirect($page.url.searchParams);
}
});
async function handleSubmit() {
const orgName = name?.length ? name : 'Personal Projects';
if (isCloud) {
if (plan === BillingPlan.FREE) {
try {
const org = await sdk.forConsole.billing.createOrganization(
ID.unique(),
orgName,
plan,
null,
null
);
trackEvent(Submit.OrganizationCreate, {
plan: tierToPlan(plan)?.name
});
await invalidate(Dependencies.ACCOUNT);
await goto(`${base}/organization-${org.$id}`);
addNotification({
message: `${orgName} organization successfully created`,
type: 'success'
});
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
trackError(error, Submit.OrganizationCreate);
}
} else {
goto(`${base}/create-organization?name=${orgName}&plan=${plan}`);
}
} else {
try {
const org = await sdk.forConsole.teams.create(ID.unique(), orgName);
await invalidate(Dependencies.ACCOUNT);
await goto(`${base}/organization-${org.$id}`);
addNotification({
message: `${orgName} organization successfully created`,
type: 'success'
});
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
trackError(error, Submit.OrganizationCreate);
}
}
}
</script>
<Container overlapCover size="large">
<Card>
<Heading size="4" tag="h2">Create a new organization</Heading>
<Form onSubmit={handleSubmit}>
<FormList gap={16}>
<InputText
id="name"
label="Name"
placeholder="Organization name"
hideRequired
bind:value={name} />
{#if isCloud}
<div class="u-margin-block-start-8">
<h3><b>Plan</b></h3>
<p>
For more details on our plans, visit our <Button
link
external
href="https://appwrite.io/pricing">pricing page</Button
>.
</p>
<FormList class="u-margin-block-start-8">
<InputSelect
placeholder="Select a plan"
label="Select plan"
showLabel={false}
id="plan"
required
hideRequired
{options}
bind:value={plan} />
</FormList>
</div>
{/if}
<Button
fullWidth
submit
disabled={isCloud && !plan}
event="create_organization"
submissionLoader
let:isSubmitting>
{#if isSubmitting}
Creating your first organization
{:else}
Get started
{/if}
</Button>
</FormList>
</Form>
</Card>
</Container>
@@ -25,16 +25,25 @@
import { sdk } from '$lib/stores/sdk';
import { loading } from '$routes/store';
import type { Models } from '@appwrite.io/console';
import { ID, Region } from '@appwrite.io/console';
import { ID, Region, PlatformType } from '@appwrite.io/console';
import { openImportWizard } from '../project-[project]/settings/migrations/(import)';
import { readOnly } from '$lib/stores/billing';
import type { RegionList } from '$lib/sdk/billing';
import { onMount } from 'svelte';
import { onMount, type ComponentType } from 'svelte';
import { organization } from '$lib/stores/organization';
import { canWriteProjects } from '$lib/stores/roles';
import { checkPricingRefAndRedirect } from '$lib/helpers/pricingRedirect';
import { Icon } from '@appwrite.io/pink-svelte';
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
import { Badge, Icon } from '@appwrite.io/pink-svelte';
import {
IconAndroid,
IconApple,
IconCode,
IconFlutter,
IconGlobe,
IconPlus,
IconReact,
IconUnity
} from '@appwrite.io/pink-icons-svelte';
import { getPlatformInfo } from '$lib/helpers/platform';
export let data;
@@ -64,6 +73,26 @@
if (isCloud) wizard.start(Create);
else showCreate = true;
}
function getIconForPlatform(platform: string): ComponentType {
switch (platform) {
case 'web':
return IconCode;
case 'flutter':
return IconFlutter;
case 'apple':
return IconApple;
case 'android':
return IconAndroid;
case 'react-native':
return IconReact;
case 'unity':
return IconUnity;
default:
return null;
}
}
$: $registerCommands([
{
label: 'Create project',
@@ -170,10 +199,10 @@
{#each platforms as platform, i}
{#if i < 3}
<Pill>
<span class={`icon-${platform.icon}`} aria-hidden="true" />
{platform.name}
</Pill>
{@const icon = getIconForPlatform(platform.icon)}
<Badge variant="secondary" content={platform.name}>
<Icon {icon} size="s" slot="start" />
</Badge>
{/if}
{/each}
{#if platforms?.length > 3}
@@ -59,24 +59,24 @@
}
</script>
<Modal title="Invite member" {error} size="big" bind:show={showCreate} onSubmit={create}>
<FormList>
<InputEmail
<Modal title="Invite member" {error} bind:show={showCreate} onSubmit={create}>
<InputEmail
required
id="email"
label="Email"
placeholder="Enter email"
autofocus={true}
bind:value={email} />
<InputText id="member-name" label="Name" placeholder="Enter name" bind:value={name} />
{#if isCloud}
<InputSelect
required
id="email"
label="Email"
placeholder="Enter email"
autofocus={true}
bind:value={email} />
<InputText
id="member-name"
label="Name (optional)"
placeholder="Enter name"
bind:value={name} />
{#if isCloud}
<InputSelect popover={Roles} id="role" label="Role" options={roles} bind:value={role} />
{/if}
</FormList>
popover={Roles}
id="role"
label="Role"
options={roles}
bind:value={role} />
{/if}
<svelte:fragment slot="footer">
<Button secondary on:click={() => (showCreate = false)}>Cancel</Button>
<Button submit submissionLoader>Send invite</Button>
@@ -8,6 +8,8 @@
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
import { ID, Region } from '@appwrite.io/console';
import { IconPencil } from '@appwrite.io/pink-icons-svelte';
import { Icon, Layout, Tag } from '@appwrite.io/pink-svelte';
import { createEventDispatcher } from 'svelte';
export let show = false;
@@ -49,21 +51,27 @@
}
</script>
<Modal title="Create project" {error} onSubmit={create} size="big" bind:show>
<FormList>
<Modal title="Create project" {error} onSubmit={create} bind:show>
<Layout.Stack gap="l">
<InputText id="name" label="Name" bind:value={name} required autofocus={true} />
{#if !showCustomId}
<div>
<Pill button on:click={() => (showCustomId = !showCustomId)}>
<span class="icon-pencil" aria-hidden="true" /><span class="text">
Project ID
</span>
</Pill>
</div>
<span>
<Tag size="s" on:click={() => (showCustomId = !showCustomId)}>
<Icon icon={IconPencil} slot="start" size="s" />
Project ID
</Tag>
</span>
{:else}
<CustomId autofocus bind:show={showCustomId} name="Project" isProject bind:id />
<CustomId
autofocus
bind:show={showCustomId}
name="Project"
isProject
bind:id
fullWidth />
{/if}
</FormList>
</Layout.Stack>
<svelte:fragment slot="footer">
<Button secondary on:click={() => (show = false)}>Cancel</Button>
<Button submit {disabled}>Create</Button>
@@ -14,9 +14,24 @@
import { isOwner } from '$lib/stores/roles';
import Edit from './edit.svelte';
import { getRoleLabel } from '$lib/stores/billing';
import { Drop } from '$lib/components';
import { IconDotsHorizontal, IconPlus } from '@appwrite.io/pink-icons-svelte';
import { Layout, Table, Badge, Button, Icon } from '@appwrite.io/pink-svelte';
import {
IconDotsHorizontal,
IconInfo,
IconPencil,
IconPlus,
IconRefresh,
IconTrash
} from '@appwrite.io/pink-icons-svelte';
import {
Layout,
Table,
Badge,
Button,
Icon,
Typography,
Popover,
ActionMenu
} from '@appwrite.io/pink-svelte';
import Upgrade from '$lib/components/roles/upgrade.svelte';
export let data: PageData;
@@ -25,7 +40,6 @@
let showDelete = false;
let showEdit = false;
let showDropdown = [];
let showPopover = false;
const resend = async (member: Models.Membership) => {
try {
@@ -54,7 +68,8 @@
</script>
<Container>
<Layout.Stack direction="row" alignItems="center" justifyContent="flex-end">
<Layout.Stack direction="row" justifyContent="space-between">
<Typography.Title>Members</Typography.Title>
<ConsoleButton on:mousedown={() => newMemberModal.set(true)} event="create_user" size="s">
<Icon size="s" icon={IconPlus} slot="start" />
<span class="text">Invite</span>
@@ -65,28 +80,16 @@
<svelte:fragment slot="header">
<Table.Header.Cell>Name</Table.Header.Cell>
<Table.Header.Cell>Email</Table.Header.Cell>
<Table.Header.Cell
>Roles
<Drop isPopover bind:show={showPopover} display="inline-block">
&nbsp;<button
type="button"
on:click={() => (showPopover = !showPopover)}
aria-label="input tooltip">
<span
class="icon-info"
aria-hidden="true"
style="font-size: var(--icon-size-small)" />
</button>
<svelte:fragment slot="list">
<div
class="dropped card u-max-width-300 u-break-word"
style:--card-border-radius="var(--border-radius-small)"
style:--p-card-padding=".75rem"
style:box-shadow="var(--shadow-large)">
<svelte:component this={Upgrade} />
</div>
</svelte:fragment>
</Drop>
<Table.Header.Cell>
<Layout.Stack direction="row" gap="xxs" alignItems="center">
Roles
<Popover let:toggle>
<Button.Button icon variant="compact" size="s" on:click={toggle}>
<Icon size="s" icon={IconInfo} />
</Button.Button>
<svelte:component this={Upgrade} slot="tooltip" />
</Popover>
</Layout.Stack>
</Table.Header.Cell>
<Table.Header.Cell>2FA</Table.Header.Cell>
{#if $isOwner}
@@ -102,7 +105,7 @@
{member.userName ? member.userName : 'n/a'}
</span>
{#if member.invited && !member.joined}
<Badge type="warning" variant="secondary" content="Pending" />
<Badge type="warning" variant="secondary" content="Pending" size="xs" />
{/if}
</Layout.Stack>
</Table.Cell>
@@ -121,49 +124,41 @@
</Table.Cell>
{#if $isOwner}
<Table.Cell>
<DropList bind:show={showDropdown[index]} placement="bottom-end" noArrow>
<Button.Button
icon
variant="text"
size="s"
aria-label="More options"
on:click={(e) => {
e.preventDefault();
showDropdown[index] = !showDropdown[index];
}}>
<Icon icon={IconDotsHorizontal} size="m" />
<Popover let:toggle padding="none" placement="bottom-end">
<Button.Button icon variant="ghost" size="s" on:click={toggle}>
<Icon size="s" icon={IconDotsHorizontal} />
</Button.Button>
<svelte:fragment slot="list">
<DropListItem
icon="pencil"
on:click={() => {
selectedMember = member;
showEdit = true;
showDropdown[index] = false;
}}>
Edit role
</DropListItem>
{#if member.invited && !member.joined}
<DropListItem
icon="refresh"
<div style:min-width="232px" slot="tooltip">
<ActionMenu.Root>
<ActionMenu.Item.Button
trailingIcon={IconPencil}
on:click={() => {
resend(member);
selectedMember = member;
showEdit = true;
showDropdown[index] = false;
}}>
Resend
</DropListItem>
{/if}
<DropListItem
icon="trash"
on:click={() => {
selectedMember = member;
showDelete = true;
showDropdown[index] = false;
}}>
Remove
</DropListItem>
</svelte:fragment>
</DropList>
Edit role
</ActionMenu.Item.Button>
{#if member.invited && !member.joined}
<ActionMenu.Item.Button
trailingIcon={IconRefresh}
on:click={() => {
resend(member);
}}>
Resend
</ActionMenu.Item.Button>
{/if}
<ActionMenu.Item.Button
trailingIcon={IconTrash}
on:click={() => {
selectedMember = member;
showDelete = true;
}}>
Remove
</ActionMenu.Item.Button>
</ActionMenu.Root>
</div>
</Popover>
</Table.Cell>
{/if}
</Table.Row>
@@ -53,14 +53,13 @@
<CardGrid>
<svelte:fragment slot="title">Name</svelte:fragment>
<svelte:fragment slot="aside">
<ul>
<InputText
id="name"
label="Name"
placeholder="Enter name"
autocomplete={false}
bind:value={name} />
</ul>
<InputText
required
id="name"
label="Name"
placeholder="Enter name"
autocomplete={false}
bind:value={name} />
</svelte:fragment>
<svelte:fragment slot="actions">
@@ -1,6 +1,6 @@
<script lang="ts">
import { Alert, Modal, SecondaryTabs, SecondaryTabsItem } from '$lib/components';
import { Button, FormList, InputText } from '$lib/elements/forms';
import { Modal, SecondaryTabs, SecondaryTabsItem } from '$lib/components';
import { Button, InputText } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
import { members, organization, organizationList } from '$lib/stores/organization';
@@ -22,6 +22,7 @@
} 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';
export let showDelete = false;
export let invoices: InvoiceList;
@@ -102,116 +103,102 @@
}
</script>
<div class="max-height-dialog">
<Modal title="Delete organization" onSubmit={deleteOrg} bind:show={showDelete} bind:error>
{#if upcomingInvoice}
<Alert type="warning">
<span slot="title">
You have a pending {formatCurrency(upcomingInvoice.grossAmount)} invoice for your
{tierToPlan(upcomingInvoice.plan).name} plan
</span>
<p>
By proceeding, your invoice will be processed within the hour. Upon successful
payment, your organization will be deleted.
</p>
</Alert>
{/if}
<p data-private>
{#if $projects.total > 0}
The following projects and all data associated with <b>{$organization.name}</b> will
be permanently deleted. <b>This action is irreversible</b>.
{:else}
All data associated with <b>{$organization.name}</b> will be permanently deleted.
<b>This action is irreversible</b>.
{/if}
</p>
<Modal title="Delete organization" onSubmit={deleteOrg} bind:show={showDelete} bind:error>
{#if upcomingInvoice}
<Alert.Inline
status="warning"
title={`You have a pending ${formatCurrency(upcomingInvoice.grossAmount)} invoice for your
${tierToPlan(upcomingInvoice.plan).name} plan`}>
By proceeding, your invoice will be processed within the hour. Upon successful payment,
your organization will be deleted.
</Alert.Inline>
{/if}
<p slot="description">
{#if $projects.total > 0}
<div class="box is-only-desktop">
<SecondaryTabs large stretch class="u-sep-block-end u-padding-8">
{#each tabs as { name, label, total }}
<SecondaryTabsItem
center
fullWidth
disabled={selectedTab === name}
on:click={() => (selectedTab = name)}>
{label.desktop} ({total})
</SecondaryTabsItem>
{/each}
</SecondaryTabs>
<TableScroll dense noMargin>
<TableHeader>
{#each tabData.headers as header}
<TableCellHead width={columnWidth}>{header}</TableCellHead>
{/each}
</TableHeader>
<TableBody>
{#each tabData.rows as row}
<TableRow>
{#each row.cells as cell}
<TableCell width={columnWidth}>{cell}</TableCell>
{/each}
</TableRow>
{/each}
</TableBody>
</TableScroll>
</div>
<div class="box is-not-desktop">
<SecondaryTabs large stretch class="u-sep-block-end u-padding-8">
{#each tabs as { name, label, total }}
<SecondaryTabsItem
center
fullWidth
disabled={selectedTab === name}
on:click={() => (selectedTab = name)}>
{label.mobile} ({total})
</SecondaryTabsItem>
{/each}
</SecondaryTabs>
<TableScroll dense noMargin>
<TableHeader>
{#each tabData.headers as header, index}
<TableCellHead width={index === 1 ? columnWidthSmall : columnWidth}
>{header}</TableCellHead>
{/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>
</div>
The following projects and all data associated with <b>{$organization.name}</b> will be
permanently deleted. <b>This action is irreversible</b>.
{:else}
All data associated with <b>{$organization.name}</b> will be permanently deleted.
<b>This action is irreversible</b>.
{/if}
</p>
<FormList>
<InputText
label={`Confirm the organization name to continue`}
placeholder="Enter {$organization.name} to continue"
id="organization-name"
required
bind:value={organizationName} />
</FormList>
{#if $projects.total > 0}
<Tabs.Root let:root stretch>
{#each tabs as { name, label, total }}
<Tabs.Item.Button
{root}
on:click={() => (selectedTab = name)}
active={selectedTab === name}>
{label.desktop} ({total})
</Tabs.Item.Button>
{/each}
</Tabs.Root>
<svelte:fragment slot="footer">
<Button text on:click={() => (showDelete = false)}>Cancel</Button>
<Button
secondary
submit
disabled={!error && (!organizationName || organizationName !== $organization.name)}>
Delete
</Button>
</svelte:fragment>
</Modal>
</div>
<Table.Root>
<svelte:fragment slot="header">
{#each tabData.headers as header}
<Table.Header.Cell width={columnWidth + 'px'}>{header}</Table.Header.Cell>
{/each}
</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 class="box is-not-desktop">
<SecondaryTabs large stretch class="u-sep-block-end u-padding-8">
{#each tabs as { name, label, total }}
<SecondaryTabsItem
center
fullWidth
disabled={selectedTab === name}
on:click={() => (selectedTab = name)}>
{label.mobile} ({total})
</SecondaryTabsItem>
{/each}
</SecondaryTabs>
<TableScroll dense noMargin>
<TableHeader>
{#each tabData.headers as header, index}
<TableCellHead width={index === 1 ? columnWidthSmall : columnWidth}
>{header}</TableCellHead>
{/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>
</div>
{/if}
<InputText
label={`Confirm the organization name to continue`}
placeholder="Enter {$organization.name} to continue"
id="organization-name"
required
bind:value={organizationName} />
<svelte:fragment slot="footer">
<Button text on:click={() => (showDelete = false)}>Cancel</Button>
<Button
secondary
submit
disabled={!error && (!organizationName || organizationName !== $organization.name)}>
Delete
</Button>
</svelte:fragment>
</Modal>
<style>
.box {
@@ -28,7 +28,7 @@
import { periodToDates } from '$lib/layout/usage.svelte';
import { canWriteProjects } from '$lib/stores/roles';
import { hasOnboardingDismissed } from '$lib/helpers/onboarding';
import { Layout } from '@appwrite.io/pink-svelte';
import { Card, Layout, Typography } from '@appwrite.io/pink-svelte';
import { writable, type Writable } from 'svelte/store';
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
@@ -89,143 +89,166 @@
{#if $project}
<Container overlapCover>
{#if $usage}
{@const storage = humanFileSize($usage.filesStorageTotal ?? 0)}
<section class="common-section">
<div class="grid-dashboard-1s-2m-6l">
<div class="card is-2-columns-medium-screen is-3-columns-large-screen">
<Bandwidth {period} on:change={(e) => changePeriod(e.detail)} />
<!-- TODO: fix with nicer solution -->
<div class="flex-container">
{#if $usage}
{@const storage = humanFileSize($usage.filesStorageTotal ?? 0)}
<section class="common-section">
<div class="grid-dashboard-1s-2m-6l" style:gap="1rem">
<Card.Base
class="is-2-columns-medium-screen is-3-columns-large-screen"
padding="s">
<Bandwidth {period} on:change={(e) => changePeriod(e.detail)} />
</Card.Base>
<Card.Base
class="is-2-columns-medium-screen is-3-columns-large-screen"
padding="s">
<Requests {period} on:change={(e) => changePeriod(e.detail)} />
</Card.Base>
<Card.Link
padding="s"
href={`${base}/project-${projectId}/databases`}
class="is-2-columns-large-screen">
<div class="grid-item-1">
<div class="grid-item-1-start-start">
<div class="eyebrow-heading-3">
<span class="icon-database" aria-hidden="true" />
<span class="text">Database</span>
</div>
</div>
<div class="grid-item-1-start-end" />
<div class="grid-item-1-end-start">
<Typography.Title>
{formatNum($usage.documentsTotal ?? 0)}
</Typography.Title>
<Typography.Text>Documents</Typography.Text>
</div>
<div class="grid-item-1-end-end">
<Typography.Text
>Databases: {formatNum(
$usage.databasesTotal ?? 0
)}</Typography.Text>
</div>
</div>
</Card.Link>
<Card.Link
padding="s"
href={`${base}/project-${projectId}/storage`}
class="is-2-columns-large-screen">
<div class="grid-item-1">
<div class="grid-item-1-start-start">
<div class="eyebrow-heading-3">
<span class="icon-folder" aria-hidden="true" />
<span class="text">Storage</span>
</div>
</div>
<div class="grid-item-1-start-end" />
<div class="grid-item-1-end-start">
<Typography.Title>
{storage.value}
<span class="body-text-2">{storage.unit}</span>
</Typography.Title>
<Typography.Text>Storage</Typography.Text>
</div>
<div class="grid-item-1-end-end">
<Typography.Text>
Buckets: {formatNum($usage.bucketsTotal ?? 0)}
</Typography.Text>
</div>
</div>
</Card.Link>
<Card.Link
padding="s"
href={`${base}/project-${projectId}/auth`}
class="is-2-columns-large-screen">
<div class="grid-item-1">
<div class="grid-item-1-start-start">
<div class="eyebrow-heading-3">
<span class="icon-user-group" aria-hidden="true" />
<span class="text">Auth</span>
</div>
</div>
<div class="grid-item-1-start-end" />
<div class="grid-item-1-end-start">
<Typography.Title>
{formatNum($usage.usersTotal ?? 0)}
</Typography.Title>
<Typography.Text>Users</Typography.Text>
</div>
</div>
</Card.Link>
<Card.Link
padding="s"
href={`${base}/project-${projectId}/functions`}
class="is-2-columns-large-screen">
<div class="grid-item-1">
<div class="grid-item-1-start-start">
<div class="eyebrow-heading-3">
<span class="icon-lightning-bolt" aria-hidden="true" />
<span class="text">Functions</span>
</div>
</div>
<div class="grid-item-1-start-end" />
<div class="grid-item-1-end-start">
<Typography.Title>
{formatNum($usage.executionsTotal ?? 0)}
</Typography.Title>
<Typography.Text>Executions</Typography.Text>
</div>
<div class="grid-item-1-end-end">
<div class="text" />
</div>
</div>
</Card.Link>
<Card.Base
padding="s"
class="is-2-columns-medium-screen is-2-columns-large-screen is-2-rows-large-screen is-location-row-2-end-large-screen">
<Realtime />
</Card.Base>
</div>
<div class="card is-2-columns-medium-screen is-3-columns-large-screen">
<Requests {period} on:change={(e) => changePeriod(e.detail)} />
</div>
<a
href={`${base}/project-${projectId}/databases`}
class="card is-2-columns-large-screen">
<div class="grid-item-1">
<div class="grid-item-1-start-start">
<div class="eyebrow-heading-3">
<span class="icon-database" aria-hidden="true" />
<span class="text">Database</span>
</div>
</div>
</section>
{/if}
<div class="grid-item-1-start-end" />
<div class="grid-item-1-end-start">
<div class="heading-level-4">
{formatNum($usage.documentsTotal ?? 0)}
</div>
<div>Documents</div>
</div>
<div class="grid-item-1-end-end">
<div class="text">
Databases: {formatNum($usage.databasesTotal ?? 0)}
</div>
</div>
</div>
</a>
<a
href={`${base}/project-${projectId}/storage`}
class="card is-2-columns-large-screen">
<div class="grid-item-1">
<div class="grid-item-1-start-start">
<div class="eyebrow-heading-3">
<span class="icon-folder" aria-hidden="true" />
<span class="text">Storage</span>
</div>
</div>
<div class="grid-item-1-start-end" />
<div class="grid-item-1-end-start">
<div class="heading-level-4">
{storage.value}
<span class="body-text-2">{storage.unit}</span>
</div>
<div>Storage</div>
</div>
<div class="grid-item-1-end-end">
<div class="text">
Buckets: {formatNum($usage.bucketsTotal ?? 0)}
</div>
</div>
</div>
</a>
<a
href={`${base}/project-${projectId}/auth`}
class="card is-2-columns-large-screen">
<div class="grid-item-1">
<div class="grid-item-1-start-start">
<div class="eyebrow-heading-3">
<span class="icon-user-group" aria-hidden="true" />
<span class="text">Auth</span>
</div>
</div>
<div class="grid-item-1-start-end" />
<div class="grid-item-1-end-start">
<div class="heading-level-4">
{formatNum($usage.usersTotal ?? 0)}
</div>
<div>Users</div>
</div>
</div>
</a>
<a
href={`${base}/project-${projectId}/functions`}
class="card is-2-columns-large-screen">
<div class="grid-item-1">
<div class="grid-item-1-start-start">
<div class="eyebrow-heading-3">
<span class="icon-lightning-bolt" aria-hidden="true" />
<span class="text">Functions</span>
</div>
</div>
<div class="grid-item-1-start-end" />
<div class="grid-item-1-end-start">
<div class="heading-level-4">
{formatNum($usage.executionsTotal ?? 0)}
</div>
<div>Executions</div>
</div>
<div class="grid-item-1-end-end">
<div class="text" />
</div>
</div>
</a>
<div
class="card is-2-columns-medium-screen is-2-columns-large-screen is-2-rows-large-screen is-location-row-2-end-large-screen">
<Realtime />
</div>
</div>
</section>
{/if}
<Layout.Stack gap="xl">
<Heading tag="h2" size="5" id="integrations">Integrations</Heading>
<Layout.Stack gap="xl" direction="row" justifyContent="space-between">
<Tabs>
<Tab
href={`${path}/platforms`}
selected={$page.url.pathname === `${path}/platforms`}
event="platforms">Platforms</Tab>
<Tab
href={`${path}/keys`}
selected={$page.url.pathname === `${path}/keys`}
event="keys">API keys</Tab>
</Tabs>
{#if $action}
<svelte:component this={$action} />
{/if}
<Layout.Stack gap="xl">
<Heading tag="h2" size="5" id="integrations">Integrations</Heading>
<Layout.Stack gap="xl" direction="row" justifyContent="space-between">
<Tabs>
<Tab
noscroll
href={`${path}/platforms`}
selected={$page.url.pathname === `${path}/platforms`}
event="platforms">Platforms</Tab>
<Tab
noscroll
href={`${path}/keys`}
selected={$page.url.pathname === `${path}/keys`}
event="keys">API keys</Tab>
</Tabs>
{#if $action}
<svelte:component this={$action} />
{/if}
</Layout.Stack>
<slot />
</Layout.Stack>
<slot />
</Layout.Stack>
</div>
</Container>
{/if}
<style>
.flex-container {
display: flex;
flex-direction: column;
gap: var(--space-12);
}
</style>
@@ -1,16 +1,16 @@
<script lang="ts">
import { Card, DropList, DropListItem } from '$lib/components';
import { Card } from '$lib/components';
import { humanFileSize } from '$lib/helpers/sizeConvertion';
import { totalMetrics } from './+layout.svelte';
import { usage } from './store';
import type { UsagePeriods } from '$lib/layout';
import { createEventDispatcher } from 'svelte';
import { BarChart } from '$lib/charts';
import { Popover, Typography, Icon, ActionMenu, Link, Layout } from '@appwrite.io/pink-svelte';
import { IconChevronDown } from '@appwrite.io/pink-icons-svelte';
export let period: UsagePeriods;
let showPeriod = false;
const dispatch = createEventDispatcher();
$: network = $usage?.network as unknown as Array<{
@@ -19,32 +19,35 @@
}>;
$: bandwidth = humanFileSize(totalMetrics($usage?.network));
$: if (period) {
showPeriod = false;
}
</script>
<div class="u-flex u-gap-16 u-main-space-between">
<Layout.Stack justifyContent="space-between" direction="row" alignItems="flex-start">
<div>
<div class="heading-level-4">
<Typography.Title>
{bandwidth.value}
<span class="body-text-2">{bandwidth.unit}</span>
</div>
<div>Bandwidth</div>
</Typography.Title>
<Typography.Text>Bandwidth</Typography.Text>
</div>
<DropList bind:show={showPeriod} placement="bottom-start" childStart noArrow>
<button class="transparent-button" on:click={() => (showPeriod = !showPeriod)}>
<span class="text">{period}</span>
<span class="icon-cheveron-down" aria-hidden="true" />
</button>
<svelte:fragment slot="list">
<DropListItem on:click={() => dispatch('change', '24h')}>24h</DropListItem>
<DropListItem on:click={() => dispatch('change', '30d')}>30d</DropListItem>
<DropListItem on:click={() => dispatch('change', '90d')}>90d</DropListItem>
<Popover let:toggle padding="none">
<Link.Button on:click={toggle} variant="quiet">
<Layout.Stack direction="row" gap="none">
<span>{period}</span>
<Icon icon={IconChevronDown} />
</Layout.Stack>
</Link.Button>
<svelte:fragment slot="tooltip">
<ActionMenu.Root>
<ActionMenu.Item.Button on:click={() => dispatch('change', '24h')}
>24h</ActionMenu.Item.Button>
<ActionMenu.Item.Button on:click={() => dispatch('change', '30d')}
>30d</ActionMenu.Item.Button>
<ActionMenu.Item.Button on:click={() => dispatch('change', '90d')}
>90d</ActionMenu.Item.Button>
</ActionMenu.Root>
</svelte:fragment>
</DropList>
</div>
</Popover>
</Layout.Stack>
{#if bandwidth.value !== '0'}
<div style="height: 12rem;">
<BarChart
@@ -10,6 +10,7 @@
import { user } from '$lib/stores/user';
import { isSmallViewport } from '$lib/stores/viewport';
import { Dependencies } from '$lib/constants';
import { trackEvent } from '$lib/actions/analytics';
</script>
{#if !$page.url.pathname.includes('get-started')}
@@ -45,6 +46,7 @@
variant="secondary"
size="s"
on:click={async () => {
trackEvent('onboarding_hub_platform_dismiss');
await setHasOnboardingDismissed($project.$id);
goto(`${base}/project-${$project.$id}/overview`);
requestAnimationFrame(() => {
@@ -37,8 +37,15 @@
import { AvatarGroup } from '$lib/components';
import type { Models } from '@appwrite.io/console';
import { getPlatformInfo } from '$lib/helpers/platform';
import { trackEvent } from '$lib/actions/analytics';
import { redirect } from '@sveltejs/kit';
import { goto } from '$app/navigation';
function createKey() {
trackEvent('onboarding_hub_platform', {
platform: 'server',
state: 'add'
});
wizard.start(Wizard);
}
@@ -386,7 +393,12 @@
<Layout.Stack
gap="l"
direction={$isSmallViewport ? 'column' : 'row'}>
<Card.Button on:click={() => addPlatform(0)} padding="s"
<Card.Button
on:click={() => {
trackEvent('onboarding_hub_setup_databases');
goto(`${base}/project-${projectId}/databases`);
}}
padding="s"
><Layout.Stack gap="xl"
><div
class="card-top-image database-card-image"
@@ -419,20 +431,38 @@
<Link.Anchor
variant="quiet-muted"
href="https://appwrite.io/docs/references"
on:click={() => {
trackEvent(
'onboarding_hub_api_references'
);
}}
target="_blank">API references</Link.Anchor>
<Link.Anchor
variant="quiet-muted"
href="https://appwrite.io/docs/tutorials"
on:click={() => {
trackEvent('onboarding_hub_tutorials');
}}
target="_blank">Tutorials</Link.Anchor>
<Link.Anchor
variant="quiet-muted"
href="https://appwrite.io/docs/products/storage/quick-start"
on:click={() => {
trackEvent(
'onboarding_hub_storage_quick_start'
);
}}
target="_blank"
>Storage quick start
</Link.Anchor>
<Link.Anchor
variant="quiet-muted"
href="https://appwrite.io/docs/products/functions/quick-start"
on:click={() => {
trackEvent(
'onboarding_hub_functions_quick_start'
);
}}
target="_blank"
>Functions quick start</Link.Anchor>
</Layout.Stack>
@@ -458,16 +488,29 @@
<Link.Anchor
variant="quiet-muted"
href={`${base}/project-${projectId}/auth/settings`}
on:click={() => {
trackEvent(
'onboarding_hub_auth_email_password'
);
}}
>E-mail and password
</Link.Anchor>
<Link.Anchor
variant="quiet-muted"
href={`${base}/project-${projectId}/auth/settings`}
>OAuth 2</Link.Anchor>
on:click={() => {
trackEvent(
'onboarding_hub_auth_oauth2'
);
}}>OAuth 2</Link.Anchor>
<Link.Anchor
variant="quiet-muted"
href={`${base}/project-${projectId}/auth/settings`}
>View all methods</Link.Anchor>
on:click={() => {
trackEvent(
'onboarding_hub_auth_all_methods'
);
}}>View all methods</Link.Anchor>
</Layout.Stack>
</Layout.Stack>
<div
@@ -481,7 +524,12 @@
</div></Card.Base>
</div>
<Card.Link href="https://appwrite.io/discord" padding="s">
<Card.Link
href="https://appwrite.io/discord"
padding="s"
on:click={() => {
trackEvent('onboarding_hub_discord');
}}>
<div class="full-height-card">
<Layout.Stack gap="xs" justifyContent="space-between">
<Layout.Stack direction="column" gap="xxxs">
@@ -8,6 +8,7 @@
import CreateReactNative from './createReactNative.svelte';
import CreateWeb from './createWeb.svelte';
import { createPlatform, versions } from './wizard/store';
import { trackEvent } from '$lib/actions/analytics';
export enum Platform {
Web,
@@ -20,6 +21,10 @@
export async function addPlatform(type: Platform) {
await versions.load();
createPlatform.reset();
trackEvent('onboarding_hub_platform', {
platform: platforms[type],
state: 'add'
});
wizard.start(platforms[type]);
}
@@ -35,6 +40,10 @@
key: key,
type: type
});
trackEvent('onboarding_hub_platform', {
platform: platforms[platform],
state: 'continue'
});
wizard.start(platforms[platform], null, 1, {
isPlatformCreated: true,
platform: type,
@@ -61,8 +70,15 @@
import type { PageData } from './$types';
import { canWritePlatforms } from '$lib/stores/roles';
import { setOverviewAction } from '../context';
import { Table } from '@appwrite.io/pink-svelte';
import { ActionMenu, Card, Empty, Popover, Table } from '@appwrite.io/pink-svelte';
import Action from './action.svelte';
import {
IconAndroid,
IconApple,
IconCode,
IconFlutter,
IconReact
} from '@appwrite.io/pink-icons-svelte';
export let data: PageData;
@@ -124,52 +140,49 @@
{/each}
</Table.Root>
{:else}
<article class="card u-grid u-cross-center u-width-full-line common-section">
<div class="u-flex u-flex-vertical u-cross-center u-gap-24">
{#if $app.themeInUse === 'dark'}
<img src={SearchDark} alt="empty" aria-hidden="true" />
{:else}
<img src={SearchLight} alt="empty" aria-hidden="true" />
{/if}
<slot>
<div class="u-text-center">
<Heading size="7" tag="h4">Create a platform to get started.</Heading>
<p class="body-text-2 u-bold u-margin-block-start-4">
Need a hand? Learn more in our documentation.
</p>
</div>
<div class="u-flex u-gap-16 u-main-center">
<Button external href="https://appwrite.io/docs/sdks" text>
Documentation
</Button>
<DropList bind:show={showDropdownEmpty} placement="bottom-start">
{#if $canWritePlatforms}
<Button
secondary
on:click={() => (showDropdownEmpty = !showDropdownEmpty)}>
<span class="text">Add platform</span>
</Button>
{/if}
<svelte:fragment slot="list">
<DropListItem on:click={() => addPlatform(Platform.Web)}>
<Card.Base padding="none">
<Empty
title="Add a platform to get started."
description="Need a hand? Learn more in our documentation.">
<svelte:fragment slot="actions">
<Button external href="https://appwrite.io/docs/sdks" text>Documentation</Button>
<Popover let:toggle padding="none" placement="bottom-end">
{#if $canWritePlatforms}
<Button secondary on:click={toggle}>
<span class="text">Add platform</span>
</Button>
{/if}
<div style:min-width="232px" slot="tooltip">
<ActionMenu.Root>
<ActionMenu.Item.Button
on:click={() => addPlatform(Platform.Web)}
leadingIcon={IconCode}>
Web
</DropListItem>
<DropListItem on:click={() => addPlatform(Platform.Flutter)}>
</ActionMenu.Item.Button>
<ActionMenu.Item.Button
on:click={() => addPlatform(Platform.Flutter)}
leadingIcon={IconFlutter}>
Flutter
</DropListItem>
<DropListItem on:click={() => addPlatform(Platform.Android)}>
</ActionMenu.Item.Button>
<ActionMenu.Item.Button
on:click={() => addPlatform(Platform.Android)}
leadingIcon={IconAndroid}>
Android
</DropListItem>
<DropListItem on:click={() => addPlatform(Platform.Apple)}>
</ActionMenu.Item.Button>
<ActionMenu.Item.Button
on:click={() => addPlatform(Platform.Apple)}
leadingIcon={IconApple}>
Apple
</DropListItem>
<DropListItem on:click={() => addPlatform(Platform.ReactNative)}>
</ActionMenu.Item.Button>
<ActionMenu.Item.Button
on:click={() => addPlatform(Platform.ReactNative)}
leadingIcon={IconReact}>
React Native
</DropListItem>
</svelte:fragment>
</DropList>
</div>
</slot>
</div>
</article>
</ActionMenu.Item.Button>
</ActionMenu.Root>
</div>
</Popover>
</svelte:fragment>
</Empty>
</Card.Base>
{/if}
@@ -3,26 +3,56 @@
import DropListItem from '$lib/components/dropListItem.svelte';
import Button from '$lib/elements/forms/button.svelte';
import { canWritePlatforms } from '$lib/stores/roles';
import { Icon } from '@appwrite.io/pink-svelte';
import { ActionMenu, Icon, Popover } from '@appwrite.io/pink-svelte';
import { addPlatform, Platform } from './+page.svelte';
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
import {
IconAndroid,
IconApple,
IconCode,
IconFlutter,
IconPlus,
IconReact
} from '@appwrite.io/pink-icons-svelte';
let showDropdown = false;
</script>
{#if $canWritePlatforms}
<DropList bind:show={showDropdown} placement="bottom-start">
<Button on:click={() => (showDropdown = !showDropdown)}>
<Icon icon={IconPlus} slot="start" size="s" />
Add platform
</Button>
<svelte:fragment slot="list">
<DropListItem on:click={() => addPlatform(Platform.Web)}>Web app</DropListItem>
<DropListItem on:click={() => addPlatform(Platform.Flutter)}>Flutter app</DropListItem>
<DropListItem on:click={() => addPlatform(Platform.Android)}>Android app</DropListItem>
<DropListItem on:click={() => addPlatform(Platform.Apple)}>Apple app</DropListItem>
<DropListItem on:click={() => addPlatform(Platform.ReactNative)}
>React Native app</DropListItem>
</svelte:fragment>
</DropList>
<Popover let:toggle padding="none" placement="bottom-end">
{#if $canWritePlatforms}
<Button on:click={toggle}>
<Icon icon={IconPlus} slot="start" />
<span class="text">Add platform</span>
</Button>
{/if}
<div style:min-width="232px" slot="tooltip">
<ActionMenu.Root>
<ActionMenu.Item.Button
on:click={() => addPlatform(Platform.Web)}
leadingIcon={IconCode}>
Web
</ActionMenu.Item.Button>
<ActionMenu.Item.Button
on:click={() => addPlatform(Platform.Flutter)}
leadingIcon={IconFlutter}>
Flutter
</ActionMenu.Item.Button>
<ActionMenu.Item.Button
on:click={() => addPlatform(Platform.Android)}
leadingIcon={IconAndroid}>
Android
</ActionMenu.Item.Button>
<ActionMenu.Item.Button
on:click={() => addPlatform(Platform.Apple)}
leadingIcon={IconApple}>
Apple
</ActionMenu.Item.Button>
<ActionMenu.Item.Button
on:click={() => addPlatform(Platform.ReactNative)}
leadingIcon={IconReact}>
React Native
</ActionMenu.Item.Button>
</ActionMenu.Root>
</div>
</Popover>
{/if}
@@ -3,6 +3,7 @@
import { BarChart } from '$lib/charts';
import { Card } from '$lib/components';
import { formatNum } from '$lib/helpers/string';
import { Link, Typography } from '@appwrite.io/pink-svelte';
import { stats } from '../store';
$: projectId = $page.params.project;
@@ -34,12 +35,11 @@
class="icon-chart-square-bar text-large"
aria-hidden="true"
style="font-size: 32px;" />
<p class="u-bold">No data to show</p>
<a
class="link"
<Typography.Text variant="m-600">No data to show</Typography.Text>
<Link.Anchor
href="https://appwrite.io/docs/apis/realtime"
target="_blank"
rel="noopener noreferrer">Get started with Realtime</a>
rel="noopener noreferrer">Get started with Realtime</Link.Anchor>
</div>
</Card>
{/if}
@@ -6,6 +6,8 @@
import { createEventDispatcher } from 'svelte';
import { LineChart } from '$lib/charts';
import { formatNum } from '$lib/helpers/string';
import { ActionMenu, Icon, Layout, Link, Popover, Typography } from '@appwrite.io/pink-svelte';
import { IconChevronDown } from '@appwrite.io/pink-icons-svelte';
export let period: UsagePeriods;
@@ -23,25 +25,31 @@
}
</script>
<div class="u-flex u-gap-16 u-main-space-between">
<Layout.Stack justifyContent="space-between" direction="row" alignItems="flex-start">
<div>
<div class="heading-level-4">
<Typography.Title>
{formatNum(totalMetrics($usage?.requests))}
</div>
<div>Requests</div>
</Typography.Title>
<Typography.Text>Requests</Typography.Text>
</div>
<DropList bind:show={showPeriod} placement="bottom-start" childStart noArrow>
<button class="transparent-button" on:click={() => (showPeriod = !showPeriod)}>
<span class="text">{period}</span>
<span class="icon-cheveron-down" aria-hidden="true" />
</button>
<svelte:fragment slot="list">
<DropListItem on:click={() => dispatch('change', '24h')}>24h</DropListItem>
<DropListItem on:click={() => dispatch('change', '30d')}>30d</DropListItem>
<DropListItem on:click={() => dispatch('change', '90d')}>90d</DropListItem>
</svelte:fragment>
</DropList>
</div>
<Popover let:toggle padding="none">
<Link.Button on:click={toggle} variant="quiet">
<Layout.Stack direction="row" gap="none">
<span>{period}</span>
<Icon icon={IconChevronDown} />
</Layout.Stack>
</Link.Button>
<ActionMenu.Root slot="tooltip">
<ActionMenu.Item.Button on:click={() => dispatch('change', '24h')}
>24h</ActionMenu.Item.Button>
<ActionMenu.Item.Button on:click={() => dispatch('change', '30d')}
>30d</ActionMenu.Item.Button>
<ActionMenu.Item.Button on:click={() => dispatch('change', '90d')}
>90d</ActionMenu.Item.Button>
</ActionMenu.Root>
</Popover>
</Layout.Stack>
{#if totalMetrics($usage?.requests) !== 0}
<div style="height: 12rem;">
<LineChart
+1 -1
View File
@@ -92,7 +92,7 @@
"color-border-neutral-stronger": "var(--neutral-500)",
"color-fgcolor-on-warning-weak": "var(--web-orange-700)",
"color-border-neutral-strongest": "var(--neutral-900)",
"color-border-focus": "var(--neutral-500)",
"color-border-focus": "var(--neutral-300)",
"color-border-accent": "var(--brand-pink-500)",
"color-border-accent-secondary": "var(--brand-pink-600)",
"color-border-success": "var(--web-green-500)",
+1 -1
View File
@@ -92,7 +92,7 @@
"color-border-neutral-stronger": "var(--neutral-500)",
"color-fgcolor-on-warning-weak": "var(--web-orange-700)",
"color-border-neutral-strongest": "var(--neutral-900)",
"color-border-focus": "var(--neutral-500)",
"color-border-focus": "var(--neutral-300)",
"color-border-accent": "var(--brand-pink-500)",
"color-border-accent-secondary": "var(--brand-pink-600)",
"color-border-success": "var(--web-green-500)",