Merge branch 'feat-pink-v2' into project-wizard-animation

This commit is contained in:
Jesse Winton
2025-02-24 11:49:38 -05:00
157 changed files with 3089 additions and 2320 deletions
+3 -3
View File
@@ -19,11 +19,11 @@
"e2e:ui": "playwright test tests/e2e --ui"
},
"dependencies": {
"@appwrite.io/console": "https://pkg.pr.new/appwrite/appwrite/@appwrite.io/console@ae1eb53",
"@appwrite.io/console": "https://pkg.pr.new/appwrite/appwrite/@appwrite.io/console@08ec153",
"@appwrite.io/pink-icons": "0.25.0",
"@appwrite.io/pink-icons-svelte": "https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@35f29290",
"@appwrite.io/pink-icons-svelte": "https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@f0102c9f",
"@appwrite.io/pink-legacy": "^1.0.1",
"@appwrite.io/pink-svelte": "https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@35f29290",
"@appwrite.io/pink-svelte": "https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@f99d781",
"@popperjs/core": "^2.11.8",
"@sentry/sveltekit": "^8.38.0",
"@stripe/stripe-js": "^3.5.0",
+19 -19
View File
@@ -9,20 +9,20 @@ importers:
.:
dependencies:
'@appwrite.io/console':
specifier: https://pkg.pr.new/appwrite/appwrite/@appwrite.io/console@ae1eb53
version: https://pkg.pr.new/appwrite/appwrite/@appwrite.io/console@ae1eb53
specifier: https://pkg.pr.new/appwrite/appwrite/@appwrite.io/console@08ec153
version: https://pkg.pr.new/appwrite/appwrite/@appwrite.io/console@08ec153
'@appwrite.io/pink-icons':
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@35f29290
version: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@35f29290(svelte@4.2.19)
specifier: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@f0102c9f
version: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@f0102c9f(svelte@4.2.19)
'@appwrite.io/pink-legacy':
specifier: ^1.0.1
version: 1.0.1
'@appwrite.io/pink-svelte':
specifier: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@35f29290
version: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@35f29290(react-dom@18.3.1(react@18.3.1))(svelte@4.2.19)
specifier: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@f99d781
version: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@f99d781(react-dom@18.3.1(react@18.3.1))(svelte@4.2.19)
'@popperjs/core':
specifier: ^2.11.8
version: 2.11.8
@@ -211,18 +211,18 @@ packages:
'@analytics/type-utils@0.6.2':
resolution: {integrity: sha512-TD+xbmsBLyYy/IxFimW/YL/9L2IEnM7/EoV9Aeh56U64Ify8o27HJcKjo38XY9Tcn0uOq1AX3thkKgvtWvwFQg==}
'@appwrite.io/console@https://pkg.pr.new/appwrite/appwrite/@appwrite.io/console@ae1eb53':
resolution: {tarball: https://pkg.pr.new/appwrite/appwrite/@appwrite.io/console@ae1eb53}
'@appwrite.io/console@https://pkg.pr.new/appwrite/appwrite/@appwrite.io/console@08ec153':
resolution: {tarball: https://pkg.pr.new/appwrite/appwrite/@appwrite.io/console@08ec153}
version: 1.2.1
'@appwrite.io/pink-icons-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@35f29290':
resolution: {tarball: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@35f29290}
'@appwrite.io/pink-icons-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@f0102c9f':
resolution: {tarball: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@f0102c9f}
version: 1.0.0-next.7
peerDependencies:
svelte: ^4.0.0
'@appwrite.io/pink-icons-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@35f2929064b9d126a377b6a3e85f59198c5901a2':
resolution: {tarball: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@35f2929064b9d126a377b6a3e85f59198c5901a2}
'@appwrite.io/pink-icons-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@f99d781924ba95e7ed83708ca8e4c26f80293c3c':
resolution: {tarball: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@f99d781924ba95e7ed83708ca8e4c26f80293c3c}
version: 1.0.0-next.7
peerDependencies:
svelte: ^4.0.0
@@ -236,8 +236,8 @@ packages:
'@appwrite.io/pink-legacy@1.0.1':
resolution: {integrity: sha512-e5IZx2drqSxgl2WXvmr0eLfg/WiOCzuqQE8oksD+9GoFBkcL9HWvXul5GjFni3RBAN9XFRL3nUExGk4T0H8sFw==}
'@appwrite.io/pink-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@35f29290':
resolution: {tarball: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@35f29290}
'@appwrite.io/pink-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@f99d781':
resolution: {tarball: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@f99d781}
version: 1.0.0-next.85
peerDependencies:
react-dom: ^18.0.0
@@ -4097,13 +4097,13 @@ snapshots:
'@analytics/type-utils@0.6.2': {}
'@appwrite.io/console@https://pkg.pr.new/appwrite/appwrite/@appwrite.io/console@ae1eb53': {}
'@appwrite.io/console@https://pkg.pr.new/appwrite/appwrite/@appwrite.io/console@08ec153': {}
'@appwrite.io/pink-icons-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@35f29290(svelte@4.2.19)':
'@appwrite.io/pink-icons-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@f0102c9f(svelte@4.2.19)':
dependencies:
svelte: 4.2.19
'@appwrite.io/pink-icons-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@35f2929064b9d126a377b6a3e85f59198c5901a2(svelte@4.2.19)':
'@appwrite.io/pink-icons-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@f99d781924ba95e7ed83708ca8e4c26f80293c3c(svelte@4.2.19)':
dependencies:
svelte: 4.2.19
@@ -4116,9 +4116,9 @@ snapshots:
'@appwrite.io/pink-icons': 1.0.0
the-new-css-reset: 1.11.3
'@appwrite.io/pink-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@35f29290(react-dom@18.3.1(react@18.3.1))(svelte@4.2.19)':
'@appwrite.io/pink-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@f99d781(react-dom@18.3.1(react@18.3.1))(svelte@4.2.19)':
dependencies:
'@appwrite.io/pink-icons-svelte': https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@35f2929064b9d126a377b6a3e85f59198c5901a2(svelte@4.2.19)
'@appwrite.io/pink-icons-svelte': https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@f99d781924ba95e7ed83708ca8e4c26f80293c3c(svelte@4.2.19)
'@floating-ui/dom': 1.6.13
'@melt-ui/pp': 0.3.2(@melt-ui/svelte@0.86.3(svelte@4.2.19))(svelte@4.2.19)
'@melt-ui/svelte': 0.86.3(svelte@4.2.19)
+1
View File
@@ -343,5 +343,6 @@ export enum Submit {
SiteDisconnectRepo = 'submit_site_disconnect_repo',
SiteActivateDeployment = 'submit_site_activate_deployment',
RecordCreate = 'submit_dns_record_create',
RecordUpdate = 'submit_dns_record_update',
RecordDelete = 'submit_dns_record_delete'
}
+117 -84
View File
@@ -1,5 +1,121 @@
{
"backgroundColor": "rgba(255,255,255,0)",
"backgroundColor": "var(--color-bgcolor-neutral-primary)",
"categoryAxis": {
"axisLine": {
"lineStyle": {
"color": "var(--color-fgcolor-neutral-tertiary)"
},
"show": true
},
"axisTick": {
"lineStyle": {
"color": "var(--color-fgcolor-neutral-tertiary)"
},
"show": true
},
"axisLabel": {
"color": "var(--color-fgcolor-neutral-tertiary)",
"show": true
},
"splitLine": {
"show": false
},
"splitArea": {
"show": false
}
},
"valueAxis": {
"axisLabel": {
"color": "var(--color-fgcolor-neutral-tertiary)",
"show": true
},
"splitLine": {
"lineStyle": {
"color": ["var(--color-border-neutral)"]
},
"show": true
},
"axisLine": {
"show": false
},
"axisTick": {
"show": false
},
"splitArea": {
"show": false
}
},
"logAxis": {
"axisLine": {
"lineStyle": {
"color": "var(--color-fgcolor-neutral-tertiary)"
},
"show": true
},
"axisTick": {
"lineStyle": {
"color": "var(--color-fgcolor-neutral-tertiary)"
},
"show": true
},
"axisLabel": {
"color": "var(--color-fgcolor-neutral-tertiary)",
"show": true
},
"splitLine": {
"lineStyle": {
"color": ["var(--color-border-neutral)"]
},
"show": true
},
"splitArea": {
"show": false
}
},
"timeAxis": {
"axisLine": {
"lineStyle": {
"color": "var(--color-fgcolor-neutral-tertiary)"
},
"show": true
},
"axisTick": {
"lineStyle": {
"color": "var(--color-fgcolor-neutral-tertiary)"
},
"show": true
},
"axisLabel": {
"color": "var(--color-fgcolor-neutral-tertiary)",
"show": true
},
"splitLine": {
"lineStyle": {
"color": ["var(--color-border-neutral)"]
},
"show": true
},
"splitArea": {
"show": false
}
},
"tooltip": {
"backgroundColor": "var(--color-bgcolor-neutral-primary)",
"borderColor": "var(--color-border-neutral)",
"textStyle": {
"color": "var(--color-fgcolor-neutral-secondary)"
},
"axisPointer": {
"lineStyle": {
"color": "var(--color-border-neutral-strong)",
"width": 1
},
"crossStyle": {
"color": "var(--color-border-neutral-strong)",
"width": 1
}
}
},
"textStyle": {},
"line": {
"itemStyle": {
@@ -12,89 +128,6 @@
"symbol": "emptyCircle",
"smooth": true
},
"bar": {
"itemStyle": {
"barBorderColor": "#ccc"
}
},
"categoryAxis": {
"axisLine": {
"show": true
},
"axisTick": {
"show": true
},
"axisLabel": {
"show": true
},
"splitLine": {
"show": false
},
"splitArea": {
"show": false
}
},
"valueAxis": {
"axisLine": {
"show": false
},
"axisTick": {
"show": false
},
"axisLabel": {
"show": true
},
"splitLine": {
"show": true
},
"splitArea": {
"show": false
}
},
"logAxis": {
"axisLine": {
"show": true
},
"axisTick": {
"show": true
},
"axisLabel": {
"show": true
},
"splitLine": {
"show": true
},
"splitArea": {
"show": false
}
},
"timeAxis": {
"axisLine": {
"show": true
},
"axisTick": {
"show": true
},
"axisLabel": {
"show": true
},
"splitLine": {
"show": true
},
"splitArea": {
"show": false
}
},
"tooltip": {
"axisPointer": {
"lineStyle": {
"width": 1
},
"crossStyle": {
"width": 1
}
}
},
"timeline": {
"lineStyle": {
"width": 1
+2 -4
View File
@@ -4,15 +4,13 @@
import { app } from '$lib/stores/app';
import { dailyFormat, hourlyFormat, defaultConfig } from './config';
import base from './base.json';
import light from './light.json';
import dark from './dark.json';
import type { ECharts } from 'echarts/core';
import type { BarSeriesOption, LineSeriesOption } from 'echarts/charts';
import type { EChartsOption } from 'echarts';
import { wizard } from '$lib/stores/wizard';
registerTheme('light', { ...base, ...light });
registerTheme('dark', { ...base, ...dark });
registerTheme('light', base);
registerTheme('dark', base);
export let options: EChartsOption;
export let series: (BarSeriesOption | LineSeriesOption)[];
+3 -3
View File
@@ -120,10 +120,10 @@
"color": "#444444"
}
},
"backgroundColor": "#373B4D",
"borderColor": "#373B4D",
"backgroundColor": "var(--color-bgcolor-neutral-primary)",
"borderColor": "var(--color-border-neutral)",
"textStyle": {
"color": "#F2F2F8"
"color": "var(--color-fgcolor-neutral-secondary)"
}
},
"timeline": {
+21 -88
View File
@@ -1,149 +1,82 @@
{
"backgroundColor": "rgba(255,255,255,0)",
"title": {
"textStyle": {
"color": "#373b4d"
},
"subtextStyle": {
"color": "#616b7c"
}
},
"bar": {
"itemStyle": {
"barBorderColor": "#ccc"
}
},
"backgroundColor": "var(--color-bgcolor-neutral-primary)",
"categoryAxis": {
"axisLine": {
"lineStyle": {
"color": "#6E7079"
"color": "var(--color-fgcolor-neutral-tertiary)"
}
},
"axisTick": {
"lineStyle": {
"color": "#6E7079"
"color": "var(--color-fgcolor-neutral-tertiary)"
}
},
"axisLabel": {
"color": "#6E7079"
"color": "var(--color-fgcolor-neutral-tertiary)"
}
},
"valueAxis": {
"axisLabel": {
"color": "#6E7079"
"color": "var(--color-fgcolor-neutral-tertiary)"
},
"splitLine": {
"lineStyle": {
"color": ["#E8E9F0"]
"color": ["var(--color-border-neutral)"]
}
}
},
"logAxis": {
"axisLine": {
"lineStyle": {
"color": "#6E7079"
"color": "var(--color-fgcolor-neutral-tertiary)"
}
},
"axisTick": {
"lineStyle": {
"color": "#6E7079"
"color": "var(--color-fgcolor-neutral-tertiary)"
}
},
"axisLabel": {
"color": "#6E7079"
"color": "var(--color-fgcolor-neutral-tertiary)"
},
"splitLine": {
"lineStyle": {
"color": ["#E0E6F1"]
"color": ["var(--color-border-neutral)"]
}
}
},
"timeAxis": {
"axisLine": {
"lineStyle": {
"color": "#6E7079"
"color": "var(--color-fgcolor-neutral-tertiary)"
}
},
"axisTick": {
"lineStyle": {
"color": "#6E7079"
"color": "var(--color-fgcolor-neutral-tertiary)"
}
},
"axisLabel": {
"color": "#6E7079"
"color": "var(--color-fgcolor-neutral-tertiary)"
},
"splitLine": {
"lineStyle": {
"color": ["#E0E6F1"]
"color": ["var(--color-border-neutral)"]
}
}
},
"toolbox": {
"iconStyle": {
"borderColor": "#999"
},
"emphasis": {
"iconStyle": {
"borderColor": "#666"
}
}
},
"legend": {
"textStyle": {
"color": "#333"
}
},
"tooltip": {
"backgroundColor": "var(--color-bgcolor-neutral-primary)",
"borderColor": "var(--color-border-neutral)",
"textStyle": {
"color": "var(--color-fgcolor-neutral-secondary)"
},
"axisPointer": {
"lineStyle": {
"color": "#ccc"
"color": "var(--color-border-neutral-strong)"
},
"crossStyle": {
"color": "#ccc"
}
}
},
"timeline": {
"lineStyle": {
"color": "#DAE1F5"
},
"itemStyle": {
"color": "#A4B1D7"
},
"controlStyle": {
"color": "#A4B1D7",
"borderColor": "#A4B1D7"
},
"checkpointStyle": {
"color": "#316bf3",
"borderColor": "#fff"
},
"label": {
"color": "#A4B1D7"
},
"emphasis": {
"itemStyle": {
"color": "#FFF"
},
"controlStyle": {
"color": "#A4B1D7",
"borderColor": "#A4B1D7"
},
"label": {
"color": "#A4B1D7"
}
}
},
"visualMap": {
"color": ["#bf444c", "#d88273", "#f6efa6"]
},
"markPoint": {
"label": {
"color": "#eee"
},
"emphasis": {
"label": {
"color": "#eee"
"color": "var(--color-border-neutral-strong)"
}
}
}
+4 -2
View File
@@ -3,8 +3,10 @@
import type { AvatarProps } from '@appwrite.io/pink-svelte/dist/avatar/Avatar.svelte';
export let size: AvatarProps['size'] = 'm';
export let src: AvatarProps['src'];
export let src: AvatarProps['src'] = undefined;
export let alt: AvatarProps['alt'];
</script>
<Avatar {alt} {src} {size} />
<Avatar {alt} {src} {size}>
<slot />
</Avatar>
+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>
+29 -6
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import { Button, Form } from '$lib/elements/forms';
import { Button, Form, InputCheckbox } from '$lib/elements/forms';
import { Alert, Dialog, Layout } from '@appwrite.io/pink-svelte';
export let open: boolean;
@@ -8,9 +8,13 @@
export let action: string = 'Delete';
export let canDelete: boolean = true;
export let disabled: boolean = false;
export let confirmDeletion: boolean = false;
export let onSubmit: (e: SubmitEvent) => Promise<void> | void = function () {
return;
};
let confirm = false;
let checkboxId = `delete_${title.replaceAll(' ', '_').toLowerCase()}`;
</script>
<Form isModal {onSubmit}>
@@ -25,13 +29,32 @@
{error}
</Alert.Inline>
{/if}
<slot />
<Layout.Stack gap="l">
<slot />
{#if confirmDeletion}
<InputCheckbox
size="s"
required
id={checkboxId}
bind:checked={confirm}
label="I understand and confirm" />
{/if}
</Layout.Stack>
<svelte:fragment slot="footer">
<Layout.Stack direction="row" gap="s" justifyContent="flex-end">
<Button text on:click={() => (open = false)}>Cancel</Button>
{#if canDelete}
<Button danger submit {disabled}>{action}</Button>
{/if}
<slot name="footer">
<Button text on:click={() => (open = false)}>Cancel</Button>
{#if canDelete}
<Button
danger
submit
disabled={disabled || (confirmDeletion ? !confirm : false)}
>{action}</Button>
{/if}
</slot>
</Layout.Stack>
</svelte:fragment>
</Dialog>
+1 -1
View File
@@ -8,7 +8,7 @@
export let optionalText: string | undefined = undefined;
</script>
<Input.Text readonly {value} {label} helper={optionalText}>
<Input.Text readonly {value} {label} helper={optionalText} required>
<Copy {value} slot="end">
<Layout.Stack>
<Input.Action icon={IconDuplicate} />
+4 -6
View File
@@ -27,7 +27,7 @@
{#if single}
<Card.Base padding="none">
<Empty title={`Create your first ${target}`} {description} {src}>
<svelte:fragment slot="actions">
<slot name="actions" slot="actions">
<Button
external
{href}
@@ -44,16 +44,14 @@
size="s">
Create {target}
</Button>
</svelte:fragment>
</slot>
</Empty>
</Card.Base>
{:else}
<Card.Button on:click on:click={track} aria-label="create">
<Layout.Stack justifyContent="center" alignItems="center" gap="m">
<Avatar>
<Icon icon={IconPlus} />
</Avatar>
<Typography.Text variation="m-400"><slot /></Typography.Text>
<Avatar size="s"><Icon icon={IconPlus} size="s" /></Avatar>
<Typography.Text variation="m-400" align="center"><slot /></Typography.Text>
</Layout.Stack>
</Card.Button>
{/if}
+13 -15
View File
@@ -14,7 +14,7 @@
ValidOperators
} from './store';
import { createEventDispatcher } from 'svelte';
import { Icon } from '@appwrite.io/pink-svelte';
import { Icon, Popover } from '@appwrite.io/pink-svelte';
import { IconFilter } from '@appwrite.io/pink-icons-svelte';
export let query = '[]';
@@ -117,19 +117,17 @@
</script>
<div class="is-not-mobile">
<Drop bind:show={showFiltersDesktop} noArrow>
<slot {disabled} toggle={toggleDropdown}>
<Button secondary on:click={toggleDropdown} {disabled}>
<Icon icon={IconFilter} slot="start" size="s" />
Filters
{#if applied > 0}
<span class="inline-tag">
{applied}
</span>
{/if}
</Button>
</slot>
<svelte:fragment slot="list">
<Popover let:toggle placement="bottom-start">
<Button secondary on:click={toggle} {disabled}>
<Icon icon={IconFilter} slot="start" size="s" />
Filters
{#if applied > 0}
<span class="inline-tag">
{applied}
</span>
{/if}
</Button>
<svelte:fragment slot="tooltip">
<div class="dropped card">
{#if displayQuickFilters}
<slot name="quick" />
@@ -171,7 +169,7 @@
</div>
</div>
</svelte:fragment>
</Drop>
</Popover>
</div>
<div class="is-only-mobile">
+3 -2
View File
@@ -1,9 +1,10 @@
<script>
import { Heading } from '$lib/components';
import { Card } from '@appwrite.io/pink-svelte';
export let href;
</script>
<a class="card" {href}>
<Card.Link class="card" {href}>
<div class="grid-item-1">
<div class="grid-item-1-start-start">
<div class="eyebrow-heading-3"><slot name="eyebrow" /></div>
@@ -26,7 +27,7 @@
</ul>
</div>
</div>
</a>
</Card.Link>
<style>
/* TODO: remove this when ui library is updated*/
+1 -1
View File
@@ -50,7 +50,7 @@
<Copy {value} {event}>
<Tag size="xs" variant="code">
<Icon icon={IconDuplicate} size="s" />
<Icon icon={IconDuplicate} size="s" slot="start" />
<span
style:white-space="nowrap"
style:overflow="hidden"
+1 -1
View File
@@ -25,7 +25,7 @@
let slotTitle: HTMLSpanElement;
</script>
<Tooltip disabled={!tooltipText || !tooltipShow} inline={false}>
<Tooltip disabled={!tooltipText || !tooltipShow}>
<Card.Selector
{src}
{alt}
+27 -8
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
@@ -115,7 +120,7 @@
</a>
<Breadcrumbs {organizations} />
{#if selectedProject && selectedProject.pingCount === 0}
<div class="only-desktop">
<div class="only-desktop" style:margin-inline-start="-10px">
<Button.Anchor
href={`${base}/project-${selectedProject.$id}/get-started`}
variant="secondary"
@@ -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>
@@ -162,7 +182,7 @@
</DropList>
</Layout.Stack>
<Layout.Stack direction="row">
<Tooltip inline={false}>
<Tooltip>
<Button.Button
variant="text"
aria-label="Toggle Command Center"
@@ -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} />
@@ -352,10 +375,6 @@
}
}
:global(.icons div:first-of-type:not(:has(.progress-card))) {
height: 20px;
}
.account-container {
position: absolute;
right: var(--space-7);
+35 -27
View File
@@ -1,14 +1,13 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import { DropList, DropListItem } from '..';
import Label from './label.svelte';
import Custom from './custom.svelte';
import Team from './team.svelte';
import User from './user.svelte';
import type { Permission } from './permissions.svelte';
import type { Writable } from 'svelte/store';
import { ActionMenu, Popover } from '@appwrite.io/pink-svelte';
export let showDropdown: boolean;
export let showUser: boolean;
export let showTeam: boolean;
export let showLabel: boolean;
@@ -16,34 +15,43 @@
export let groups: Writable<Map<string, Permission>>;
const dispatch = createEventDispatcher();
$: if (showUser || showTeam || showCustom) {
showDropdown = false;
}
</script>
<DropList bind:show={showDropdown} placement="bottom-end" fixed>
<slot />
<svelte:fragment slot="list">
<DropListItem disabled={$groups.has('any')} on:click={() => dispatch('create', ['any'])}>
Any
</DropListItem>
<DropListItem
disabled={$groups.has('guests')}
on:click={() => dispatch('create', ['guests'])}>
All guests
</DropListItem>
<DropListItem
disabled={$groups.has('users')}
on:click={() => dispatch('create', ['users'])}>
All users
</DropListItem>
<DropListItem on:click={() => (showUser = true)}>Select users</DropListItem>
<DropListItem on:click={() => (showTeam = true)}>Select teams</DropListItem>
<DropListItem on:click={() => (showLabel = true)}>Label</DropListItem>
<DropListItem on:click={() => (showCustom = true)}>Custom permission</DropListItem>
<Popover let:toggle>
<slot {toggle} />
<svelte:fragment slot="tooltip">
<ActionMenu.Root>
<ActionMenu.Item.Button
disabled={$groups.has('any')}
on:click={() => dispatch('create', ['any'])}>
Any
</ActionMenu.Item.Button>
<ActionMenu.Item.Button
disabled={$groups.has('any')}
on:click={() => dispatch('create', ['any'])}>
Any
</ActionMenu.Item.Button>
<ActionMenu.Item.Button
disabled={$groups.has('guests')}
on:click={() => dispatch('create', ['guests'])}>
All guests
</ActionMenu.Item.Button>
<ActionMenu.Item.Button
disabled={$groups.has('users')}
on:click={() => dispatch('create', ['users'])}>
All users
</ActionMenu.Item.Button>
<ActionMenu.Item.Button on:click={() => (showUser = true)}
>Select users</ActionMenu.Item.Button>
<ActionMenu.Item.Button on:click={() => (showTeam = true)}
>Select teams</ActionMenu.Item.Button>
<ActionMenu.Item.Button on:click={() => (showLabel = true)}
>Label</ActionMenu.Item.Button>
<ActionMenu.Item.Button on:click={() => (showCustom = true)}
>Custom permission</ActionMenu.Item.Button>
</ActionMenu.Root>
</svelte:fragment>
</DropList>
</Popover>
<User bind:show={showUser} on:create {groups} />
<Team
@@ -34,7 +34,6 @@
let showTeam = false;
let showLabel = false;
let showCustom = false;
let showDropdown = false;
const groups = writable<Map<string, Permission>>(new Map());
@@ -71,8 +70,6 @@
return n;
});
showDropdown = false;
}
function fromPermissionString(permission: string): void {
@@ -212,12 +209,12 @@
<Actions
bind:showLabel
bind:showCustom
bind:showDropdown
bind:showTeam
bind:showUser
{groups}
on:create={create}>
<Button text on:click={() => (showDropdown = !showDropdown)}>
on:create={create}
let:toggle>
<Button text on:click={toggle}>
<Icon icon={IconPlus} slot="start" size="s" />
Add role
</Button>
@@ -229,12 +226,12 @@
<Actions
bind:showLabel
bind:showCustom
bind:showDropdown
bind:showTeam
bind:showUser
{groups}
on:create={create}>
<Button secondary icon on:click={() => (showDropdown = !showDropdown)}>
on:create={create}
let:toggle>
<Button secondary icon on:click={toggle}>
<Icon icon={IconPlus} size="s" />
</Button>
</Actions>
+6 -6
View File
@@ -117,12 +117,12 @@
<Actions
bind:showLabel
bind:showCustom
bind:showDropdown
bind:showTeam
bind:showUser
{groups}
on:create={create}>
<Button text on:click={() => (showDropdown = !showDropdown)}>
on:create={create}
let:toggle>
<Button text on:click={toggle}>
<Icon icon={IconPlus} slot="start" size="s" />
Add role
</Button>
@@ -134,12 +134,12 @@
<Actions
bind:showLabel
bind:showCustom
bind:showDropdown
bind:showTeam
bind:showUser
{groups}
on:create={create}>
<Button secondary icon on:click={() => (showDropdown = !showDropdown)}>
on:create={create}
let:toggle>
<Button secondary icon on:click={toggle}>
<Icon icon={IconPlus} size="s" />
</Button>
</Actions>
+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 gap="s">
{#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>
+6 -27
View File
@@ -1,37 +1,16 @@
<script lang="ts">
import { trackEvent } from '$lib/actions/analytics';
import { IconDuplicate } from '@appwrite.io/pink-icons-svelte';
import { Copy } from '.';
import { Tooltip } from '@appwrite.io/pink-svelte';
import { Input } from '@appwrite.io/pink-svelte';
export let show = false;
export let value: string;
export let copyEvent: string = null;
</script>
<div class="interactive-text-output" class:is-textarea={show} data-private>
{#if show}
<span class="text u-line-height-1-5 u-break-all">{value}</span>
{:else}
<span class="text">••••••••••••</span>
{/if}
<div class="u-flex u-cross-child-start u-gap-8">
<Tooltip>
<button
class="interactive-text-output-button"
aria-label="show hidden text"
type="button"
on:click={() => {
show = !show;
trackEvent(`click_secret_${show ? 'show' : 'hide'}`);
}}>
<span class:icon-eye-off={show} class:icon-eye={!show} aria-hidden="true" />
</button>
<span slot="tooltip">{show ? 'Hide secret' : 'Show secret'}</span>
</Tooltip>
<Input.Password {value}>
<div slot="end">
<Copy {value} event={copyEvent} eventContext="click_secret_copy">
<button class="interactive-text-output-button" aria-label="copy text" type="button">
<span class="icon-duplicate" aria-hidden="true" />
</button>
<Input.Action icon={IconDuplicate} />
</Copy>
</div>
</div>
</Input.Password>
+39 -13
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' },
@@ -106,11 +108,12 @@
</div>
<div slot="middle" class:icons={state === 'icons'}>
{#if progressCard}
<Tooltip inline={false} placement="right" disabled={state !== 'icons'}>
<Tooltip placement="right" disabled={state !== 'icons'}>
<a
class="progress-card"
href={`/console/project-${project.$id}/get-started`}
on:click={() => {
trackEvent('click_menu_get_started');
sideBarIsOpen = false;
}}>
<div class="progressCircle">
@@ -132,12 +135,13 @@
{/if}
{#if project}
<Layout.Stack direction="column" gap="s">
<Tooltip inline={false} placement="right" disabled={state !== 'icons'}>
<Tooltip placement="right" disabled={state !== 'icons'}>
<a
href={`/console/project-${project.$id}/overview`}
class="link"
class:active={pathname.includes('platforms')}
class:active={pathname.includes('overview')}
on:click={() => {
trackEvent('click_menu_overview');
sideBarIsOpen = false;
}}
><span class="link-icon"
@@ -159,12 +163,13 @@
></span>
</div>
{#each projectOptions as projectOption}
<Tooltip inline={false} placement="right" disabled={state !== 'icons'}>
<Tooltip placement="right" disabled={state !== 'icons'}>
<a
href={`/console/project-${project.$id}/${projectOption.slug}`}
class="link"
class:active={pathname.includes(projectOption.slug)}
on:click={() => {
trackEvent(`click_menu_${projectOption.slug}`);
sideBarIsOpen = false;
}}
><span class="link-icon"
@@ -181,8 +186,13 @@
<Divider />
</div>
<div class="only-mobile">
<Tooltip inline={false} placement="right" disabled={state !== 'icons'}>
<a href={`/console/project-${project.$id}/settings`} class="link"
<Tooltip placement="right" disabled={state !== 'icons'}>
<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">
@@ -228,11 +244,14 @@
<div slot="bottom" class="bottom" class:icons={state === 'icons'}>
{#if project}
<div class="only-desktop">
<Tooltip inline={false} placement="right" disabled={state !== 'icons'}>
<Tooltip placement="right" disabled={state !== 'icons'}>
<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,8 +270,11 @@
<Button.Button
variant="secondary"
size="s"
on:click={() => toggleFeedback()}
>Feedback
on:click={() => {
toggleFeedback();
trackEvent('click_menu_feedback', { source: 'side_nav' });
}}
><span>Feedback</span>
</Button.Button>
<svelte:fragment slot="other">
<MobileFeedbackModal />
@@ -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">
@@ -279,6 +304,7 @@
</div>
</Sidebar.Base>
</div>
{#if subNavigation}
<div class="sub-navigation" class:icons={state === 'icons'}>
<svelte:component this={subNavigation} />
+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}
+1 -1
View File
@@ -5,7 +5,7 @@
let container: HTMLSpanElement | null;
</script>
<Tooltip maxWidth="auto" inline={false}>
<Tooltip maxWidth="auto">
<span class={`text ${alternativeTrim ? 'u-trim-1' : 'u-trim'}`} bind:this={container}>
<slot />
</span>
+7 -6
View File
@@ -3,19 +3,20 @@
import { Layout, Skeleton, Typography } from '@appwrite.io/pink-svelte';
export let description: string;
export let value: string = null;
export let value: string | number = null;
</script>
<Card radius="s" padding="xs" isTile>
<Layout.Stack gap="xxs">
<Typography.Text variant="m-400" color="--color-fgcolor-neutral-tertiary">
<Layout.Stack gap="xxxs">
<Typography.Caption variant="400" color="--color-fgcolor-neutral-tertiary">
{description}
</Typography.Text>
</Typography.Caption>
<slot>
{#if value !== null && value !== undefined}
<Typography.Title size="s" truncate>{value}</Typography.Title>
<Typography.Text size="s" truncate color="--color-fgcolor-neutral-primary"
>{value}</Typography.Text>
{:else}
<Skeleton variant="line" width={100} height={26} />
<Skeleton variant="line" width={100} height={19.5} />
{/if}
</slot>
</Layout.Stack>
+1 -1
View File
@@ -86,7 +86,7 @@
{#if !hideColumns && view === View.Table}
{#if $columns?.length}
<Popover let:toggle>
<Popover let:toggle placement="bottom-end">
<Button.Button
size="s"
variant="secondary"
+12 -5
View File
@@ -29,6 +29,7 @@
export let submissionLoader = false;
export let forceShowLoader = false;
export let compact = false;
export let extraCompact = false;
const isSubmitting = hasContext('form')
? getContext<FormContext>('form').isSubmitting
@@ -47,7 +48,13 @@
});
}
function getVariant(): 'primary' | 'secondary' | 'text' | 'compact' | 'danger' {
function getVariant():
| 'primary'
| 'secondary'
| 'text'
| 'compact'
| 'danger'
| 'extra-compact' {
switch (true) {
case secondary:
return 'secondary';
@@ -55,6 +62,8 @@
return 'text';
case compact:
return 'compact';
case extraCompact:
return 'extra-compact';
case danger:
return 'danger';
default:
@@ -86,8 +95,7 @@
target={external ? '_blank' : ''}
rel={external ? 'noopener noreferrer' : ''}
class={resolvedClasses}
aria-label={ariaLabel}
--button-width={fullWidth ? '100%' : 'max-content'}>
aria-label={ariaLabel}>
<slot name="start" slot="start" />
<slot />
<slot slot="end" name="end" />
@@ -104,8 +112,7 @@
{variant}
class={resolvedClasses}
aria-label={ariaLabel}
type={submit ? 'submit' : 'button'}
--button-width={fullWidth ? '100%' : 'max-content'}>
type={submit ? 'submit' : 'button'}>
<slot name="start" slot="start" />
{#if ($isSubmitting && submissionLoader) || (forceShowLoader && submissionLoader)}
<span
@@ -11,6 +11,7 @@
element?: HTMLInputElement | undefined;
indeterminate?: boolean;
size?: 's' | 'm';
description?: string;
}
export let id: string = '';
@@ -20,6 +21,7 @@
export let disabled = false;
export let element: HTMLInputElement | undefined = undefined;
export let size: $$Props['size'] = 's';
export let description = '';
let error: string;
const handleInvalid = (event: Event) => {
@@ -47,6 +49,7 @@
{size}
{label}
{required}
{description}
on:invalid={handleInvalid}
on:click
on:change />
@@ -52,6 +52,7 @@
{placeholder}
{disabled}
{isSearchable}
{required}
helper={error}
state={error ? 'error' : 'default'}
on:invalid={handleInvalid}
@@ -11,6 +11,7 @@
export let disabled = false;
export let readonly = false;
export let autofocus = false;
export let spellcheck: boolean = undefined;
export let maxlength: number = null;
export let rows = 3;
export let helper: string = '';
@@ -45,6 +46,8 @@
autofocus={autofocus || undefined}
helper={error || helper}
state={error ? 'error' : 'default'}
{spellcheck}
autocorrect={spellcheck ? undefined : 'off'}
on:invalid={handleInvalid}
on:input
bind:value />
+9 -10
View File
@@ -1,16 +1,15 @@
<script lang="ts">
import { ActionMenu, Layout } from '@appwrite.io/pink-svelte';
import InputCheckbox from './forms/inputCheckbox.svelte';
export let value = false;
export let padding: number | null = null;
</script>
<li class="drop-list-item">
<button
class="drop-button u-flex u-cross-center u-gap-12"
style:--button-padding-horizontal={padding ? `${padding / 16}rem` : ''}
style:--button-padding-vertical={padding ? `${padding / 16}rem` : ''}
on:click|preventDefault
type="button">
<input type="checkbox" class="is-small" bind:checked={value} />
<ActionMenu.Item.Button on:click>
<Layout.Stack direction="row" gap="xs" alignItems="center">
<InputCheckbox bind:checked={value} id="filter" size="s" />
<slot />
</button>
</li>
</Layout.Stack>
</ActionMenu.Item.Button>
+1 -4
View File
@@ -165,13 +165,10 @@
<svelte:component this={$page.data.header} />
{/if}
<slot />
{#if showFooter && showSideNavigation}
{#if showFooter}
<slot name="footer" />
{/if}
</section>
{#if showFooter && !showSideNavigation}
<slot name="footer" />
{/if}
</div>
<button
+14 -11
View File
@@ -1,6 +1,7 @@
<script lang="ts">
import { Heading } from '$lib/components';
import { wizard } from '$lib/stores/wizard';
import { Layout } from '@appwrite.io/pink-svelte';
import { onDestroy } from 'svelte';
/**
@@ -20,18 +21,20 @@
}
</script>
<header class="form-header" class:hide-divider={!$$slots.subtitle}>
<Heading tag="h1" size="6">
<slot name="title" />
</Heading>
{#if $$slots.subtitle}
<p>
<slot name="subtitle" />
</p>
{/if}
</header>
<Layout.Stack>
<header class="form-header" class:hide-divider={!$$slots.subtitle}>
<Heading tag="h1" size="6">
<slot name="title" />
</Heading>
{#if $$slots.subtitle}
<p>
<slot name="subtitle" />
</p>
{/if}
</header>
<slot />
<slot />
</Layout.Stack>
<style>
.hide-divider {
+33 -30
View File
@@ -23,6 +23,7 @@
import { wizard } from '$lib/stores/wizard';
import { createEventDispatcher, type SvelteComponent } from 'svelte';
import WizardExitModal from './wizardExitModal.svelte';
import { Layout } from '@appwrite.io/pink-svelte';
export let title: string;
export let steps: WizardStepsType;
@@ -192,38 +193,40 @@
{/if}
</div>
<div class="wizard-main">
<Form noStyle onSubmit={submit}>
{#each sortedSteps as [step, { component }]}
{#if $wizard.step === step}
<svelte:component this={component} />
{/if}
{/each}
<div class="u-z-index-5 form-footer">
<div class="u-flex u-main-end u-gap-12">
{#if !isLastStep && currentStep?.optional}
<Button text on:click={() => dispatch('finish')}>
Skip optional steps
<Form onSubmit={submit}>
<Layout.Stack>
{#each sortedSteps as [step, { component }]}
{#if $wizard.step === step}
<svelte:component this={component} />
{/if}
{/each}
<div class="u-z-index-5 form-footer">
<div class="u-flex u-main-end u-gap-12">
{#if !isLastStep && currentStep?.optional}
<Button text on:click={() => dispatch('finish')}>
Skip optional steps
</Button>
{/if}
{#if $wizard.step === 1}
<Button secondary on:click={handleExit}>Cancel</Button>
{:else}
<Button secondary on:click={previousStep}>Back</Button>
{/if}
{#if currentStep?.actions}
{#each currentStep.actions as action}
<Button secondary on:click={action.onClick}>
{action.label}</Button>
{/each}
{/if}
<Button submit disabled={$wizard.nextDisabled}>
{isLastStep ? finalAction : 'Next'}
</Button>
{/if}
{#if $wizard.step === 1}
<Button secondary on:click={handleExit}>Cancel</Button>
{:else}
<Button secondary on:click={previousStep}>Back</Button>
{/if}
{#if currentStep?.actions}
{#each currentStep.actions as action}
<Button secondary on:click={action.onClick}>
{action.label}</Button>
{/each}
{/if}
<Button submit disabled={$wizard.nextDisabled}>
{isLastStep ? finalAction : 'Next'}
</Button>
</div>
</div>
</div>
</Layout.Stack>
</Form>
</div>
</section>
+15 -6
View File
@@ -1,11 +1,9 @@
<script lang="ts">
import { Modal } from '$lib/components';
import { Button } from '$lib/elements/forms';
import { sdk } from '$lib/stores/sdk';
import { addNotification } from '$lib/stores/notifications';
import { invalidate } from '$app/navigation';
import type { Models } from '@appwrite.io/console';
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import type { Dependencies } from '$lib/constants';
import Confirm from '$lib/components/confirm.svelte';
@@ -15,6 +13,18 @@
let error: string;
function getCorrectMessage(): string {
const resourceType = selectedDomain.resourceType;
switch (resourceType) {
case 'api':
return `access ${resourceType}`;
case 'function':
return `execute your ${resourceType}`;
case 'site':
return `view your ${resourceType} `;
}
}
async function deleteDomain() {
try {
await sdk.forProject.proxy.deleteRule(selectedDomain.$id);
@@ -32,8 +42,7 @@
}
</script>
a
<Confirm onSubmit={deleteDomain} title="Delete domain" bind:open={showDelete} bind:error>
Are you sure you want to delete <b>{selectedDomain?.domain}</b>? You will no longer be able to
execute your function by visiting this domain.
<p data-private>Are you sure you want to delete <b>{selectedDomain?.domain}</b>?</p>
<p>You will no longer be able to {getCorrectMessage()} by visiting this domain.</p>
</Confirm>
@@ -55,10 +55,6 @@
$templateConfig.repositoryBehaviour === 'manual'
? undefined
: $choices.rootDir || undefined,
$template.providerRepositoryId || undefined,
$template.providerOwner || undefined,
runtimeDetail.providerRootDirectory || undefined,
$template.providerVersion || undefined,
$templateConfig.specification || undefined
);
@@ -3,10 +3,8 @@
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
import Confirm from '$lib/components/confirm.svelte';
import { Dependencies } from '$lib/constants';
import { Button, Form } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
import { Dialog, Layout } from '@appwrite.io/pink-svelte';
export let showDelete = false;
let error: string;
@@ -38,6 +38,6 @@
title="Delete authentication method"
bind:open={showDelete}
bind:error>
Are you sure you want to delete this authentication method? You will no longer be able to use
this method to authenticate your account.
<p>Are you sure you want to delete this authentication method?</p>
<p>You will no longer be able to use this method to authenticate your account.</p>
</Confirm>
@@ -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>
@@ -1,6 +1,5 @@
<script lang="ts">
import { base } from '$app/paths';
import { Pill } from '$lib/elements';
import { Button } from '$lib/elements/forms';
import { Container } from '$lib/layout';
import Create from './createProjectCloud.svelte';
@@ -29,12 +28,19 @@
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 { organization } from '$lib/stores/organization';
import { onMount, type ComponentType } from 'svelte';
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,
IconPlus,
IconReact,
IconUnity
} from '@appwrite.io/pink-icons-svelte';
import { getPlatformInfo } from '$lib/helpers/platform';
export let data;
@@ -64,6 +70,26 @@
if (isCloud) wizard.start(Create);
else showCreate = true;
}
function getIconForPlatform(platform: string): ComponentType {
switch (platform) {
case 'code':
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',
@@ -118,100 +144,94 @@
}
</script>
{#if $organization?.$id}
<Container>
<div class="u-flex u-gap-12 common-section u-main-space-between">
<Heading tag="h2" size="5">Projects</Heading>
<Container>
<div class="u-flex u-gap-12 common-section u-main-space-between">
<Heading tag="h2" size="5">Projects</Heading>
<DropList bind:show={showDropdown} placement="bottom-end">
{#if $canWriteProjects}
<Button
on:click={handleCreateProject}
event="create_project"
disabled={$readOnly && !GRACE_PERIOD_OVERRIDE}>
<Icon icon={IconPlus} slot="start" size="s" />
Create project
</Button>
{/if}
<svelte:fragment slot="list">
<DropListItem on:click={() => (showCreate = true)}>Empty project</DropListItem>
<DropListItem on:click={importProject}>
<div class="u-flex u-gap-8 u-cross-center">
Import project <span class="tag eyebrow-heading-3">Experimental</span>
</div>
</DropListItem>
</svelte:fragment>
</DropList>
</div>
<DropList bind:show={showDropdown} placement="bottom-end">
{#if $canWriteProjects}
<Button
on:click={handleCreateProject}
event="create_project"
disabled={$readOnly && !GRACE_PERIOD_OVERRIDE}>
<Icon icon={IconPlus} slot="start" size="s" />
Create project
</Button>
{/if}
<svelte:fragment slot="list">
<DropListItem on:click={() => (showCreate = true)}>Empty project</DropListItem>
<DropListItem on:click={importProject}>
<div class="u-flex u-gap-8 u-cross-center">
Import project <span class="tag eyebrow-heading-3">Experimental</span>
</div>
</DropListItem>
</svelte:fragment>
</DropList>
</div>
{#if data.projects.total}
<CardContainer
showEmpty={$canWriteProjects}
total={data.projects.total}
offset={data.offset}
on:click={handleCreateProject}>
{#each data.projects.projects as project}
{@const platforms = filterPlatforms(
project.platforms.map((platform) => getPlatformInfo(platform.type))
)}
<li>
<GridItem1 href={`${base}/project-${project.$id}`}>
<svelte:fragment slot="eyebrow">
{project?.platforms?.length ? project?.platforms?.length : 'No'} apps
</svelte:fragment>
<svelte:fragment slot="title">
{project.name}
</svelte:fragment>
{#if allServiceDisabled(project)}
<p>
<span class="icon-pause" aria-hidden="true" /> All services are disabled.
</p>
{/if}
{#each platforms as platform, i}
{#if i < 3}
<Pill>
<span class={`icon-${platform.icon}`} aria-hidden="true" />
{platform.name}
</Pill>
{/if}
{/each}
{#if platforms?.length > 3}
<Pill>
+{project.platforms.length - 3}
</Pill>
{/if}
<svelte:fragment slot="icons">
{#if isCloud && regions}
{@const region = findRegion(project)}
<span class="u-color-text-gray u-medium u-line-height-2">
{region?.name}
</span>
{/if}
</svelte:fragment>
</GridItem1>
</li>
{/each}
<svelte:fragment slot="empty">
<p>Create a new project</p>
</svelte:fragment>
</CardContainer>
{:else}
<Empty
single
allowCreate={$canWriteProjects}
on:click={handleCreateProject}
target="project"
href="https://appwrite.io/docs/quick-starts"></Empty>
{/if}
<PaginationWithLimit
name="Projects"
limit={data.limit}
{#if data.projects.total}
<CardContainer
showEmpty={$canWriteProjects}
total={data.projects.total}
offset={data.offset}
total={data.projects.total} />
</Container>
on:click={handleCreateProject}>
{#each data.projects.projects as project}
{@const platforms = filterPlatforms(
project.platforms.map((platform) => getPlatformInfo(platform.type))
)}
<GridItem1 href={`${base}/project-${project.$id}`}>
<svelte:fragment slot="eyebrow">
{project?.platforms?.length ? project?.platforms?.length : 'No'} apps
</svelte:fragment>
<svelte:fragment slot="title">
{project.name}
</svelte:fragment>
{#if allServiceDisabled(project)}
<p>
<span class="icon-pause" aria-hidden="true" /> All services are disabled.
</p>
{/if}
<CreateOrganization bind:show={addOrganization} />
<CreateProject bind:show={showCreate} teamId={$page.params.organization} />
{/if}
{#each platforms as platform, i}
{#if i < 3}
{@const icon = getIconForPlatform(platform.icon)}
<Badge variant="secondary" content={platform.name}>
<Icon {icon} size="s" slot="start" />
</Badge>
{/if}
{/each}
{#if platforms?.length > 3}
<Badge variant="secondary" content={`+${project.platforms.length - 3}`} />
{/if}
<svelte:fragment slot="icons">
{#if isCloud && regions}
{@const region = findRegion(project)}
<span class="u-color-text-gray u-medium u-line-height-2">
{region?.name}
</span>
{/if}
</svelte:fragment>
</GridItem1>
{/each}
<svelte:fragment slot="empty">
<p>Create a new project</p>
</svelte:fragment>
</CardContainer>
{:else}
<Empty
single
allowCreate={$canWriteProjects}
on:click={handleCreateProject}
target="project"
href="https://appwrite.io/docs/quick-starts"></Empty>
{/if}
<PaginationWithLimit
name="Projects"
limit={data.limit}
offset={data.offset}
total={data.projects.total} />
</Container>
<CreateOrganization bind:show={addOrganization} />
<CreateProject bind:show={showCreate} teamId={$page.params.organization} />
@@ -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>
@@ -35,7 +35,7 @@
let showDelete = false;
let showRetry = false;
let selectedDomain: Models.ProxyRule = null;
let selectedDomain: Models.Domain = null;
$: console.log(data.domains);
@@ -66,7 +66,7 @@
<Table.Header.Cell>Auto renewal</Table.Header.Cell>
<Table.Header.Cell />
</svelte:fragment>
{#each data.domains.rules as domain}
{#each data.domains.domains as domain}
<Table.Link
href={`${base}/organization-${$page.params.organization}/domains/domain-${domain.$id}`}>
<Table.Cell>
@@ -81,24 +81,26 @@
<Icon icon={IconExternalLink} size="s" />
</Layout.Stack>
</Link>
{#if domain.status === 'created'}
{#if !domain.verified}
<Badge
size="s"
variant="secondary"
content="Verification failed"
type="error" />
{:else if domain.status === 'veryfing'}
<Badge
size="s"
variant="secondary"
content="Pending verification"
type="warning" />
{/if}
</Layout.Stack>
</Table.Cell>
<Table.Cell>{domain?.registrar ?? '-'}</Table.Cell>
<Table.Cell>{domain?.nameservers ?? '-'}</Table.Cell>
<Table.Cell>{domain?.expiry ?? '-'}</Table.Cell>
<Table.Cell>{domain?.registrar || '-'}</Table.Cell>
<Table.Cell>{domain?.nameservers || '-'}</Table.Cell>
<Table.Cell>{domain?.expiry || '-'}</Table.Cell>
<Table.Cell>
{domain.renewAt ? toLocaleDateTime(domain.renewAt) : '-'}
{domain.renewal ? toLocaleDateTime(domain.renewal) : '-'}
</Table.Cell>
<Table.Cell>{domain.autoRenewal ? 'On' : 'Off'}</Table.Cell>
<Table.Cell>
@@ -116,7 +118,7 @@
<svelte:fragment slot="tooltip" let:toggle>
<ActionMenu.Root>
{#if domain.status !== 'verified'}
<!-- {#if domain.status !== 'verified'}
<ActionMenu.Item.Button
leadingIcon={IconRefresh}
on:click={(e) => {
@@ -127,7 +129,7 @@
}}>
Retry
</ActionMenu.Item.Button>
{/if}
{/if} -->
<ActionMenu.Item.Button
status="danger"
leadingIcon={IconTrash}
@@ -20,14 +20,11 @@ export const load = async ({ depends, url, route }) => {
limit,
query,
search,
domains: await sdk.forProject.proxy.listRules(
[
Query.limit(limit),
Query.offset(offset),
Query.orderDesc(''),
...parsedQueries.values()
],
search || undefined
)
domains: await sdk.forConsole.domains.list([
Query.limit(limit),
Query.offset(offset),
Query.orderDesc(''),
...parsedQueries.values()
])
};
};
@@ -23,15 +23,11 @@
const backPage = `${base}/project-${$page.params.project}/sites/site-${$page.params.site}/domains`;
let domain = '';
let domainData: Models.ProxyRule;
let domainData: Models.Domain;
async function addDomain() {
try {
domainData = await sdk.forProject.proxy.createRule(
domain,
ResourceType.Site,
$page.params.site
);
domainData = await sdk.forConsole.domains.create($page.params.organization, domain);
console.log(domainData);
invalidate(Dependencies.SITES_DOMAINS);
} catch (error) {
@@ -44,7 +40,7 @@
async function verifyStatus() {
try {
domainData = await sdk.forProject.proxy.updateRuleVerification(domainData.$id);
domainData = await sdk.forConsole.domains.updateNameservers(domainData.$id);
console.log(domainData);
} catch (error) {
addNotification({
@@ -56,7 +52,7 @@
async function back() {
try {
await sdk.forProject.proxy.deleteRule(domainData.$id);
await sdk.forConsole.domains.delete(domainData.$id);
domainData = undefined;
} catch (error) {
addNotification({
@@ -1,5 +1,4 @@
<script lang="ts">
import { Button, InputCheckbox } from '$lib/elements/forms';
import { sdk } from '$lib/stores/sdk';
import { addNotification } from '$lib/stores/notifications';
import { invalidate } from '$app/navigation';
@@ -9,13 +8,13 @@
import { Confirm } from '$lib/components';
export let show = false;
export let selectedDomain: Models.ProxyRule;
let confirm = false;
export let selectedDomain: Models.Domain;
let error = '';
async function deleteDomain() {
try {
await sdk.forProject.proxy.deleteRule(selectedDomain.$id);
await sdk.forConsole.domains.delete(selectedDomain.$id);
await invalidate(Dependencies.DOMAINS);
show = false;
addNotification({
@@ -30,21 +29,7 @@
}
</script>
<Confirm title="Delete domain" bind:open={show} onSubmit={deleteDomain} bind:error>
{#if selectedDomain}
<p data-private>
Are you sure you want to delete <b>{selectedDomain.domain}</b>? You will no longer be
able to execute your function by visiting this domain.
</p>
{/if}
<InputCheckbox
required
bind:checked={confirm}
id="confirm"
label="I understand and confirm"
size="s" />
<svelte:fragment slot="footer">
<Button text on:click={() => (show = false)}>Cancel</Button>
<Button secondary submit disabled={!confirm}>Delete</Button>
</svelte:fragment>
<Confirm onSubmit={deleteDomain} title="Delete domain" bind:open={show} bind:error>
<p data-private>Are you sure you want to delete <b>{selectedDomain?.domain}</b>?</p>
<p>Your site will no longer be available at this domain. This action is irreversible.</p>
</Confirm>
@@ -4,10 +4,10 @@ import { Dependencies } from '$lib/constants';
import { sdk } from '$lib/stores/sdk';
export const load = async ({ depends, params }) => {
depends(Dependencies.SITE);
depends(Dependencies.DOMAIN);
return {
domain: await sdk.forProject.proxy.getRule(params.domain),
domain: await sdk.forConsole.domains.get(params.domain),
header: Header,
breadcrumbs: Breadcrumbs
};
@@ -36,7 +36,7 @@
<DomainMetrics domain={data.domain} />
<Layout.Stack gap="l">
{#if data.records.total}
{#if data.recordList.total}
<Layout.Stack direction="row" justifyContent="space-between">
<Layout.Stack direction="row" gap="s" inline>
<Button secondary>
@@ -3,6 +3,7 @@ import { Dependencies } from '$lib/constants';
import { pageToOffset } from '$lib/helpers/load';
import { getLimit } from '$lib/helpers/load';
import { getPage } from '$lib/helpers/load';
import { sdk } from '$lib/stores/sdk.js';
export const load = async ({ parent, depends, url, route }) => {
depends(Dependencies.DOMAIN);
@@ -14,47 +15,8 @@ export const load = async ({ parent, depends, url, route }) => {
return {
domain,
records: mockDNSRecords,
recordList: await sdk.forConsole.domains.listRecords(domain.$id),
offset,
limit
};
};
const mockDNSRecords = {
total: 4,
records: [
{
type: 'A',
name: '@',
value: '192.0.2.1',
ttl: 3600,
proxied: true,
$createdAt: '2023-01-15T10:30:00Z'
},
{
type: 'CNAME',
name: 'www',
value: 'example.com',
ttl: 3600,
proxied: true,
$createdAt: '2023-01-15T10:30:00Z'
},
{
type: 'MX',
name: '@',
value: 'mail.example.com',
priority: 10,
ttl: 3600,
proxied: false,
$createdAt: '2023-01-15T10:30:00Z'
},
{
type: 'TXT',
name: '@',
value: 'v=spf1 include:_spf.example.com ~all',
ttl: 3600,
proxied: false,
$createdAt: '2023-01-15T10:30:00Z'
}
]
};
@@ -4,7 +4,6 @@
import { addNotification } from '$lib/stores/notifications';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { Skeleton, Table } from '@appwrite.io/pink-svelte';
import { onMount } from 'svelte';
import { capitalize } from '$lib/helpers/string';
export let show = false;
@@ -1,8 +1,9 @@
import { PAGE_LIMIT, Dependencies } from '$lib/constants';
import { getSearch, pageToOffset, getLimit, getPage } from '$lib/helpers/load';
import { sdk } from '$lib/stores/sdk.js';
export const load = async ({ parent, depends, url, route }) => {
depends(Dependencies.DOMAINS);
depends(Dependencies.DOMAIN);
const page = getPage(url);
const limit = getLimit(url, route, PAGE_LIMIT);
const offset = pageToOffset(page, limit);
@@ -1,11 +1,9 @@
<script lang="ts">
import { Button } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { invalidate } from '$app/navigation';
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
import { Dependencies } from '$lib/constants';
import { Confirm } from '$lib/components';
import { Typography } from '@appwrite.io/pink-svelte';
export let show = false;
export let selectedCertificate;
@@ -14,8 +12,8 @@
async function deleteCertificate() {
try {
await invalidate(Dependencies.DOMAIN);
//TODO: add method
await invalidate(Dependencies.DOMAIN);
show = false;
addNotification({
type: 'success',
@@ -30,11 +28,5 @@
</script>
<Confirm title="Delete certificate" bind:open={show} onSubmit={deleteCertificate} bind:error>
<Typography.Text>
Are you sure you want to delete SSL certificate? This action is irreversible.
</Typography.Text>
<svelte:fragment slot="footer">
<Button text on:click={() => (show = false)}>Cancel</Button>
<Button secondary submit danger>Delete</Button>
</svelte:fragment>
Are you sure you want to delete this SSL certificate? This action is irreversible.
</Confirm>
@@ -68,7 +68,7 @@
}
];
let name: string;
let type: RecordType;
let type: RecordType = 'A';
let value: string;
let ttl = 3600;
let priority: number;
@@ -99,9 +99,10 @@
<Layout.Stack gap="l">
<InputText id="name" label="Name" placeholder="subdomain" bind:value={name} />
<Layout.Stack gap="xs">
<InputSelect {options} bind:value={type} id="type" label="Type" />
<InputSelect {options} bind:value={type} id="type" label="Type" required />
<Layout.Stack direction="row" gap="xs">
<Icon icon={IconInfo} />
{options.find((option) => option.value === type)?.helper}
</Layout.Stack>
</Layout.Stack>
@@ -109,7 +110,8 @@
<Tooltip slot="info">
<Icon icon={IconInfo} />
<span slot="tooltip">
{options.find((option) => option.value === type)?.helper}
Enter the target or destination for this DNS record (e.g., IP address, hostname,
or other required data)
</span>
</Tooltip>
</InputText>
@@ -0,0 +1,35 @@
<script lang="ts">
import { sdk } from '$lib/stores/sdk';
import { addNotification } from '$lib/stores/notifications';
import { invalidate } from '$app/navigation';
import type { Models } from '@appwrite.io/console';
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
import { Dependencies } from '$lib/constants';
import { Confirm } from '$lib/components';
import { page } from '$app/stores';
export let show = false;
export let selectedRecord: Models.DnsRecord;
let error = '';
async function deleteDomain() {
try {
await sdk.forConsole.domains.deleteRecord($page.params.domain, selectedRecord.$id);
await invalidate(Dependencies.DOMAINS);
show = false;
addNotification({
type: 'success',
message: 'Record has been deleted'
});
trackEvent(Submit.DomainDelete);
} catch (e) {
error = e.message;
trackError(e, Submit.DomainDelete);
}
}
</script>
<Confirm onSubmit={deleteDomain} title="Delete domain" bind:open={show} bind:error>
<p>Are you sure you want to delete this record? This action is irreversible.</p>
</Confirm>
@@ -4,7 +4,7 @@
import type { Models } from '@appwrite.io/console';
import { toLocaleDate } from '$lib/helpers/date';
export let domain: Models.ProxyRule;
export let domain: Models.Domain;
let metrics = [
{
value: domain.domain,
@@ -19,11 +19,11 @@
description: 'Nameservers'
},
{
value: domain?.expiresAt ? toLocaleDate(domain?.expiresAt) : '-',
value: domain?.expiry ? toLocaleDate(domain?.expiry) : '-',
description: 'Exipiry date'
},
{
value: domain?.autoRenewals ? 'On' : 'off',
value: domain?.autoRenewal ? 'On' : 'off',
description: 'Auto renewal'
},
{
@@ -0,0 +1,158 @@
<script lang="ts">
import {
Button,
InputNumber,
InputSelect,
InputText,
InputTextarea
} from '$lib/elements/forms';
import { Modal } from '$lib/components';
import { Icon, Layout, Tooltip } from '@appwrite.io/pink-svelte';
import { IconInfo } from '@appwrite.io/pink-icons-svelte';
import { addNotification } from '$lib/stores/notifications';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import type { Models } from '@appwrite.io/console';
import { symmetricDifference } from '$lib/helpers/array';
import { deepClone } from '$lib/helpers/object';
// type RecordType = 'A' | 'AAAA' | 'CNAME' | 'MX' | 'TXT' | 'NS' | 'SRV' | 'CAA' | 'PTR';
export let show = false;
export let selectedRecord: Models.DnsRecord;
const options = [
{
value: 'A',
label: 'A ',
helper: 'A records map a domain to an IPv4 address, allowing browsers to find your website by translating the domain name to an IP address.'
},
{
value: 'AAAA',
label: 'AAAA ',
helper: 'AAAA records map a domain to an IPv6 address, providing the same function as A records but for IPv6-enabled devices.'
},
{
value: 'CNAME',
label: 'CNAME',
helper: 'CNAME records alias one domain name to another, allowing you to point subdomains or other domain names to an existing domain.'
},
{
value: 'MX',
label: 'MX ',
helper: 'MX records specify mail servers responsible for receiving emails for a domain, helping route email traffic to the correct mail server.'
},
{
value: 'TXT',
label: 'TXT',
helper: 'TXT records store arbitrary text data in DNS, commonly used for verification purposes, such as domain ownership or email security settings.'
},
{
value: 'NS',
label: 'NS',
helper: "NS records define the authoritative DNS servers for a domain, directing queries to the servers that manage the domain's DNS settings."
},
{
value: 'SRV',
label: 'SRV',
helper: 'SRV records specify the location (hostname and port number) of servers for specific services, directing traffic to particular servers based on service types.'
},
{
value: 'CAA',
label: 'CAA',
helper: 'CAA records specify which certificate authorities are allowed to issue SSL certificates for your domain, helping to prevent unauthorized certificate issuance.'
},
{
value: 'HTTPS',
label: 'HTTPS',
helper: 'HTTPS records define which service or endpoint handles secure HTTPS traffic for your domain, typically used in SSL/TLS configurations.'
},
{
value: 'ALIAS',
label: 'ALIAS',
helper: 'ALIAS records are similar to CNAMEs but can be used for the root domain, allowing you to point your domain to another domain or server.'
}
];
let record = deepClone(selectedRecord);
let error = '';
async function handleSubmit() {
try {
//TODO: update DNS record
show = false;
addNotification({
type: 'success',
message: `Record has been updated`
});
trackEvent(Submit.RecordUpdate);
} catch (e) {
error = e.message;
trackError(e, Submit.RecordUpdate);
}
}
$: if (!show) {
selectedRecord = null;
}
$: isDisabled = !symmetricDifference(Object.values(record), Object.values(selectedRecord))
?.length;
</script>
<Modal title="Update DNS record" bind:show bind:error onSubmit={handleSubmit}>
<span slot="description">
DNS records map domain names to IP addresses or other resources.
</span>
<Layout.Stack gap="l">
<InputText id="name" label="Name" placeholder="subdomain" bind:value={record.name} />
<Layout.Stack gap="xs">
<InputSelect {options} bind:value={record.type} id="type" label="Type" />
<Layout.Stack direction="row" gap="xs">
<Icon icon={IconInfo} />
</Layout.Stack>
</Layout.Stack>
<InputText id="value" label="Value" placeholder="76.75.21.21" bind:value={record.value}>
<Tooltip slot="info">
<Icon icon={IconInfo} />
<span slot="tooltip">
{options.find((option) => option.value === record.type)?.helper}
</span>
</Tooltip>
</InputText>
<Layout.Stack direction="row" gap="l">
<InputNumber id="ttl" label="TTL" placeholder="Enter number" bind:value={record.ttl}>
<Tooltip slot="info">
<Icon icon={IconInfo} />
<span slot="tooltip">
TTL defines how long DNS information is cached. Lower values update faster;
higher values reduce server load.
</span>
</Tooltip>
</InputNumber>
<InputNumber
id="priority"
label="Priority"
placeholder="Enter number"
bind:value={record.priority}>
<Tooltip slot="info">
<Icon icon={IconInfo} />
<span slot="tooltip">
Sets the priority for this DNS record. Lower numbers indicate higher
priority (e.g., 10 is higher than 20).
</span>
</Tooltip>
</InputNumber>
</Layout.Stack>
<InputTextarea
id="comment"
label="Comment"
placeholder="Provide an explanation of this DNS record's purpose"
bind:value={record.comment} />
</Layout.Stack>
<svelte:fragment slot="footer">
<Button text on:click={() => (show = false)}>Cancel</Button>
<Button secondary submit disabled={isDisabled}>Update DNS record</Button>
</svelte:fragment>
</Modal>
@@ -3,14 +3,17 @@
import { CardGrid } from '$lib/components';
import { Button, InputSelect } from '$lib/elements/forms';
import type { OrganizationList } from '$lib/stores/organization';
import { sdk } from '$lib/stores/sdk';
import type { Models } from '@appwrite.io/console';
export let domain: Models.ProxyRule;
export let domain: Models.Domain;
export let organizations: OrganizationList;
let selectedOrg: string = null;
async function moveDomain() {
console.log(selectedOrg);
try {
await sdk.forConsole.domains.updateTeam(domain.$id, selectedOrg);
} catch (error) {}
}
$: options = organizations.total
@@ -27,6 +30,7 @@
<InputSelect
id="orgs"
label="Move to"
required
placeholder="Select destination"
disabled={options?.length === 0}
{options}
@@ -5,7 +5,7 @@
import type { Models } from '@appwrite.io/console';
import Delete from '../../deleteDomainModal.svelte';
export let domain: Models.ProxyRule;
export let domain: Models.Domain;
let show = false;
</script>
@@ -3,8 +3,7 @@ import type { Column } from '$lib/helpers/types';
import type { Models } from '@appwrite.io/console';
import { derived, writable } from 'svelte/store';
export const domain = derived(page, ($page) => $page.data.domain as Models.ProxyRule);
// columns: namem type, value, ttl, priority, comment, created
export const domain = derived(page, ($page) => $page.data.domain as Models.Domain);
export const columns = writable<Column[]>([
{ id: 'name', title: 'Name', type: 'string', show: true },
{ id: 'type', title: 'Type', type: 'string', show: true },
@@ -3,6 +3,7 @@
import {
ActionMenu,
Button,
HiddenText,
Icon,
Layout,
@@ -13,13 +14,16 @@
import { IconDotsHorizontal, IconPencil, IconTrash } from '@appwrite.io/pink-icons-svelte';
import { columns } from './store';
import { timeFromNow } from '$lib/helpers/date';
import { Button } from '$lib/elements/forms';
import type { Models } from '@appwrite.io/console';
import DeleteRecordModal from './deleteRecordModal.svelte';
import { capitalize } from '$lib/helpers/string';
import EditRecordModal from './editRecordModal.svelte';
export let data;
let showEdit = false;
let showDelete = false;
let selectedRecord; //TODO: add type
let selectedRecord: Models.DnsRecord = null;
</script>
<Layout.Stack>
@@ -35,7 +39,7 @@
<Table.Header.Cell width="40" />
</svelte:fragment>
{#each data.records.records as record}
{#each data.recordList.dnsRecords as record}
<Table.Row>
{#each $columns as column}
{#if column.show}
@@ -78,7 +82,7 @@
{:else if column.id === '$createdAt'}
<Table.Cell>
<Typography.Text>
{timeFromNow(record.$createdAt)}
{capitalize(timeFromNow(record.$createdAt))}
</Typography.Text>
</Table.Cell>
{/if}
@@ -87,15 +91,16 @@
<Table.Cell>
<Layout.Stack direction="row" justifyContent="flex-end">
<Popover let:toggle placement="bottom-start" padding="none">
<Button
text
<Button.Button
variant="text"
icon
size="s"
on:click={(e) => {
e.preventDefault();
toggle(e);
}}>
<Icon icon={IconDotsHorizontal} size="s" />
</Button>
</Button.Button>
<svelte:fragment slot="tooltip" let:toggle>
<ActionMenu.Root>
@@ -133,5 +138,12 @@
name="Domains"
limit={data.limit}
offset={data.offset}
total={data.records.total} />
total={data.recordList.total} />
</Layout.Stack>
{#if showDelete}
<DeleteRecordModal bind:show={showDelete} {selectedRecord} />
{/if}
{#if showEdit}
<EditRecordModal bind:show={showEdit} {selectedRecord} />
{/if}
@@ -54,7 +54,8 @@
{
href: `${path}/domains`,
event: 'domains',
title: 'Domains'
title: 'Domains',
disabled: !isCloud
// disabled: !(isCloud && $canSeeDomains) //TODO: enable this before release
},
{
@@ -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="xxxs" alignItems="center">
Roles
<Popover let:toggle>
<Button.Button icon variant="extra-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 {
@@ -78,7 +78,7 @@
<Table.Cell width={column.width + 'px'}>
<Copy value={user.$id} event="user">
<Tag size="xs" variant="code">
<Icon size="s" icon={IconDuplicate} />
<Icon size="s" icon={IconDuplicate} slot="start" />
{user.$id}
</Tag>
</Copy>
@@ -2,7 +2,7 @@
import { onMount } from 'svelte';
import { invalidate } from '$app/navigation';
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
import { CardGrid, Heading } from '$lib/components';
import { CardGrid } from '$lib/components';
import { Dependencies } from '$lib/constants';
import { Button, Form, Helper, InputTags } from '$lib/elements/forms';
import { symmetricDifference } from '$lib/helpers/array';
@@ -28,8 +28,6 @@
const projectId = $page.params.project;
const databaseId = $page.params.database;
let showCreateDropdown = false;
let showEmptyCreateDropdown = false;
let showDropdown = [];
let selectedOption: Option['name'] = null;
let selectedAttribute: Attributes = null;
@@ -53,7 +51,7 @@
<Heading tag="h2" size="5">Attributes</Heading>
{#if $canWriteCollections}
<CreateAttributeDropdown bind:showCreateDropdown bind:selectedOption bind:showCreate />
<CreateAttributeDropdown bind:selectedOption bind:showCreate />
{/if}
</div>
@@ -197,18 +195,8 @@
<p class="text">Total results: {$attributes.length}</p>
</div>
{:else}
<Empty
allowCreate={$canWriteCollections}
single
target="attribute"
on:click={() => (showEmptyCreateDropdown = true)}>
<div class="u-text-center">
<Heading size="7" tag="h2">Create your first attribute 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">
<Empty single target="attribute">
<svelte:fragment slot="actions">
<Button
external
href="https://appwrite.io/docs/products/databases/collections#attributes"
@@ -216,21 +204,13 @@
event="empty_documentation"
ariaLabel={`create {target}`}>Documentation</Button>
{#if $canWriteCollections}
<CreateAttributeDropdown
bind:showCreateDropdown={showEmptyCreateDropdown}
bind:selectedOption
bind:showCreate>
<Button
secondary
event="create_attribute"
on:click={() => {
showEmptyCreateDropdown = !showEmptyCreateDropdown;
}}>
<CreateAttributeDropdown bind:selectedOption bind:showCreate let:toggle>
<Button secondary event="create_attribute" on:click={toggle}>
Create attribute
</Button>
</CreateAttributeDropdown>
{/if}
</div>
</svelte:fragment>
</Empty>
{/if}
</Container>
@@ -1,36 +1,30 @@
<script lang="ts">
import { DropList, DropListItem } from '$lib/components';
import { Button } from '$lib/elements/forms';
import { Icon } from '@appwrite.io/pink-svelte';
import { ActionMenu, Icon, Popover } from '@appwrite.io/pink-svelte';
import { attributeOptions, type Option } from './store';
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
export let showCreateDropdown = false;
export let selectedOption: Option['name'] = null;
export let showCreate = false;
</script>
<DropList bind:show={showCreateDropdown} scrollable>
<slot>
<Button
on:click={() => (showCreateDropdown = !showCreateDropdown)}
event="create_attribute">
<Popover let:toggle padding="none" placement="bottom-start">
<slot {toggle}>
<Button on:click={toggle} event="create_attribute">
<Icon icon={IconPlus} slot="start" size="s" />
Create attribute
</Button>
</slot>
<svelte:fragment slot="list">
<ActionMenu.Root slot="tooltip">
{#each attributeOptions as attribute}
<DropListItem
icon={attribute.icon}
<ActionMenu.Item.Button
leadingIcon={attribute.icon}
on:click={() => {
selectedOption = attribute.name;
showCreateDropdown = false;
showCreate = true;
}}>
{attribute.name}
</DropListItem>
</ActionMenu.Item.Button>
{/each}
</svelte:fragment>
</DropList>
</ActionMenu.Root>
</Popover>
@@ -14,6 +14,7 @@ export const templateConfig = writable<{
repositoryId: string;
execute?: boolean;
scopes?: string[];
specification?: string;
}>();
export const choices = writable<{
@@ -49,10 +49,7 @@
$templateConfig.repositoryBehaviour === 'manual'
? undefined
: $choices.rootDir || undefined,
$template.providerRepositoryId || undefined,
$template.providerOwner || undefined,
runtimeDetail.providerRootDirectory || undefined,
$template.providerVersion || undefined
$templateConfig.specification
);
if ($templateConfig.variables) {
@@ -1,31 +1,30 @@
<script lang="ts">
import { DropList, DropListItem } from '$lib/components';
import { createEventDispatcher } from 'svelte';
import { targetsById } from './wizard/store';
import UserTargetsModal from './userTargetsModal.svelte';
import TopicsModal from './topicsModal.svelte';
import { topicsById } from './store';
import type { MessagingProviderType } from '@appwrite.io/console';
import { ActionMenu, Popover } from '@appwrite.io/pink-svelte';
export let showDropdown: boolean;
export let showUserTargets: boolean;
export let showTopics: boolean;
export let providerType: MessagingProviderType = null;
const dispatch = createEventDispatcher();
$: if (showUserTargets || showTopics) {
showDropdown = false;
}
</script>
<DropList bind:show={showDropdown} placement="bottom-end" fixed>
<slot />
<svelte:fragment slot="list">
<DropListItem on:click={() => (showTopics = true)}>Select topics</DropListItem>
<DropListItem on:click={() => (showUserTargets = true)}>Select targets</DropListItem>
<Popover let:toggle placement="bottom-end">
<slot {toggle} />
<svelte:fragment slot="tooltip">
<ActionMenu.Root>
<ActionMenu.Item.Button on:click={() => (showTopics = true)}
>Select topics</ActionMenu.Item.Button>
<ActionMenu.Item.Button on:click={() => (showUserTargets = true)}
>Select targets</ActionMenu.Item.Button>
</ActionMenu.Root>
</svelte:fragment>
</DropList>
</Popover>
<TopicsModal
{providerType}
@@ -17,7 +17,6 @@
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
import { Icon } from '@appwrite.io/pink-svelte';
let showDropdown = false;
let showTopics = false;
let showUserTargets = false;
let targetIdsLength = 0;
@@ -65,12 +64,12 @@
<div class="u-flex u-cross-center u-flex-vertical u-main-center u-flex">
<Actions
providerType={$providerType}
bind:showDropdown
bind:showUserTargets
bind:showTopics
on:addTargets={addTargets}
on:addTopics={addTopics}>
<Button secondary icon on:click={() => (showDropdown = !showDropdown)}>
on:addTopics={addTopics}
let:toggle>
<Button secondary icon on:click={toggle}>
<Icon icon={IconPlus} size="s" />
</Button>
</Actions>
@@ -143,12 +142,12 @@
</div>
<Actions
providerType={$providerType}
bind:showDropdown
bind:showUserTargets
bind:showTopics
on:addTargets={addTargets}
on:addTopics={addTopics}>
<Button text on:click={() => (showDropdown = !showDropdown)}>
on:addTopics={addTopics}
let:toggle>
<Button text on:click={toggle}>
<Icon icon={IconPlus} slot="start" size="s" />
Add
</Button>
@@ -6,7 +6,7 @@
</script>
<script lang="ts">
import { afterNavigate } from '$app/navigation';
import { afterNavigate, goto } from '$app/navigation';
import { base } from '$app/paths';
import { page } from '$app/stores';
import { addSubPanel, registerCommands, updateCommandGroupRanks } from '$lib/commandCenter';
@@ -15,10 +15,7 @@
import { humanFileSize } from '$lib/helpers/sizeConvertion';
import { Container, type UsagePeriods } from '$lib/layout';
import { onMount, setContext, SvelteComponent } from 'svelte';
import { project } from '../store';
import Bandwidth from './bandwidth.svelte';
import { createApiKey } from './keys/+page.svelte';
import Onboard from './onboard.svelte';
import Realtime from './realtime.svelte';
import Requests from './requests.svelte';
import { usage } from './store';
@@ -27,8 +24,7 @@
import type { Metric } from '$lib/sdk/usage';
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';
@@ -70,7 +66,7 @@
label: 'Create API Key',
icon: IconPlus,
callback() {
createApiKey();
goto(`${base}/project-[project]/overview/keys/create`);
},
keys: ['c', 'k'],
group: 'integrations',
@@ -87,21 +83,27 @@
<title>Console - Appwrite</title>
</svelte:head>
{#if $project}
<Container overlapCover>
<Container overlapCover>
<!-- 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">
<div class="card is-2-columns-medium-screen is-3-columns-large-screen">
<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)} />
</div>
<div class="card is-2-columns-medium-screen is-3-columns-large-screen">
</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)} />
</div>
<a
</Card.Base>
<Card.Link
padding="s"
href={`${base}/project-${projectId}/databases`}
class="card is-2-columns-large-screen">
class="is-2-columns-large-screen">
<div class="grid-item-1">
<div class="grid-item-1-start-start">
<div class="eyebrow-heading-3">
@@ -113,22 +115,17 @@
<div class="grid-item-1-start-end" />
<div class="grid-item-1-end-start">
<div class="heading-level-4">
<Typography.Title>
{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>
</Typography.Title>
<Typography.Text>Documents</Typography.Text>
</div>
</div>
</a>
<a
</Card.Link>
<Card.Link
padding="s"
href={`${base}/project-${projectId}/storage`}
class="card is-2-columns-large-screen">
class="is-2-columns-large-screen">
<div class="grid-item-1">
<div class="grid-item-1-start-start">
<div class="eyebrow-heading-3">
@@ -140,23 +137,18 @@
<div class="grid-item-1-start-end" />
<div class="grid-item-1-end-start">
<div class="heading-level-4">
<Typography.Title>
{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>
</Typography.Title>
<Typography.Text>Storage</Typography.Text>
</div>
</div>
</a>
<a
</Card.Link>
<Card.Link
padding="s"
href={`${base}/project-${projectId}/auth`}
class="card is-2-columns-large-screen">
class="is-2-columns-large-screen">
<div class="grid-item-1">
<div class="grid-item-1-start-start">
<div class="eyebrow-heading-3">
@@ -168,16 +160,17 @@
<div class="grid-item-1-start-end" />
<div class="grid-item-1-end-start">
<div class="heading-level-4">
<Typography.Title>
{formatNum($usage.usersTotal ?? 0)}
</div>
<div>Users</div>
</Typography.Title>
<Typography.Text>Users</Typography.Text>
</div>
</div>
</a>
<a
</Card.Link>
<Card.Link
padding="s"
href={`${base}/project-${projectId}/functions`}
class="card is-2-columns-large-screen">
class="is-2-columns-large-screen">
<div class="grid-item-1">
<div class="grid-item-1-start-start">
<div class="eyebrow-heading-3">
@@ -189,21 +182,22 @@
<div class="grid-item-1-start-end" />
<div class="grid-item-1-end-start">
<div class="heading-level-4">
<Typography.Title>
{formatNum($usage.executionsTotal ?? 0)}
</div>
<div>Executions</div>
</Typography.Title>
<Typography.Text>Executions</Typography.Text>
</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">
</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 />
</div>
</Card.Base>
</div>
</section>
{/if}
@@ -213,10 +207,12 @@
<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>
@@ -227,5 +223,13 @@
</Layout.Stack>
<slot />
</Layout.Stack>
</Container>
{/if}
</div>
</Container>
<style>
.flex-container {
display: flex;
flex-direction: column;
gap: var(--space-12);
}
</style>
@@ -1,16 +1,24 @@
<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,
Button
} from '@appwrite.io/pink-svelte';
import { IconChevronDown, IconChevronUp } 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 +27,33 @@
}>;
$: 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" let:showing>
<Button.Button on:click={toggle} variant="extra-compact">
{period}
<Icon icon={showing ? IconChevronUp : IconChevronDown} slot="end" />
</Button.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(() => {
@@ -1,23 +1,14 @@
<script lang="ts" context="module">
export function createApiKey() {
if (!get(canWriteKeys)) {
return;
}
wizard.start(Wizard);
}
</script>
<script lang="ts">
import { Empty } from '$lib/components';
import { toLocaleDate, toLocaleDateTime } from '$lib/helpers/date';
import { canWriteKeys } from '$lib/stores/roles';
import { wizard } from '$lib/stores/wizard';
import { get } from 'svelte/store';
import type { PageData } from './$types';
import Wizard from './wizard.svelte';
import Action from './action.svelte';
import { setOverviewAction } from '../context';
import { Table } from '@appwrite.io/pink-svelte';
import { goto } from '$app/navigation';
import { base } from '$app/paths';
import { page } from '$app/stores';
export let data: PageData;
@@ -55,5 +46,5 @@
allowCreate={$canWriteKeys}
href="https://appwrite.io/docs/advanced/platform/api-keys"
target="API key"
on:click={createApiKey} />
on:click={() => goto(`${base}/project-${$page.params.project}/overview/keys/create`)} />
{/if}
@@ -62,9 +62,7 @@
<p>Update the expiration date to keep the key active</p>
</Alert>
{/if}
<FormList>
<ExpirationInput bind:value={expiration} />
</FormList>
<ExpirationInput bind:value={expiration} />
</svelte:fragment>
<svelte:fragment slot="actions">
@@ -2,12 +2,13 @@
import Button from '$lib/elements/forms/button.svelte';
import { canWriteKeys } from '$lib/stores/roles';
import { Icon } from '@appwrite.io/pink-svelte';
import { createApiKey } from './+page.svelte';
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
import { base } from '$app/paths';
import { page } from '$app/stores';
</script>
{#if $canWriteKeys}
<Button on:click={createApiKey}>
<Button href={`${base}/project-${$page.params.project}/overview/keys/create`}>
<Icon icon={IconPlus} slot="start" size="s" />
Create API key
</Button>
@@ -0,0 +1,98 @@
<script lang="ts">
import { base } from '$app/paths';
import { page } from '$app/stores';
import { InputText } from '$lib/elements/forms/index.js';
import { Wizard } from '$lib/layout';
import { Fieldset, Layout, Typography } from '@appwrite.io/pink-svelte';
import ExpirationInput from '../expirationInput.svelte';
import Scopes from '../scopes.svelte';
import Button from '$lib/elements/forms/button.svelte';
import Form from '$lib/elements/forms/form.svelte';
import { sdk } from '$lib/stores/sdk';
import { onboarding } from '$routes/(console)/project-[project]/store';
import { goto, invalidate } from '$app/navigation';
import { Dependencies } from '$lib/constants';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { addNotification } from '$lib/stores/notifications';
import { writable } from 'svelte/store';
let showExitModal = false;
let formComponent: Form;
let isSubmitting = writable(false);
let name = '',
expire = '';
let scopes = [];
async function create() {
try {
const { $id } = await sdk.forConsole.projects.createKey(
$page.params.project,
name,
scopes,
expire || undefined
);
if ($onboarding) {
await invalidate(Dependencies.PROJECT);
}
trackEvent(Submit.KeyCreate, {
scopes: scopes
});
goto(`${base}/project-${$page.params.project}/overview/keys/${$id}`);
addNotification({
message: `API key has been created`,
type: 'success'
});
} catch (error) {
addNotification({
type: 'error',
message: error.message
});
trackError(error, Submit.KeyCreate);
}
}
</script>
<Wizard
title="Create API key"
href={`${base}/project-${$page.params.project}/overview/keys/`}
bind:showExitModal
column
confirmExit>
<Form bind:this={formComponent} onSubmit={create} bind:isSubmitting>
<Layout.Stack gap="xxl">
<Fieldset legend="Settings">
<Layout.Stack>
<InputText
id="name"
label="Name"
placeholder="API key name"
required
bind:value={name} />
<ExpirationInput bind:value={expire} />
</Layout.Stack>
</Fieldset>
<Fieldset legend="Scopes">
<Layout.Stack gap="xl">
<Typography.Text>
Choose which permission scopes to grant your application. It is best
practice to allow only the permissions you need to meet your project goals.
</Typography.Text>
<Scopes bind:scopes />
</Layout.Stack>
</Fieldset>
</Layout.Stack>
</Form>
<svelte:fragment slot="footer">
<Button fullWidthMobile secondary on:click={() => (showExitModal = true)}>Cancel</Button>
<Button
fullWidthMobile
on:click={() => formComponent.triggerSubmit()}
disabled={$isSubmitting}>
Create
</Button>
</svelte:fragment>
</Wizard>
@@ -73,7 +73,7 @@
}
</script>
<InputSelect bind:value={expirationSelect} {options} id="preset" label="Expiration Date">
<InputSelect bind:value={expirationSelect} {options} id="preset" label="Expiration date">
<svelte:fragment slot="helper">
{#if expirationSelect !== 'custom' && expirationSelect !== null}
<Helper type="neutral">Your key will expire in {toLocaleDate(value)}</Helper>
@@ -5,6 +5,7 @@
import { onMount } from 'svelte';
import { symmetricDifference } from '$lib/helpers/array';
import Checkbox from './checkbox.svelte';
import { Accordion, Divider, Input, Layout, Selector } from '@appwrite.io/pink-svelte';
export let scopes: string[];
@@ -40,7 +41,7 @@
}
}
function categoryState(category: string, s: string[]): boolean | null {
function categoryState(category: string, s: string[]): boolean | 'indeterminate' {
const scopesByCategory = allScopes.filter((n) => n.category === category);
const filtered = scopesByCategory.filter((n) => s.includes(n.scope));
if (filtered.length === 0) {
@@ -48,16 +49,15 @@
} else if (filtered.length === scopesByCategory.length) {
return true;
} else {
return null;
return 'indeterminate';
}
}
function onCategoryChange(event: Event, category: Category) {
function onCategoryChange(event: CustomEvent<boolean | 'indeterminate'>, category: Category) {
if (event.detail === 'indeterminate') return;
allScopes.forEach((s) => {
if (s.category === category) {
activeScopes[s.scope] = (
event.currentTarget as EventTarget & HTMLInputElement
).checked;
activeScopes[s.scope] = event.detail;
}
});
}
@@ -81,47 +81,39 @@
}
</script>
<ul class="buttons-list u-main-end">
<li class="buttons-list-item">
<Button text on:click={deselectAll}>Deselect all</Button>
</li>
<li class="buttons-list-item">
<Button text on:click={selectAll}>Select all</Button>
</li>
</ul>
<Layout.Stack>
<Layout.Stack direction="row" alignItems="center" gap="s">
<Button compact on:click={selectAll}>Select all</Button>
<span style:height="20px">
<Divider vertical />
</span>
<Button compact on:click={deselectAll}>Deselect all</Button>
</Layout.Stack>
<Layout.Stack gap="none">
<Divider />
<Collapsible>
{#each [Category.Auth, Category.Database, Category.Functions, Category.Storage, Category.Messaging, Category.Other] as category}
{@const checked = categoryState(category, scopes)}
<CollapsibleItem withIndentation>
<svelte:fragment slot="beforetitle">
<Checkbox
{checked}
indeterminate={checked === null ? true : false}
on:change={(e) => onCategoryChange(e, category)} />
</svelte:fragment>
<svelte:fragment slot="title">
{category}
</svelte:fragment>
<svelte:fragment slot="subtitle">
{@const scopesLength = allScopes.filter(
(n) => n.category === category && scopes.includes(n.scope)
).length}
({scopesLength}
{scopesLength === 1 ? 'Scope' : 'Scopes'})
</svelte:fragment>
<div class="form">
<FormList>
{#each [Category.Auth, Category.Database, Category.Functions, Category.Storage, Category.Messaging, Category.Other] as category}
{@const checked = categoryState(category, scopes)}
{@const scopesLength = allScopes.filter(
(n) => n.category === category && scopes.includes(n.scope)
).length}
<Accordion
selectable
title={category}
badge={`${scopesLength} ${scopesLength === 1 ? 'Scope' : 'Scopes'}`}
{checked}
on:change={(event) => onCategoryChange(event, category)}>
<Layout.Stack>
{#each allScopes.filter((s) => s.category === category) as scope}
<InputChoice
<Selector.Checkbox
size="s"
id={scope.scope}
label={scope.scope}
bind:value={activeScopes[scope.scope]}>
{scope.description}
</InputChoice>
description={scope.description}
bind:checked={activeScopes[scope.scope]} />
{/each}
</FormList>
</div>
</CollapsibleItem>
{/each}
</Collapsible>
</Layout.Stack>
</Accordion>
{/each}
</Layout.Stack>
</Layout.Stack>
@@ -1,56 +0,0 @@
<script lang="ts">
import { WizardWithSteps } from '$lib/layout';
import { goto, invalidate } from '$app/navigation';
import type { WizardStepsType } from '$lib/layout/wizardWithSteps.svelte';
import Step1 from './wizard/step1.svelte';
import Step2 from './wizard/step2.svelte';
import { key } from './wizard/store';
import { sdk } from '$lib/stores/sdk';
import { page } from '$app/stores';
import { addNotification } from '$lib/stores/notifications';
import { onDestroy } from 'svelte';
import { onboarding } from '../../store';
import { Dependencies } from '$lib/constants';
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
import { base } from '$app/paths';
async function onFinish() {
try {
const { $id } = await sdk.forConsole.projects.createKey(
$page.params.project,
$key.name,
$key.scopes,
$key.expire || undefined
);
if ($onboarding) {
await invalidate(Dependencies.PROJECT);
}
trackEvent(Submit.KeyCreate, {
scopes: $key.scopes
});
goto(`${base}/project-${$page.params.project}/overview/keys/${$id}`);
} catch (error) {
addNotification({
type: 'error',
message: error.message
});
trackError(error, Submit.KeyCreate);
}
}
onDestroy(() => {
key.reset();
});
const stepsComponents: WizardStepsType = new Map();
stepsComponents.set(1, {
label: 'Details',
component: Step1
});
stepsComponents.set(2, {
label: 'Scopes',
component: Step2
});
</script>
<WizardWithSteps title="Create an API key" steps={stepsComponents} on:finish={onFinish} />
@@ -1,20 +0,0 @@
<script lang="ts">
import { FormList, InputText } from '$lib/elements/forms';
import { WizardStep } from '$lib/layout';
import ExpirationInput from '../expirationInput.svelte';
import { key } from './store';
</script>
<WizardStep>
<svelte:fragment slot="title">API key</svelte:fragment>
<svelte:fragment slot="subtitle">Let's create an API key.</svelte:fragment>
<FormList>
<InputText
id="name"
label="Name"
placeholder="API key name"
required
bind:value={$key.name} />
<ExpirationInput bind:value={$key.expire} />
</FormList>
</WizardStep>
@@ -1,17 +0,0 @@
<script lang="ts">
import { FormList } from '$lib/elements/forms';
import { WizardStep } from '$lib/layout';
import Scopes from '../scopes.svelte';
import { key } from './store';
</script>
<WizardStep>
<svelte:fragment slot="title">Scopes</svelte:fragment>
<svelte:fragment slot="subtitle">
Choose which permission scopes to grant your application. It is best practice to allow only
the permissions you need to meet your project goals.
</svelte:fragment>
<FormList>
<Scopes bind:scopes={$key.scopes} />
</FormList>
</WizardStep>
@@ -1,25 +0,0 @@
import { writable } from 'svelte/store';
import type { Models } from '@appwrite.io/console';
function createKeyStore() {
const { subscribe, update, set } = writable<Partial<Models.Key>>({
name: null,
expire: null,
scopes: []
});
return {
subscribe,
update,
set,
reset() {
set({
name: null,
expire: null,
scopes: []
});
}
};
}
export const key = createKeyStore();
@@ -31,15 +31,21 @@
import PlatformFlutterImgSource from './assets/platform-flutter.svg';
import PlatformFlutterImgSourceDark from './assets/platform-flutter-dark.svg';
import { base } from '$app/paths';
import Wizard from './keys/wizard.svelte';
import { wizard } from '$lib/stores/wizard';
import { isSmallViewport } from '$lib/stores/viewport';
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 { goto } from '$app/navigation';
function createKey() {
wizard.start(Wizard);
trackEvent('onboarding_hub_platform', {
platform: 'server',
state: 'add'
});
goto(`${base}/project-${projectId}/overview/keys/create`, {
replaceState: true
});
}
export let projectId: string;
@@ -386,7 +392,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 +430,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 +487,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 +523,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">
@@ -1,12 +1,12 @@
<script lang="ts" context="module">
import SearchDark from '$lib/images/search-dark.svg';
import SearchLight from '$lib/images/search-light.svg';
import { wizard } from '$lib/stores/wizard';
import CreateAndroid from './createAndroid.svelte';
import CreateApple from './createApple.svelte';
import CreateFlutter from './createFlutter.svelte';
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,
@@ -19,6 +19,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]);
}
@@ -34,13 +38,19 @@
key: key,
type: type
});
trackEvent('onboarding_hub_platform', {
platform: platforms[platform],
state: 'continue'
});
wizard.start(platforms[platform], null, 1, {
isPlatformCreated: true,
platform: type
platform: type,
key: key
});
}
const platforms = {
[Platform.Web]: CreateWeb,
[Platform.Flutter]: CreateFlutter,
[Platform.Android]: CreateAndroid,
[Platform.Apple]: CreateApple,
@@ -51,15 +61,29 @@
<script lang="ts">
import { base } from '$app/paths';
import { page } from '$app/stores';
import { DropList, DropListItem, Heading } from '$lib/components';
import { Button } from '$lib/elements/forms';
import { toLocaleDate } from '$lib/helpers/date';
import { app } from '$lib/stores/app';
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,
Icon,
Layout,
Popover,
Table
} from '@appwrite.io/pink-svelte';
import Action from './action.svelte';
import {
IconAndroid,
IconApple,
IconCode,
IconFlutter,
IconReact
} from '@appwrite.io/pink-icons-svelte';
import type { ComponentType } from 'svelte';
export let data: PageData;
@@ -79,22 +103,21 @@
'react-native-ios' = 'iOS',
'web' = 'Web'
}
let showDropdownEmpty = false;
const path = `${base}/project-${$page.params.project}/overview/platforms`;
const getPlatformInfo = (platform: string) => {
function getPlatformInfo(platform: string): ComponentType {
if (platform.includes('flutter')) {
return 'color/flutter';
return IconFlutter;
} else if (platform.includes('react-native')) {
return 'color/react';
return IconReact;
} else if (platform.includes('apple')) {
return 'color/apple';
return IconApple;
} else if (platform.includes('android')) {
return 'color/android';
return IconAndroid;
} else {
return 'unknown';
return IconCode;
}
};
}
setOverviewAction(Action);
</script>
@@ -112,7 +135,10 @@
{platform.name}
</Table.Cell>
<Table.Cell>
{PlatformTypes[platform.type]}
<Layout.Stack direction="row" gap="s" alignItems="center">
<Icon icon={getPlatformInfo(platform.type)} />
{PlatformTypes[platform.type]}
</Layout.Stack>
</Table.Cell>
<Table.Cell>
{platform.$updatedAt ? toLocaleDate(platform.$updatedAt) : 'never'}
@@ -121,49 +147,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.Flutter)}>
<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}
<svelte:fragment 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
</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>
</svelte:fragment>
</Popover>
</svelte:fragment>
</Empty>
</Card.Base>
{/if}
@@ -49,14 +49,12 @@
<svelte:fragment slot="title">Package name</svelte:fragment>
Your package name is generally the applicationId in your app-level build.gradle file.
<svelte:fragment slot="aside">
<FormList>
<InputText
id="key"
label="Package Name"
bind:value={key}
required
placeholder="com.company.appname" />
</FormList>
<InputText
id="key"
label="Package name"
bind:value={key}
required
placeholder="com.company.appname" />
</svelte:fragment>
<svelte:fragment slot="actions">
@@ -49,14 +49,12 @@
<svelte:fragment slot="title">Package name</svelte:fragment>
Your package name is generally the applicationId in your app-level build.gradle file.
<svelte:fragment slot="aside">
<FormList>
<InputText
id="package-name"
label="Package Name"
bind:value={key}
required
placeholder="com.company.appname" />
</FormList>
<InputText
id="package-name"
label="Package name"
bind:value={key}
required
placeholder="com.company.appname" />
</svelte:fragment>
<svelte:fragment slot="actions">
@@ -3,25 +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.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}
@@ -0,0 +1,5 @@
<script>
import { SvgIcon } from '$lib/components';
</script>
<SvgIcon name="angular" type="color" iconSize="" />
@@ -0,0 +1,5 @@
<script>
import { SvgIcon } from '$lib/components';
</script>
<SvgIcon name="nextjs" type="color" iconSize="" />
@@ -0,0 +1,11 @@
<script lang="ts">
import noFrameworkIcon from './noFrameworkIcon.svg';
import noFrameworkIconDark from './noFrameworkIconDark.svg';
import { app } from '$lib/stores/app';
</script>
{#if $app.themeInUse === 'light'}
<img src={noFrameworkIcon} alt="" />
{:else}
<img src={noFrameworkIconDark} alt="" />
{/if}
@@ -0,0 +1,5 @@
<script>
import { SvgIcon } from '$lib/components';
</script>
<SvgIcon name="nuxt" type="color" iconSize="" />
@@ -0,0 +1,5 @@
<script>
import { SvgIcon } from '$lib/components';
</script>
<SvgIcon name="react" type="color" iconSize="" />
@@ -0,0 +1,5 @@
<script>
import { SvgIcon } from '$lib/components';
</script>
<SvgIcon name="svelte" type="color" iconSize="" />
@@ -0,0 +1,5 @@
<script>
import { SvgIcon } from '$lib/components';
</script>
<SvgIcon name="vue" type="color" iconSize="" />

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