Merge branch 'mgn-dv' of https://github.com/appwrite/console into mgn-dv

This commit is contained in:
Torsten Dittmann
2025-05-08 20:43:02 +02:00
81 changed files with 1466 additions and 846 deletions
+3 -3
View File
@@ -21,18 +21,18 @@
"e2e:ui": "playwright test --ui"
},
"dependencies": {
"@appwrite.io/console": "https://pkg.pr.new/appwrite/appwrite/@appwrite.io/console@e2f082e",
"@ai-sdk/svelte": "^1.1.22",
"@appwrite.io/console": "https://pkg.pr.new/appwrite/appwrite/@appwrite.io/console@f76aff1",
"@appwrite.io/pink": "0.25.0",
"@appwrite.io/pink-icons": "0.25.0",
"@appwrite.io/pink-icons-svelte": "https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@bd21ff7f",
"@appwrite.io/pink-legacy": "^1.0.3",
"@appwrite.io/pink-svelte": "https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@cbf05a1",
"@appwrite.io/pink-svelte": "https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@de27ca4",
"@popperjs/core": "^2.11.8",
"@sentry/sveltekit": "^8.38.0",
"@stripe/stripe-js": "^3.5.0",
"ai": "^2.2.37",
"analytics": "^0.8.16",
"@ai-sdk/svelte": "^1.1.22",
"cron-parser": "^4.9.0",
"dayjs": "^1.11.13",
"deep-equal": "^2.2.3",
+14 -14
View File
@@ -12,8 +12,8 @@ importers:
specifier: ^1.1.22
version: 1.1.24(svelte@5.25.3)(zod@3.24.3)
'@appwrite.io/console':
specifier: https://pkg.pr.new/appwrite/appwrite/@appwrite.io/console@e2f082e
version: https://pkg.pr.new/appwrite/appwrite/@appwrite.io/console@e2f082e
specifier: https://pkg.pr.new/appwrite/appwrite/@appwrite.io/console@f76aff1
version: https://pkg.pr.new/appwrite/appwrite/@appwrite.io/console@f76aff1
'@appwrite.io/pink':
specifier: 0.25.0
version: 0.25.0
@@ -27,8 +27,8 @@ importers:
specifier: ^1.0.3
version: 1.0.3
'@appwrite.io/pink-svelte':
specifier: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@cbf05a1
version: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@cbf05a1(react-dom@18.3.1(react@18.3.1))(svelte@5.25.3)
specifier: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@de27ca4
version: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@de27ca4(react-dom@18.3.1(react@18.3.1))(svelte@5.25.3)
'@popperjs/core':
specifier: ^2.11.8
version: 2.11.8
@@ -272,8 +272,8 @@ 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@e2f082e':
resolution: {tarball: https://pkg.pr.new/appwrite/appwrite/@appwrite.io/console@e2f082e}
'@appwrite.io/console@https://pkg.pr.new/appwrite/appwrite/@appwrite.io/console@f76aff1':
resolution: {tarball: https://pkg.pr.new/appwrite/appwrite/@appwrite.io/console@f76aff1}
version: 1.2.1
'@appwrite.io/pink-icons-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@bd21ff7f':
@@ -282,8 +282,8 @@ packages:
peerDependencies:
svelte: ^4.0.0
'@appwrite.io/pink-icons-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@cbf05a106412315e451530f8384924da515b10c8':
resolution: {tarball: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@cbf05a106412315e451530f8384924da515b10c8}
'@appwrite.io/pink-icons-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@de27ca407599e51b378814b421fb63625cb98c10':
resolution: {tarball: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@de27ca407599e51b378814b421fb63625cb98c10}
version: 1.0.0-next.7
peerDependencies:
svelte: ^4.0.0
@@ -297,8 +297,8 @@ packages:
'@appwrite.io/pink-legacy@1.0.3':
resolution: {integrity: sha512-GGde5fmPhs+s6/3aFeMPc/kKADG/gTFkYQSy6oBN8pK0y0XNCLrZZgBv+EBbdhwdtqVEWXa0X85Mv9w7jcIlwQ==}
'@appwrite.io/pink-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@cbf05a1':
resolution: {tarball: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@cbf05a1}
'@appwrite.io/pink-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@de27ca4':
resolution: {tarball: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@de27ca4}
version: 1.0.0-next.85
peerDependencies:
react-dom: ^18.0.0
@@ -3830,13 +3830,13 @@ snapshots:
'@analytics/type-utils@0.6.2': {}
'@appwrite.io/console@https://pkg.pr.new/appwrite/appwrite/@appwrite.io/console@e2f082e': {}
'@appwrite.io/console@https://pkg.pr.new/appwrite/appwrite/@appwrite.io/console@f76aff1': {}
'@appwrite.io/pink-icons-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@bd21ff7f(svelte@5.25.3)':
dependencies:
svelte: 5.25.3
'@appwrite.io/pink-icons-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@cbf05a106412315e451530f8384924da515b10c8(svelte@5.25.3)':
'@appwrite.io/pink-icons-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@de27ca407599e51b378814b421fb63625cb98c10(svelte@5.25.3)':
dependencies:
svelte: 5.25.3
@@ -3849,9 +3849,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@cbf05a1(react-dom@18.3.1(react@18.3.1))(svelte@5.25.3)':
'@appwrite.io/pink-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@de27ca4(react-dom@18.3.1(react@18.3.1))(svelte@5.25.3)':
dependencies:
'@appwrite.io/pink-icons-svelte': https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@cbf05a106412315e451530f8384924da515b10c8(svelte@5.25.3)
'@appwrite.io/pink-icons-svelte': https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@de27ca407599e51b378814b421fb63625cb98c10(svelte@5.25.3)
'@floating-ui/dom': 1.6.13
'@melt-ui/pp': 0.3.2(@melt-ui/svelte@0.86.6(svelte@5.25.3))(svelte@5.25.3)
'@melt-ui/svelte': 0.86.6(svelte@5.25.3)
+3
View File
@@ -346,6 +346,9 @@ export enum Submit {
FileCreate = 'submit_file_create',
FileDelete = 'submit_file_delete',
FileUpdatePermissions = 'submit_file_update_permissions',
FileTokenCreate = 'submit_file_token',
FileTokenDelete = 'submit_file_delete',
FileTokenUpdate = 'submit_file_update_expiry',
BudgetCapUpdate = 'submit_budget_cap_update',
BudgetAlertsUpdate = 'submit_budget_alert_conditions_update',
CreditRedeem = 'submit_credit_redeem',
@@ -1,5 +1,5 @@
<script lang="ts">
import { Helper, InputDateTime, InputSelect } from '$lib/elements/forms';
import { InputDateTime, InputSelect } from '$lib/elements/forms';
import { isSameDay, isValidDate, toLocaleDate } from '$lib/helpers/date';
function incrementToday(value: number, type: 'day' | 'month' | 'year'): string {
@@ -47,6 +47,9 @@
];
export let value: string | null = null;
export let dateSelectorLabel: string | undefined = undefined;
export let selectorLabel: string | undefined = 'Expiration Date';
export let resourceType: string | 'key' | 'token' | undefined = 'key';
function initExpirationSelect() {
if (value === null || !isValidDate(value)) return null;
@@ -65,6 +68,10 @@
}
let expirationSelect = initExpirationSelect();
let expirationCustom: string | null = value ?? null;
$: helper =
expirationSelect !== 'custom' && expirationSelect !== null
? `Your ${resourceType} will expire in ${toLocaleDate(value)}`
: null;
$: {
if (!isSameDay(new Date(expirationSelect), new Date(value))) {
@@ -73,13 +80,8 @@
}
</script>
<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>
{/if}
</svelte:fragment>
<InputSelect bind:value={expirationSelect} {options} id="preset" label={selectorLabel} {helper}>
</InputSelect>
{#if expirationSelect === 'custom'}
<InputDateTime required id="expire" label="" bind:value={expirationCustom} />
<InputDateTime required id="expire" label={dateSelectorLabel} bind:value={expirationCustom} />
{/if}
@@ -12,7 +12,9 @@
<ActionMenu.Item.Button
on:click={() => {
show = true;
}}>Custom filters</ActionMenu.Item.Button>
}}>
Custom filters
</ActionMenu.Item.Button>
</ActionMenu.Root>
{#if show}
+20 -4
View File
@@ -11,11 +11,19 @@
} from '$lib/elements/forms';
import type { Writable } from 'svelte/store';
import Modal from '../modal.svelte';
import { addFilter, generateTag, operators, queries, type TagValue } from './store';
import {
addFilter,
generateTag,
operators,
queries,
ValidOperators,
type TagValue
} from './store';
import type { Column } from '$lib/helpers/types';
import { TagList } from '.';
import { Icon, Layout } from '@appwrite.io/pink-svelte';
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
import { addFilterAndApply } from './quickFilters';
export let show = false;
export let columns: Writable<Column[]>;
@@ -35,9 +43,17 @@
function apply() {
localQueries.forEach((query) => {
addFilter($columns, query.id, query.operator, query.value, query.arrayValues);
addFilterAndApply(
query.id,
$columns.find((c) => c.id === query.id).title,
query.operator as ValidOperators,
query.value,
query.arrayValues,
$columns,
analyticsSource
);
});
queries.apply();
localTags = [];
localQueries = [];
trackEvent(Submit.FilterApply, {
@@ -48,7 +64,7 @@
}
function addCondition() {
const newTag = generateTag(selectedColumn, operatorKey, value, arrayValues);
const newTag = generateTag(selectedColumn, operatorKey, value || arrayValues);
if (localTags.some((t) => t.tag === newTag.tag && t.value === newTag.value)) {
return;
} else {
+56 -93
View File
@@ -1,107 +1,70 @@
<script lang="ts">
import { afterNavigate } from '$app/navigation';
import { queryParamToMap } from '$lib/components/filters/store';
import type { Column } from '$lib/helpers/types';
import { type Writable } from 'svelte/store';
import { CustomFilters, FiltersBottomSheet } from '$lib/components/filters';
import { addFilterAndApply, buildFilterCol } from './quickFilters';
import { parsedTags, setFilters } from './setFilters';
import { addFilterAndApply, type FilterData } from './quickFilters';
import { parsedTags } from './setFilters';
import Menu from '../menu/menu.svelte';
import { Button } from '$lib/elements/forms';
import { Icon } from '@appwrite.io/pink-svelte';
import { IconFilterLine } from '@appwrite.io/pink-icons-svelte';
import QuickfiltersSubMenu from './quickfiltersSubMenu.svelte';
import { isSmallViewport } from '$lib/stores/viewport';
export let columns: Writable<Column[]>;
export let analyticsSource: string;
export let openBottomSheet = false;
let {
columns,
filterCols,
analyticsSource
}: {
columns: Writable<Column[]>;
filterCols: FilterData[];
analyticsSource?: string;
} = $props();
//TODO: remove this when all header are replace with `ResponsiveContainerHeader`
let filterCols = $columns
.map((col) => (col.filter !== false ? buildFilterCol(col) : null))
.filter((f) => f?.options);
afterNavigate((p) => {
const paramQueries = p.to.url.searchParams.get('query');
const localQueries = queryParamToMap(paramQueries || '[]');
const localTags = Array.from(localQueries.keys());
// console.log(paramQueries, localQueries, localTags);
setFilters(localTags, filterCols, $columns);
filterCols = filterCols;
});
$inspect(filterCols);
</script>
{#if $isSmallViewport}
{#if $parsedTags?.length}
<Button
secondary
badge={`${$parsedTags?.length}`}
on:click={() => (openBottomSheet = true)}>
<Icon icon={IconFilterLine} slot="start" size="s" />
Filters
</Button>
{:else}
<Button secondary on:click={() => (openBottomSheet = true)}>
<Icon icon={IconFilterLine} slot="start" size="s" />
Filters
</Button>
{/if}
{:else}
<Menu>
{#if $parsedTags?.length}
<Button secondary badge={`${$parsedTags?.length}`}>
<Icon icon={IconFilterLine} slot="start" size="s" />
Filters
</Button>
{:else}
<Button secondary>
<Icon icon={IconFilterLine} slot="start" size="s" />
Filters
</Button>
{/if}
<svelte:fragment slot="menu">
{#each filterCols as filter (filter.title + filter.id)}
{#if filter.options}
<QuickfiltersSubMenu
{filter}
variant={filter?.array ? 'checkbox' : 'radio'}
on:add={(e) => {
addFilterAndApply(
filter.id,
filter.title,
filter.operator,
e.detail.value,
filter?.array
? (filter.options
.filter((opt) => opt.checked)
.map((opt) => opt.value) ?? [])
: [],
$columns,
analyticsSource
);
}}
on:clear={() => {
addFilterAndApply(
filter.id,
filter.title,
filter.operator,
null,
[],
$columns,
analyticsSource
);
}} />
{/if}
{/each}
</svelte:fragment>
<Menu>
<Button secondary badge={$parsedTags?.length ? `${$parsedTags.length}` : undefined}>
<Icon icon={IconFilterLine} slot="start" size="s" />
Filters
</Button>
<svelte:fragment slot="menu">
{#each filterCols.filter((f) => f?.options) as filter (filter.title + filter.id)}
{#if filter.options}
<QuickfiltersSubMenu
{filter}
variant={filter?.array ? 'checkbox' : 'radio'}
on:add={(e) => {
addFilterAndApply(
filter.id,
filter.title,
filter.operator,
e.detail.value,
filter?.array
? (filter.options
.filter((opt) => opt.checked)
.map((opt) => opt.value) ?? [])
: [],
$columns,
analyticsSource
);
}}
on:clear={() => {
addFilterAndApply(
filter.id,
filter.title,
filter.operator,
null,
[],
$columns,
analyticsSource
);
}} />
{/if}
{/each}
</svelte:fragment>
<svelte:fragment slot="end">
<CustomFilters {columns} />
</svelte:fragment>
</Menu>
{/if}
{#if $isSmallViewport && openBottomSheet}
<FiltersBottomSheet bind:openBottomSheet {columns} {analyticsSource} bind:filterCols />
{/if}
<svelte:fragment slot="end">
<CustomFilters {columns} />
</svelte:fragment>
</Menu>
@@ -34,9 +34,10 @@
</script>
<div use:melt={$subTrigger}>
<ActionMenu.Root noPadding>
<ActionMenu.Item.Button trailingIcon={IconChevronRight}
>{filter.title}</ActionMenu.Item.Button>
<ActionMenu.Root>
<ActionMenu.Item.Button trailingIcon={IconChevronRight}>
{filter.title}
</ActionMenu.Item.Button>
</ActionMenu.Root>
</div>
+18 -15
View File
@@ -9,17 +9,18 @@ export function setFilters(localTags: TagValue[], filterCols: FilterData[], $col
if (!localTags?.length) {
filterCols.forEach((filter) => {
resetOptions(filter);
cleanOldTags(filter.title);
cleanOldTags(filter?.title);
});
} else {
filterCols.forEach((filter) => {
if (filter.id.toLowerCase().includes('duration')) {
const id = filter?.id?.toLowerCase();
if (id?.includes('duration')) {
setTimeFilter(filter, $columns);
} else if (filter.id.toLocaleLowerCase().includes('size')) {
} else if (id?.includes('size')) {
setSizeFilter(filter, $columns);
} else if (filter.id.toLocaleLowerCase().includes('statuscode')) {
} else if (id?.includes('statuscode')) {
setStatusCodeFilter(filter, $columns);
} else if (filter.id === '$createdAt' || filter.id === '$updatedAt') {
} else if (id === '$createdat' || id === '$updatedat') {
setDateFilter(filter, $columns);
} else {
setFilterData(filter);
@@ -40,7 +41,7 @@ export function setFilterData(filter: FilterData) {
option.checked = values.includes(option.value);
});
}
cleanOldTags(filter.title);
cleanOldTags(filter?.title);
const newTag = {
tag: tagData.tag.replace(',', ' or '),
value: tagData.value
@@ -52,7 +53,7 @@ export function setFilterData(filter: FilterData) {
});
} else {
resetOptions(filter);
cleanOldTags(filter.title);
cleanOldTags(filter?.title);
}
}
@@ -76,7 +77,7 @@ export function setTimeFilter(filter: FilterData, columns: Column[]) {
value: timeTag.value
};
cleanOldTags(filter.title);
cleanOldTags(filter?.title);
parsedTags.update((tags) => {
tags.push(newTag);
@@ -84,7 +85,7 @@ export function setTimeFilter(filter: FilterData, columns: Column[]) {
});
}
} else {
cleanOldTags(filter.title);
cleanOldTags(filter?.title);
}
}
@@ -102,7 +103,7 @@ export function setSizeFilter(filter: FilterData, columns: Column[]) {
return prev;
});
if (sizeRange) {
cleanOldTags(filter.title);
cleanOldTags(filter?.title);
const newTag = {
tag: `**${filter.title}** is **${sizeRange.label}**`,
@@ -114,7 +115,7 @@ export function setSizeFilter(filter: FilterData, columns: Column[]) {
});
}
} else {
cleanOldTags(filter.title);
cleanOldTags(filter?.title);
}
}
@@ -127,7 +128,7 @@ export function setStatusCodeFilter(filter: FilterData, columns: Column[]) {
const codeRange = ranges.find((c) => c?.value && c.value === statusCodeTag.value);
if (codeRange) {
cleanOldTags(filter.title);
cleanOldTags(filter?.title);
const newTag = {
tag: `**${filter.title}** is **${codeRange.label}**`,
value: statusCodeTag.value
@@ -139,7 +140,7 @@ export function setStatusCodeFilter(filter: FilterData, columns: Column[]) {
});
}
} else {
cleanOldTags(filter.title);
cleanOldTags(filter?.title);
}
}
@@ -158,7 +159,7 @@ export function setDateFilter(filter: FilterData, columns: Column[]) {
return prev;
});
if (dateRange) {
cleanOldTags(filter.title);
cleanOldTags(filter?.title);
const newTag = {
tag: `**${filter.title}** is **${dateRange.label}**`,
value: dateTag.value
@@ -169,11 +170,12 @@ export function setDateFilter(filter: FilterData, columns: Column[]) {
});
}
} else {
cleanOldTags(filter.title);
cleanOldTags(filter?.title);
}
}
function cleanOldTags(title: string) {
if (!title) return;
parsedTags.update((tags) => {
tags = tags.filter((tag) => !tag.tag.includes(`**${title}**`));
return tags;
@@ -181,6 +183,7 @@ function cleanOldTags(title: string) {
}
export function resetOptions(filter: FilterData) {
if (!filter?.options) return;
filter.options.forEach((option) => {
option.checked = false;
});
@@ -42,12 +42,12 @@
</Link>
{#if sortedDomains.length > 1}
<Popover padding="none" let:toggle>
<Popover padding="none" let:toggle placement="bottom-end">
<Tag size="xs" on:click={toggle}>
+{sortedDomains.length - 1}
</Tag>
<svelte:fragment slot="tooltip">
<ActionMenu.Root>
<ActionMenu.Root width="20px">
{#each sortedDomains as rule, i}
{#if i !== 0}
<ActionMenu.Item.Anchor
+15 -3
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import { EmptySearch, Paginator } from '$lib/components';
import { Empty, EmptySearch, Paginator } from '$lib/components';
import { Button, InputSearch, InputSelect } from '$lib/elements/forms';
import { timeFromNow } from '$lib/helpers/date';
import { sdk } from '$lib/stores/sdk';
@@ -20,6 +20,7 @@
import { VCSDetectionType, type Models } from '@appwrite.io/console';
import { getFrameworkIcon } from '$lib/stores/sites';
import { connectGitHub } from '$lib/stores/git';
import Card from '../card.svelte';
let {
action = $bindable('select'),
@@ -147,7 +148,10 @@
);
}}
bind:value={selectedInstallation} />
<InputSearch placeholder="Search repositories" bind:value={search} />
<InputSearch
placeholder="Search repositories"
bind:value={search}
disabled={!search && !$repositories?.repositories?.length} />
</Layout.Stack>
{/await}
</Layout.Stack>
@@ -259,7 +263,7 @@
</Table.Root>
{/snippet}
</Paginator>
{:else}
{:else if search}
<EmptySearch hidePages hidePagination bind:search target="repositories">
<svelte:fragment slot="actions">
{#if search}
@@ -269,6 +273,14 @@
{/if}
</svelte:fragment>
</EmptySearch>
{:else}
<Card>
<Layout.Stack alignItems="center" justifyContent="center">
<Typography.Text variation="m-500" color="--fgcolor-neutral-tertiary">
No repositories available
</Typography.Text>
</Layout.Stack>
</Card>
{/if}
{/await}
{/if}
+1
View File
@@ -83,3 +83,4 @@ export { default as Confirm } from './confirm.svelte';
export { default as UsageCard } from './usageCard.svelte';
export { default as ViewToggle } from './viewToggle.svelte';
export { default as Terminal } from './studio/terminal.svelte';
export { default as ExpirationInput } from './expirationInput.svelte';
+1 -1
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import { Card, Tooltip } from '@appwrite.io/pink-svelte';
import { Card, Tooltip, Icon } from '@appwrite.io/pink-svelte';
import { type ComponentProps } from 'svelte';
import type Selector from '@appwrite.io/pink-svelte/dist/card/Selector.svelte';
+9 -8
View File
@@ -32,6 +32,12 @@
let responseTab: 'logs' | 'errors' | 'headers' | 'body' = $state('logs');
let href = $derived(
product === 'site'
? 'https://appwrite.io/docs/products/sites/logs#log-details'
: 'https://appwrite.io/docs/products/functions/develop#logging'
);
onMount(() => {
if (selectedLog?.errors) {
responseTab = 'errors';
@@ -128,8 +134,8 @@
<Input.Helper state="default">
<span>
Missing headers? Check the <Link variant="muted" href="#" external>docs</Link> to
see the supported data and how to log it.
Missing headers? Check the <Link variant="muted" {href} external>docs</Link> to see
the supported data and how to log it.
</span>
</Input.Helper>
{:else}
@@ -146,12 +152,7 @@
Body data is not captured by Appwrite for your user's security and privacy. To
display body data in the Logs tab, use <InlineCode
code="context.log()"
size="s" />. <Link
external
href="https://appwrite.io/docs/products/functions/develop#logging"
variant="muted">
Learn more</Link
>.
size="s" />. <Link external {href} variant="muted">Learn more</Link>.
</Typography.Text>
</Card>
{/if}
+34 -12
View File
@@ -12,6 +12,7 @@
export let showTeam: boolean;
export let showLabel: boolean;
export let showCustom: boolean;
export let hideOnClick: boolean = false;
export let groups: Writable<Map<string, Permission>>;
const dispatch = createEventDispatcher();
@@ -19,31 +20,52 @@
<Popover let:toggle padding="none" placement="bottom-start">
<slot {toggle} />
<svelte:fragment slot="tooltip">
<svelte:fragment slot="tooltip" let:hide>
<ActionMenu.Root>
<ActionMenu.Item.Button
disabled={$groups.has('any')}
on:click={() => dispatch('create', ['any'])}>
on:click={(e) => {
if (hideOnClick) hide(e);
dispatch('create', ['any']);
}}>
Any
</ActionMenu.Item.Button>
<ActionMenu.Item.Button
disabled={$groups.has('guests')}
on:click={() => dispatch('create', ['guests'])}>
on:click={(e) => {
if (hideOnClick) hide(e);
dispatch('create', ['guests']);
}}>
All guests
</ActionMenu.Item.Button>
<ActionMenu.Item.Button
disabled={$groups.has('users')}
on:click={() => dispatch('create', ['users'])}>
on:click={(e) => {
if (hideOnClick) hide(e);
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.Item.Button
on:click={(e) => {
showUser = true;
if (hideOnClick) hide(e);
}}>Select users</ActionMenu.Item.Button>
<ActionMenu.Item.Button
on:click={(e) => {
showTeam = true;
if (hideOnClick) hide(e);
}}>Select teams</ActionMenu.Item.Button>
<ActionMenu.Item.Button
on:click={(e) => {
showLabel = true;
if (hideOnClick) hide(e);
}}>Label</ActionMenu.Item.Button>
<ActionMenu.Item.Button
on:click={(e) => {
showCustom = true;
if (hideOnClick) hide(e);
}}>Custom permission</ActionMenu.Item.Button>
</ActionMenu.Root>
</svelte:fragment>
</Popover>
@@ -21,6 +21,7 @@
import type { PinkColumn } from '$lib/helpers/types';
export let withCreate = false;
export let hideOnClick = false;
export let permissions: string[] = [];
let showUser = false;
@@ -127,11 +128,11 @@
}
const columns: PinkColumn[] = [
{ id: 'role', width: { min: 100 } },
{ id: 'create', width: { min: 100 }, hide: !withCreate },
{ id: 'read', width: { min: 100 } },
{ id: 'update', width: { min: 100 } },
{ id: 'delete', width: { min: 100 } },
{ id: 'role', width: { min: 80 } },
{ id: 'create', width: { min: 80 }, hide: !withCreate },
{ id: 'read', width: { min: 80 } },
{ id: 'update', width: { min: 80 } },
{ id: 'delete', width: { min: 80 } },
{ id: 'action', width: 40 }
];
</script>
@@ -195,6 +196,7 @@
bind:showTeam
bind:showUser
{groups}
{hideOnClick}
on:create={create}
let:toggle>
<Button secondary on:click={toggle}>
@@ -213,6 +215,7 @@
bind:showTeam
bind:showUser
{groups}
{hideOnClick}
on:create={create}
let:toggle>
<Button compact icon on:click={toggle}>
+18 -8
View File
@@ -12,7 +12,8 @@
Button,
Layout,
Avatar,
Typography
Typography,
Badge
} from '@appwrite.io/pink-svelte';
import {
@@ -77,7 +78,7 @@
{ name: 'Functions', icon: IconLightningBolt, slug: 'functions', category: 'build' },
{ name: 'Messaging', icon: IconChatBubble, slug: 'messaging', category: 'build' },
{ name: 'Storage', icon: IconFolder, slug: 'storage', category: 'build' },
{ name: 'Sites', icon: IconGlobeAlt, slug: 'sites', category: 'deploy' }
{ name: 'Sites', icon: IconGlobeAlt, slug: 'sites', category: 'deploy', badge: 'New' }
];
</script>
@@ -208,14 +209,23 @@
on:click={() => {
trackEvent(`click_menu_${projectOption.slug}`);
sideBarIsOpen = false;
}}
><span class="link-icon"
><Icon icon={projectOption.icon} size="s" />
</span><span
}}>
<span class="link-icon">
<Icon icon={projectOption.icon} size="s" />
</span>
<span
class:no-text={state === 'icons'}
class:has-text={state === 'open'}
class="link-text">{projectOption.name}</span
></a>
class="link-text">
{projectOption.name}
{#if projectOption?.badge}
<Badge
variant="secondary"
content={projectOption.badge}
size="s" />
{/if}
</span>
</a>
<span slot="tooltip">{projectOption.name}</span>
</Tooltip>
{/each}
+1
View File
@@ -34,6 +34,7 @@ export enum Dependencies {
DOCUMENTS = 'dependency:documents',
BUCKET = 'dependency:bucket',
FILE = 'dependency:file',
FILE_TOKENS = 'dependency:file_tokens',
FILES = 'dependency:files',
FUNCTION = 'dependency:function',
FUNCTION_DOMAINS = 'dependency:function_domains',
+39 -36
View File
@@ -1,12 +1,10 @@
<script lang="ts">
import { Input } from '@appwrite.io/pink-svelte';
import { onMount } from 'svelte';
import { Input, Layout, Selector } from '@appwrite.io/pink-svelte';
export let label: string = undefined;
export let id: string;
export let name: string = id;
export let helper: string = undefined;
export let value = '';
export let placeholder = '';
export let label: string;
export let value: string;
export let required = false;
export let nullable = false;
export let disabled = false;
@@ -16,18 +14,23 @@
export let step: number | 'any' = 0.001;
let error: string;
let element: HTMLInputElement;
function handleInvalid(event: Event) {
event.preventDefault();
const inputNode = event.currentTarget as HTMLInputElement;
if (inputNode.validity.valueMissing) {
error = 'This field is required';
return;
onMount(() => {
if (element && autofocus) {
element.focus();
}
});
error = inputNode.validationMessage;
let prevValue = '';
function handleNullChange(e: CustomEvent<boolean>) {
const isNull = e.detail;
if (isNull) {
prevValue = value;
value = null;
} else {
value = prevValue;
}
}
$: if (value) {
@@ -35,24 +38,24 @@
}
</script>
<Input.DateTime
{id}
{name}
{placeholder}
{disabled}
{required}
{label}
{step}
{nullable}
{readonly}
autofocus={autofocus || undefined}
autocomplete={autocomplete ? 'on' : 'off'}
helper={error || helper}
state={error ? 'error' : 'default'}
on:invalid={handleInvalid}
on:input
bind:value>
<slot name="start" slot="start" />
<slot name="info" slot="info" />
<slot name="end" slot="end" />
</Input.DateTime>
<Layout.Stack gap="s" direction="row">
<Input.DateTime
{id}
{label}
{disabled}
{readonly}
{required}
{value}
{step}
helper={error}
autocomplete={autocomplete ? 'on' : 'off'}>
{#if nullable}
<Selector.Checkbox
size="s"
slot="end"
label="NULL"
checked={value === null}
on:change={handleNullChange} />
{/if}
</Input.DateTime>
</Layout.Stack>
+2 -1
View File
@@ -5,6 +5,7 @@
export let id: string;
export let label: string | undefined = undefined;
export let value: string | number | boolean | null;
export let helper: string | undefined = undefined;
export let optionalText: string | number | boolean | null | undefined = undefined;
export let placeholder = '';
export let required = false;
@@ -54,8 +55,8 @@
{placeholder}
{disabled}
{isSearchable}
helper={error ?? helper}
{required}
helper={error}
state={error ? 'error' : 'default'}
on:invalid={handleInvalid}
on:input
+8 -2
View File
@@ -15,13 +15,17 @@ if (browser) {
dayjs.extend(relativeTime);
}
export const toLocaleDate = (datetime: string) => {
export const toLocaleDate = (datetime: string, format: 'dd mm yyyy' | 'default' = 'default') => {
const date = new Date(datetime);
if (isNaN(date.getTime())) {
return 'n/a';
}
if (format === 'dd mm yyyy') {
return `${date.getDate().toString().padStart(2, '0')} ${date.toLocaleString('en', { month: 'short' })} ${date.getFullYear()}`;
}
const options: Intl.DateTimeFormatOptions = {
year: 'numeric',
month: 'short',
@@ -33,6 +37,7 @@ export const toLocaleDate = (datetime: string) => {
export const toLocaleDateTime = (
datetime: string | number,
is12HourFormat: boolean = false,
timeZone: string | undefined = undefined
) => {
const date = new Date(datetime);
@@ -48,7 +53,8 @@ export const toLocaleDateTime = (
day: 'numeric',
hour: 'numeric',
minute: 'numeric',
hourCycle: 'h23'
hourCycle: is12HourFormat ? 'h12' : 'h23',
...(is12HourFormat && { hour12: true })
};
return date.toLocaleDateString('en', options);
+9
View File
@@ -70,3 +70,12 @@ export function saveTerminalOpenToPrefs(isOpen: boolean) {
};
sdk.forConsole.account.updatePrefs(newPrefs);
}
export function disableBodySelect() {
document.body.style.userSelect = 'none';
document.body.style.webkitUserSelect = 'none';
}
export function enabledBodySelect() {
document.body.style.userSelect = '';
document.body.style.webkitUserSelect = '';
}
+6
View File
@@ -101,6 +101,12 @@ export function secsToUnit(time: number, unit: Unit) {
* @returns Formatted string like "3d 4h 30m 23s"
*/
export function formatTimeDetailed(time: number): string {
if (typeof time !== 'number' || time === undefined || time === null) {
return '-';
}
if (time <= 0) {
return '0s';
}
const seconds = Math.floor(time % 60);
const minutes = Math.floor((time / 60) % 60);
const hours = Math.floor((time / (60 * 60)) % 24);
@@ -46,9 +46,7 @@
let showFilters = $state(false);
let filterCols = $derived(
$columns
.map((col) => (col.filter !== false ? buildFilterCol(col) : null))
.filter((f) => f?.options)
$columns.map((col) => (col.filter !== false ? buildFilterCol(col) : null)).filter(Boolean)
);
afterNavigate((p) => {
@@ -109,14 +107,16 @@
<SearchQuery placeholder={searchPlaceholder} />
{/if}
{#if hasFilters && $columns?.length}
<QuickFilters {columns} {analyticsSource} />
<QuickFilters {columns} {analyticsSource} {filterCols} />
{/if}
</Layout.Stack>
<Layout.Stack direction="row" alignItems="center" justifyContent="flex-end">
{#if hasDisplaySettings}
<ViewSelector {view} {columns} {hideView} {hideColumns} />
{/if}
{@render children()}
{#if children}
{@render children()}
{/if}
</Layout.Stack>
</Layout.Stack>
{/if}
@@ -138,7 +138,7 @@
bind:openBottomSheet={showFilters}
{columns}
{analyticsSource}
bind:filterCols />
filterCols={filterCols.filter((f) => f?.options)} />
{/if}
<!-- SNIPPETS -->
+11 -5
View File
@@ -40,7 +40,12 @@
import { sdk } from '$lib/stores/sdk';
import { filesystem } from '$lib/components/editor/filesystem';
import { previewFrameRef } from '$routes/(console)/project-[project]/store';
import { getChatWidthFromPrefs, saveChatWidthToPrefs } from '$lib/helpers/studioLayout';
import {
disableBodySelect,
enabledBodySelect,
getChatWidthFromPrefs,
saveChatWidthToPrefs
} from '$lib/helpers/studioLayout';
import { synapse } from '$lib/components/studio/synapse.svelte';
let hasProjectSidebar = $state(false);
@@ -114,7 +119,7 @@
let resizerLeftPosition = $state(
page.data?.subNavigation ? getChatWidthFromPrefs() + 24 : getChatWidthFromPrefs()
);
let resizerLeftOffset = $state(page.data?.subNavigation ? 52 : 60);
let resizerLeftOffset = $state(page.data?.subNavigation ? 52 : 9);
let chatWidth = $derived(resizerLeftPosition - resizerLeftOffset);
$effect(() => {
@@ -129,7 +134,7 @@
window.addEventListener('mouseup', stopResize);
window.addEventListener('touchmove', resize);
window.addEventListener('touchend', stopResize);
document.body.style.userSelect = 'none';
disableBodySelect();
if ($previewFrameRef) {
$previewFrameRef.style.pointerEvents = 'none';
@@ -140,7 +145,7 @@
if (!isResizing) return;
if (resizer) {
resizerLeftPosition =
('touches' in event ? event.touches[0].clientX : event.clientX) - 10;
('touches' in event ? event.touches[0].clientX : event.clientX) - 60;
const maxSize = page.data?.subNavigation
? window.innerWidth - 660
: window.innerWidth - 460;
@@ -172,7 +177,7 @@
window.removeEventListener('mouseup', stopResize);
window.removeEventListener('touchmove', resize);
window.removeEventListener('touchend', stopResize);
document.body.style.userSelect = '';
enabledBodySelect();
if ($previewFrameRef) {
$previewFrameRef.style.pointerEvents = '';
@@ -489,6 +494,7 @@
min-height: calc(100vh - 54px);
margin-top: 32px;
width: 100vw;
position: relative;
padding-right: var(--space-4);
padding-left: var(--space-4);
+3 -1
View File
@@ -20,7 +20,8 @@ import {
Teams,
Users,
Vcs,
Sites
Sites,
Tokens
} from '@appwrite.io/console';
import { Billing } from '../sdk/billing';
import { Backups } from '../sdk/backups';
@@ -57,6 +58,7 @@ const sdkForProject = {
project: new Project(clientProject),
projectApi: new ProjectApi(clientProject),
storage: new Storage(clientProject),
tokens: new Tokens(clientProject),
teams: new Teams(clientProject),
users: new Users(clientProject),
vcs: new Vcs(clientProject),
@@ -102,11 +102,13 @@
Email templates <Badge variant="secondary" content="Experimental" />
</svelte:fragment>
Use templates to send and process account management emails.
<Link.Anchor href="https://appwrite.io/docs/advanced/platform/message-templates">
<Link.Anchor
target="_blank"
href="https://appwrite.io/docs/advanced/platform/message-templates">
Learn more
</Link.Anchor>
<svelte:fragment slot="aside">
<Layout.Stack>
<Layout.Stack gap="none">
<Accordion
title="Verification"
bind:open={emailVerificationOpen}
@@ -126,8 +128,12 @@
e.preventDefault();
openEmail('magicSession');
}}>
Send an email to users that sign in with a magic URL.
<EmailMagicUrlTemplate />
<Layout.Stack>
<Typography.Text>
Send an email to users that sign in with a magic URL.
</Typography.Text>
<EmailMagicUrlTemplate />
</Layout.Stack>
</Accordion>
<Accordion
title="OTP session"
@@ -136,8 +142,11 @@
e.preventDefault();
openEmail('otpSession');
}}>
Send an email to users that sign in with a email OTP.
<EmailMagicUrlTemplate />
<Layout.Stack>
<Typography.Text>
Send an email to users that sign in with a email OTP.</Typography.Text>
<EmailMagicUrlTemplate />
</Layout.Stack>
</Accordion>
<Accordion
title="Reset password"
@@ -146,8 +155,10 @@
e.preventDefault();
openEmail('recovery');
}}>
Send a recovery email to users that forget their password.
<EmailRecoveryTemplate />
<Layout.Stack>
<Typography.Text>
Send a recovery email to users that forget their password.</Typography.Text>
<EmailRecoveryTemplate /></Layout.Stack>
</Accordion>
<Accordion
title="Invite user"
@@ -156,8 +167,11 @@
e.preventDefault();
openEmail('invitation');
}}>
Send an invitation email to become a member of your project.
<EmailInviteTemplate />
<Layout.Stack>
<Typography.Text>
Send an invitation email to become a member of your project.</Typography.Text>
<EmailInviteTemplate />
</Layout.Stack>
</Accordion>
<Accordion
title="2FA verification"
@@ -166,8 +180,10 @@
e.preventDefault();
openEmail('mfaChallenge');
}}>
Send a two-factor authentication email to a user.
<Email2FaTemplate />
<Layout.Stack>
<Typography.Text>
Send a two-factor authentication email to a user.</Typography.Text>
<Email2FaTemplate /></Layout.Stack>
</Accordion>
<Accordion
title="Session alert"
@@ -176,8 +192,10 @@
e.preventDefault();
openEmail('sessionAlert');
}}>
Send an email to users when a new session is created.
<EmailSessionAlertTemplate />
<Layout.Stack>
<Typography.Text>
Send an email to users when a new session is created.</Typography.Text>
<EmailSessionAlertTemplate /></Layout.Stack>
</Accordion>
</Layout.Stack>
</svelte:fragment>
@@ -7,7 +7,7 @@
import { addNotification } from '$lib/stores/notifications';
import { Id } from '$lib/components';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { Layout } from '@appwrite.io/pink-svelte';
import { Layout, Card } from '@appwrite.io/pink-svelte';
const projectId = page.params.project;
let locale = 'en';
@@ -35,11 +35,13 @@
}
</script>
<Layout.Stack>
<LocaleOptions on:change={onLocaleChange} bind:value={locale} />
<EmailTemplate bind:loading>
<Id value={'{{user}}'}>{'{{user}}'}</Id>
<Id value={'{{project}}'}>{'{{project}}'}</Id>
<Id value={'{{otp}}'}>{'{{otp}}'}</Id>
</EmailTemplate>
</Layout.Stack>
<Card.Base variant="secondary" padding="s">
<Layout.Stack>
<LocaleOptions on:change={onLocaleChange} bind:value={locale} />
<EmailTemplate bind:loading>
<Id value={'{{user}}'}>{'{{user}}'}</Id>
<Id value={'{{project}}'}>{'{{project}}'}</Id>
<Id value={'{{otp}}'}>{'{{otp}}'}</Id>
</EmailTemplate>
</Layout.Stack>
</Card.Base>
@@ -7,7 +7,7 @@
import { addNotification } from '$lib/stores/notifications';
import { Id } from '$lib/components';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { Layout } from '@appwrite.io/pink-svelte';
import { Layout, Card } from '@appwrite.io/pink-svelte';
const projectId = page.params.project;
let locale = 'en';
@@ -35,12 +35,14 @@
}
</script>
<Layout.Stack>
<LocaleOptions on:change={onLocaleChange} bind:value={locale} />
<EmailTemplate bind:loading>
<Id value={'{{team}}'}>{'{{team}}'}</Id>
<Id value={'{{user}}'}>{'{{user}}'}</Id>
<Id value={'{{project}}'}>{'{{project}}'}</Id>
<Id value={'{{redirect}}'}>{'{{redirect}}'}</Id>
</EmailTemplate>
</Layout.Stack>
<Card.Base variant="secondary" padding="s">
<Layout.Stack>
<LocaleOptions on:change={onLocaleChange} bind:value={locale} />
<EmailTemplate bind:loading>
<Id value={'{{team}}'}>{'{{team}}'}</Id>
<Id value={'{{user}}'}>{'{{user}}'}</Id>
<Id value={'{{project}}'}>{'{{project}}'}</Id>
<Id value={'{{redirect}}'}>{'{{redirect}}'}</Id>
</EmailTemplate>
</Layout.Stack>
</Card.Base>
@@ -7,7 +7,7 @@
import { addNotification } from '$lib/stores/notifications';
import { Id } from '$lib/components';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { Layout } from '@appwrite.io/pink-svelte';
import { Layout, Card } from '@appwrite.io/pink-svelte';
const projectId = page.params.project;
@@ -37,11 +37,13 @@
}
</script>
<Layout.Stack>
<LocaleOptions on:change={onLocaleChange} bind:value={locale} />
<EmailTemplate bind:loading>
<Id value={'{{user}}'}>{'{{user}}'}</Id>
<Id value={'{{project}}'}>{'{{project}}'}</Id>
<Id value={'{{redirect}}'}>{'{{redirect}}'}</Id>
</EmailTemplate>
</Layout.Stack>
<Card.Base variant="secondary" padding="s">
<Layout.Stack>
<LocaleOptions on:change={onLocaleChange} bind:value={locale} />
<EmailTemplate bind:loading>
<Id value={'{{user}}'}>{'{{user}}'}</Id>
<Id value={'{{project}}'}>{'{{project}}'}</Id>
<Id value={'{{redirect}}'}>{'{{redirect}}'}</Id>
</EmailTemplate>
</Layout.Stack>
</Card.Base>
@@ -7,7 +7,7 @@
import { addNotification } from '$lib/stores/notifications';
import { Id } from '$lib/components';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { Layout } from '@appwrite.io/pink-svelte';
import { Layout, Card } from '@appwrite.io/pink-svelte';
const projectId = page.params.project;
@@ -36,11 +36,13 @@
}
</script>
<Layout.Stack>
<LocaleOptions on:change={onLocaleChange} bind:value={locale} />
<EmailTemplate bind:loading>
<Id value={'{{user}}'}>{'{{user}}'}</Id>
<Id value={'{{project}}'}>{'{{project}}'}</Id>
<Id value={'{{redirect}}'}>{'{{redirect}}'}</Id>
</EmailTemplate>
</Layout.Stack>
<Card.Base variant="secondary" padding="s">
<Layout.Stack>
<LocaleOptions on:change={onLocaleChange} bind:value={locale} />
<EmailTemplate bind:loading>
<Id value={'{{user}}'}>{'{{user}}'}</Id>
<Id value={'{{project}}'}>{'{{project}}'}</Id>
<Id value={'{{redirect}}'}>{'{{redirect}}'}</Id>
</EmailTemplate>
</Layout.Stack>
</Card.Base>
@@ -7,7 +7,7 @@
import { addNotification } from '$lib/stores/notifications';
import { Id } from '$lib/components';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { Layout } from '@appwrite.io/pink-svelte';
import { Layout, Card } from '@appwrite.io/pink-svelte';
const projectId = page.params.project;
let locale = 'en';
@@ -35,13 +35,15 @@
}
</script>
<Layout.Stack>
<LocaleOptions on:change={onLocaleChange} bind:value={locale} />
<EmailTemplate bind:loading>
<Id value={'{{user}}'}>{'{{user}}'}</Id>
<Id value={'{{project}}'}>{'{{project}}'}</Id>
<Id value={'{{device}}'}>{'{{device}}'}</Id>
<Id value={'{{ipAddress'}>{'{{ipAddress}}'}</Id>
<Id value={'{{country'}>{'{{country}}'}</Id>
</EmailTemplate>
</Layout.Stack>
<Card.Base variant="secondary" padding="s">
<Layout.Stack>
<LocaleOptions on:change={onLocaleChange} bind:value={locale} />
<EmailTemplate bind:loading>
<Id value={'{{user}}'}>{'{{user}}'}</Id>
<Id value={'{{project}}'}>{'{{project}}'}</Id>
<Id value={'{{device}}'}>{'{{device}}'}</Id>
<Id value={'{{ipAddress'}>{'{{ipAddress}}'}</Id>
<Id value={'{{country'}>{'{{country}}'}</Id>
</EmailTemplate>
</Layout.Stack>
</Card.Base>
@@ -83,74 +83,72 @@
$: isSmtpEnabled = $project?.smtpEnabled;
</script>
<Card.Base variant="secondary" padding="s">
{#if loading}
<div
class="u-position-absolute u-width-full-line u-flex u-flex-vertical u-main-center u-cross-center u-gap-16 u-margin-block-start-32"
style="inset-inline-start: 0;">
<div class="loader"></div>
<p class="text">Loading template...</p>
</div>
{/if}
<div class:u-opacity-0={loading} style={loading ? 'pointer-events: none' : ''}>
<Form onSubmit={saveEmailTemplate}>
<Layout.Stack>
<InputText
id="senderName"
label="Sender name"
bind:value={$emailTemplate.senderName}
placeholder="Enter sender name"
disabled={!isSmtpEnabled} />
<InputEmail
bind:value={$emailTemplate.senderEmail}
id="senderEmail"
label="Sender email"
placeholder="Enter sender email"
disabled={!isSmtpEnabled} />
<InputEmail
bind:value={$emailTemplate.replyTo}
id="replyTo"
label="Reply to"
placeholder="noreply@appwrite.io" />
{#if $$slots.default}
<p class="text">
Click to copy variables for the fields below. Learn more <a
class="link"
href="https://appwrite.io/docs/advanced/platform/message-templates"
>here</a
>.
</p>
<Layout.Stack direction="row">
<slot />
</Layout.Stack>
{/if}
<InputText
bind:value={$emailTemplate.subject}
id="subject"
label="Subject"
placeholder="Enter subject" />
<InputTextarea
bind:value={$emailTemplate.message}
id="message"
label="Message"
placeholder="Enter your message"
readonly={!isSmtpEnabled}>
<Tooltip slot="info" maxWidth="15rem">
<Icon icon={IconInfo} size="s" />
<Typography.Caption variant="400" slot="tooltip">
Set up an SMTP server to edit the message body
</Typography.Caption>
</Tooltip>
</InputTextarea>
</Layout.Stack>
<div class="u-sep-block-start u-margin-block-start-24"></div>
<div class="u-flex u-gap-16 u-main-end u-margin-block-start-24">
<Button on:click={() => (openResetModal = true)} text>Reset changes</Button>
<Button submit disabled={isButtonDisabled}>Update</Button>
</div>
</Form>
{#if loading}
<div
class="u-position-absolute u-width-full-line u-flex u-flex-vertical u-main-center u-cross-center u-gap-16 u-margin-block-start-32"
style="inset-inline-start: 0;">
<div class="loader"></div>
<p class="text">Loading template...</p>
</div>
</Card.Base>
{/if}
<div class:u-opacity-0={loading} style={loading ? 'pointer-events: none' : ''}>
<Form onSubmit={saveEmailTemplate}>
<Layout.Stack>
<InputText
id="senderName"
label="Sender name"
bind:value={$emailTemplate.senderName}
placeholder="Enter sender name"
disabled={!isSmtpEnabled} />
<InputEmail
bind:value={$emailTemplate.senderEmail}
id="senderEmail"
label="Sender email"
placeholder="Enter sender email"
disabled={!isSmtpEnabled} />
<InputEmail
bind:value={$emailTemplate.replyTo}
id="replyTo"
label="Reply to"
placeholder="noreply@appwrite.io" />
{#if $$slots.default}
<p class="text">
Click to copy variables for the fields below. Learn more <a
class="link"
href="https://appwrite.io/docs/advanced/platform/message-templates">here</a
>.
</p>
<Layout.Stack direction="row">
<slot />
</Layout.Stack>
{/if}
<InputText
bind:value={$emailTemplate.subject}
id="subject"
label="Subject"
placeholder="Enter subject" />
<InputTextarea
bind:value={$emailTemplate.message}
id="message"
label="Message"
placeholder="Enter your message"
readonly={!isSmtpEnabled}
rows={8}>
<Tooltip slot="info" maxWidth="15rem">
<Icon icon={IconInfo} size="s" />
<Typography.Caption variant="400" slot="tooltip">
Set up an SMTP server to edit the message body
</Typography.Caption>
</Tooltip>
</InputTextarea>
</Layout.Stack>
<div class="u-sep-block-start u-margin-block-start-24"></div>
<div class="u-flex u-gap-16 u-main-end u-margin-block-start-24">
<Button on:click={() => (openResetModal = true)} text>Reset changes</Button>
<Button submit disabled={isButtonDisabled}>Update</Button>
</div>
</Form>
</div>
<ResetEmail bind:show={openResetModal} />
@@ -7,7 +7,7 @@
import { Id } from '$lib/components';
import { addNotification } from '$lib/stores/notifications';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { Layout } from '@appwrite.io/pink-svelte';
import { Layout, Card } from '@appwrite.io/pink-svelte';
const projectId = page.params.project;
@@ -36,11 +36,13 @@
}
</script>
<Layout.Stack>
<LocaleOptions on:change={onLocaleChange} bind:value={locale} />
<EmailTemplate bind:loading>
<Id value={'{{user}}'}>{'{{user}}'}</Id>
<Id value={'{{project}}'}>{'{{project}}'}</Id>
<Id value={'{{redirect}}'}>{'{{redirect}}'}</Id>
</EmailTemplate>
</Layout.Stack>
<Card.Base variant="secondary" padding="s">
<Layout.Stack gap="l">
<LocaleOptions on:change={onLocaleChange} bind:value={locale} />
<EmailTemplate bind:loading>
<Id value={'{{user}}'}>{'{{user}}'}</Id>
<Id value={'{{project}}'}>{'{{project}}'}</Id>
<Id value={'{{redirect}}'}>{'{{redirect}}'}</Id>
</EmailTemplate>
</Layout.Stack>
</Card.Base>
@@ -1,5 +1,5 @@
<script lang="ts">
import { Card, Input } from '@appwrite.io/pink-svelte';
import { Input } from '@appwrite.io/pink-svelte';
import { localeCodes } from './store';
export let value = 'en';
@@ -10,14 +10,12 @@
}));
</script>
<Card.Base variant="secondary" padding="s">
<Input.ComboBox
label="Template language"
on:change
id="language"
placeholder="Select a language"
name="locale"
bind:value
required
{options} />
</Card.Base>
<Input.ComboBox
label="Template language"
on:change
id="language"
placeholder="Select a language"
name="locale"
bind:value
required
{options} />
@@ -8,6 +8,7 @@
import { project } from '../../store';
import { loadEmailTemplate } from './+page.svelte';
import { baseEmailTemplate, emailTemplate } from './store';
import { Dialog, Layout, Alert } from '@appwrite.io/pink-svelte';
export let show = false;
@@ -44,15 +45,24 @@
}
</script>
<Modal onSubmit={reset} {error} bind:show>
<Dialog bind:open={show} title="Reset changes">
<svelte:fragment slot="title">Reset email template?</svelte:fragment>
<p class="text">
Are you sure you want to reset the email template?
<b>Default values will be set in all inputs.</b>
</p>
<Layout.Stack>
<p class="text">
Are you sure you want to reset the email template?
<b>Default values will be set in all inputs.</b>
</p>
{#if error}
<Alert.Inline status="warning">{error}</Alert.Inline>
{/if}
</Layout.Stack>
<svelte:fragment slot="footer">
<Button text secondary on:click={() => (show = false)}>Cancel</Button>
<Button secondary submit>Reset</Button>
<Layout.Stack direction="row" gap="s" justifyContent="flex-end">
<Button text secondary on:click={() => (show = false)}>Cancel</Button>
<Button on:click={reset}>Reset</Button>
</Layout.Stack>
</svelte:fragment>
</Modal>
</Dialog>
@@ -324,7 +324,7 @@
variant="secondary"
padding="s"
--input-background-color="var(--bgcolor-neutral-primary)">
<div class="modal-form">
<div>
<div class="u-flex-vertical u-gap-24">
<div class="u-flex-vertical u-gap-4">
<Layout.Grid columns={4}>
@@ -9,12 +9,20 @@
InputURL
} from '$lib/elements/forms';
import type { Models } from '@appwrite.io/console';
import { Fieldset, Layout, Popover, Icon, Accordion } from '@appwrite.io/pink-svelte';
import {
Fieldset,
Layout,
Popover,
Icon,
Accordion,
ActionMenu
} from '@appwrite.io/pink-svelte';
import { IconInfo } from '@appwrite.io/pink-icons-svelte';
import type { Component } from 'svelte';
import { getApiEndpoint } from '$lib/stores/sdk';
import { page } from '$app/state';
import { project } from '$routes/(console)/project-[project]/store';
import Root from '$lib/commandCenter/panels/root.svelte';
export let variables: Partial<Models.TemplateVariable>[] = [];
export let templateVariables: Models.TemplateVariable[] = [];
@@ -93,7 +101,7 @@
bind:value={variable.value} />
</Layout.Stack>
<Layout.Stack gap="s" justifyContent="flex-end" inline>
<Popover let:toggle>
<Popover let:toggle placement="bottom-end">
<Button
secondary
icon
@@ -101,8 +109,10 @@
e.preventDefault();
toggle(e);
}}>
<Icon size="s" icon={IconInfo} /></Button>
<p slot="tooltip">{@html variable.description}</p>
<Icon icon={IconInfo} /></Button>
<ActionMenu.Root slot="tooltip">
<p>{@html variable.description}</p>
</ActionMenu.Root>
</Popover>
</Layout.Stack>
</Layout.Stack>
@@ -134,7 +144,7 @@
bind:value={variable.value} />
</Layout.Stack>
<Layout.Stack gap="s" justifyContent="flex-end" inline>
<Popover let:toggle>
<Popover let:toggle placement="bottom-end">
<Button
secondary
icon
@@ -144,7 +154,9 @@
}}>
<Icon size="s" icon={IconInfo} />
</Button>
<p slot="tooltip">{@html variable.description}</p>
<ActionMenu.Root slot="tooltip">
<p>{@html variable.description}</p>
</ActionMenu.Root>
</Popover>
</Layout.Stack>
</Layout.Stack>
@@ -16,7 +16,7 @@
<ActionMenu.Item.Button trailingIcon={IconDownload}>Download</ActionMenu.Item.Button>
</ActionMenu.Root>
<svelte:fragment slot="menu">
<ActionMenu.Root noPadding>
<ActionMenu.Root>
<ActionMenu.Item.Anchor
on:click={toggle}
href={getSourceDownload(page.params.function, deployment.$id)}
@@ -122,7 +122,9 @@
<Modal title="Create Git deployment" bind:show onSubmit={createDeployment} bind:error>
<span slot="description">
Enter a valid commit reference to create a new deployment. <Link href="#">Learn more</Link>
Enter a valid commit reference to create a new deployment. <Link
href="https://appwrite.io/docs/products/functions/deployments#create-deployment"
external>Learn more</Link>
</span>
{#if hasRepository}
{#await load()}
@@ -4,7 +4,7 @@
import { PaginationWithLimit, ViewSelector, EmptyFilter } from '$lib/components';
import { Button } from '$lib/elements/forms';
import { columns, deploymentList, func } from './store';
import { Container } from '$lib/layout';
import { Container, ResponsiveContainerHeader } from '$lib/layout';
import type { Models } from '@appwrite.io/console';
import { GRACE_PERIOD_OVERRIDE, isCloud } from '$lib/system';
import { readOnly } from '$lib/stores/billing';
@@ -78,7 +78,7 @@
<Layout.Stack direction="row" gap="s" alignItems="center" inline>
<Menu>
<Button secondary icon text>
<Icon icon={IconDotsHorizontal} size="s" />
<Icon icon={IconDotsHorizontal} />
</Button>
<svelte:fragment slot="menu" let:toggle>
<ActionMenu.Root>
@@ -168,25 +168,18 @@
{/if}
<Layout.Stack gap="l">
<Layout.Stack>
<Layout.Stack direction="row" alignItems="center">
<Layout.Stack direction="row" gap="s" wrap="wrap">
<QuickFilters {columns} analyticsSource="function_deployments" />
</Layout.Stack>
<Layout.Stack direction="row" gap="s" inline>
{#if data.deploymentList.total}
<ViewSelector view={View.Table} {columns} hideView />
{/if}
<CreateActionMenu let:toggle installations={data.installations}>
<Button on:click={toggle} event="create_deployment">
<Icon icon={IconPlus} size="s" slot="start" />
Create deployment
</Button>
</CreateActionMenu>
</Layout.Stack>
</Layout.Stack>
<ParsedTagList />
</Layout.Stack>
<ResponsiveContainerHeader
hasFilters
{columns}
hideView
analyticsSource="function_deployments">
<CreateActionMenu let:toggle installations={data.installations}>
<Button on:click={toggle} event="create_deployment">
<Icon icon={IconPlus} size="s" slot="start" />
Create deployment
</Button>
</CreateActionMenu>
</ResponsiveContainerHeader>
{#if data.deploymentList.total}
<Table columns={$columns} {data} />
@@ -15,6 +15,7 @@
Accordion,
ActionMenu,
Card,
Icon,
Layout,
Logs,
Popover,
@@ -25,13 +26,23 @@
import { formatTimeDetailed } from '$lib/helpers/timeConversion';
import { timer } from '$lib/actions/timer';
import { app } from '$lib/stores/app';
import { getOutputDownload, getSourceDownload } from '../store';
import { getOutputDownload, getSourceDownload, showCreateDeployment } from '../store';
import { IconDotsHorizontal, IconRefresh, IconTerminal } from '@appwrite.io/pink-icons-svelte';
import { Menu } from '$lib/components/menu';
import { canWriteFunctions } from '$lib/stores/roles';
import { Click, trackEvent } from '$lib/actions/analytics';
import DownloadActionMenuItem from '../(components)/downloadActionMenuItem.svelte';
import { base } from '$app/paths';
import { isCloud } from '$lib/system';
import { readOnly } from '$lib/stores/billing';
import RedeployModal from '../(modals)/redeployModal.svelte';
export let data;
let showDelete = false;
let showCancel = false;
let showActivate = false;
let showRedeploy = false;
onMount(() => {
const unsubscribe = sdk.forConsole.client.subscribe<Models.Deployment>(
@@ -73,7 +84,7 @@
<Container>
<DeploymentCard proxyRuleList={data.proxyRuleList} deployment={data.deployment}>
{#snippet footer()}
<Layout.Stack direction="row" inline>
<Layout.Stack direction="row" alignItems="center" inline>
{#if data.deployment.status === 'processing' || data.deployment.status === 'building' || data.deployment.status === 'waiting'}
<Button
text
@@ -82,34 +93,35 @@
}}>Cancel</Button>
{/if}
{#if !!data.deployment?.sourceSize || !!data.deployment?.sourceSize}
<Popover let:toggle placement="bottom-end">
<Button secondary on:click={toggle}>Download</Button>
<svelte:fragment slot="tooltip" let:toggle>
<ActionMenu.Root noPadding>
<ActionMenu.Item.Anchor
on:click={toggle}
href={getSourceDownload(
page.params.function,
data.deployment.$id
)}
external>
Download source
</ActionMenu.Item.Anchor>
<ActionMenu.Item.Anchor
disabled={data.deployment?.status !== 'ready'}
on:click={toggle}
href={getOutputDownload(
page.params.function,
data.deployment.$id
)}
external>
Download output
</ActionMenu.Item.Anchor>
</ActionMenu.Root>
</svelte:fragment>
</Popover>
<Menu>
<Button secondary icon text>
<Icon icon={IconDotsHorizontal} />
</Button>
<svelte:fragment slot="menu" let:toggle>
<ActionMenu.Root>
{#if $canWriteFunctions}
<ActionMenu.Item.Button
trailingIcon={IconRefresh}
on:click={() => {
showRedeploy = true;
trackEvent(Click.FunctionsRedeployClick);
toggle();
}}>
Redeploy
</ActionMenu.Item.Button>
{/if}
{#if !!data.deployment?.sourceSize || !!data.deployment?.sourceSize}
<DownloadActionMenuItem deployment={data.deployment} {toggle} />
{/if}
</ActionMenu.Root>
</svelte:fragment>
</Menu>
{#if data.func.deploymentId === data.deployment.$id && data.deployment.status === 'ready'}
<Button
href={`${base}/project-${page.params.project}/functions/function-${page.params.function}/executions/execute-function`}
disabled={isCloud && $readOnly}>
Execute
</Button>
{/if}
{#if data.func.deploymentId !== data.deployment.$id && data.deployment.status === 'ready'}
@@ -165,3 +177,7 @@
selectedDeployment={data.deployment}
bind:showActivate
on:activated={() => invalidate(Dependencies.DEPLOYMENTS)} />
{#if showRedeploy}
<RedeployModal bind:show={showRedeploy} selectedDeployment={data.deployment} redirect />
{/if}
@@ -1,19 +1,17 @@
<script lang="ts">
import { invalidate } from '$app/navigation';
import { Empty, EmptyFilter, PaginationWithLimit, ViewSelector } from '$lib/components';
import { Empty, EmptyFilter, PaginationWithLimit } from '$lib/components';
import { Dependencies } from '$lib/constants';
import { Button } from '$lib/elements/forms';
import { Container } from '$lib/layout';
import { Container, ResponsiveContainerHeader } from '$lib/layout';
import { sdk } from '$lib/stores/sdk';
import { onMount } from 'svelte';
import { project } from '$routes/(console)/project-[project]/store';
import { base } from '$app/paths';
import { View } from '$lib/helpers/load';
import { Icon, Layout, Tooltip } from '@appwrite.io/pink-svelte';
import { Icon, Tooltip } from '@appwrite.io/pink-svelte';
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
import Table from './table.svelte';
import { columns } from './store';
import { ParsedTagList, QuickFilters } from '$lib/components/filters';
export let data;
@@ -27,32 +25,22 @@
</script>
<Container>
<Layout.Stack>
<Layout.Stack direction="row" alignItems="center" justifyContent="space-between">
<QuickFilters {columns} analyticsSource="function_executions" />
<Layout.Stack gap="s" inline direction="row" alignItems="center">
{#if data?.executions?.total}
<ViewSelector view={View.Table} {columns} hideView />
{/if}
<Tooltip disabled={!!data.func?.deploymentId}>
<div>
<Button
event="execute_function"
href={`${base}/project-${$project.$id}/functions/function-${data.func.$id}/executions/execute-function`}
disabled={!data.func.$id || !data.func?.deploymentId}>
<Icon icon={IconPlus} size="s" slot="start" />
Create execution
</Button>
</div>
<span slot="tooltip">
Execution cannot be created because there is no active deployment.
</span>
</Tooltip>
</Layout.Stack>
</Layout.Stack>
<ParsedTagList />
</Layout.Stack>
<ResponsiveContainerHeader hasFilters {columns} hideView analyticsSource="function_executions">
<Tooltip disabled={!!data.func?.deploymentId}>
<div>
<Button
event="execute_function"
href={`${base}/project-${$project.$id}/functions/function-${data.func.$id}/executions/execute-function`}
disabled={!data.func.$id || !data.func?.deploymentId}>
<Icon icon={IconPlus} size="s" slot="start" />
Create execution
</Button>
</div>
<span slot="tooltip">
Execution cannot be created because there is no active deployment.
</span>
</Tooltip>
</ResponsiveContainerHeader>
{#if data?.executions?.total}
<Table columns={$columns} logs={data.executions} />
@@ -1,5 +1,5 @@
<script lang="ts">
import { WizardWithSteps } from '$lib/layout';
import { Wizard, WizardWithSteps } from '$lib/layout';
import type { WizardStepsType } from '$lib/layout/wizardWithSteps.svelte';
import Provider from './wizard/provider.svelte';
import Settings from './wizard/settings.svelte';
@@ -10,9 +10,26 @@
import { base } from '$app/paths';
import { project } from '../../store';
import { wizard } from '$lib/stores/wizard';
import { provider, providerParams } from './wizard/store';
import { ID, type Models } from '@appwrite.io/console';
import { provider, providerParams, providerType } from './wizard/store';
import { ID, MessagingProviderType, type Models } from '@appwrite.io/console';
import { Providers } from '../provider.svelte';
import { Button, Form } from '$lib/elements/forms';
import {
Fieldset,
Layout,
Typography,
ActionList,
Accordion,
Card
} from '@appwrite.io/pink-svelte';
import { providers } from '$routes/(console)/project-[project]/messaging/providers/store';
import { getProviderText } from '$routes/(console)/project-[project]/messaging/helper';
import { newMemberModal } from '$lib/stores/organization';
import CreateMember from '$routes/(console)/organization-[organization]/createMember.svelte';
import { IconBookOpen, IconInfo, IconUserGroup } from '@appwrite.io/pink-icons-svelte';
import { getProviderDisplayNameAndIcon } from '$routes/(console)/project-[project]/messaging/provider.svelte';
let open = false;
async function create() {
try {
@@ -165,4 +182,61 @@
});
</script>
<WizardWithSteps title="Create provider" steps={stepsComponents} on:finish={create} />
<Wizard title="Create provider" columnSize="l">
<Form onSubmit={create} isModal={false}>
<Layout.Stack gap="xxl">
<Fieldset legend="Provider">
<Provider />
</Fieldset>
<Fieldset legend="Settings">
<Settings />
</Fieldset>
<Layout.Stack justifyContent="flex-end" direction="row">
<Button submit>Create</Button>
</Layout.Stack>
</Layout.Stack>
</Form>
<svelte:fragment slot="aside">
<Card.Base padding="s">
<Layout.Stack gap="s">
<Typography.Text variant="m-500">Need a hand?</Typography.Text>
<ActionList.Root>
{#if providers[$providerType].providers[$provider].needAHand}
{@const needAHand = providers[$providerType].providers[$provider].needAHand}
<ActionList.Item.Accordion
hasDivider
title={`How to enable ${getProviderText($provider)} ${
$providerType == MessagingProviderType.Push ||
$providerType == MessagingProviderType.Sms
? `${getProviderDisplayNameAndIcon($provider).displayName} notifications`
: getProviderText($provider)
}?`}
icon={IconInfo}>
<Layout.Stack>
{#each needAHand as p}
<p>
{@html p}
</p>
{/each}
</Layout.Stack>
</ActionList.Item.Accordion>
{/if}
<ActionList.Item.Anchor
hasDivider
href={`https://appwrite.io/docs/products/messaging/${$provider}`}
title="Read the guide in the docs"
icon={IconBookOpen} />
<ActionList.Item.Button
on:click={() => {
$newMemberModal = true;
}}
title="Invite a team member"
icon={IconUserGroup} />
</ActionList.Root>
</Layout.Stack>
</Card.Base>
</svelte:fragment>
{#if $newMemberModal}
<CreateMember bind:showCreate={$newMemberModal} />
{/if}
</Wizard>
@@ -29,5 +29,5 @@
</script>
<Confirm onSubmit={deleteProvider} title="Delete provider" bind:open={showDelete} bind:error>
Are you sure you want to delete <b>{$provider.name}</b> from '{$project.name}'?
<span>Are you sure you want to delete <b>{$provider.name}</b> from '{$project.name}'?</span>
</Confirm>
@@ -12,6 +12,7 @@
import { Button, Form } from '$lib/elements/forms';
import { onMount } from 'svelte';
import CreateMember from '$routes/(console)/organization-[organization]/createMember.svelte';
import { Link } from '@appwrite.io/pink-svelte';
export let inputs: (ProviderInput | ProviderInput[])[];
export let providerType: MessagingProviderType;
@@ -219,11 +220,11 @@
<Form onSubmit={update}>
<CardGrid>
<svelte:fragment slot="title">Settings</svelte:fragment>
Configure the settings to <Button
Configure the settings to <Link.Anchor
href={`https://appwrite.io/docs/products/messaging/${provider}`}
external>enable {displayName}</Button> to send {message}, or <Button
on:click={() => (newMemberModal = true)}>invite a team member</Button> to complete the provider
settings.
>enable {displayName}</Link.Anchor> to send {message}, or <Link.Button
on:click={() => (newMemberModal = true)}>invite a team member</Link.Button> to complete the
provider settings.
<svelte:fragment slot="aside">
<!-- Must wait until ready or else the files input won't be set properly -->
{#if ready}
@@ -12,6 +12,7 @@
import { provider as providerData } from './store';
import { Typography } from '@appwrite.io/pink-svelte';
import { getProviderText } from '../../helper';
import { Layout } from '@appwrite.io/pink-svelte';
let enabled: boolean = null;
@@ -143,9 +144,9 @@
bind:value={enabled} />
</ul>
<div>
<p class="title">
Provider: <Provider noIcon provider={$providerData.provider} />
</p>
<Layout.Stack direction="row" gap="xs">
<span>Provider:</span><Provider noIcon provider={$providerData.provider} />
</Layout.Stack>
<p class="title">Type: {getProviderText($providerData.type)}</p>
<p>Created: {toLocaleDateTime($providerData.$createdAt)}</p>
</div>
@@ -25,8 +25,12 @@
APNSProviderParams
} from './store';
import TooltipPopover from './tooltipPopover.svelte';
import { Icon, Tooltip, Upload, Layout, Typography } from '@appwrite.io/pink-svelte';
import { IconInfo } from '@appwrite.io/pink-icons-svelte';
import { removeFile } from '$lib/helpers/files';
export let files: Record<string, FileList>;
export let input: ProviderInput;
export let params: Partial<
| TwilioProviderParams
@@ -100,14 +104,40 @@
<TooltipPopover slot="info" {popover} {popoverProps} tooltip={input.tooltip} />
</InputPhone>
{:else if input.type === 'file'}
<InputFile
label={input.label}
allowedFileExtensions={input.allowedFileExtensions}
required={!input.optional}
tooltip={input.tooltip}
{popover}
{popoverProps}
bind:files={files[input.name]} />
<Upload.Dropzone
extensions={['json']}
bind:files={files[input.name]}
required={!input.optional}>
<Layout.Stack alignItems="center" gap="s">
<Layout.Stack alignItems="center" gap="s">
<Layout.Stack alignItems="center" justifyContent="center" direction="row" gap="s">
<Typography.Text variant="l-500">
Drag and drop service account JSON here or click to upload
</Typography.Text>
<Tooltip>
<Layout.Stack alignItems="center" justifyContent="center" inline>
<Icon icon={IconInfo} size="s" />
</Layout.Stack>
<svelte:fragment slot="tooltip">Only .json files allowed</svelte:fragment>
</Tooltip>
</Layout.Stack>
<Typography.Caption variant="400">Max file size 10MB</Typography.Caption>
</Layout.Stack>
</Layout.Stack>
</Upload.Dropzone>
{#if files[input.name]?.length}
<Upload.List
bind:files={files[input.name]}
on:remove={(e) => (files[input.name] = removeFile(e.detail, files[input.name]))} />
{/if}
<!-- <InputFile-->
<!-- label={input.label}-->
<!-- allowedFileExtensions={input.allowedFileExtensions}-->
<!-- required={!input.optional}-->
<!-- tooltip={input.tooltip}-->
<!-- {popover}-->
<!-- {popoverProps}-->
<!-- bind:files={files[input.name]} />-->
{:else if input.type === 'switch'}
<InputSwitch label={input.label} id={input.name} bind:value={params[input.name]}>
<svelte:fragment slot="description">
@@ -12,6 +12,14 @@
let showCustomId = false;
let id: string = null;
$: {
name;
id;
$provider;
beforeSubmit();
}
async function beforeSubmit() {
switch ($provider) {
case Providers.Twilio:
@@ -133,9 +141,6 @@
</script>
<WizardStep {beforeSubmit}>
<svelte:fragment slot="title">Provider</svelte:fragment>
<div class="u-sep-block-start u-padding-block-end-32"></div>
<InputText
id="name"
label="Name"
@@ -4,8 +4,7 @@
import { providers } from '../store';
import { providerType, provider, providerParams } from './store';
import SettingsFormList from '../settingsFormList.svelte';
import { newMemberModal } from '$lib/stores/organization';
import CreateMember from '$routes/(console)/organization-[organization]/createMember.svelte';
import { MessagingProviderType } from '@appwrite.io/console';
import {
Collapsible,
@@ -16,9 +15,15 @@
import { getProviderText } from '../../helper';
let files: Record<string, FileList> = {};
const inputs = providers[$providerType].providers[$provider].configure;
let open = false;
$: inputs = providers[$providerType].providers[$provider].configure;
$: {
files;
$providerType;
$provider;
beforeSubmit();
}
onMount(() => {
const flattened = [];
for (const i of inputs) {
@@ -29,7 +34,11 @@
}
}
for (const input of flattened) {
if (input.type === 'file' && $providerParams[$provider][input.name].length > 0) {
if (
input.type === 'file' &&
$providerParams[$provider] &&
$providerParams[$provider][input.name].length > 0
) {
const dataTransfer = new DataTransfer();
const f = new File(
[$providerParams[$provider][input.name]],
@@ -54,7 +63,6 @@
</script>
<WizardStep {beforeSubmit}>
<svelte:fragment slot="title">Settings</svelte:fragment>
<svelte:fragment slot="subtitle">
Set up the credentials below to enable {providers[$providerType].providers[$provider].title}
for sending
@@ -62,120 +70,4 @@
</svelte:fragment>
<SettingsFormList bind:files {inputs} bind:params={$providerParams[$provider]} />
<div class="need-a-hand u-flex-vertical u-gap-8">
<p class="body-text-2 u-bold u-margin-block-start-48">Need a hand?</p>
<div>
{#if providers[$providerType].providers[$provider].needAHand}
{@const needAHand = providers[$providerType].providers[$provider].needAHand}
<div
class="how-to-enable u-padding-inline-8 u-padding-0"
style={!open ? '' : 'background-color: hsl(var(--p-bg-color-hover));'}>
<Collapsible>
<CollapsibleItem withIndentation bind:open>
<svelte:fragment slot="beforetitle">
<div class="u-flex u-cross-center u-gap-16">
<div class="avatar is-size-small">
<span
class="icon-info"
style:--p-text-size="1.25rem"
aria-hidden="true"></span>
</div>
<span class="title body-text-2 u-small" class:u-bold={open}
>How to enable {getProviderText($provider)}
{#if $providerType == MessagingProviderType.Push}notifications{:else}{getProviderText(
$provider
)}{/if} service?</span>
</div>
</svelte:fragment>
<div class="u-flex-vertical u-gap-16">
{#each needAHand as p}
<p>
{@html p}
</p>
{/each}
</div>
</CollapsibleItem>
</Collapsible>
</div>
<div class="u-margin-block-start-4 u-sep-block-end"></div>
{/if}
<ClickableList>
<ClickableListItem
href={`https://appwrite.io/docs/products/messaging/${$provider}`}
external>
<div class="u-flex u-cross-center u-main-space-between">
<div class="u-flex u-cross-center u-gap-16">
<div class="avatar is-size-small">
<span
class="icon-book-open"
style:--p-text-size="1.25rem"
aria-hidden="true"></span>
</div>
<p>Read the full guide in the documentation</p>
</div>
<span class="icon-arrow-sm-right u-font-size-20" aria-hidden="true"></span>
</div>
</ClickableListItem>
<ClickableListItem on:click={() => ($newMemberModal = true)}>
<div class="u-flex u-cross-center u-main-space-between">
<div class="u-flex u-cross-center u-gap-16">
<div class="avatar is-size-small">
<span
class="icon-user-group"
style:--p-text-size="1.25rem"
aria-hidden="true"></span>
</div>
<p>Invite a team member to complete this step</p>
</div>
<span class="icon-arrow-sm-right u-font-size-20" aria-hidden="true"></span>
</div>
</ClickableListItem>
</ClickableList>
</div>
</div>
</WizardStep>
<CreateMember bind:showCreate={$newMemberModal} />
<style lang="scss">
.need-a-hand {
--p-bg-color-hover: var(--color-neutral-5);
.how-to-enable {
border-radius: var(--border-radius-small);
&:hover,
&:focus {
background-color: hsl(var(--p-bg-color-hover));
.title {
font-weight: 600;
}
}
}
:global(.clickable-list) {
--border: var(--color-neutral-5);
--p-clickable-button-bg-color-hover: var(--border);
:global(.theme-dark) & {
--border: var(--color-neutral-85);
}
}
:global(.clickable-list-button) {
padding-inline: 0.5rem;
}
:global(.theme-dark) & {
--p-bg-color-hover: var(--color-neutral-85);
}
}
:global(.clickable-list-button):hover p,
:global(.clickable-list-button):focus p {
font-weight: 600;
}
</style>
@@ -9,7 +9,7 @@
import { sdk } from '$lib/stores/sdk';
import { Alert } from '@appwrite.io/pink-svelte';
import { project } from '../../../store';
import ExpirationInput from '../expirationInput.svelte';
import { ExpirationInput } from '$lib/components';
import { key } from './store';
let expiration = $key.expire;
@@ -4,7 +4,7 @@
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 { ExpirationInput } from '$lib/components';
import Scopes from '../scopes.svelte';
import Button from '$lib/elements/forms/button.svelte';
import Form from '$lib/elements/forms/form.svelte';
@@ -5,18 +5,21 @@
import { Button } from '$lib/elements/forms';
import Container from '$lib/layout/container.svelte';
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
import { Card, Empty, Icon, Layout } from '@appwrite.io/pink-svelte';
import SearchQuery from '$lib/components/searchQuery.svelte';
import { Card, Empty, Icon } from '@appwrite.io/pink-svelte';
import { app } from '$lib/stores/app';
import { Click, trackEvent } from '$lib/actions/analytics';
import Table from './table.svelte';
import { ResponsiveContainerHeader } from '$lib/layout';
let { data } = $props();
</script>
<Container>
<Layout.Stack direction="row" justifyContent="space-between">
<SearchQuery placeholder="Search domain" />
<ResponsiveContainerHeader
hasSearch
hideView
searchPlaceholder="Search by domain"
analyticsSource="settings_domain_overview">
<Button
href={`${base}/project-${page.params.project}/settings/domains/add-domain`}
on:click={() => {
@@ -27,8 +30,7 @@
<Icon icon={IconPlus} size="s" />
Add domain
</Button>
</Layout.Stack>
</ResponsiveContainerHeader>
{#if data.rules.total}
<Table domains={data.rules} />
@@ -5,21 +5,27 @@
import { Wizard } from '$lib/layout';
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
import { Fieldset, Layout } from '@appwrite.io/pink-svelte';
import { Fieldset } from '@appwrite.io/pink-svelte';
import { goto, invalidate } from '$app/navigation';
import { Dependencies } from '$lib/constants';
import { writable } from 'svelte/store';
import { onMount } from 'svelte';
const routeBase = `${base}/project-${page.params.project}/settings/domains`;
//TODO: enable once combo box is fixed
// let { data } = $props();
let formComponent: Form;
let isSubmitting = $state(writable(false));
let showConnectRepo = $state(false);
let domainName = $state('');
onMount(() => {
if (page.url.searchParams.has('domain')) {
domainName = page.url.searchParams.get('domain');
}
});
async function addDomain() {
try {
const rule = await sdk.forProject.proxy.createAPIRule(domainName);
@@ -41,27 +47,12 @@
<Wizard title="Add domain" href={routeBase} column columnSize="s" confirmExit>
<Form bind:this={formComponent} onSubmit={addDomain} bind:isSubmitting>
<Layout.Stack gap="xxl">
<Fieldset legend="Domain">
<!-- <Input.ComboBox
label="Domain"
id="domain"
name="domain"
bind:value={domain}
options={data.domains.rules.map((domain) => ({
label: domain.domain,
value: domain.domain
}))}
required
placeholder="appwrite.example.com" /> -->
<InputDomain
label="Domain"
id="domain"
bind:value={domainName}
required
placeholder="appwrite.example.com" />
</Fieldset>
</Layout.Stack>
<InputDomain
label="Domain"
id="domain"
bind:value={domainName}
required
placeholder="appwrite.example.com" />
</Form>
<svelte:fragment slot="footer">
@@ -1,7 +1,14 @@
<script lang="ts">
import { IconGlobeAlt } from '@appwrite.io/pink-icons-svelte';
import { Card, Divider, Icon, Layout, Typography } from '@appwrite.io/pink-svelte';
import VerificationFieldset from './verificationFieldset.svelte';
import {
Card,
Divider,
Fieldset,
Icon,
Layout,
Tabs,
Typography
} from '@appwrite.io/pink-svelte';
import { Button, Form } from '$lib/elements/forms';
import { sdk } from '$lib/stores/sdk';
import { organization } from '$lib/stores/organization';
@@ -13,21 +20,34 @@
import Wizard from '$lib/layout/wizard.svelte';
import { base } from '$app/paths';
import { writable } from 'svelte/store';
import { consoleVariables } from '$routes/(console)/store';
import NameserverTable from '$lib/components/domains/nameserverTable.svelte';
import RecordTable from '$lib/components/domains/recordTable.svelte';
let { data } = $props();
const ruleId = page.url.searchParams.get('rule');
const routeBase = `${base}/project-${page.params.project}/settings/domains`;
let selectedTab: 'cname' | 'nameserver' = $state();
let isSubDomain = $derived(page.params.domain?.split('.')?.length >= 3);
let selectedTab: 'cname' | 'nameserver' | 'a' | 'aaaa' = $state(
!!$consoleVariables._APP_DOMAIN_TARGET_CNAME && isSubDomain
? 'cname'
: !!$consoleVariables._APP_DOMAIN_TARGET_A
? 'a'
: !!$consoleVariables._APP_DOMAIN_TARGET_AAAA
? 'aaaa'
: 'nameserver'
);
let verified = $state(false);
let routeBase = `${base}/project-${page.params.project}/settings/domains`;
let isSubmitting = $state(writable(false));
async function verify() {
const isNewDomain =
data.domainsList.domains.findIndex((rule) => rule.domain === page.params.domain) === -1;
try {
if (selectedTab === 'cname') {
if (selectedTab !== 'nameserver') {
const ruleData = await sdk.forProject.proxy.updateRuleVerification(ruleId);
verified = ruleData.status === 'verified';
} else if (isNewDomain && isCloud) {
@@ -44,9 +64,9 @@
});
await goto(routeBase);
await invalidate(Dependencies.DOMAINS);
await invalidate(Dependencies.SITES_DOMAINS);
} catch (error) {
verified = false;
isSubmitting.set(false);
addNotification({
type: 'error',
message: error.message
@@ -82,14 +102,58 @@
</Layout.Stack>
</Card.Base>
<VerificationFieldset domain={page.params.domain} {verified} bind:selectedTab>
<Divider />
<Layout.Stack direction="row" justifyContent="flex-end">
<Fieldset legend="Verification">
<Layout.Stack gap="xl">
<div>
<Button submit disabled={$isSubmitting}>Verify</Button>
<Tabs.Root variant="secondary" let:root>
{#if isSubDomain && !!$consoleVariables._APP_DOMAIN_TARGET_CNAME && $consoleVariables._APP_DOMAIN_TARGET_CNAME !== 'localhost'}
<Tabs.Item.Button
{root}
on:click={() => (selectedTab = 'cname')}
active={selectedTab === 'cname'}>
CNAME
</Tabs.Item.Button>
{/if}
{#if isCloud}
<Tabs.Item.Button
{root}
on:click={() => (selectedTab = 'nameserver')}
active={selectedTab === 'nameserver'}>
Nameservers
</Tabs.Item.Button>
{/if}
{#if !!$consoleVariables._APP_DOMAIN_TARGET_A && $consoleVariables._APP_DOMAIN_TARGET_A !== '127.0.0.1'}
<Tabs.Item.Button
{root}
on:click={() => (selectedTab = 'a')}
active={selectedTab === 'a'}>
A
</Tabs.Item.Button>
{/if}
{#if !!$consoleVariables._APP_DOMAIN_TARGET_AAAA && $consoleVariables._APP_DOMAIN_TARGET_AAAA !== '::1'}
<Tabs.Item.Button
{root}
on:click={() => (selectedTab = 'aaaa')}
active={selectedTab === 'aaaa'}>
AAAA
</Tabs.Item.Button>
{/if}
</Tabs.Root>
<Divider />
</div>
{#if selectedTab === 'nameserver'}
<NameserverTable domain={page.params.domain} {verified} />
{:else}
<RecordTable domain={page.params.domain} {verified} variant={selectedTab} />
{/if}
<Divider />
<Layout.Stack direction="row" justifyContent="flex-end">
<div>
<Button submit disabled={$isSubmitting}>Verify</Button>
</div>
</Layout.Stack>
</Layout.Stack>
</VerificationFieldset>
</Fieldset>
</Layout.Stack>
</Form>
</Wizard>
@@ -3,7 +3,7 @@ import { Dependencies } from '$lib/constants.js';
import { isCloud } from '$lib/system';
import type { Domain, DomainsList } from '$lib/sdk/domains.js';
export const load = async ({ parent, depends, params }) => {
export const load = async ({ depends, params }) => {
depends(Dependencies.DOMAINS);
let domain: Domain;
@@ -1,45 +0,0 @@
<script lang="ts">
import CnameTable from '$lib/components/domains/cnameTable.svelte';
import NameserverTable from '$lib/components/domains/nameserverTable.svelte';
import { isCloud, isSelfHosted } from '$lib/system';
import { Divider, Fieldset, Layout, Tabs } from '@appwrite.io/pink-svelte';
export let domain: string;
export let verified: boolean;
$: isSubDomain = domain?.split('.')?.length >= 3;
export let selectedTab: 'cname' | 'nameserver' =
isSubDomain || isSelfHosted ? 'cname' : 'nameserver';
</script>
<Fieldset legend="Verification">
<Layout.Stack gap="xl">
<div>
<Tabs.Root variant="secondary" let:root>
{#if isSubDomain}
<Tabs.Item.Button
{root}
on:click={() => (selectedTab = 'cname')}
active={selectedTab === 'cname'}>
CNAME
</Tabs.Item.Button>
{/if}
{#if isCloud}
<Tabs.Item.Button
{root}
on:click={() => (selectedTab = 'nameserver')}
active={selectedTab === 'nameserver'}>
Nameservers
</Tabs.Item.Button>
{/if}
</Tabs.Root>
<Divider />
</div>
{#if selectedTab === 'cname' && isSubDomain}
<CnameTable {domain} {verified} />
{:else if isCloud}
<NameserverTable {domain} {verified} />
{/if}
<slot />
</Layout.Stack>
</Fieldset>
@@ -21,7 +21,7 @@
async function deleteDomain() {
try {
await sdk.forProject.proxy.deleteRule(selectedDomain.$id);
await invalidate(Dependencies.SITES_DOMAINS);
await invalidate(Dependencies.DOMAINS);
show = false;
addNotification({
type: 'success',
@@ -38,11 +38,7 @@
<Confirm title="Delete domain" bind:open={show} onSubmit={deleteDomain} bind:error confirmDeletion>
{#if selectedDomain}
<Typography.Text variant="m-400">
Are you sure you want to delete this domain? You will no longer be able to view your
site by visiting:
</Typography.Text>
<Typography.Text variant="m-500">
{selectedDomain.domain}
Are you sure you want to delete this domain?
</Typography.Text>
{/if}
</Confirm>
@@ -21,7 +21,7 @@
async function retryDomain() {
try {
await sdk.forProject.proxy.updateRuleVerification(selectedDomain.$id);
await invalidate(Dependencies.SITES_DOMAINS);
await invalidate(Dependencies.DOMAINS);
show = false;
addNotification({
type: 'success',
@@ -5,7 +5,15 @@
import { protocol } from '$routes/(console)/store';
import type { Models } from '@appwrite.io/console';
import { IconDotsHorizontal, IconRefresh, IconTrash } from '@appwrite.io/pink-icons-svelte';
import { ActionMenu, Icon, Layout, Popover, Table, Typography } from '@appwrite.io/pink-svelte';
import {
ActionMenu,
Badge,
Icon,
Layout,
Popover,
Table,
Typography
} from '@appwrite.io/pink-svelte';
import DeleteDomainModal from './deleteDomainModal.svelte';
import RetryDomainModal from './retryDomainModal.svelte';
import { columns } from './store';
@@ -19,7 +27,6 @@
let showDelete = $state(false);
let showRetry = $state(false);
let selectedDomain: Models.ProxyRule = $state(null);
let showPreviewDomainModal = $state(false);
</script>
<Table.Root columns={[...$columns, { id: 'actions', width: 40 }]} let:root>
@@ -39,14 +46,15 @@
<Link external href={`${$protocol}${domain.domain}`} variant="quiet" icon>
<Typography.Text truncate>
{domain.domain}
{#if domain.status !== 'verified'}
<Badge
variant="secondary"
type="error"
content="Verification failed"
size="s" />
{/if}
</Typography.Text>
</Link>
{:else if column.id === 'redirectUrl'}
<!-- TODO design redirect status code -->
{domain?.redirectUrl || 'No redirect'}
{domain?.redirectStatusCode ? `(${domain.redirectStatusCode})` : ''}
{:else if column.id === 'deploymentVcsProviderBranch'}
{domain.deploymentVcsProviderBranch || '-'}
{/if}
</Table.Cell>
{/each}
@@ -69,7 +77,6 @@
<ActionMenu.Item.Button
leadingIcon={IconRefresh}
on:click={(e) => {
e.preventDefault();
selectedDomain = domain;
showRetry = true;
toggle(e);
@@ -81,12 +88,11 @@
status="danger"
leadingIcon={IconTrash}
on:click={(e) => {
e.preventDefault();
selectedDomain = domain;
showDelete = true;
toggle(e);
trackEvent(Click.DomainDeleteClick, {
source: 'sites_domain_overview'
source: 'settings_domain_overview'
});
}}>
Delete
@@ -25,6 +25,7 @@
try {
await sdk.forConsole.projects.update($project.$id, name);
await invalidate(Dependencies.PROJECT);
await invalidate(Dependencies.ORGANIZATION);
addNotification({
type: 'success',
message: 'Project name has been updated'
@@ -85,7 +85,7 @@
</ActionMenu.Item.Button>
</ActionMenu.Root>
<svelte:fragment slot="menu">
<ActionMenu.Root noPadding>
<ActionMenu.Root>
<ActionMenu.Item.Anchor
on:click={toggle}
href={getSourceDownload(deployment.$id)}
@@ -57,8 +57,9 @@
Missing a repository?
</Typography.Text>
<Typography.Text variation="m-400">
Check your permissions in GitHub, your repository might be set to
private.
Make sure Appwrite has access to your GitHub repositories. If you chose
specific repos, you may need to update your permissions to include the
missing one.
</Typography.Text>
</Layout.Stack>
<Layout.Stack gap="s" direction="row">
@@ -9,7 +9,14 @@
InputURL
} from '$lib/elements/forms';
import type { Models } from '@appwrite.io/console';
import { Fieldset, Layout, Popover, Icon, Accordion } from '@appwrite.io/pink-svelte';
import {
Fieldset,
Layout,
Popover,
Icon,
Accordion,
ActionMenu
} from '@appwrite.io/pink-svelte';
import { IconInfo } from '@appwrite.io/pink-icons-svelte';
import { type Component } from 'svelte';
import { getApiEndpoint } from '$lib/stores/sdk';
@@ -100,8 +107,10 @@
e.preventDefault();
toggle(e);
}}>
<Icon size="s" icon={IconInfo} /></Button>
<p slot="tooltip">{variable.description}</p>
<Icon icon={IconInfo} /></Button>
<ActionMenu.Root slot="tooltip">
<p>{variable.description}</p>
</ActionMenu.Root>
</Popover>
</Layout.Stack>
</Layout.Stack>
@@ -140,8 +149,10 @@
e.preventDefault();
toggle(e);
}}>
<Icon size="s" icon={IconInfo} /></Button>
<p slot="tooltip">{variable.description}</p>
<Icon icon={IconInfo} /></Button>
<ActionMenu.Root slot="tooltip">
<p>{variable.description}</p>
</ActionMenu.Root>
</Popover>
</Layout.Stack>
</Layout.Stack>
@@ -122,7 +122,11 @@
{#if secretVariables?.length > 0}
<Alert.Inline status="info">
{secretVariables.length} secret variables are hidden from the editor. Their values will
remain unchanged. <Link href="#" variant="muted">Learn more</Link>.
remain unchanged. <Link
href="https://appwrite.io/docs/products/sites/develop#accessing-environment-variables"
external
variant="muted">Learn more</Link
>.
</Alert.Inline>
{/if}
<Layout.Stack gap="s">
@@ -75,7 +75,6 @@
<Layout.Stack gap="xxxl">
<DeploymentMetrics />
<!-- TODO: re-enable once component is complete -->
<Layout.Stack gap="l">
<ResponsiveContainerHeader
view={View.Table}
@@ -83,7 +82,7 @@
hasFilters
analyticsSource="site_deployments"
hideView>
<Popover padding="none" let:toggle>
<Popover padding="none" let:toggle placement="bottom-end">
<Button size="s" on:click={toggle}>
<Icon size="s" icon={IconPlus} />
Create deployment
@@ -120,58 +119,6 @@
</svelte:fragment>
</Popover>
</ResponsiveContainerHeader>
<!-- <Layout.Stack>
<Layout.Stack justifyContent="space-between" direction="row">
<Layout.Stack alignItems="center" direction="row">
{#if data.deploymentList.total || data?.query}
<QuickFilters {columns} analyticsSource="site_deployments" />
{/if}
</Layout.Stack>
<Layout.Stack direction="row" inline>
{#if data.deploymentList.total}
<ViewSelector view={View.Table} {columns} hideView />
{/if}
<Popover padding="none" let:toggle>
<Button size="s" on:click={toggle}>
<Icon size="s" icon={IconPlus} />
Create deployment
</Button>
<svelte:fragment slot="tooltip" let:toggle>
<ActionMenu.Root>
<ActionMenu.Item.Button
badge="Recommended"
on:click={(e) => {
toggle(e);
if (!hasInstallation) {
showConnectRepo = true;
} else {
showCreateDeployment = true;
}
}}>
Git
</ActionMenu.Item.Button>
<ActionMenu.Item.Button
on:click={(e) => {
toggle(e);
showConnectCLI = true;
}}>
CLI
</ActionMenu.Item.Button>
<ActionMenu.Item.Button
on:click={(e) => {
toggle(e);
showConnectManual = true;
}}>
Manual
</ActionMenu.Item.Button>
</ActionMenu.Root>
</svelte:fragment>
</Popover>
</Layout.Stack>
</Layout.Stack>
<ParsedTagList />
</Layout.Stack> -->
{#if data.deploymentList.total}
<Table {data} />
@@ -36,6 +36,6 @@
<p data-private>Are you sure you want to cancel this deployment?</p>
<svelte:fragment slot="footer">
<Button text on:click={() => (showCancel = false)}>Cancel</Button>
<Button secondary submit>Continue</Button>
<Button submit>Confirm</Button>
</svelte:fragment>
</Confirm>
@@ -127,7 +127,9 @@
<Modal title="Create Git deployment" bind:show onSubmit={createDeployment} bind:error>
<span slot="description">
Enter a valid commit reference to create a new deployment. <Link href="#">Learn more</Link>
Enter a valid commit reference to create a new deployment. <Link
href="https://appwrite.io/docs/products/sites/deployments#create-deployment"
external>Learn more</Link>
</span>
{#if hasRepository}
{#await load()}
@@ -7,6 +7,8 @@
import { onMount } from 'svelte';
import { page } from '$app/state';
import { UsageCard } from '$lib/components';
import { humanFileSize } from '$lib/helpers/sizeConvertion';
import { formatTimeDetailed } from '$lib/helpers/timeConversion';
const now = new Date();
const rangeOptions = [
@@ -33,17 +35,17 @@
description: 'Total builds time'
},
{
id: 'avgTime',
id: 'buildsTimeAverage',
value: null,
description: 'Avg. builds time'
},
{
id: 'success',
id: 'buildsSuccessTotal',
value: null,
description: 'Successful'
},
{
id: 'failed',
id: 'buildsFailedTotal',
value: null,
description: 'Failed '
}
@@ -63,7 +65,21 @@
try {
const usage = await sdk.forProject.sites.getUsage(page.params.site, range);
metrics = metrics.map((metric) => {
metric.value = usage[metric.id] ?? '-';
if (metric.id === 'buildsStorageTotal') {
const size = humanFileSize(usage[metric.id]);
metric.value = parseInt(size?.value) > 0 ? size.value + size.unit : '-';
} else if (
metric.id === 'buildsTimeTotal' ||
metric.id === 'buildsTimeAverage'
) {
const seconds = usage[metric.id] > 0 ? usage[metric.id] / 1000 : 0;
const time = formatTimeDetailed(seconds);
metric.value = time !== '0s' ? time : '-';
} else {
metric.value = usage[metric.id] ?? '-';
}
return metric;
});
metrics = metrics;
@@ -58,7 +58,7 @@
<svelte:fragment slot="actions">
<Button
external
href="#/"
href="https://appwrite.io/docs/products/sites/domains#add-a-custom-domain"
text
event="empty_documentation"
size="s"
@@ -54,7 +54,8 @@
<Modal title="Instant rollback" bind:show onSubmit={handleSubmit}>
<svelte:fragment slot="description">
Rollback is only available for deployments that are <b>ready and previously active</b>. <Link
href="#/">Learn more</Link
href="https://appwrite.io/docs/products/sites/deployments#instant-rollback"
external>Learn more</Link
>.
</svelte:fragment>
<Layout.Stack gap="xl">
@@ -3,7 +3,7 @@
import { EmptyFilter, PaginationWithLimit, ViewSelector } from '$lib/components';
import { Dependencies } from '$lib/constants';
import { Button } from '$lib/elements/forms';
import { Container } from '$lib/layout';
import { Container, ResponsiveContainerHeader } from '$lib/layout';
import { sdk } from '$lib/stores/sdk';
import { onMount } from 'svelte';
import Table from './table.svelte';
@@ -26,16 +26,8 @@
</script>
<Container>
<Layout.Stack>
<Layout.Stack direction="row" justifyContent="space-between">
<QuickFilters {columns} analyticsSource="site_logs" />
{#if data.logs.total}
<ViewSelector view={View.Table} {columns} hideView />
{/if}
</Layout.Stack>
<ResponsiveContainerHeader hasFilters {columns} hideView analyticsSource="site_logs" />
<ParsedTagList />
</Layout.Stack>
{#if data?.logs?.total}
<Table columns={$columns} logs={data.logs} />
@@ -55,7 +47,7 @@
<svelte:fragment slot="actions">
<Button
external
href="#"
href="https://appwrite.io/docs/products/sites/logs"
secondary
event="empty_documentation"
size="s"
@@ -7,11 +7,13 @@ import { error } from '@sveltejs/kit';
export const load: LayoutLoad = async ({ params, depends }) => {
depends(Dependencies.FILE);
depends(Dependencies.FILE_TOKENS);
try {
return {
header: Header,
breadcrumbs: Breadcrumbs,
tokens: await sdk.forProject.tokens.list(params.bucket, params.file),
file: await sdk.forProject.storage.getFile(params.bucket, params.file)
};
} catch (e) {
@@ -1,8 +1,8 @@
<script lang="ts">
import { CardGrid, BoxAvatar, CopyInput } from '$lib/components';
import { CardGrid, BoxAvatar, CopyInput, Empty } from '$lib/components';
import { Container } from '$lib/layout';
import { Button } from '$lib/elements/forms';
import { file } from './store';
import { columns, file, tokens } from './store';
import { toLocaleDate, toLocaleDateTime } from '$lib/helpers/date';
import { sdk } from '$lib/stores/sdk';
import { addNotification } from '$lib/stores/notifications';
@@ -15,27 +15,53 @@
import { Dependencies } from '$lib/constants';
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
import { bucket } from '../store';
import { Alert, Typography } from '@appwrite.io/pink-svelte';
import {
ActionMenu,
Alert,
Button as PinkButton,
InteractiveText,
Layout,
Icon,
Popover,
Table,
Typography
} from '@appwrite.io/pink-svelte';
import {
IconChevronRight,
IconDotsHorizontal,
IconDuplicate,
IconKey,
IconPencil,
IconPlus,
IconTrash
} from '@appwrite.io/pink-icons-svelte';
import FileTokensCopyUrl from './fileTokensCopyUrl.svelte';
import ManageFileTokenModal, { cleanFormattedDate } from './manageFileToken.svelte';
import { type Models, Permission, Role } from '@appwrite.io/console';
import { isSmallViewport } from '$lib/stores/viewport';
import { Menu } from '$lib/components/menu';
let showFileAlert = true;
onMount(async () => {
filePermissions = $file?.$permissions;
});
let showDelete = false;
let filePermissions = $file?.$permissions;
let arePermsDisabled = true;
let filePermissions = $file?.$permissions;
let showManageToken = false;
let tokenDeleteMode = false;
let showCopyUrlModal = false;
let tokenPermissionsMode = false;
let selectedFileToken: Models.ResourceToken | null = null;
const getPreview = (fileId: string) =>
sdk.forProject.storage.getFilePreview($file.bucketId, fileId, 640, 300).toString() +
'&mode=admin';
const getView = (fileId: string) =>
sdk.forProject.storage.getFileView($file.bucketId, fileId).toString() + '&mode=admin';
$: if (filePermissions) {
if (symmetricDifference(filePermissions, $file.$permissions).length) {
arePermsDisabled = false;
} else arePermsDisabled = true;
arePermsDisabled = !symmetricDifference(filePermissions, $file.$permissions).length;
}
function downloadFile() {
@@ -66,8 +92,98 @@
type: 'error'
});
trackError(error, Submit.FileUpdatePermissions);
} finally {
selectedFileToken = null;
}
}
async function deleteFileToken(token: Models.ResourceToken) {
try {
await sdk.forProject.tokens.delete(token.$id);
await invalidate(Dependencies.FILE_TOKENS);
addNotification({
message: 'File token deleted',
type: 'success'
});
trackEvent(Submit.FileTokenDelete);
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
trackError(error, Submit.FileTokenDelete);
} finally {
selectedFileToken = null;
}
}
async function createFileToken(expiry: string) {
try {
await sdk.forProject.tokens.createFileToken($file.bucketId, $file.$id, expiry, [
Permission.read(Role.any())
]);
await invalidate(Dependencies.FILE_TOKENS);
addNotification({
message: 'File token created',
type: 'success'
});
trackEvent(Submit.FileTokenCreate);
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
trackError(error, Submit.FileTokenCreate);
} finally {
selectedFileToken = null;
}
}
async function updateFileToken(fileToken: Models.ResourceToken) {
try {
await sdk.forProject.tokens.update(
selectedFileToken.$id,
fileToken.expire ? fileToken.expire : null,
fileToken.$permissions
);
await invalidate(Dependencies.FILE_TOKENS);
addNotification({
message: 'File token updated',
type: 'success'
});
trackEvent(Submit.FileTokenUpdate);
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
trackError(error, Submit.FileTokenUpdate);
} finally {
selectedFileToken = null;
}
}
function getPermissionGroups(permissions: string[]): string {
const map = {
read: 'Read',
update: 'Update',
delete: 'Delete'
};
const groups = permissions.reduce((set, perm) => {
const match = perm.match(/^(\w+)\(/);
const key = match?.[1];
if (key && map[key]) set.add(map[key]);
return set;
}, new Set<string>());
return [...groups].join(', ');
}
onMount(async () => {
filePermissions = $file?.$permissions;
});
</script>
<Container>
@@ -116,6 +232,120 @@
</svelte:fragment>
</CardGrid>
<div class="tokens-section">
<CardGrid hideOverflow>
<svelte:fragment slot="title">File tokens</svelte:fragment>
Use tokens to provide public access to the file.
<svelte:fragment slot="aside">
{#if $tokens.total}
<Layout.Stack
justifyContent="flex-end"
direction={$isSmallViewport ? 'column' : 'row'}>
<Button secondary on:click={() => (showManageToken = true)}>
<Icon size="s" icon={IconPlus} />
<span class="text">Create token</span>
</Button>
</Layout.Stack>
<Table.Root columns={$columns} let:root>
<svelte:fragment slot="header" let:root>
{#each $columns as { id, title } (id)}
<Table.Header.Cell column={id} {root}
>{title}</Table.Header.Cell>
{/each}
</svelte:fragment>
{#each $tokens.tokens as token}
<Table.Row.Base {root}>
<Table.Cell column="created" {root}
>{toLocaleDate(token.$createdAt)}</Table.Cell>
<Table.Cell column="expiry" {root}
>{token.expire
? cleanFormattedDate(token.expire)
: 'never'}</Table.Cell>
<Table.Cell column="last_accessed" {root}
>{token.accessedAt
? cleanFormattedDate(token.accessedAt, true)
: 'never'}</Table.Cell>
<Table.Cell column="permissions" {root}>
{#if token.$permissions.length}
<Typography.Text truncate slot="tooltip">
{getPermissionGroups(token.$permissions)}
</Typography.Text>
{:else}
none
{/if}
</Table.Cell>
<Table.Cell column="actions" {root}>
<Layout.Stack alignItems="flex-end">
<Menu>
<PinkButton.Button icon variant="ghost">
<Icon size="s" icon={IconDotsHorizontal} />
</PinkButton.Button>
<svelte:fragment slot="menu" let:toggle>
<ActionMenu.Root>
<ActionMenu.Root noPadding>
<ActionMenu.Item.Button
on:click={(e) => {
toggle(e);
showCopyUrlModal = true;
selectedFileToken = token;
}}
leadingIcon={IconDuplicate}>
Copy URL
</ActionMenu.Item.Button>
</ActionMenu.Root>
<ActionMenu.Item.Button
leadingIcon={IconPencil}
on:click={(e) => {
toggle(e);
showManageToken = true;
selectedFileToken = token;
}}>
Edit expiry
</ActionMenu.Item.Button>
<ActionMenu.Item.Button
leadingIcon={IconKey}
on:click={(e) => {
toggle(e);
showManageToken = true;
tokenPermissionsMode = true;
selectedFileToken = token;
}}>
Edit Permissions
</ActionMenu.Item.Button>
<ActionMenu.Item.Button
status="danger"
leadingIcon={IconTrash}
on:click={async (e) => {
toggle(e);
tokenDeleteMode = true;
showManageToken = true;
selectedFileToken = token;
}}>
Delete
</ActionMenu.Item.Button>
</ActionMenu.Root>
</svelte:fragment>
</Menu>
</Layout.Stack>
</Table.Cell>
</Table.Row.Base>
{/each}
</Table.Root>
{:else}
<Empty on:click={() => (showManageToken = true)}
>Create new file token</Empty>
{/if}
</svelte:fragment>
</CardGrid>
</div>
<CardGrid>
<svelte:fragment slot="title">Permissions</svelte:fragment>
Assign read or write permissions at the bucket level or file level. If bucket level permissions
@@ -135,8 +365,8 @@
{:else}
<Alert.Inline status="info" title="File security is disabled">
<Typography.Text>
If you want to assign document permissions. Go to Bucket settings and
enable file security. Otherwise, only Bucket permissions will be used.
If you want to assign file permissions. Go to Bucket settings and enable
file security. Otherwise, only Bucket permissions will be used.
</Typography.Text>
</Alert.Inline>
{/if}
@@ -175,3 +405,20 @@
</Container>
<Delete bind:showDelete />
<ManageFileTokenModal
bind:show={showManageToken}
bind:isDelete={tokenDeleteMode}
bind:fileToken={selectedFileToken}
bind:isUpdatePermissions={tokenPermissionsMode}
on:created={({ detail: expiry }) => createFileToken(expiry)}
on:updated={({ detail: token }) => updateFileToken(token)}
on:deleted={async () => await deleteFileToken(selectedFileToken)} />
<FileTokensCopyUrl file={$file} bind:token={selectedFileToken} bind:show={showCopyUrlModal} />
<style>
:global(.tokens-section div) {
overflow: auto;
}
</style>
@@ -13,12 +13,13 @@
let error: string;
const deleteFile = async () => {
try {
const fileName = $file.name;
await sdk.forProject.storage.deleteFile($file.bucketId, $file.$id);
showDelete = false;
await goto(`${base}/project-${page.params.project}/storage/bucket-${$file.bucketId}`);
addNotification({
type: 'success',
message: `${$file.name} has been deleted`
message: `${fileName} has been deleted`
});
trackEvent(Submit.FileDelete);
} catch (error) {
@@ -0,0 +1,141 @@
<script lang="ts">
import { cachedJwts } from './store';
import { sdk } from '$lib/stores/sdk';
import type { Models } from '@appwrite.io/console';
import { Modal, Card, CopyInput } from '$lib/components';
import { Button, InputSelect, InputText } from '$lib/elements/forms';
import {
Alert,
Code,
Icon,
Image,
Input,
Layout,
Tabs,
Tooltip,
Typography
} from '@appwrite.io/pink-svelte';
export let show = false;
export let file: Models.File;
export let token: Models.ResourceToken;
let error = null;
let codeSnippets = {};
let selectedTab = 'Preview';
async function getJwtForToken(): Promise<string> {
// Return cached, if it's valid based on the token's expiration.
if ($cachedJwts.has(token.$id)) {
const cachedJwt = $cachedJwts.get(token.$id);
const isExpired = !!token.expire && new Date(token.expire) < new Date();
if (!isExpired) return cachedJwt;
}
const { jwt } = await sdk.forProject.tokens.getJWT(token.$id);
// update the cached object!
cachedJwts.update((map) => {
map.set(token.$id, jwt);
return map;
});
return jwt;
}
function copyPreviewWithToken(
token: string,
method: 'preview' | 'view' | 'download' = 'preview'
) {
error = false;
try {
let url: string;
if (method === 'view') {
url = sdk.forProject.storage.getFileView(file.bucketId, file.$id);
} else if (method === 'download') {
url = sdk.forProject.storage.getFileDownload(file.bucketId, file.$id);
} else {
url = sdk.forProject.storage.getFilePreview(file.bucketId, file.$id);
}
url = `${url}&token=${token}`;
return url;
} catch (ignore) {
error = true;
}
}
async function setCodeSnippets() {
const jwt = await getJwtForToken();
return {
Preview: `${copyPreviewWithToken(jwt)}`,
View: `${copyPreviewWithToken(jwt, 'view')}`,
Download: `${copyPreviewWithToken(jwt, 'download')}`
};
}
function getMoreInfo(): string {
let message = '';
switch (selectedTab.toLowerCase()) {
case 'preview':
message = 'Apply transformations or filters. Good for thumbnails or previews.';
break;
case 'view':
message =
'Open the file in the browser (if supported). Works well for images and documents.';
break;
case 'download':
message = 'Download the file directly. Use when users need a local copy.';
break;
default:
message = '';
}
return message;
}
$: if (show) {
setCodeSnippets().then((snippets) => (codeSnippets = snippets));
}
</script>
<Modal title="Copy File URL" bind:show hideFooter>
<span slot="description"> Use the token-based URL below to access this file securely. </span>
{#if error}
<Alert.Inline title="Error" status="error">
Something went wrong, could not generate the URL.
</Alert.Inline>
{:else}
{@const tokenUrl = codeSnippets[selectedTab]}
<Layout.Stack gap="xl">
<Tabs.Root let:root stretch>
{#each ['Preview', 'View', 'Download'] as cat}
<Tabs.Item.Button
{root}
on:click={() => (selectedTab = cat)}
active={selectedTab === cat}>
{cat}
</Tabs.Item.Button>
{/each}
</Tabs.Root>
<Typography.Text variant="m-400">
{getMoreInfo()}
</Typography.Text>
{#if !token.expire}
<Alert.Inline title="No expiration date" status="warning">
This token doesn't expire. Be cautious when sharing links.
</Alert.Inline>
{/if}
<CopyInput value={tokenUrl} />
</Layout.Stack>
{/if}
<svelte:fragment slot="footer">
<Button secondary on:click={() => (show = false)}>Close</Button>
</svelte:fragment>
</Modal>
@@ -0,0 +1,101 @@
<script context="module" lang="ts">
import { toLocaleDate, toLocaleDateTime } from '$lib/helpers/date';
export function cleanFormattedDate(date: string, time: boolean = false): string {
return time
? toLocaleDateTime(date, true)
: toLocaleDate(date, 'dd mm yyyy').replaceAll(', ', ' ');
}
</script>
<script lang="ts">
import { Button } from '$lib/elements/forms';
import { createEventDispatcher } from 'svelte';
import { Link } from '@appwrite.io/pink-svelte';
import { type Models } from '@appwrite.io/console';
import { Permissions } from '$lib/components/permissions';
import { Confirm, ExpirationInput, Modal } from '$lib/components';
export let show = false;
export let isDelete = false;
export let isUpdatePermissions = false;
export let fileToken: Models.ResourceToken | null = null;
let expire = null;
const dispatch = createEventDispatcher();
function close() {
show = false;
expire = null;
isDelete = false;
fileToken = null;
}
function handleFileToken() {
if (isDelete) {
dispatch('deleted');
} else {
dispatch(
fileToken ? 'updated' : 'created',
fileToken ? { ...fileToken, expire } : expire
);
}
close();
}
$: if (!show) {
close();
}
</script>
{#if isDelete}
<Confirm bind:open={show} title="Delete token" onSubmit={handleFileToken}>
{@const formattedDate = cleanFormattedDate(fileToken.$createdAt)}
<p data-private>
Are you sure you want to delete the file token created on <b>{formattedDate}</b>?
</p>
<p data-private>
{#if fileToken.accessedAt}
{@const formattedDate = cleanFormattedDate(fileToken.accessedAt, true)}
This token was last accessed on <b>{formattedDate}</b>
{:else}
This token has never been accessed.
{/if}
</p>
</Confirm>
{:else}
<Modal
bind:show
size="s"
onSubmit={handleFileToken}
title={`${fileToken ? 'Edit' : 'Create'} file token`}>
<!-- TODO: docs link needed-->
<svelte:fragment slot="description">
{#if fileToken}
{@const formattedDate = cleanFormattedDate(fileToken.$createdAt)}
Edit the expiry of the file token created on <b>{formattedDate}</b>
{:else}
Create a file token to grant access to a file.
<Link.Anchor href="https://appwrite.com/docs/">Learn more</Link.Anchor>.
{/if}
</svelte:fragment>
{#if isUpdatePermissions}
<Permissions bind:permissions={fileToken.$permissions} hideOnClick />
{:else}
<ExpirationInput
bind:value={expire}
resourceType="token"
selectorLabel="Expiry"
dateSelectorLabel="Expiry date" />
{/if}
<svelte:fragment slot="footer">
<Button secondary on:click={close}>Cancel</Button>
<Button submit>{fileToken ? 'Update' : 'Create'}</Button>
</svelte:fragment>
</Modal>
{/if}
@@ -1,5 +1,21 @@
import { derived } from 'svelte/store';
import { derived, writable } from 'svelte/store';
import { page } from '$app/stores';
import type { Models } from '@appwrite.io/console';
import type { Column } from '$lib/helpers/types';
export const file = derived(page, ($page) => $page.data.file as Models.File);
export const tokens = derived(page, ($page) => $page.data.tokens as Models.ResourceTokenList);
/**
* Caches JWTs in memory by `token.$id` to avoid regenerating them on each URL copy.
*/
export const cachedJwts = writable<Map<string, string>>(new Map<string, string>());
export const columns = writable<Column[]>([
{ id: 'created', title: 'Created', type: 'datetime', width: 200 },
{ id: 'expiry', title: 'Expiration', type: 'datetime', width: 200 },
{ id: 'last_accessed', title: 'Last accessed', type: 'datetime', width: 200 },
{ id: 'permissions', title: 'Permissions', type: 'string', width: 200 },
{ id: 'actions', title: '', type: 'string', width: 60 }
]);
@@ -113,15 +113,12 @@
let isExtensionsDisabled = true;
$: if (permissions) {
if (symmetricDifference(permissions, data.bucket.$permissions).length) {
$arePermsDisabled = false;
} else $arePermsDisabled = true;
$arePermsDisabled = !symmetricDifference(permissions, data.bucket.$permissions).length;
}
$: if (extensions) {
if (JSON.stringify(extensions) !== JSON.stringify(data.bucket.allowedFileExtensions)) {
isExtensionsDisabled = false;
} else isExtensionsDisabled = true;
isExtensionsDisabled =
JSON.stringify(extensions) === JSON.stringify(data.bucket.allowedFileExtensions);
}
function toggleBucket() {
@@ -121,14 +121,15 @@
</svelte:head>
<Modal
title={state === 'authenticated' ? 'Request access' : 'Sign up or log in to view this preview'}
title={state === 'authenticated' ? 'Access required' : 'Sign up or log in to view this preview'}
dismissible={false}
show
hideFooter={state !== 'authenticated'}
hideFooter
{error}>
<span slot="description">
{#if state === 'authenticated'}
Request access to view this preview. You'll gain access once your request is approved.
Access is required to view this preview. You'll be able to see it once an admin invites
you to the organization.
{/if}
</span>
<Form onSubmit={handleSubmit}>
@@ -225,21 +226,23 @@
{data.account.email}
</Typography.Text>
</Layout.Stack>
<Link
variant="muted"
on:click={() => {
logout(false);
state = 'login';
}}>Switch account</Link>
<div>
<Link
variant="muted"
on:click={() => {
logout(false);
state = 'login';
}}>Switch account</Link>
</div>
</Layout.Stack>
</Card>
{/if}
</Layout.Stack>
</Form>
<svelte:fragment slot="footer">
<!-- <svelte:fragment slot="footer">
<Button on:click={requestAccess}>Request access</Button>
</svelte:fragment>
</svelte:fragment> -->
</Modal>
<style lang="scss">