mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
Merge branch 'mgn-dv' of https://github.com/appwrite/console into mgn-dv
This commit is contained in:
+3
-3
@@ -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",
|
||||
|
||||
Generated
+14
-14
@@ -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)
|
||||
|
||||
@@ -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',
|
||||
|
||||
+10
-8
@@ -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}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,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';
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 = '';
|
||||
}
|
||||
|
||||
@@ -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 -->
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
+13
-11
@@ -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} />
|
||||
|
||||
+11
-9
@@ -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>
|
||||
|
||||
+1
-1
@@ -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}>
|
||||
|
||||
+18
-6
@@ -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>
|
||||
|
||||
+1
-1
@@ -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)}
|
||||
|
||||
+3
-1
@@ -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} />
|
||||
|
||||
+46
-30
@@ -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}
|
||||
|
||||
+19
-31
@@ -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>
|
||||
|
||||
+1
-1
@@ -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>
|
||||
|
||||
+5
-4
@@ -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}
|
||||
|
||||
+4
-3
@@ -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>
|
||||
|
||||
+38
-8
@@ -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"
|
||||
|
||||
+14
-122
@@ -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>
|
||||
|
||||
+1
-1
@@ -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">
|
||||
|
||||
+75
-11
@@ -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>
|
||||
|
||||
+1
-1
@@ -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;
|
||||
|
||||
-45
@@ -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'
|
||||
|
||||
+1
-1
@@ -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">
|
||||
|
||||
+16
-5
@@ -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} />
|
||||
|
||||
+1
-1
@@ -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>
|
||||
|
||||
+3
-1
@@ -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()}
|
||||
|
||||
+20
-4
@@ -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"
|
||||
|
||||
+2
-1
@@ -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) {
|
||||
|
||||
+259
-12
@@ -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>
|
||||
|
||||
+2
-1
@@ -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) {
|
||||
|
||||
+141
@@ -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>
|
||||
+101
@@ -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}
|
||||
+17
-1
@@ -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 }
|
||||
]);
|
||||
|
||||
+3
-6
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user