Merge branch 'main' into fix-usage-layout-shifts

This commit is contained in:
Darshan
2025-10-05 19:14:39 +05:30
106 changed files with 3435 additions and 464 deletions
+3 -1
View File
@@ -3,4 +3,6 @@ PUBLIC_CONSOLE_FEATURE_FLAGS=
PUBLIC_APPWRITE_MULTI_REGION=false
PUBLIC_APPWRITE_ENDPOINT=http://localhost/v1
PUBLIC_STRIPE_KEY=
PUBLIC_GROWTH_ENDPOINT=
PUBLIC_GROWTH_ENDPOINT=
PUBLIC_CONSOLE_EMAIL_VERIFICATION=false
PUBLIC_CONSOLE_MOCK_AI_SUGGESTIONS=true
+8
View File
@@ -41,6 +41,8 @@ jobs:
"PUBLIC_CONSOLE_MODE=cloud"
"PUBLIC_CONSOLE_FEATURE_FLAGS="
"PUBLIC_APPWRITE_MULTI_REGION=true"
"PUBLIC_CONSOLE_EMAIL_VERIFICATION=true"
"PUBLIC_CONSOLE_MOCK_AI_SUGGESTIONS=false"
"PUBLIC_GROWTH_ENDPOINT=${{ secrets.PUBLIC_GROWTH_ENDPOINT }}"
"PUBLIC_STRIPE_KEY=${{ secrets.PUBLIC_STRIPE_KEY }}"
"SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}"
@@ -81,6 +83,8 @@ jobs:
"PUBLIC_CONSOLE_MODE=cloud"
"PUBLIC_CONSOLE_FEATURE_FLAGS="
"PUBLIC_APPWRITE_MULTI_REGION=true"
"PUBLIC_CONSOLE_EMAIL_VERIFICATION=false"
"PUBLIC_CONSOLE_MOCK_AI_SUGGESTIONS=false"
"PUBLIC_GROWTH_ENDPOINT=${{ secrets.PUBLIC_GROWTH_ENDPOINT }}"
"PUBLIC_STRIPE_KEY=${{ secrets.PUBLIC_STRIPE_KEY_STAGE }}"
publish-self-hosted:
@@ -118,6 +122,8 @@ jobs:
build-args: |
"PUBLIC_CONSOLE_MODE=self-hosted"
"PUBLIC_APPWRITE_MULTI_REGION=false"
"PUBLIC_CONSOLE_EMAIL_VERIFICATION=false"
"PUBLIC_CONSOLE_MOCK_AI_SUGGESTIONS=true"
"PUBLIC_CONSOLE_FEATURE_FLAGS="
"PUBLIC_GROWTH_ENDPOINT=${{ secrets.PUBLIC_GROWTH_ENDPOINT }}"
@@ -156,6 +162,8 @@ jobs:
build-args: |
"PUBLIC_CONSOLE_MODE=cloud"
"PUBLIC_APPWRITE_MULTI_REGION=false"
"PUBLIC_CONSOLE_EMAIL_VERIFICATION=true"
"PUBLIC_CONSOLE_MOCK_AI_SUGGESTIONS=false"
"PUBLIC_CONSOLE_FEATURE_FLAGS="
"PUBLIC_STRIPE_KEY=${{ secrets.PUBLIC_STRIPE_KEY_STAGE }}"
"PUBLIC_GROWTH_ENDPOINT=${{ secrets.PUBLIC_GROWTH_ENDPOINT }}"
+4
View File
@@ -23,6 +23,8 @@ ADD ./static /app/static
ARG PUBLIC_CONSOLE_MODE
ARG PUBLIC_CONSOLE_FEATURE_FLAGS
ARG PUBLIC_APPWRITE_MULTI_REGION
ARG PUBLIC_CONSOLE_EMAIL_VERIFICATION
ARG PUBLIC_CONSOLE_MOCK_AI_SUGGESTIONS
ARG PUBLIC_APPWRITE_ENDPOINT
ARG PUBLIC_GROWTH_ENDPOINT
ARG PUBLIC_STRIPE_KEY
@@ -34,6 +36,8 @@ ENV PUBLIC_GROWTH_ENDPOINT=$PUBLIC_GROWTH_ENDPOINT
ENV PUBLIC_CONSOLE_MODE=$PUBLIC_CONSOLE_MODE
ENV PUBLIC_CONSOLE_FEATURE_FLAGS=$PUBLIC_CONSOLE_FEATURE_FLAGS
ENV PUBLIC_APPWRITE_MULTI_REGION=$PUBLIC_APPWRITE_MULTI_REGION
ENV PUBLIC_CONSOLE_EMAIL_VERIFICATION=$PUBLIC_CONSOLE_EMAIL_VERIFICATION
ENV PUBLIC_CONSOLE_MOCK_AI_SUGGESTIONS=$PUBLIC_CONSOLE_MOCK_AI_SUGGESTIONS
ENV PUBLIC_STRIPE_KEY=$PUBLIC_STRIPE_KEY
ENV SENTRY_AUTH_TOKEN=$SENTRY_AUTH_TOKEN
ENV SENTRY_RELEASE=$SENTRY_RELEASE
+2
View File
@@ -28,6 +28,8 @@ async function main() {
logEnv('MULTI REGION', env?.PUBLIC_APPWRITE_MULTI_REGION);
logEnv('APPWRITE ENDPOINT', env?.PUBLIC_APPWRITE_ENDPOINT, 'relative');
logEnv('GROWTH ENDPOINT', env?.PUBLIC_GROWTH_ENDPOINT);
logEnv('CONSOLE EMAIL VERIFICATION', env?.PUBLIC_CONSOLE_EMAIL_VERIFICATION);
logEnv('CONSOLE MOCK AI SUGGESTIONS', env?.PUBLIC_CONSOLE_MOCK_AI_SUGGESTIONS);
log();
logDelimiter();
await build();
+3 -3
View File
@@ -22,11 +22,11 @@
},
"dependencies": {
"@ai-sdk/svelte": "^1.1.24",
"@appwrite.io/console": "https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@7747562",
"@appwrite.io/console": "https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@636ed39",
"@appwrite.io/pink-icons": "0.25.0",
"@appwrite.io/pink-icons-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@f2198f1",
"@appwrite.io/pink-icons-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@8f82877",
"@appwrite.io/pink-legacy": "^1.0.3",
"@appwrite.io/pink-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@f2198f1",
"@appwrite.io/pink-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@8f82877",
"@faker-js/faker": "^9.9.0",
"@popperjs/core": "^2.11.8",
"@sentry/sveltekit": "^8.38.0",
+15 -15
View File
@@ -12,20 +12,20 @@ importers:
specifier: ^1.1.24
version: 1.1.24(svelte@5.25.3)(zod@3.24.3)
'@appwrite.io/console':
specifier: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@7747562
version: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@7747562
specifier: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@636ed39
version: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@636ed39
'@appwrite.io/pink-icons':
specifier: 0.25.0
version: 0.25.0
'@appwrite.io/pink-icons-svelte':
specifier: https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@f2198f1
version: https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@f2198f1(svelte@5.25.3)
specifier: https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@8f82877
version: https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@8f82877(svelte@5.25.3)
'@appwrite.io/pink-legacy':
specifier: ^1.0.3
version: 1.0.3
'@appwrite.io/pink-svelte':
specifier: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@f2198f1
version: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@f2198f1(svelte@5.25.3)
specifier: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@8f82877
version: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@8f82877(svelte@5.25.3)
'@faker-js/faker':
specifier: ^9.9.0
version: 9.9.0
@@ -260,8 +260,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-labs/cloud/@appwrite.io/console@7747562':
resolution: {tarball: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@7747562}
'@appwrite.io/console@https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@636ed39':
resolution: {tarball: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@636ed39}
version: 1.10.0
'@appwrite.io/pink-icons-svelte@2.0.0-RC.1':
@@ -269,8 +269,8 @@ packages:
peerDependencies:
svelte: ^4.0.0
'@appwrite.io/pink-icons-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@f2198f1':
resolution: {tarball: https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@f2198f1}
'@appwrite.io/pink-icons-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@8f82877':
resolution: {tarball: https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@8f82877}
version: 2.0.0-RC.1
peerDependencies:
svelte: ^4.0.0
@@ -284,8 +284,8 @@ packages:
'@appwrite.io/pink-legacy@1.0.3':
resolution: {integrity: sha512-GGde5fmPhs+s6/3aFeMPc/kKADG/gTFkYQSy6oBN8pK0y0XNCLrZZgBv+EBbdhwdtqVEWXa0X85Mv9w7jcIlwQ==}
'@appwrite.io/pink-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@f2198f1':
resolution: {tarball: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@f2198f1}
'@appwrite.io/pink-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@8f82877':
resolution: {tarball: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@8f82877}
version: 2.0.0-RC.2
peerDependencies:
svelte: ^4.0.0
@@ -3703,13 +3703,13 @@ snapshots:
'@analytics/type-utils@0.6.2': {}
'@appwrite.io/console@https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@7747562': {}
'@appwrite.io/console@https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@636ed39': {}
'@appwrite.io/pink-icons-svelte@2.0.0-RC.1(svelte@5.25.3)':
dependencies:
svelte: 5.25.3
'@appwrite.io/pink-icons-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@f2198f1(svelte@5.25.3)':
'@appwrite.io/pink-icons-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@8f82877(svelte@5.25.3)':
dependencies:
svelte: 5.25.3
@@ -3722,7 +3722,7 @@ snapshots:
'@appwrite.io/pink-icons': 1.0.0
the-new-css-reset: 1.11.3
'@appwrite.io/pink-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@f2198f1(svelte@5.25.3)':
'@appwrite.io/pink-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@8f82877(svelte@5.25.3)':
dependencies:
'@appwrite.io/pink-icons-svelte': 2.0.0-RC.1(svelte@5.25.3)
'@floating-ui/dom': 1.6.13
+3
View File
@@ -274,9 +274,12 @@ export enum Submit {
DatabaseDelete = 'submit_database_delete',
DatabaseUpdateName = 'submit_database_update_name',
DatabaseImportCsv = 'submit_database_import_csv',
ColumnCreate = 'submit_column_create',
ColumnUpdate = 'submit_column_update',
ColumnDelete = 'submit_column_delete',
ColumnSuggestions = 'submit_column_suggestions',
RowCreate = 'submit_row_create',
RowDelete = 'submit_row_delete',
RowUpdate = 'submit_row_update',
+7 -4
View File
@@ -21,13 +21,16 @@ export const timer: Action<HTMLElement, Props> = (node, props) => {
}
function step() {
const diffInSeconds = Math.floor((new Date().getTime() - startDate.getTime()) / 1000);
const minutes = Math.floor(diffInSeconds / 60);
const elapsedSeconds = Math.max(
0,
Math.floor((new Date().getTime() - startDate.getTime()) / 1000)
);
const minutes = Math.floor(elapsedSeconds / 60);
if (minutes > 0) {
const seconds = diffInSeconds % 60;
const seconds = elapsedSeconds % 60;
node.textContent = `${minutes}m ${seconds}s`;
} else {
node.textContent = calculateTime(diffInSeconds);
node.textContent = calculateTime(elapsedSeconds);
}
frame = window.requestAnimationFrame(step);
}
@@ -4,16 +4,21 @@
import { Typography } from '@appwrite.io/pink-svelte';
import { user } from '$lib/stores/user';
import SendVerificationEmailModal from '../account/sendVerificationEmailModal.svelte';
import { page } from '$app/stores';
import { page } from '$app/state';
import { wizard, isNewWizardStatusOpen } from '$lib/stores/wizard';
import { isCloud } from '$lib/system';
import { isCloud, VARS } from '$lib/system';
const hasUser = $derived(!!$user);
const needsEmailVerification = $derived(hasUser && !$user.emailVerification);
const notOnOnboarding = $derived(!$page.route.id.includes('/onboarding'));
const notOnOnboarding = $derived(!page.route.id.includes('/onboarding'));
const notOnWizard = $derived(!$wizard.show && !$isNewWizardStatusOpen);
const shouldShowEmailBanner = $derived(
isCloud && hasUser && needsEmailVerification && notOnOnboarding && notOnWizard
VARS.EMAIL_VERIFICATION &&
isCloud &&
hasUser &&
needsEmailVerification &&
notOnOnboarding &&
notOnWizard
);
let showSendVerification = $state(false);
@@ -1,7 +1,7 @@
<script lang="ts">
import { BillingPlan } from '$lib/constants';
import { formatNum } from '$lib/helpers/string';
import { plansInfo, tierFree, tierPro, tierScale, type Tier } from '$lib/stores/billing';
import { plansInfo, tierToPlan, type Tier } from '$lib/stores/billing';
import { Card, Layout, Tabs, Typography } from '@appwrite.io/pink-svelte';
export let downgrade = false;
@@ -9,29 +9,22 @@
let selectedTab: Tier = BillingPlan.FREE;
$: plan = $plansInfo.get(selectedTab);
const allTiers: Tier[] = [BillingPlan.FREE, BillingPlan.PRO, BillingPlan.SCALE];
$: visibleTiers = allTiers.filter((tier) => tier !== BillingPlan.SCALE);
</script>
<Card.Base>
<Layout.Stack>
<Tabs.Root stretch let:root>
<Tabs.Item.Button
{root}
active={selectedTab === BillingPlan.FREE}
on:click={() => (selectedTab = BillingPlan.FREE)}>
{tierFree.name}
</Tabs.Item.Button>
<Tabs.Item.Button
{root}
active={selectedTab === BillingPlan.PRO}
on:click={() => (selectedTab = BillingPlan.PRO)}>
{tierPro.name}
</Tabs.Item.Button>
<Tabs.Item.Button
{root}
active={selectedTab === BillingPlan.SCALE}
on:click={() => (selectedTab = BillingPlan.SCALE)}>
{tierScale.name}
</Tabs.Item.Button>
{#each visibleTiers as tier}
<Tabs.Item.Button
{root}
active={selectedTab === tier}
on:click={() => (selectedTab = tier)}>
{tierToPlan(tier).name}
</Tabs.Item.Button>
{/each}
</Tabs.Root>
<Typography.Text variant="m-600">{plan.name} plan</Typography.Text>
+10 -1
View File
@@ -62,7 +62,7 @@
if (isSuccess) {
await invalidate(Dependencies.ROWS);
$spreadsheetRenderKey = hash(Date.now().toString());
spreadsheetRenderKey.set(hash(Date.now().toString()));
}
}
@@ -90,6 +90,15 @@
}
}
if (tableId && tableName === null) {
importItems.update((items) => {
const next = new Map(items);
next.delete(importData.$id);
return next;
});
return;
}
importItems.update((items) => {
const existing = items.get(importData.$id);
+3 -1
View File
@@ -27,10 +27,12 @@
id = null;
}
if (!id?.length) {
if (id !== null && !id.length) {
id = null;
}
});
$effect(() => {
if (show) {
trackEvent(Click.ShowCustomIdClick);
}
+13 -1
View File
@@ -47,8 +47,16 @@ export function addFilterAndApply(
addSizeFilter(value, colId, columns);
} else if (colId === 'statusCode') {
addStatusCodeFilter(value, colId, columns);
} else if (colId === '$createdAt' || colId === '$updatedAt' || colId === 'buildDuration') {
} else if (
colId === '$createdAt' ||
colId === '$updatedAt' ||
colId === 'buildDuration' ||
colId === 'scheduledAt' ||
colId === 'deliveredAt'
) {
addDateFilter(value, colId, columns);
} else if (colId === 'duration') {
addDurationFilter(value, colId, columns);
} else {
addFilter(columns, colId, operator, value, arrayValues);
}
@@ -75,3 +83,7 @@ export function addDateFilter(value: string, colId: string, columns: Column[]) {
export function addSizeFilter(value: string, colId: string, columns: Column[]) {
addFilter(columns, colId, ValidOperators.GreaterThanOrEqual, value);
}
export function addDurationFilter(value: string, colId: string, columns: Column[]) {
addFilter(columns, colId, ValidOperators.GreaterThan, value);
}
+10 -13
View File
@@ -20,7 +20,12 @@ export function setFilters(localTags: TagValue[], filterCols: FilterData[], $col
setSizeFilter(filter, $columns);
} else if (id?.includes('statuscode')) {
setStatusCodeFilter(filter, $columns);
} else if (id === '$createdat' || id === '$updatedat') {
} else if (
id === '$createdat' ||
id === '$updatedat' ||
id === 'scheduledat' ||
id === 'deliveredat'
) {
setDateFilter(filter, $columns);
} else {
setFilterData(filter);
@@ -61,20 +66,12 @@ export function setTimeFilter(filter: FilterData, columns: Column[]) {
const col = columns.find((c) => c.id === filter.id);
const timeTag = get(tags).find((tag) => tag.tag.includes(`**${filter.title}**`));
if (timeTag) {
const now = new Date();
const diff = now.getTime() - new Date(timeTag.value as string).getTime();
const ranges = col.elements as { value: string; label: string }[];
const dateRange = ranges.reduce((prev, curr) => {
if (parseInt(curr.value) < diff && curr.value > prev.value) {
return curr;
}
return prev;
});
if (dateRange) {
const timeRange = ranges.find((range) => range.value === timeTag.value);
if (timeRange) {
const newTag = {
tag: `**${filter.title}** is **${dateRange.label}**`,
value: timeTag.value
tag: `**${filter.title}** is **${timeRange.label}**`,
value: timeRange.value
};
cleanOldTags(filter?.title);
+62 -20
View File
@@ -1,41 +1,83 @@
<script context="module" lang="ts">
import { tick } from 'svelte';
import { debounce } from '$lib/helpers/debounce';
const batchQueue = new Set<() => void>();
let batchPromise: Promise<void> | null = null;
async function processBatch() {
try {
await tick();
batchQueue.forEach((fn) => {
try {
fn();
} catch {
/* empty */
}
});
} finally {
// clear queue!
batchQueue.clear();
batchPromise = null;
}
}
function addToBatch(fn: () => void) {
batchQueue.add(fn);
if (!batchPromise) {
batchPromise = processBatch();
}
}
export function truncateText(node: HTMLElement) {
const MAX_TRIES = 100;
let originalText = node.textContent;
function checkOverflow() {
node.textContent = originalText;
if (node.scrollWidth > node.clientWidth) {
let truncatedText = originalText;
let tries = 0;
while (
node.scrollWidth > node.clientWidth &&
truncatedText.length > 0 &&
tries < MAX_TRIES
) {
tries++;
if (truncatedText.includes('…')) {
const [left, right] = truncatedText.split('…');
truncatedText = `${left.slice(0, -1)}…${right.slice(1)}`;
let left = 0;
let right = originalText.length;
let bestFit = '…';
while (left <= right) {
// total chars to keep
const keep = (left + right) >> 1;
const head = Math.ceil(keep / 2);
const tail = keep - head;
const truncated =
keep === originalText.length
? originalText
: `${originalText.slice(0, head)}…${originalText.slice(-tail)}`;
node.textContent = truncated;
if (node.scrollWidth <= node.clientWidth) {
bestFit = truncated;
left = keep + 1;
} else {
const left = truncatedText.slice(0, truncatedText.length / 2);
const right = truncatedText.slice(truncatedText.length / 2);
truncatedText = `${left.slice(0, -1)}…${right.slice(1)}`;
right = keep - 1;
}
node.textContent = truncatedText;
}
node.textContent = bestFit;
}
}
requestAnimationFrame(checkOverflow);
window.addEventListener('resize', checkOverflow);
const debouncedCheck = debounce(checkOverflow, 16);
addToBatch(checkOverflow);
window.addEventListener('resize', debouncedCheck);
return {
update() {
originalText = node.textContent;
checkOverflow();
addToBatch(checkOverflow);
},
destroy() {
window.removeEventListener('resize', checkOverflow);
batchQueue.delete(checkOverflow);
window.removeEventListener('resize', debouncedCheck);
debouncedCheck.cancel();
}
};
}
+2 -2
View File
@@ -69,8 +69,8 @@
.separator {
height: 1px;
margin-block: 2px;
margin-inline-start: calc(var(--base-4) * -1);
width: calc(100% + var(--base-8));
width: 100%;
margin-inline: 0;
background-color: var(--border-neutral);
}
</style>
+2
View File
@@ -8,11 +8,13 @@ export const REGION_FRA = 'fra';
export const REGION_SYD = 'syd';
export const REGION_NYC = 'nyc';
export const REGION_SFO = 'sfo';
export const REGION_SGP = 'sgp';
export const SUBDOMAIN_FRA = 'fra.';
export const SUBDOMAIN_SYD = 'syd.';
export const SUBDOMAIN_NYC = 'nyc.';
export const SUBDOMAIN_SFO = 'sfo.';
export const SUBDOMAIN_SGP = 'sgp.';
export enum Dependencies {
FACTORS = 'dependency:factors',
+1 -1
View File
@@ -44,7 +44,7 @@ export const testimonials: Testimonial[] = [
{
id: 'phil-majikkids',
logo: Phil,
headline: 'Just like a Swiss Army Knife - choose and use the tools you need',
headline: 'With Appwrite, Majik Kids built an entire app with one developer.',
blurb: 'Just like a Swiss Army Knife you can choose and use the tools that you need with Appwrite.',
name: 'Phil McCluskey',
title: 'App Manager, Majik Kids',
+18 -17
View File
@@ -4,7 +4,7 @@
export let id: string;
export let label: string = '';
export let value: string;
export let value: string | null;
export let required = false;
export let nullable = false;
export let disabled = false;
@@ -17,6 +17,7 @@
let error: string;
let element: HTMLInputElement;
let previousValue: string | null = null;
onMount(() => {
if (element && autofocus) {
@@ -24,14 +25,16 @@
}
});
let prevValue = '';
function handleNullChange(e: CustomEvent<boolean>) {
const isNull = e.detail;
if (isNull) {
prevValue = value;
if (value !== null) {
previousValue = value;
}
value = null;
} else {
value = prevValue;
value = previousValue;
}
}
@@ -39,9 +42,7 @@
error = null;
}
function onChange(event: CustomEvent) {
value = (event.target as HTMLInputElement).value;
}
$: isValueNull = value === null;
</script>
<Layout.Stack gap="s" direction="row">
@@ -51,20 +52,20 @@
{disabled}
{readonly}
{required}
{value}
bind:value
{step}
{type}
helper={error}
{leadingIcon}
on:change={onChange}
autocomplete={autocomplete ? 'on' : 'off'}>
{#if nullable}
<Selector.Checkbox
size="s"
slot="end"
label="NULL"
checked={value === null}
on:change={handleNullChange} />
{/if}
<svelte:fragment slot="end">
{#if nullable}
<Selector.Checkbox
size="s"
label="NULL"
checked={isValueNull}
on:change={handleNullChange} />
{/if}
</svelte:fragment>
</Input.DateTime>
</Layout.Stack>
+1 -5
View File
@@ -12,11 +12,7 @@
const handleInvalid = (event: Event & { currentTarget: EventTarget & HTMLInputElement }) => {
event.preventDefault();
if (event.currentTarget.validity.patternMismatch) {
error = true;
return;
}
error = true; // show on any error
};
$: if (value) {
+1 -5
View File
@@ -12,11 +12,7 @@
const handleInvalid = (event: Event & { currentTarget: EventTarget & HTMLInputElement }) => {
event.preventDefault();
if (event.currentTarget.validity.patternMismatch) {
error = true;
return;
}
error = true; // show on any error
};
$: if (value) {
+10 -4
View File
@@ -198,15 +198,21 @@ function generateSingleValue(
case 'integer': {
const intAttr = column as Models.ColumnInteger;
const min = !isWithinSafeRange(intAttr.min) ? 0 : intAttr.min;
const max = !isWithinSafeRange(intAttr.max) ? 100 : intAttr.max;
const min = isWithinSafeRange(intAttr.min) ? intAttr.min : 0;
const fallbackMax = Math.max(min + 100, 100);
const max = isWithinSafeRange(intAttr.max)
? intAttr.max
: Math.min(fallbackMax, Number.MAX_SAFE_INTEGER);
return faker.number.int({ min, max });
}
case 'double': {
const floatAttr = column as Models.ColumnFloat;
const min = !isWithinSafeRange(floatAttr.min) ? 0 : floatAttr.min;
const max = !isWithinSafeRange(floatAttr.max) ? 100 : floatAttr.max;
const min = isWithinSafeRange(floatAttr.min) ? floatAttr.min : 0;
const fallbackMax = Math.max(min + 100, 100);
const max = isWithinSafeRange(floatAttr.max)
? floatAttr.max
: Math.min(fallbackMax, Number.MAX_SAFE_INTEGER);
const precision = 4;
return faker.number.float({ min, max, fractionDigits: precision });
Binary file not shown.

Before

Width:  |  Height:  |  Size: 451 KiB

After

Width:  |  Height:  |  Size: 449 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 279 KiB

After

Width:  |  Height:  |  Size: 266 KiB

+3 -3
View File
@@ -15,10 +15,10 @@
import { formatCurrency } from '$lib/helpers/numbers';
let {
projectName = $bindable(''),
id = $bindable(''),
projectName = $bindable(),
id = $bindable(),
regions = [],
region = $bindable(''),
region = $bindable(),
showTitle = true,
billingPlan = undefined,
projects = undefined,
+5
View File
@@ -181,6 +181,11 @@
&.hide {
display: none;
}
& :global(i) {
// because the `IconCloud` shows above floating action bars.
position: unset;
}
}
:global(main:has(.sub-navigation)) footer {
+10 -6
View File
@@ -1,22 +1,26 @@
<script lang="ts">
import { dismissNotification, notifications } from '../stores/notifications';
import { flip } from 'svelte/animate';
import { fly } from 'svelte/transition';
import { Layout, Toast } from '@appwrite.io/pink-svelte';
import { dismissNotification, notifications } from '../stores/notifications';
</script>
{#if $notifications}
<section>
<Layout.Stack gap="s">
{#each $notifications as notification (notification.id)}
<span animate:flip={{ duration: 500 }}>
<span animate:flip={{ duration: 500 }} transition:fly|global={{ x: 50 }}>
<Toast
isHtml={notification.isHtml}
title={notification.title}
status={notification.type}
icon={notification.icon}
description={notification.message}
actions={notification.buttons?.map((button) => {
return {
label: button.name,
onClick: button.method
onClick: button.method,
isHtml: button.isHtml
};
})}
on:dismiss={() => dismissNotification(notification.id)} />
@@ -28,16 +32,16 @@
<style lang="scss">
section {
right: 3.25px;
z-index: 1001;
position: fixed;
top: calc(var(--main-header-height) + 12px);
right: 12px;
z-index: 1001;
}
@media (min-width: 768px) {
section {
top: calc(var(--main-header-height) + 24px);
right: 24px;
top: calc(var(--main-header-height) + 24px);
}
}
</style>
+31 -10
View File
@@ -1,13 +1,14 @@
import { writable } from 'svelte/store';
import type { ComponentType } from 'svelte';
export type Notification = {
id: number;
type: 'success' | 'error' | 'info' | 'warning';
type?: 'success' | 'error' | 'info' | 'warning';
dismissible?: boolean;
timeout?: number;
message: string;
title?: string;
icon?: string;
icon?: ComponentType;
buttons?: Buttons[];
isHtml?: boolean;
};
@@ -15,32 +16,52 @@ export type Notification = {
export type Buttons = {
method: () => void | Promise<void>;
name: string;
isHtml?: boolean;
};
let counter = 0;
const activeTimeouts = new Map<number, NodeJS.Timeout>();
export const notifications = writable<Notification[]>([]);
export const dismissNotification = (id: number) => {
const timeoutId = activeTimeouts.get(id);
if (timeoutId) {
clearTimeout(timeoutId);
activeTimeouts.delete(id);
}
notifications.update((all) => all.filter((t) => t.id !== id));
};
export const dismissAllNotifications = () => {
activeTimeouts.forEach((timeoutId) => clearTimeout(timeoutId));
activeTimeouts.clear();
notifications.set([]);
};
export const addNotification = (notification: Omit<Notification, 'id'>) => {
const defaults = {
export const addNotification = (notification: Omit<Notification, 'id'>): number => {
const defaults: Notification = {
id: counter++,
type: 'info',
dismissible: true,
timeout: 6000
timeout: 6000,
...notification
};
const n = { ...defaults, ...notification };
notifications.update((all) => {
return [n, ...all.slice(0, 4)];
});
notifications.update((all) => [defaults, ...all.slice(0, 4)]);
if (n.timeout) setTimeout(() => dismissNotification(n.id), n.timeout);
if (defaults.timeout) {
const timeoutId = setTimeout(() => {
activeTimeouts.delete(defaults.id);
dismissNotification(defaults.id);
}, defaults.timeout);
activeTimeouts.set(defaults.id, timeoutId);
}
return defaults.id;
};
+13 -8
View File
@@ -66,17 +66,20 @@ function safePrefsKey(widthPreferences: TeamPreferences['widths'], from: string,
}
}
function safePrefsKeyForOrder(
orderPreferences: TeamPreferences['order'],
from: string,
to: string
) {
if (orderPreferences.includes(from)) {
const index = orderPreferences.indexOf(from);
function safePrefsKeyForOrder(order: TeamPreferences['order'], from: string, to: string) {
if (order.includes(from)) {
const index = order.indexOf(from);
if (index !== -1) {
orderPreferences[index] = to;
order[index] = to;
}
}
// move to first since its primary!
const toIndex = order.indexOf(to);
if (toIndex !== -1 && toIndex !== 0) {
order.splice(toIndex, 1);
order.unshift(to);
}
}
// rare cases where the value was an array, probably due to PHP backend.
@@ -221,6 +224,8 @@ function createPreferences() {
delete teamPreferences?.displayNames?.[tableId];
delete teamPreferences?.columnOrder?.[tableId];
delete teamPreferences?.columnWidths?.[tableId];
delete teamPreferences?.columnWidths?.[tableId + '#columns'];
delete teamPreferences?.columnWidths?.[tableId + '#indexes'];
const removeTablePreferences = sdk.forConsole.teams.updatePrefs({
teamId: orgId,
+8 -3
View File
@@ -31,10 +31,12 @@ import {
REGION_NYC,
REGION_SYD,
REGION_SFO,
REGION_SGP,
SUBDOMAIN_FRA,
SUBDOMAIN_NYC,
SUBDOMAIN_SFO,
SUBDOMAIN_SYD
SUBDOMAIN_SYD,
SUBDOMAIN_SGP
} from '$lib/constants';
import { building } from '$app/environment';
import { getProjectId } from '$lib/helpers/project';
@@ -63,6 +65,8 @@ const getSubdomain = (region?: string) => {
return SUBDOMAIN_NYC;
case REGION_SFO:
return SUBDOMAIN_SFO;
case REGION_SGP:
return SUBDOMAIN_SGP;
default:
return '';
}
@@ -99,8 +103,8 @@ const clientProject = new Client();
const clientRealtime = new Client();
if (!building) {
scopedConsoleClient.setProject('console');
clientConsole.setEndpoint(endpoint).setProject('console');
scopedConsoleClient.setMode(endpoint).setProject('console');
clientRealtime.setEndpoint(endpoint).setProject('console');
clientProject.setEndpoint(endpoint).setMode('admin');
@@ -126,7 +130,8 @@ const sdkForProject = {
proxy: new Proxy(clientProject),
migrations: new Migrations(clientProject),
sites: new Sites(clientProject),
tablesDB: new TablesDB(clientProject)
tablesDB: new TablesDB(clientProject),
console: new Console(clientProject) // for suggestions API
};
export const realtime = {
+3 -1
View File
@@ -10,7 +10,9 @@ export const VARS = {
CONSOLE_MODE: (env.PUBLIC_CONSOLE_MODE as Mode) ?? undefined,
APPWRITE_ENDPOINT: env.PUBLIC_APPWRITE_ENDPOINT ?? undefined,
GROWTH_ENDPOINT: env.PUBLIC_GROWTH_ENDPOINT ?? undefined,
PUBLIC_STRIPE_KEY: env.PUBLIC_STRIPE_KEY ?? undefined
PUBLIC_STRIPE_KEY: env.PUBLIC_STRIPE_KEY ?? undefined,
EMAIL_VERIFICATION: env.PUBLIC_CONSOLE_EMAIL_VERIFICATION === 'true',
MOCK_AI_SUGGESTIONS: (env.PUBLIC_CONSOLE_MOCK_AI_SUGGESTIONS ?? 'true') === 'true'
};
export const ENV = {
@@ -89,25 +89,34 @@
id="address"
label="Street address"
placeholder="Enter street address"
autocomplete
required />
<InputText
bind:value={address2}
id="address2"
label="Address line 2"
autocomplete
placeholder="Unit number, floor, etc." />
<InputText
bind:value={city}
id="city"
label="City or suburb"
placeholder="Enter your city"
autocomplete
required />
<InputText
bind:value={state}
id="state"
label="State"
placeholder="Enter your state"
autocomplete
required />
<InputText bind:value={zip} id="zip" label="Postal code" placeholder="Enter postal code" />
<InputText
bind:value={zip}
id="zip"
label="Postal code"
placeholder="Enter postal code"
autocomplete />
<svelte:fragment slot="footer">
<Button text on:click={() => (show = false)}>Cancel</Button>
@@ -15,10 +15,13 @@
import { user } from '$lib/stores/user';
let isLoading = false;
let id: string = ID.unique();
let startAnimation = false;
let projectName = 'Appwrite project';
let region = Region.Fra;
let projectId = ID.unique();
let projectRegion = Region.Fra;
let projectName = 'Appwrite Project';
const projectIdForLog = projectId;
export let data;
@@ -39,17 +42,17 @@
try {
const teamId = data.organization.$id;
const project = await sdk.forConsole.projects.create({
projectId: id ?? ID.unique(),
projectId: projectId ?? ID.unique(),
name: projectName,
teamId,
region: isCloud ? region : undefined
region: isCloud ? projectRegion : undefined
});
markOnboardingComplete();
trackEvent(Submit.ProjectCreate, {
customId: !!id,
teamId
teamId,
customId: projectId !== projectIdForLog
});
startAnimation = true;
@@ -95,11 +98,11 @@
alt="Appwrite Logo" />
<Card.Base variant="primary" padding="l">
<CreateProject
regions={$regionsStore?.regions}
showTitle
bind:projectName
bind:id
bind:region
showTitle={true}>
bind:id={projectId}
bind:region={projectRegion}
regions={$regionsStore?.regions}>
{#snippet submit()}
<Layout.Stack direction="row" justifyContent="flex-end">
<Button.Button
@@ -19,6 +19,8 @@
import { trackEvent, Click } from '$lib/actions/analytics';
import { type Models } from '@appwrite.io/console';
import { getServiceLimit, readOnly, upgradeURL } from '$lib/stores/billing';
import { BillingPlan } from '$lib/constants';
import { hideNotification, shouldShowNotification } from '$lib/helpers/notifications';
import { onMount, type ComponentType } from 'svelte';
import { canWriteProjects } from '$lib/stores/roles';
import { checkPricingRefAndRedirect } from '$lib/helpers/pricingRedirect';
@@ -45,6 +47,7 @@
let addOrganization = false;
let showSelectProject = false;
let showCreateProjectCloud = false;
let freePlanAlertDismissed = false;
let searchQuery: SearchQuery;
@@ -80,10 +83,13 @@
}
$: projectCreationDisabled =
(isCloud && getServiceLimit('projects') <= data.allProjectsCount) ||
(isCloud && getServiceLimit('projects') <= data.projects.total) ||
(isCloud && $readOnly && !GRACE_PERIOD_OVERRIDE) ||
!$canWriteProjects;
$: reachedProjectLimit = isCloud && getServiceLimit('projects') <= data.projects.total;
$: projectsLimit = getServiceLimit('projects');
$: $registerCommands([
{
label: 'Create project',
@@ -97,7 +103,23 @@
}
]);
onMount(async () => checkPricingRefAndRedirect(page.url.searchParams));
function dismissFreePlanAlert() {
freePlanAlertDismissed = true;
const notificationId = `freePlanAlert_${data.organization.$id}`;
hideNotification(notificationId, { coolOffPeriod: 24 });
trackEvent(Click.OrganizationClickUpgrade, {
from: 'button',
source: 'free_plan_info_alert_dismiss'
});
}
onMount(async () => {
checkPricingRefAndRedirect(page.url.searchParams);
const notificationId = `freePlanAlert_${data.organization.$id}`;
const shouldShow = shouldShowNotification(notificationId);
freePlanAlertDismissed = !shouldShow;
});
function findRegion(project: Models.Project) {
return $regionsStore.regions.find((region) => region.$id === project.region);
@@ -129,13 +151,27 @@
<SearchQuery bind:this={searchQuery} placeholder="Search by name or ID" />
{#if $canWriteProjects}
<Button
on:click={handleCreateProject}
event="create_project"
disabled={projectCreationDisabled}>
<Icon icon={IconPlus} slot="start" size="s" />
Create project
</Button>
{#if projectCreationDisabled && reachedProjectLimit}
<Tooltip placement="bottom">
<div>
<Button event="create_project" disabled>
<Icon icon={IconPlus} slot="start" size="s" />
Create project
</Button>
</div>
<span slot="tooltip">
You have reached your limit of {projectsLimit} projects.
</span>
</Tooltip>
{:else}
<Button
on:click={handleCreateProject}
event="create_project"
disabled={projectCreationDisabled}>
<Icon icon={IconPlus} slot="start" size="s" />
Create project
</Button>
{/if}
{/if}
</Layout.Stack>
@@ -162,6 +198,28 @@
</Alert.Inline>
{/if}
{#if isCloud && data.organization.billingPlan === BillingPlan.FREE && projectsToArchive.length === 0 && !freePlanAlertDismissed}
<Alert.Inline dismissible on:dismiss={dismissFreePlanAlert}>
<Typography.Text
>Your Free plan includes up to 2 projects and limited resources. Upgrade to unlock
more capacity and features.</Typography.Text>
<svelte:fragment slot="actions">
<Button
compact
size="s"
href={$upgradeURL}
on:click={() => {
trackEvent(Click.OrganizationClickUpgrade, {
from: 'button',
source: 'free_plan_info_alert'
});
}}>
Upgrade to Pro
</Button>
</svelte:fragment>
</Alert.Inline>
{/if}
{#if activeProjects.length > 0}
<CardContainer
disableEmpty={!$canWriteProjects}
@@ -3,7 +3,7 @@
import { onMount } from 'svelte';
import { CardGrid, PaginationInline } from '$lib/components';
import { Button } from '$lib/elements/forms';
import { toLocaleDate } from '$lib/helpers/date';
import DualTimeView from '$lib/components/dualTimeView.svelte';
import { formatCurrency } from '$lib/helpers/numbers';
import type { Invoice, InvoiceList } from '$lib/sdk/billing';
import { getApiEndpoint, sdk } from '$lib/stores/sdk';
@@ -112,8 +112,9 @@
{#each invoiceList?.invoices as invoice (invoice.$id)}
{@const status = invoice.status}
<Table.Row.Base {root}>
<Table.Cell column="dueDate" {root}
>{toLocaleDate(invoice.dueAt)}</Table.Cell>
<Table.Cell column="dueDate" {root}>
<DualTimeView time={invoice.dueAt} />
</Table.Cell>
<Table.Cell column="status" {root}>
{@const isDanger =
status === 'overdue' ||
@@ -3,7 +3,7 @@
import { onDestroy } from 'svelte';
import { goto, invalidate } from '$app/navigation';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { ID, type Models, Region as ConsoleRegion, Region } from '@appwrite.io/console';
import { ID, type Models, Region } from '@appwrite.io/console';
import { base } from '$app/paths';
import CreateProject from '$lib/layout/createProject.svelte';
import { Modal } from '$lib/components';
@@ -16,10 +16,12 @@
export let showCreateProjectCloud: boolean;
export let regions: Array<Models.ConsoleRegion> = [];
let id: string = null;
let error: string = null;
let name: string = 'New project';
let region: ConsoleRegion = Region.Fra;
let projectId = ID.unique();
let projectRegion = Region.Fra;
let projectName = 'New project';
let projectIdForLog = projectId;
let showSubmissionLoader = false;
@@ -30,14 +32,18 @@
try {
project = await sdk.forConsole.projects.create({
projectId: id ?? ID.unique(),
name,
projectId: projectId ?? ID.unique(),
name: projectName,
teamId,
region
region: projectRegion
});
await goto(`${base}/project-${project.region}-${project.$id}`);
trackEvent(Submit.ProjectCreate, { customId: !!id, teamId, region: region });
trackEvent(Submit.ProjectCreate, {
teamId,
region: projectRegion,
customId: projectId !== projectIdForLog
});
} catch (e) {
error = e.message;
trackError(e, Submit.ProjectCreate);
@@ -52,11 +58,13 @@
}
onDestroy(() => {
id = null;
name = null;
error = null;
region = Region.Fra;
projectId = ID.unique();
projectName = 'New project';
projectRegion = Region.Fra;
showCreateProjectCloud = false;
projectIdForLog = projectId;
});
</script>
@@ -67,12 +75,12 @@
onSubmit={create}
bind:error>
<CreateProject
{regions}
{projects}
showTitle={false}
bind:id
bind:projectName={name}
bind:region
{regions} />
bind:projectName
bind:id={projectId}
bind:region={projectRegion} />
<svelte:fragment slot="footer">
<Button
submit
@@ -4,7 +4,7 @@
import { EmptySearch, PaginationWithLimit, ViewSelector } from '$lib/components/index.js';
import { Button } from '$lib/elements/forms';
import Link from '$lib/elements/link.svelte';
import { toLocaleDateTime } from '$lib/helpers/date';
import DualTimeView from '$lib/components/dualTimeView.svelte';
import Container from '$lib/layout/container.svelte';
import { protocol } from '$routes/(console)/store.js';
import {
@@ -103,9 +103,13 @@
{:else if column.id === 'nameservers'}
{domain.nameservers || '-'}
{:else if column.id === 'expiry_date'}
{domain?.expire ? toLocaleDateTime(domain.expire) : '-'}
{#if domain?.expire}
<DualTimeView time={domain.expire} />
{:else}-{/if}
{:else if column.id === 'renewal'}
{domain?.renewal ? toLocaleDateTime(domain.renewal) : '-'}
{#if domain?.renewal}
<DualTimeView time={domain.renewal} />
{:else}-{/if}
{:else if column.id === 'auto_renewal'}
{domain?.autoRenewal ? 'On' : 'Off'}
{/if}
@@ -138,8 +138,13 @@
@media (max-width: 768px) {
.layout-level-progress-bars {
position: relative;
width: 100%;
align-items: center;
box-sizing: border-box;
}
:global(main:has([data-side-sheet-visible='true']) .layout-level-progress-bars) {
visibility: hidden;
}
}
</style>
@@ -15,7 +15,7 @@
SearchQuery
} from '$lib/components';
import { Button } from '$lib/elements/forms';
import { toLocaleDate, toLocaleDateTime } from '$lib/helpers/date';
import DualTimeView from '$lib/components/dualTimeView.svelte';
import { Container } from '$lib/layout';
import type { Models } from '@appwrite.io/console';
import { writable } from 'svelte/store';
@@ -196,9 +196,13 @@
{user.labels.join(', ')}
</Typography.Text>
{:else if id === 'joined'}
{toLocaleDateTime(user.registration)}
<DualTimeView time={user.registration} />
{:else if id === 'lastActivity'}
{user.accessedAt ? toLocaleDate(user.accessedAt) : 'never'}
{#if user.accessedAt}
<DualTimeView time={user.accessedAt} />
{:else}
never
{/if}
{:else}
{user[id]}
{/if}
@@ -6,7 +6,7 @@
import type { Models } from '@appwrite.io/console';
import { invalidate } from '$app/navigation';
import { base } from '$app/paths';
import { toLocaleDateTime } from '$lib/helpers/date';
import DualTimeView from '$lib/components/dualTimeView.svelte';
import type { PageData } from './$types';
import CreateMember from '../createMembership.svelte';
import DeleteMembership from '../deleteMembership.svelte';
@@ -117,7 +117,7 @@
{membership.roles}
</Table.Cell>
<Table.Cell column="joined" {root}>
{toLocaleDateTime(membership.joined)}
<DualTimeView time={membership.joined} />
</Table.Cell>
<Table.Cell column="actions" {root}>
<button
@@ -54,7 +54,7 @@
</p>
{#if error}
<Alert.Inline status="warning">{error}</Alert.Inline>
<Alert.Inline status="warning" title={error} />
{/if}
</Layout.Stack>
@@ -2,7 +2,7 @@
import { Id } from '$lib/components';
import { Button } from '$lib/elements/forms';
import type { PageData } from './$types';
import { toLocaleDateTime } from '$lib/helpers/date';
import DualTimeView from '$lib/components/dualTimeView.svelte';
import { sdk } from '$lib/stores/sdk';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { Dependencies } from '$lib/constants';
@@ -87,7 +87,7 @@
{#if !identity[column.id]}
-
{:else}
{toLocaleDateTime(identity[column.id])}
<DualTimeView time={identity[column.id]} />
{/if}
{:else}
{identity[column.id]}
@@ -7,7 +7,7 @@
import DeleteMembership from '../deleteMembership.svelte';
import type { Models } from '@appwrite.io/console';
import { trackEvent, Submit, trackError } from '$lib/actions/analytics';
import { toLocaleDateTime } from '$lib/helpers/date';
import DualTimeView from '$lib/components/dualTimeView.svelte';
import {
Table,
Layout,
@@ -108,7 +108,7 @@
{membership.roles}
</Table.Cell>
<Table.Cell column="joined" {root}>
{toLocaleDateTime(membership.joined)}
<DualTimeView time={membership.joined} />
</Table.Cell>
<Table.Cell column="actions" {root}>
<button
@@ -4,7 +4,7 @@
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
import { Modal, CustomId } from '$lib/components';
import { Dependencies } from '$lib/constants';
import { Badge, Icon } from '@appwrite.io/pink-svelte';
import { Tag, Icon } from '@appwrite.io/pink-svelte';
import { IconPencil } from '@appwrite.io/pink-icons-svelte';
import { Button, InputText, InputSelect, InputPhone } from '$lib/elements/forms';
import InputEmail from '$lib/elements/forms/inputEmail.svelte';
@@ -18,7 +18,7 @@
let identifier = '';
let name = '';
let providerId = '';
let id: string = null;
let id: string | null = null;
let showCustomId = false;
const providerTypeOptions = [
@@ -115,12 +115,10 @@
{#if !showCustomId}
<div>
<Badge
variant="secondary"
content="Target ID"
on:click={() => (showCustomId = !showCustomId)}>
<Icon icon={IconPencil} size="s" slot="start" />
</Badge>
<Tag size="s" on:click={() => (showCustomId = !showCustomId)}>
<Icon icon={IconPencil} size="s" />
Target ID
</Tag>
</div>
{:else}
<CustomId bind:show={showCustomId} name="Target" bind:id autofocus={false} />
@@ -3,7 +3,7 @@
import { Button } from '$lib/elements/forms';
import type { PageData } from './$types';
import { columns } from './store';
import { toLocaleDateTime } from '$lib/helpers/date';
import DualTimeView from '$lib/components/dualTimeView.svelte';
import ProviderType from '$routes/(console)/project-[region]-[project]/messaging/providerType.svelte';
import Provider from '$routes/(console)/project-[region]-[project]/messaging/provider.svelte';
import { sdk } from '$lib/stores/sdk';
@@ -83,7 +83,7 @@
<Provider provider={provider.provider} />
{/if}
{:else if column.id === '$createdAt'}
{toLocaleDateTime(target[column.id])}
<DualTimeView time={target[column.id]} />
{:else}
{target[column.id]}
{/if}
@@ -31,7 +31,10 @@
</script>
<svelte:head>
<title>Databases - Appwrite</title>
<!-- svelte bug, the table header just stays! -->
{#key page.url.pathname}
<title>Databases - Appwrite</title>
{/key}
</svelte:head>
<slot />
@@ -2,9 +2,9 @@
import { goto } from '$app/navigation';
import { base } from '$app/paths';
import { page } from '$app/state';
import { Empty, PaginationWithLimit, SearchQuery, ViewSelector } from '$lib/components';
import { Empty, PaginationWithLimit } from '$lib/components';
import { Button } from '$lib/elements/forms';
import { Container } from '$lib/layout';
import { Container, ResponsiveContainerHeader } from '$lib/layout';
import type { Models } from '@appwrite.io/console';
import type { PageData } from './$types';
@@ -14,7 +14,7 @@
import Table from './table.svelte';
import { registerCommands } from '$lib/commandCenter';
import { canWriteDatabases } from '$lib/stores/roles';
import { Icon, Layout } from '@appwrite.io/pink-svelte';
import { Icon } from '@appwrite.io/pink-svelte';
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
import EmptySearch from '$lib/components/emptySearch.svelte';
@@ -46,25 +46,18 @@
</script>
<Container>
<Layout.Stack direction="row" justifyContent="space-between">
<Layout.Stack direction="row" alignItems="center">
<SearchQuery placeholder="Search by name or ID" />
</Layout.Stack>
<Layout.Stack direction="row" alignItems="center" justifyContent="flex-end">
<ViewSelector
ui="new"
{columns}
view={data.view}
hideColumns={!data.databases.total}
hideView={!data.databases.total} />
{#if $canWriteDatabases}
<Button event="create_database" on:click={() => (showCreate = true)}>
<Icon icon={IconPlus} slot="start" size="s" />
Create database
</Button>
{/if}
</Layout.Stack>
</Layout.Stack>
<ResponsiveContainerHeader
hasSearch
{columns}
bind:view={data.view}
searchPlaceholder="Search by name or ID">
{#if $canWriteDatabases}
<Button event="create_database" on:click={() => (showCreate = true)}>
<Icon icon={IconPlus} slot="start" size="s" />
Create database
</Button>
{/if}
</ResponsiveContainerHeader>
{#if data.databases.total}
{#if data.view === 'grid'}
@@ -0,0 +1,82 @@
<script lang="ts">
import { Layout } from '@appwrite.io/pink-svelte';
let {
notification = false
}: {
notification?: boolean;
} = $props();
</script>
<Layout.Stack
inline
alignItems="center"
justifyContent="center"
class="ai-icon-holder {notification ? 'notification' : ''}">
<svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 30 30" fill="none">
<path
d="M11.0156 6.20801C11.3539 5.29392 12.6461 5.29392 12.9844 6.20801L14.5801 10.5186C14.7776 11.052 15.198 11.4724 15.7314 11.6699L20.042 13.2656C20.9561 13.6039 20.9561 14.8961 20.042 15.2344L15.7314 16.8301C15.198 17.0276 14.7776 17.448 14.5801 17.9814L12.9844 22.292C12.6461 23.2061 11.3539 23.2061 11.0156 22.292L9.41992 17.9814C9.2224 17.448 8.80197 17.0276 8.26855 16.8301L3.95801 15.2344C3.04392 14.8961 3.04392 13.6039 3.95801 13.2656L8.26855 11.6699C8.80196 11.4724 9.2224 11.052 9.41992 10.5186L11.0156 6.20801Z"
fill="url(#paint0_linear_3662_39953)"
stroke="#FD366E"
stroke-width="0.9" />
<path
d="M22.3828 19.6611C22.552 19.2042 23.198 19.2042 23.3672 19.6611L23.8994 21.0977C24.0286 21.4466 24.3034 21.7214 24.6523 21.8506L26.0889 22.3828C26.5458 22.552 26.5458 23.198 26.0889 23.3672L24.6523 23.8994C24.3034 24.0286 24.0286 24.3034 23.8994 24.6523L23.3672 26.0889C23.198 26.5458 22.552 26.5458 22.3828 26.0889L21.8506 24.6523C21.7214 24.3034 21.4466 24.0286 21.0977 23.8994L19.6611 23.3672C19.2042 23.198 19.2042 22.552 19.6611 22.3828L21.0977 21.8506C21.4466 21.7214 21.7214 21.4466 21.8506 21.0977L22.3828 19.6611Z"
fill="url(#paint1_linear_3662_39953)"
stroke="#FD366E"
stroke-width="0.75" />
<path
d="M22.3828 3.91113C22.552 3.45421 23.198 3.45421 23.3672 3.91113L23.8994 5.34766C24.0286 5.6966 24.3034 5.97145 24.6523 6.10059L26.0889 6.63281C26.5458 6.802 26.5458 7.448 26.0889 7.61719L24.6523 8.14941C24.3034 8.27855 24.0286 8.5534 23.8994 8.90234L23.3672 10.3389C23.198 10.7958 22.552 10.7958 22.3828 10.3389L21.8506 8.90234C21.7214 8.5534 21.4466 8.27855 21.0977 8.14941L19.6611 7.61719C19.2042 7.448 19.2042 6.802 19.6611 6.63281L21.0977 6.10059C21.4466 5.97145 21.7214 5.6966 21.8506 5.34766L22.3828 3.91113Z"
fill="url(#paint2_linear_3662_39953)"
stroke="#FD366E"
stroke-width="0.75" />
<defs>
<linearGradient
id="paint0_linear_3662_39953"
x1="12"
y1="2.25"
x2="12"
y2="26.25"
gradientUnits="userSpaceOnUse">
<stop stop-color="#FD366E" stop-opacity="0" />
<stop offset="1" stop-color="#FD366E" stop-opacity="0.4" />
</linearGradient>
<linearGradient
id="paint1_linear_3662_39953"
x1="22.875"
y1="17.25"
x2="22.875"
y2="28.5"
gradientUnits="userSpaceOnUse">
<stop stop-color="#FD366E" stop-opacity="0" />
<stop offset="1" stop-color="#FD366E" stop-opacity="0.4" />
</linearGradient>
<linearGradient
id="paint2_linear_3662_39953"
x1="22.875"
y1="1.5"
x2="22.875"
y2="12.75"
gradientUnits="userSpaceOnUse">
<stop stop-color="#FD366E" stop-opacity="0" />
<stop offset="1" stop-color="#FD366E" stop-opacity="0.4" />
</linearGradient>
</defs>
</svg>
</Layout.Stack>
<style lang="scss">
:global(.ai-icon-holder) {
border-radius: 8px;
background: rgba(253, 54, 110, 0.04);
border: 1.25px solid rgba(253, 54, 110, 0.12);
padding: 5px 0;
width: 40px !important;
height: 40px !important;
}
:global(.ai-icon-holder.notification) {
width: 36px !important;
height: 32px !important;
}
</style>
@@ -0,0 +1,5 @@
<script lang="ts">
import IconAI from './ai.svelte';
</script>
<IconAI notification />
@@ -0,0 +1,3 @@
export * from './store';
export { default as Empty } from './empty.svelte';
export { default as Input } from './input.svelte';
@@ -0,0 +1,567 @@
<script lang="ts">
import { Alert, Accordion, Icon, Layout, Skeleton, Typography } from '@appwrite.io/pink-svelte';
import { IconPlus, IconX } from '@appwrite.io/pink-icons-svelte';
import { Button, InputSelect /*InputNumber*/ } from '$lib/elements/forms';
import {
showIndexesSuggestions,
IndexOrder,
mockSuggestions,
type SuggestedIndexSchema
} from './store';
import { Modal, Confirm } from '$lib/components';
import SideSheet from '../table-[table]/layout/sidesheet.svelte';
import { isSmallViewport } from '$lib/stores/viewport';
import { IndexType, type Models } from '@appwrite.io/console';
import { capitalize } from '$lib/helpers/string';
import { type Columns, table } from '../table-[table]/store';
import { isRelationship } from '../table-[table]/rows/store';
import { VARS } from '$lib/system';
import { sleep } from '$lib/helpers/promises';
import { sdk } from '$lib/stores/sdk';
import { page } from '$app/state';
import { invalidate } from '$app/navigation';
import { Dependencies } from '$lib/constants';
import { addNotification } from '$lib/stores/notifications';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { type ComponentType, onDestroy, onMount } from 'svelte';
import { columnOptions as baseColumnOptions } from '../table-[table]/columns/store';
const MAX_INDEXES = 5;
let modalError = $state(null);
let creatingIndexes = $state(false);
let loadingSuggestions = $state(false);
let indexes = $state<SuggestedIndexSchema[]>([]);
let confirmDismiss = $state(false);
let columnOptions: Array<{
value: string;
label: string;
leadingIcon?: ComponentType;
}> = $state();
const tableId = page.params.table;
const databaseId = page.params.database;
function makeColumnOptions() {
if (VARS.MOCK_AI_SUGGESTIONS) {
columnOptions = mockSuggestions.columns.map((column) => ({
value: column.name,
label: column.name,
leadingIcon: baseColumnOptions.find((option) => option.type === column.type)?.icon
}));
} else {
columnOptions = $table.columns
.filter((column) => !isRelationship(column))
.map((column) => ({
value: column.key,
label: column.key,
leadingIcon: baseColumnOptions.find((option) => option.type === column.type)
?.icon
}));
}
}
async function loadIndexSuggestions(): Promise<SuggestedIndexSchema[]> {
modalError = null;
loadingSuggestions = true;
if (VARS.MOCK_AI_SUGGESTIONS) {
await sleep(1250);
indexes = mockSuggestions.columns.slice(0, 3).map((column, index) => ({
key: column.name,
type: IndexType.Key,
columns: [column.name],
orders: index === 2 ? IndexOrder.DESC : IndexOrder.ASC,
lengths: []
}));
} else {
try {
const suggestions = await sdk
.forProject(page.params.region, page.params.project)
.console.suggestIndexes({
databaseId,
tableId: $table.$id
});
indexes = suggestions.indexes.map((index) => {
return {
key: index.columns[0],
type: index.type as IndexType,
orders: (index.orders?.[0] as IndexOrder) || IndexOrder.ASC,
columns: index.columns,
lengths: index.lengths ?? []
};
});
} catch (error) {
addNotification({
type: 'error',
message: error.message
});
await closeAndInvalidate();
return []; // formality, UI is closed by now!
}
}
makeColumnOptions();
loadingSuggestions = false;
return indexes;
}
function addIndex() {
if (indexes.length < MAX_INDEXES) {
indexes.push({
key: '',
type: IndexType.Key,
orders: IndexOrder.ASC,
columns: [],
lengths: null
});
}
}
function removeIndex(index: number) {
indexes.splice(index, 1);
}
function syncIndexState(event: CustomEvent, index: SuggestedIndexSchema) {
const selected = event.detail;
index.key = selected;
index.columns = selected ? [selected] : [];
if (index.lengths) {
index.lengths = index.lengths.slice(0, index.columns.length);
}
}
function getOrderOptions(selectedType: IndexType) {
const base = [IndexOrder.ASC, IndexOrder.DESC];
const values = selectedType === IndexType.Spatial ? [...base, IndexOrder.NONE] : base;
return values.map((order) => ({
label: capitalize(String(order)),
value: order
}));
}
function generateUniqueIndexKey(index: SuggestedIndexSchema, usedKeys: Set<string>): string {
const existingKeys = $table.indexes.map((idx) => idx.key);
let suggestedKey = `${index.key || index.columns[0]}_${index.type.toLowerCase()}`;
let uniqueKey = suggestedKey;
let counter = 1;
while (existingKeys.includes(uniqueKey) || usedKeys.has(uniqueKey)) {
uniqueKey = `${suggestedKey}_${counter}`;
counter++;
}
usedKeys.add(uniqueKey);
return uniqueKey;
}
function prepareIndexForCreation(index: SuggestedIndexSchema, columnMap: Map<string, Columns>) {
// prepare orders array
const orders = index.orders !== null ? index.columns.map(() => String(index.orders)) : [];
// prepare lengths array
let lengths: (number | null)[];
if (index.type === IndexType.Key) {
// only validate if it's a key index
lengths = index.columns.map((columnKey, i) => {
const column = columnMap.get(columnKey);
if (column?.type === 'string') {
const stringColumn = column as Models.ColumnString;
const requestedLength = index.lengths?.[i];
if (
requestedLength &&
stringColumn.size &&
requestedLength > stringColumn.size
) {
return stringColumn.size;
}
return requestedLength || null;
}
return null;
});
} else {
// non-key indexes, lengths are null!
lengths = Array(index.columns.length).fill(null);
}
return { orders, lengths };
}
function dismissIndexes() {
indexes = [];
confirmDismiss = false;
$showIndexesSuggestions = false;
}
async function closeAndInvalidate() {
// close modal/sheet.
$showIndexesSuggestions = false;
/**
* Invalidate dependencies after modal/sheet close,
* because otherwise the `await` block will re-run in the modal.
*/
await invalidate(Dependencies.TABLE);
}
async function applySuggestedIndexes(): Promise<boolean> {
modalError = null;
creatingIndexes = true;
for (const [i, index] of indexes.entries()) {
if (!index.key || !index.type || !index.columns || index.columns.length === 0) {
modalError = `Index ${i + 1}: Selected column or type invalid`;
creatingIndexes = false;
return true; // keep sheet open!
}
}
let successCount = 0;
const usedKeys = new Set<string>();
const columnMap = new Map($table.columns.map((col) => [col.key, col]));
const sdkClient = sdk.forProject(page.params.region, page.params.project);
for (const [_, index] of indexes.entries()) {
try {
// prepare and validate index data
const { orders, lengths } = prepareIndexForCreation(index, columnMap);
// generate unique key name for the index
const uniqueIndexKey = generateUniqueIndexKey(index, usedKeys);
await sdkClient.tablesDB.createIndex({
databaseId,
tableId,
key: uniqueIndexKey,
type: index.type,
columns: index.columns,
lengths,
...(orders.length ? { orders } : {})
});
successCount++;
} catch (error) {
if (!modalError) {
modalError = error.message;
}
trackError(error, Submit.IndexCreate);
creatingIndexes = false;
return true; // keep sheet open!
}
}
creatingIndexes = false;
if (successCount > 0) {
trackEvent(Submit.IndexCreate, { type: 'suggestions', count: successCount });
}
if (successCount === indexes.length) {
// all succeeded
addNotification({
message: `${successCount} index${successCount > 1 ? 'es are' : ' is'} being created`,
type: 'success'
});
await closeAndInvalidate();
} else if (successCount > 0) {
// some succeeded, some failed
addNotification({
message: 'Some indexes could not be created',
type: 'warning'
});
await closeAndInvalidate();
} else {
// all failed
addNotification({
message: 'Failed to create indexes',
type: 'error'
});
return true; // keep sheet open!
}
return false; // close the sheet!
}
const typeOptions = Object.values(IndexType).map((type) => ({
label: capitalize(type),
value: type
}));
onMount(() => showIndexesSuggestions.set(false));
onDestroy(() => showIndexesSuggestions.set(false));
</script>
{#if !$isSmallViewport}
<Modal
dismissible={false}
title="Suggested indexes"
bind:error={modalError}
bind:show={$showIndexesSuggestions}
onSubmit={async () => {
await applySuggestedIndexes();
}}>
<Layout.Stack gap="m">
{#await loadIndexSuggestions()}
<Layout.Stack gap="xs">
<Layout.Stack gap="m">
{#each Array(3) as _, index}
{@const firstItem = index === 0}
<Layout.Stack gap="m" direction="row" alignItems="center">
{@render fieldSkeleton({ label: 'Column', showLabel: firstItem })}
{@render fieldSkeleton({ label: 'Type', showLabel: firstItem })}
{@render fieldSkeleton({ label: 'Order', showLabel: firstItem })}
<!--{@render fieldSkeleton({ label: 'Length', showLabel: firstItem })}-->
<div style:margin-top={firstItem ? '27.6px' : '0'}>
<Skeleton
variant="square"
width="33px"
height="33px"
style="opacity: 0.25" />
</div>
</Layout.Stack>
{/each}
</Layout.Stack>
{@render addIndexButton()}
</Layout.Stack>
{:then suggestedIndexes}
<Layout.Stack gap="xs">
<Layout.Stack gap="m">
{#each suggestedIndexes as index, count}
{@render indexEditForm({ index, count, isDesktop: true })}
{/each}
</Layout.Stack>
{@render addIndexButton()}
</Layout.Stack>
{/await}
</Layout.Stack>
<svelte:fragment slot="footer">
<Layout.Stack direction="row" justifyContent="flex-end" alignItems="center" inline>
<Layout.Stack direction="row" gap="m">
<Button
text
size="s"
disabled={loadingSuggestions || creatingIndexes}
on:click={() => {
if (indexes.length > 0 && !creatingIndexes) {
confirmDismiss = true;
} else {
$showIndexesSuggestions = false;
}
}}>Cancel</Button>
<Button
size="s"
submit
submissionLoader
disabled={indexes.length === 0 || loadingSuggestions}
forceShowLoader={creatingIndexes}>
Create
</Button>
</Layout.Stack>
</Layout.Stack>
</svelte:fragment>
</Modal>
{/if}
{#if $isSmallViewport}
<SideSheet
closeOnBlur={false}
title="Suggested indexes"
data-index-edit-form-sheet
bind:show={$showIndexesSuggestions}
submit={{
text: 'Create',
disabled: indexes.length === 0 || creatingIndexes || loadingSuggestions,
onClick: async () => await applySuggestedIndexes()
}}
cancel={{
disabled: loadingSuggestions || creatingIndexes,
onClick: () => {
if (indexes.length > 0 && !creatingIndexes) {
confirmDismiss = true;
} else {
$showIndexesSuggestions = false;
}
}
}}>
{#if modalError}
<Alert.Inline status="error" title={modalError} />
{/if}
{#await loadIndexSuggestions()}
<Layout.Stack gap="m" alignItems="center">
{#each Array(3) as _}
{@render fieldSkeleton({
label: undefined,
showLabel: false,
isDesktop: false
})}
{/each}
</Layout.Stack>
{:then suggestedIndexes}
<Layout.Stack gap="s">
{#each suggestedIndexes as index, count}
<Accordion title="{index.key}_index">
{@render indexEditForm({ index, count, isDesktop: false })}
</Accordion>
{/each}
{@render addIndexButton()}
</Layout.Stack>
{/await}
</SideSheet>
{/if}
{#snippet fieldSkeleton({ label, showLabel, isDesktop = true })}
<Layout.Stack gap="xxxs">
{#if showLabel}
<Typography.Text variant="m-500" style="margin-block-end: var(--gap-s);"
>{label}</Typography.Text>
{/if}
<Skeleton
variant="line"
width="100%"
height={isDesktop ? 33.602 : 39.398}
style="opacity: 0.25" />
</Layout.Stack>
{/snippet}
{#snippet indexEditForm({ index, count, isDesktop = true })}
{@const firstItem = count === 0}
{#if isDesktop}
<Layout.Stack gap="m" direction="row" alignItems="center" class="desktop-selects-form">
<InputSelect
id="key-{count}"
label={firstItem ? 'Column' : undefined}
bind:value={index.key}
on:change={(event) => syncIndexState(event, index)}
options={columnOptions}
placeholder="Select column"
required
disabled={creatingIndexes} />
<InputSelect
id="type-{count}"
label={firstItem ? 'Type' : ''}
bind:value={index.type}
options={typeOptions}
required
disabled={creatingIndexes} />
<InputSelect
id="order-{count}"
label={firstItem ? 'Order' : ''}
bind:value={index.orders}
options={getOrderOptions(index.type)}
required
disabled={creatingIndexes} />
{@render removeIndexButton({ count, isDesktop: true })}
</Layout.Stack>
{:else}
<!-- Mobile: Vertical layout -->
<Layout.Stack gap="m">
<Layout.Stack gap="s">
<InputSelect
id="key-{count}-mobile"
bind:value={index.key}
on:change={(event) => syncIndexState(event, index)}
options={columnOptions}
placeholder="Select column"
required />
<InputSelect
id="type-{count}-mobile"
bind:value={index.type}
options={typeOptions}
required />
<InputSelect
id="order-{count}-mobile"
bind:value={index.orders}
options={getOrderOptions(index.type)}
required />
</Layout.Stack>
{@render removeIndexButton({ count, isDesktop: false })}
</Layout.Stack>
{/if}
{/snippet}
{#snippet addIndexButton()}
{#if indexes.length < MAX_INDEXES}
<Layout.Stack direction="row" justifyContent="flex-start">
<Button
icon
size="s"
compact
on:click={addIndex}
disabled={loadingSuggestions || creatingIndexes}>
<Icon icon={IconPlus} size="s" />
Add Index
</Button>
</Layout.Stack>
{/if}
{/snippet}
{#snippet removeIndexButton({ count, isDesktop = true })}
{#if isDesktop}
<div style:margin-top={count === 0 ? '27.6px' : '0'}>
<Button
icon
size="xs"
secondary
on:click={() => removeIndex(count)}
disabled={indexes.length <= 1 || creatingIndexes}>
<Icon icon={IconX} color="--fgcolor-danger-primary" />
</Button>
</div>
{:else if indexes.length > 1}
<Layout.Stack direction="row" justifyContent="flex-start">
<Button
size="s"
secondary
on:click={() => removeIndex(count)}
disabled={indexes.length <= 1 || creatingIndexes}>Delete</Button>
</Layout.Stack>
{/if}
{/snippet}
<Confirm
confirmDeletion
action="Dismiss"
title="Dismiss indexes"
bind:open={confirmDismiss}
onSubmit={dismissIndexes}>
Are you sure you want to dismiss these suggested indexes? This action cannot be undone.
</Confirm>
<style lang="scss">
// Custom logic to hide the Sheet's
// `X` close button (not configurable via props)
:global([data-index-edit-form-sheet] header) {
& :global(.divider),
& :global(button) {
visibility: hidden !important;
}
}
// stack > div > input-select button!
:global(.desktop-selects-form :first-child button:first-of-type) {
width: 164px;
max-width: 164px;
}
</style>
@@ -0,0 +1,81 @@
<script lang="ts">
import { onMount } from 'svelte';
import { isCloud } from '$lib/system';
import IconAI from './icon/ai.svelte';
import { slide } from 'svelte/transition';
import { tableColumnSuggestions } from './store';
import { Button, InputTextarea } from '$lib/elements/forms';
import { Card, Layout, Selector, Typography } from '@appwrite.io/pink-svelte';
onMount(() => {
if (featureActive) {
$tableColumnSuggestions.enabled = true;
}
});
const featureActive = $derived(isCloud);
const title = $derived.by(() => {
return featureActive
? 'Smart column suggestions'
: 'Smart column suggestions available on Cloud';
});
const subtitle = $derived.by(() => {
return featureActive
? 'Enable AI to suggest useful columns based on your table name'
: 'Sign up for Cloud to generate columns based on your table name';
});
</script>
<Card.Base variant="secondary" radius="s" padding="xs">
<Layout.Stack gap={featureActive ? 'm' : 'l'}>
<Layout.Stack gap="s" direction="row" alignItems="flex-start">
<IconAI />
<Layout.Stack direction="column" gap="none">
<Typography.Text variant="m-500" color="--fgcolor-neutral-primary"
>{title}</Typography.Text>
<Typography.Text color="--fgcolor-neutral-secondary">
{subtitle}
</Typography.Text>
</Layout.Stack>
{#if featureActive}
<div class="suggestions-switch">
<Selector.Switch
id="suggestions"
label={undefined}
bind:checked={$tableColumnSuggestions.enabled} />
</div>
{/if}
</Layout.Stack>
{#if !featureActive}
<Layout.Stack>
<Button external secondary href="https://cloud.appwrite.io/register">
Sign up
</Button>
</Layout.Stack>
{/if}
<!-- just being safe with extra guard! -->
{#if $tableColumnSuggestions.enabled && featureActive}
<div transition:slide={{ duration: 200 }}>
<InputTextarea
id="context"
rows={3}
maxlength={255}
bind:value={$tableColumnSuggestions.context}
placeholder="Optional: Add context to improve suggestions" />
</div>
{/if}
</Layout.Stack>
</Card.Base>
<style lang="scss">
.suggestions-switch :global(button):not(:disabled) {
cursor: pointer;
}
</style>
@@ -0,0 +1,61 @@
<script lang="ts">
import type { Snippet } from 'svelte';
import { Popover } from '@appwrite.io/pink-svelte';
import { isSmallViewport } from '$lib/stores/viewport';
import SideSheet from '../table-[table]/layout/sidesheet.svelte';
let {
children,
tooltipChildren,
toggleOnTapClick = true,
onShowStateChanged = null,
enabled = true
}: {
children: Snippet<[toggle: (event: Event) => void]>;
tooltipChildren: Snippet<[toggle: (event: Event) => void]>;
toggleOnTapClick?: boolean;
onShowStateChanged?: (showing: boolean) => void;
enabled?: boolean;
} = $props();
let showSheet = $state(false);
$effect(() => {
if (!$isSmallViewport) {
showSheet = false;
}
});
</script>
<Popover let:toggle let:showing portal padding="none" placement="bottom-start">
{onShowStateChanged?.(showing || showSheet)}
{#if toggleOnTapClick && $isSmallViewport}
<button onclick={() => enabled && (showSheet = true)} aria-label="Open column review modal">
<!-- not a `toggle` but more like `hide` -->
{@render children(() => (showSheet = false))}
</button>
{:else}
<button style:cursor={enabled ? 'pointer' : undefined}>
{@render children(toggle)}
</button>
{/if}
<div let:toggle slot="tooltip" style:width="480px" style:padding="16px">
{@render tooltipChildren(toggle)}
</div>
</Popover>
{#if $isSmallViewport}
<SideSheet
bind:show={showSheet}
title="Edit suggested column"
submit={{
text: 'Update',
onClick: () => {
showSheet = false;
}
}}>
{@render tooltipChildren(() => (showSheet = false))}
</SideSheet>
{/if}
@@ -0,0 +1,151 @@
import { writable } from 'svelte/store';
import { IndexType } from '@appwrite.io/console';
import { columnOptions } from '../table-[table]/columns/store';
export type TableColumnSuggestions = {
enabled: boolean;
thinking: boolean;
context?: string | undefined;
/* for safety when in tables page */
table?: {
id: string;
name: string;
};
};
export type SuggestedColumnSchema = {
key: string;
type: string;
required: boolean;
default?: string | number | boolean | number[] | number[][] | number[][][] | null;
size?: number;
min?: number;
max?: number;
format?: string | null;
};
export enum IndexOrder {
ASC = 'ASC',
DESC = 'DESC',
NONE = null
}
export type SuggestedIndexSchema = {
key: string;
type: IndexType;
orders: IndexOrder;
columns: string[];
lengths?: number[] | undefined;
};
export const tableColumnSuggestions = writable<TableColumnSuggestions>({
enabled: false,
context: null,
thinking: false,
table: null
});
export const showIndexesSuggestions = writable<boolean>(false);
export const mockSuggestions: { total: number; columns: ColumnInput[] } = {
total: 7,
columns: [
{
name: 'title',
type: 'string',
size: 255,
format: null,
required: true,
formatOptions: null
},
{
name: 'authorName',
type: 'string',
size: 128,
format: null,
required: true,
formatOptions: null
},
{
name: 'publishedYear',
type: 'integer',
size: null,
format: null,
required: true,
formatOptions: {
min: 1500,
max: null
}
},
{
name: 'genre',
type: 'string',
size: 64,
format: null,
required: false,
formatOptions: null,
default: null
},
{
name: 'isbn',
type: 'string',
size: 13,
required: false,
formatOptions: null,
default: null
},
{
name: 'language',
type: 'string',
size: 32,
format: null,
required: false,
formatOptions: null,
default: null
},
{
name: 'pageCount',
type: 'integer',
required: false,
min: 1,
max: 10000,
default: null
}
]
};
export type ColumnInput = {
name: string;
type: string;
required?: boolean;
default?: string | number | boolean | number[] | number[][] | number[][][] | null;
size?: number;
min?: number;
max?: number;
format?: string;
formatOptions?: {
min?: number;
max?: number;
};
};
export function mapSuggestedColumns<T extends ColumnInput>(columns: T[]): SuggestedColumnSchema[] {
return columns.map((col) => ({
key: col.name,
type: col.type,
required: col.required ?? false,
default: col.default ?? null,
size: col.type === 'string' ? (col.size ?? undefined) : undefined,
min:
col.type === 'integer' || col.type === 'double'
? (col.min ?? col.formatOptions?.min ?? undefined)
: undefined,
max:
col.type === 'integer' || col.type === 'double'
? (col.max ?? col.formatOptions?.max ?? undefined)
: undefined,
format: col.format ?? null
}));
}
export const basicColumnOptions = columnOptions.slice(0, -1);
@@ -10,7 +10,6 @@
} from '$lib/commandCenter';
import { tablesSearcher } from '$lib/commandCenter/searchers';
import { Dependencies } from '$lib/constants';
import type { Models } from '@appwrite.io/console';
import CreateTable from './createTable.svelte';
import { showCreateTable } from './store';
import { TablesPanel } from '$lib/commandCenter/panels';
@@ -24,14 +23,6 @@
const project = page.params.project;
const databaseId = page.params.database;
async function handleCreate(event: CustomEvent<Models.Table>) {
$showCreateTable = false;
await invalidate(Dependencies.DATABASE);
await goto(
`${base}/project-${page.params.region}-${project}/databases/database-${databaseId}/table-${event.detail.$id}`
);
}
$: $registerCommands([
{
label: 'Create table',
@@ -147,9 +138,19 @@
</script>
<svelte:head>
<title>Database - Appwrite</title>
<!-- svelte bug, the table header just stays! -->
{#key page.url.pathname}
<title>Database - Appwrite</title>
{/key}
</svelte:head>
<slot />
<CreateTable bind:showCreate={$showCreateTable} on:created={handleCreate} />
<CreateTable
bind:showCreate={$showCreateTable}
onTableCreated={async (table) => {
await invalidate(Dependencies.DATABASE);
await goto(
`${base}/project-${page.params.region}-${project}/databases/database-${databaseId}/table-${table.$id}`
);
}} />
@@ -23,6 +23,7 @@
import { trackEvent } from '$lib/actions/analytics';
import { Layout, Typography } from '@appwrite.io/pink-svelte';
import { page } from '$app/state';
import IconQuestionMarkCircle from './components/questionIcon.svelte';
let policyCreateError: string;
let totalPolicies: UserBackupPolicy[] = [];
@@ -40,7 +41,7 @@
if (parsedCounter === showOnCount || !counter) {
addNotification({
type: 'info',
icon: 'question-mark-circle',
icon: IconQuestionMarkCircle,
message:
'How was your experience with our new Backups feature? Give us your feedback and help us improve!',
timeout: 15000,
@@ -0,0 +1,6 @@
<script lang="ts">
import { Icon } from '@appwrite.io/pink-svelte';
import { IconQuestionMarkCircle } from '@appwrite.io/pink-icons-svelte';
</script>
<Icon icon={IconQuestionMarkCircle} />
@@ -6,25 +6,43 @@
import { Button, InputText } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
import { ID } from '@appwrite.io/console';
import { createEventDispatcher } from 'svelte';
import { ID, type Models } from '@appwrite.io/console';
import { subNavigation } from '$lib/stores/database';
import { Input as SuggestionsInput, tableColumnSuggestions } from './(suggestions)/index';
let {
showCreate = $bindable(false)
showCreate = $bindable(false),
onTableCreated
}: {
showCreate: boolean;
onTableCreated: (table: Models.Table) => void | Promise<void>;
} = $props();
const databaseId = page.params.database;
const dispatch = createEventDispatcher();
const isOnTablesPage = $derived(page.route?.id.endsWith('table-[table]'));
let name = $state('');
let id: string = $state(null);
let touchedId = $state(false);
let error: string = $state(null);
const create = async () => {
let creatingTable = $state(false);
function enableThinkingModeForSuggestions(table: Models.Table) {
if ($tableColumnSuggestions.enabled) {
// if enabled, trigger thinking mode!
tableColumnSuggestions.update((store) => ({
...store,
thinking: true,
table: {
id: table.$id,
name: table.name
}
}));
}
}
async function createTable() {
error = null;
try {
const table = await sdk
@@ -35,23 +53,32 @@
name
});
showCreate = false;
subNavigation.update();
updateAndCleanup();
await onTableCreated(table);
dispatch('created', table);
addNotification({
type: 'success',
message: `${name} has been created`
});
name = id = null;
trackEvent(Submit.TableCreate, {
customId: !!id
});
showCreate = false;
creatingTable = false;
// don't wait for UI to mount!
enableThinkingModeForSuggestions(table);
} catch (e) {
error = e.message;
trackError(e, Submit.TableCreate);
}
};
}
function updateAndCleanup() {
subNavigation.update();
addNotification({
type: 'success',
message: `${name} has been created`
});
trackEvent(Submit.TableCreate, { customId: !!id });
}
function toIdFormat(str: string): string {
return str
@@ -76,9 +103,18 @@
touchedId = false;
}
});
$effect(() => {
if (showCreate && isOnTablesPage && $tableColumnSuggestions.table) {
tableColumnSuggestions.update((store) => ({
...store,
table: null
}));
}
});
</script>
<Modal title="Create table" size="m" bind:show={showCreate} onSubmit={create} bind:error>
<Modal title="Create table" size="m" bind:show={showCreate} onSubmit={createTable} bind:error>
<InputText
id="name"
label="Name"
@@ -104,8 +140,12 @@
}
}} />
<SuggestionsInput />
<svelte:fragment slot="footer">
<Button secondary on:click={() => (showCreate = false)}>Cancel</Button>
<Button submit>Create</Button>
<Button secondary disabled={creatingTable} on:click={() => (showCreate = false)}
>Cancel</Button>
<Button submit disabled={creatingTable} submissionLoader forceShowLoader={creatingTable}
>Create</Button>
</svelte:fragment>
</Modal>
@@ -8,7 +8,7 @@
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
import { database } from './store';
import { toLocaleDate } from '$lib/helpers/date';
import DualTimeView from '$lib/components/dualTimeView.svelte';
import { type Models, Query } from '@appwrite.io/console';
import { Spinner, Table } from '@appwrite.io/pink-svelte';
@@ -126,7 +126,9 @@
{#each tableItems as table}
<Table.Row.Base {root}>
<Table.Cell {root}>{table.name}</Table.Cell>
<Table.Cell {root}>{toLocaleDate(table.updatedAt)}</Table.Cell>
<Table.Cell {root}>
<DualTimeView time={table.updatedAt} />
</Table.Cell>
</Table.Row.Base>
{/each}
</Table.Root>
@@ -1,9 +1,11 @@
<script lang="ts">
import { base } from '$app/paths';
import { page } from '$app/state';
import { showCreateTable, databaseSubNavigationItems } from './store';
import type { PageData } from './$types';
import { showSubNavigation } from '$lib/stores/layout';
import { bannerSpacing } from '$lib/layout/headerAlert.svelte';
import { showCreateTable, databaseSubNavigationItems } from './store';
import {
Icon,
Sidebar,
@@ -27,12 +29,12 @@
import { onMount } from 'svelte';
import { subNavigation } from '$lib/stores/database';
let data = $derived(page.data) as PageData;
const data = $derived(page.data) as PageData;
let region = $derived(page.params.region);
let project = $derived(page.params.project);
let tableId = $derived(page.params.table);
let databaseId = $derived(page.params.database);
const region = $derived(page.params.region);
const project = $derived(page.params.project);
const tableId = $derived(page.params.table);
const databaseId = $derived(page.params.database);
let openBottomSheet = $state(false);
@@ -41,7 +43,9 @@
tables: []
});
const sortedTables = $derived.by(() => tables?.tables);
const sortedTables = $derived.by(() =>
tables?.tables?.slice().sort((a, b) => a.name.localeCompare(b.name))
);
const selectedTable = $derived.by(() =>
sortedTables?.find((table: Models.Table) => table.$id === tableId)
@@ -51,6 +55,11 @@
const isMainDatabaseScreen = $derived(page.route.id.endsWith('database-[database]'));
// If banner open, `-1rem` to adjust banner size, else `-70.5px`.
// 70.5px is the size of the container of the banner holder and not just the banner!
// Needed because things vary a bit much on how different browsers treat bottom layouts.
const bottomNavHeight = $derived(`calc(20% ${$bannerSpacing ? '- 1rem' : '- 70.5px'})`);
async function loadTables() {
tables = await sdk.forProject(region, project).tablesDB.listTables({
databaseId: databaseId,
@@ -145,7 +154,10 @@
</Layout.Stack>
</div>
<Layout.Stack direction="column" gap="xxs" style="bottom: 1rem; position: sticky;">
<Layout.Stack
gap="xxs"
direction="column"
style="bottom: 1rem; position: relative; height: {bottomNavHeight}">
<div class="action-menu-divider">
<Divider />
</div>
@@ -62,6 +62,10 @@
import { preferences } from '$lib/stores/preferences';
import { buildRowUrl, isRelationship } from './rows/store';
import { chunks } from '$lib/helpers/array';
import { Submit, trackEvent } from '$lib/actions/analytics';
import IndexesSuggestions from '../(suggestions)/indexes.svelte';
import { showIndexesSuggestions, tableColumnSuggestions } from '../(suggestions)';
let editRow: EditRow;
let editRelatedRow: EditRelatedRow;
@@ -88,7 +92,15 @@
response.events.includes('databases.*.tables.*.columns.*') ||
response.events.includes('databases.*.tables.*.indexes.*')
) {
if (!isWaterfallFromFaker) {
// don't invalidate when -
// 1. from faker
// 2. ai columns creation
// 3. ai indexes creation
if (
!isWaterfallFromFaker &&
!$showIndexesSuggestions &&
!$tableColumnSuggestions.table
) {
invalidate(Dependencies.TABLE);
}
}
@@ -256,6 +268,7 @@
columns = await generateColumns($project, page.params.database, page.params.table);
await invalidate(Dependencies.TABLE);
trackEvent(Submit.ColumnCreate, { type: 'faker' });
} catch (e) {
addNotification({
type: 'error',
@@ -468,3 +481,5 @@
</Layout.Stack>
</svelte:fragment>
</Dialog>
<IndexesSuggestions />
@@ -6,7 +6,7 @@
import { Container } from '$lib/layout';
import { preferences } from '$lib/stores/preferences';
import { canWriteTables, canWriteRows } from '$lib/stores/roles';
import { Icon, Layout, Divider } from '@appwrite.io/pink-svelte';
import { Icon, Layout, Divider, Tooltip } from '@appwrite.io/pink-svelte';
import type { PageData } from './$types';
import {
table,
@@ -31,6 +31,8 @@
import EmptySheet from './layout/emptySheet.svelte';
import CreateRow from './rows/create.svelte';
import { onDestroy } from 'svelte';
import { isCloud } from '$lib/system';
import { Empty as SuggestionsEmptySheet, tableColumnSuggestions } from '../(suggestions)';
export let data: PageData;
@@ -74,6 +76,12 @@
$: hasColumns = !!$table.columns.length;
$: hasValidColumns = $table?.columns?.some((col) => col.status === 'available');
$: canShowSuggestionsSheet =
// enabled, has table details
// and it matches current table
$tableColumnSuggestions.enabled &&
$tableColumnSuggestions.table &&
$tableColumnSuggestions.table.id === page.params.table;
async function onSelect(file: Models.File, localFile = false) {
$isCsvImportInProgress = true;
@@ -113,21 +121,31 @@
<Layout.Stack direction="column" gap="xl">
<Layout.Stack direction="row" justifyContent="space-between">
<Layout.Stack direction="row" gap="s">
<ViewSelector
onlyIcon
ui="new"
view={data.view}
columns={tableColumns}
hideView
showAnyway
isCustomTable />
<Tooltip>
<div>
<ViewSelector
onlyIcon
ui="new"
view={data.view}
columns={tableColumns}
hideView
showAnyway
isCustomTable />
</div>
<Filters
onlyIcon
query={data.query}
columns={filterColumns}
disabled={!(hasColumns && hasValidColumns)}
analyticsSource="database_rows" />
<svelte:fragment slot="tooltip">Columns</svelte:fragment>
</Tooltip>
<Tooltip>
<Filters
onlyIcon
query={data.query}
columns={filterColumns}
disabled={!(hasColumns && hasValidColumns)}
analyticsSource="database_tables" />
<svelte:fragment slot="tooltip">Filters</svelte:fragment>
</Tooltip>
</Layout.Stack>
<Layout.Stack direction="row" alignItems="center" justifyContent="flex-end">
<Button
@@ -191,7 +209,7 @@
queries.clearAll();
queries.apply();
trackEvent(Submit.FilterClear, {
source: 'database_rows'
source: 'database_tables'
});
}
}
@@ -215,6 +233,8 @@
}
}} />
{/if}
{:else if isCloud && canShowSuggestionsSheet}
<SuggestionsEmptySheet />
{:else}
<EmptySheet
mode="rows"
@@ -20,6 +20,7 @@
import {
columns,
type Columns,
type ColumnsWidth,
indexes,
isCsvImportInProgress,
reorderItems,
@@ -51,6 +52,14 @@
import { type Models } from '@appwrite.io/console';
import { preferences } from '$lib/stores/preferences';
import { page } from '$app/state';
import { debounce } from '$lib/helpers/debounce';
import type { PageData } from './$types';
const {
data
}: {
data: PageData;
} = $props();
const updatedColumnsForSheet = $derived.by(() => {
const baseAttrs = [
@@ -104,8 +113,11 @@
let selectedColumn: Columns = $state(null);
let columnIndexMap: Record<string, boolean> = $state({});
let columnsOrder = $state([]);
let columnsOrder = $state<string[]>([]);
let columnsWidth = $state<ColumnsWidth | null>(null);
const tableId = page.params.table;
const organizationId = data.organization.$id ?? data.project.teamId;
let showEdit = $state(false);
let editColumn: EditColumn;
@@ -126,6 +138,7 @@
onMount(() => {
columnsOrder = preferences.getColumnOrder(tableId);
columnsWidth = preferences.getColumnWidths(tableId + '#columns');
});
function getColumnStatusBadge(status: string): ComponentProps<Badge>['type'] {
@@ -164,8 +177,92 @@
}
}
function getRelationshipTypeForColumn(column: Columns): string | null {
if (!isRelationship(column)) {
return null;
}
const relationshipMap = {
oneToOne: 'One to one',
oneToMany: 'One to many',
manyToOne: 'Many to one',
manyToMany: 'Many to many'
};
const relationType = (column as Models.ColumnRelationship).relationType;
const formattedType = relationshipMap[relationType] || relationType;
return `Type: ${formattedType}`;
}
function isSystemColumnKey(column: Columns) {
return column.key.startsWith('$');
}
function getColumnWidth(columnId: string, defaultWidth: number): number {
const savedWidth = columnsWidth?.[columnId];
if (!savedWidth) return defaultWidth;
return savedWidth.resized;
}
function saveColumnsWidth({ columnId, newWidth }: { columnId: string; newWidth: number }) {
const existing = columnsWidth?.[columnId];
const fixed = existing
? typeof existing?.fixed === 'number'
? existing.fixed
: existing?.fixed?.min
: newWidth;
columnsWidth = {
...(columnsWidth ?? {}),
[columnId]: {
fixed,
resized: Math.ceil(newWidth)
}
};
saveColumnWidthsToPreferences({ columnId, newWidth, fixedWidth: fixed });
}
const saveColumnWidthsToPreferences = debounce(
(column: { columnId: string; newWidth: number; fixedWidth: number }) => {
if (!organizationId) return;
preferences.saveColumnWidths(organizationId, tableId + '#columns', {
[column.columnId]: {
fixed: column.fixedWidth,
resized: Math.ceil(column.newWidth)
}
});
},
1000
);
onDestroy(() => ($showCreateColumnSheet.show = false));
const spreadsheetColumns = $derived([
{
id: 'key',
width: getColumnWidth('key', 300),
minimumWidth: 300,
resizable: true
},
{
id: 'indexed',
width: getColumnWidth('indexed', 150),
minimumWidth: 150,
resizable: true
},
{
id: 'default',
width: getColumnWidth('default', 200),
minimumWidth: 200,
resizable: true
},
{ id: 'actions', width: 40, isAction: true, resizable: false }
]);
$effect(() => {
if (!$showCreateIndexSheet.show && $showCreateIndexSheet.column) {
const columnKey = $showCreateIndexSheet.column;
@@ -206,14 +303,9 @@
height="100%"
emptyCells={emptyCellsCount}
bind:selectedRows={selectedColumns}
columns={[
// more size until we decide if we want a new column!
{ id: 'key', width: { min: $isSmallViewport ? 250 : 200 } },
{ id: 'indexed', width: { min: 150 } },
{ id: 'default', width: { min: 200 } },
{ id: 'actions', width: 40, isAction: true }
]}
bottomActionClick={() => ($showCreateColumnSheet.show = true)}>
columns={spreadsheetColumns}
bottomActionClick={() => ($showCreateColumnSheet.show = true)}
on:columnsResize={(resize) => saveColumnsWidth(resize.detail)}>
<svelte:fragment slot="header" let:root>
<Spreadsheet.Header.Cell column="key" {root}>Column name</Spreadsheet.Header.Cell>
<Spreadsheet.Header.Cell column="indexed" {root}>Indexed</Spreadsheet.Header.Cell>
@@ -222,15 +314,23 @@
<Spreadsheet.Header.Cell column="actions" {root} />
</svelte:fragment>
<!-- TODO: variable and terminology changes -->
{#each updatedColumnsForSheet as column, index}
{#each updatedColumnsForSheet as column, index (column.key)}
{@const option = columnOptions.find((option) => option.type === column.type)}
{@const isSelectable =
column['system'] || column.type === 'relationship' ? 'disabled' : true}
<Spreadsheet.Row.Base {root} select={isSelectable} id={column.key}>
<Spreadsheet.Cell column="key" {root} isEditable={false}>
<Layout.Stack direction="row" justifyContent="space-between">
<Layout.Stack direction="row" alignItems="center" inline>
<Layout.Stack
direction="row"
alignItems="center"
justifyContent="space-between"
style="min-width:0">
<Layout.Stack
gap="s"
inline
direction="row"
alignItems="center"
style="min-width:0; flex:1 1 auto;">
{#if isRelationship(column)}
<Icon
size="s"
@@ -246,29 +346,35 @@
<Icon icon={option.icon} size="s" />
{/if}
<Layout.Stack direction="row" alignItems="center" gap="s">
<Layout.Stack
inline
direction="row"
alignItems="center"
gap="xxs">
<span class="text u-trim-1" data-private>
{#if column.key === '$id' || column.key === '$sequence' || column.key === '$createdAt' || column.key === '$updatedAt'}
{column['name']}
{:else}
{column.key} {column.array ? '[]' : undefined}
{/if}
</span>
{#if isString(column) && column.encrypt}
<Tooltip>
<Icon
size="s"
icon={IconLockClosed}
color="--fgcolor-neutral-tertiary" />
<div slot="tooltip">Encrypted</div>
</Tooltip>
<Layout.Stack
gap="s"
inline
direction="row"
alignItems="center"
style="min-width:0; flex:1 1 auto; overflow:hidden;">
<Typography.Text truncate>
{#if isSystemColumnKey(column)}
{column.key}
{:else}
{column.key}{column.array ? '[]' : undefined}
{/if}
</Layout.Stack>
</Typography.Text>
{#if isString(column) && column.encrypt}
<Tooltip>
<Icon
size="s"
icon={IconLockClosed}
color="--fgcolor-neutral-tertiary" />
<div slot="tooltip">Encrypted</div>
</Tooltip>
{/if}
</Layout.Stack>
<Layout.Stack
gap="s"
inline
direction="row"
alignItems="center"
style="flex:0 0 auto; white-space:nowrap;">
{#if column.status !== 'available'}
<Badge
size="s"
@@ -290,12 +396,19 @@
</Layout.Stack>
</Layout.Stack>
{@const minMaxSize = getMinMaxSizeForColumn(column)}
{@const relationType = getRelationshipTypeForColumn(column)}
{#if minMaxSize}
<Typography.Caption
variant="400"
color="--fgcolor-neutral-tertiary">
{minMaxSize}
</Typography.Caption>
{:else if relationType}
<Typography.Caption
variant="400"
color="--fgcolor-neutral-tertiary">
{relationType}
</Typography.Caption>
{/if}
</Layout.Stack>
</Spreadsheet.Cell>
@@ -410,7 +523,6 @@
</Spreadsheet.Root>
</SpreadsheetContainer>
<!-- TODO: terminologies -->
{#if selectedColumns.length > 0}
<div class="floating-action-bar">
<FloatingActionBar>
@@ -435,7 +547,7 @@
</div>
{#if selectedColumn}
<DeleteColumn bind:showDelete {selectedColumn} />
<DeleteColumn bind:showDelete bind:selectedColumn />
{:else if selectedColumns && selectedColumns.length}
<DeleteColumn bind:showDelete bind:selectedColumn={selectedColumns} />
{/if}
@@ -453,3 +565,13 @@
{#if showFailed}
<FailedModal bind:show={showFailed} title="Create attribute" header="Creation failed" {error} />
{/if}
<style>
.floating-action-bar {
left: 50%;
width: 100%;
z-index: 14;
position: absolute;
transform: translateX(-50%);
}
</style>
@@ -68,6 +68,7 @@
array: false,
...data
});
$: listen(data);
$: handleDefaultState($required || $array);
@@ -61,7 +61,7 @@
await invalidate(Dependencies.TABLE);
showDelete = false;
selectedColumn = [];
selectedColumn = Array.isArray(selectedColumn) ? [] : null;
} catch (e) {
error = e.message;
trackError(e, Submit.ColumnDelete);
@@ -85,14 +85,14 @@
label="Min"
placeholder="Enter size"
bind:value={data.min}
step="any"
step={0.1}
required={editing} />
<InputNumber
id="max"
label="Max"
placeholder="Enter size"
bind:value={data.max}
step="any"
step={0.1}
required={editing} />
</Layout.Stack>
<InputNumber
@@ -104,7 +104,7 @@
bind:value={data.default}
disabled={data.required || data.array}
nullable={!data.required && !data.array}
step="any" />
step={0.1} />
<Selector.Checkbox
size="s"
id="required"
@@ -149,7 +149,8 @@
type: 'success',
message: `Column ${key ?? data?.key} has been created`
});
trackEvent(Submit.ColumnCreate);
trackEvent(Submit.ColumnCreate, { type: 'manual' });
if (createMore) {
init();
@@ -1,6 +1,6 @@
<script lang="ts">
import { Container } from '$lib/layout';
import { table } from '../store';
import { table, type ColumnsWidth } from '../store';
import Delete from './deleteIndex.svelte';
import CreateIndex from './createIndex.svelte';
import Overview from './overviewIndex.svelte';
@@ -8,6 +8,8 @@
import { Button } from '$lib/elements/forms';
import FailedModal from '../failedModal.svelte';
import { canWriteTables } from '$lib/stores/roles';
import { preferences } from '$lib/stores/preferences';
import { debounce } from '$lib/helpers/debounce';
import {
ActionMenu,
Badge,
@@ -26,7 +28,7 @@
IconPlus,
IconTrash
} from '@appwrite.io/pink-icons-svelte';
import { type ComponentProps, onDestroy } from 'svelte';
import { type ComponentProps, onDestroy, onMount } from 'svelte';
import { Click, trackEvent } from '$lib/actions/analytics';
import EmptySheet from '../layout/emptySheet.svelte';
import SpreadsheetContainer from '../layout/spreadsheet.svelte';
@@ -34,6 +36,7 @@
import type { PageData } from './$types';
import { showCreateColumnSheet } from '../store';
import { isSmallViewport } from '$lib/stores/viewport';
import { page } from '$app/state';
let {
data
@@ -52,15 +55,49 @@
let showDelete = $state(false);
let showOverview = $state(false);
let columns = $state([
{ id: 'key' },
{ id: 'type' },
{ id: 'columns' },
let columnsWidth = $state<ColumnsWidth | null>(null);
const tableId = page.params.table;
const organizationId = data.organization.$id ?? data.project.teamId;
const spreadsheetColumns = $derived([
{
id: 'key',
width: getColumnWidth('key', $isSmallViewport ? 250 : 200),
minimumWidth: $isSmallViewport ? 250 : 200,
resizable: true
},
{
id: 'type',
width: getColumnWidth('type', 120),
minimumWidth: 120,
resizable: true
},
{
id: 'columns',
width: getColumnWidth('columns', 200),
minimumWidth: 200,
resizable: true
},
// { id: 'orders' }, // design doesn't have orders atm
{ id: 'lengths' },
{ id: 'actions', width: 40, isAction: true }
{
id: 'lengths',
width: getColumnWidth('lengths', 180),
minimumWidth: 180,
resizable: true
},
{
id: 'actions',
width: 40,
isAction: true,
resizable: false
}
]);
onMount(() => {
columnsWidth = preferences.getColumnWidths(tableId + '#indexes');
});
function getColumnStatusBadge(status: string): ComponentProps<Badge>['type'] {
switch (status) {
case 'processing':
@@ -74,6 +111,46 @@
}
}
function getColumnWidth(columnId: string, defaultWidth: number): number {
const savedWidth = columnsWidth?.[columnId];
if (!savedWidth) return defaultWidth;
return savedWidth.resized;
}
function saveColumnsWidth({ columnId, newWidth }: { columnId: string; newWidth: number }) {
const existing = columnsWidth?.[columnId];
const fixed = existing
? typeof existing?.fixed === 'number'
? existing.fixed
: existing?.fixed?.min
: newWidth;
columnsWidth = {
...(columnsWidth ?? {}),
[columnId]: {
fixed,
resized: Math.ceil(newWidth)
}
};
saveColumnWidthsToPreferences({ columnId, newWidth, fixedWidth: fixed });
}
const saveColumnWidthsToPreferences = debounce(
(column: { columnId: string; newWidth: number; fixedWidth: number }) => {
if (!organizationId) return;
preferences.saveColumnWidths(organizationId, tableId + '#indexes', {
[column.columnId]: {
fixed: column.fixedWidth,
resized: Math.ceil(column.newWidth)
}
});
},
1000
);
onDestroy(() => ($showCreateColumnSheet.show = false));
const emptyCellsLimit = $derived($isSmallViewport ? 14 : 17);
@@ -108,12 +185,13 @@
<SpreadsheetContainer>
<Spreadsheet.Root
let:root
{columns}
columns={spreadsheetColumns}
height="100%"
allowSelection
emptyCells={emptyCellsCount}
bind:selectedRows={selectedIndexes}
bottomActionClick={() => (showCreateIndex = true)}>
bottomActionClick={() => (showCreateIndex = true)}
on:columnsResize={(resize) => saveColumnsWidth(resize.detail)}>
<svelte:fragment slot="header" let:root>
<Spreadsheet.Header.Cell column="key" {root}>Key</Spreadsheet.Header.Cell>
<Spreadsheet.Header.Cell column="type" {root}>Type</Spreadsheet.Header.Cell>
@@ -12,8 +12,9 @@
import { isRelationship, isSpatialType } from '../rows/store';
import { table, indexes } from '../store';
import { Icon, Layout } from '@appwrite.io/pink-svelte';
import { IconPlus, IconX } from '@appwrite.io/pink-icons-svelte';
import { IconCalendar, IconFingerPrint, IconPlus, IconX } from '@appwrite.io/pink-icons-svelte';
import { isSmallViewport } from '$lib/stores/viewport';
import { columnOptions as baseColumnOptions } from '../columns/store';
let {
showCreateIndex = $bindable(false),
@@ -37,7 +38,11 @@
}
return !isRelationship(column) && !isSpatialType(column); // keep non-relationship and non-spatial
})
.map((column) => ({ value: column.key, label: column.key }))
.map((column) => ({
value: column.key,
label: column.key,
leadingIcon: baseColumnOptions.find((option) => option.type === column.type)?.icon
}))
);
let columnList = $state([{ value: '', order: '', length: null }]);
@@ -154,7 +159,7 @@
]
: undefined
});
trackEvent(Submit.IndexCreate);
trackEvent(Submit.IndexCreate, { type: 'manual' });
showCreateIndex = false;
} catch (err) {
addNotification({
@@ -196,9 +201,17 @@
...(selectedType === IndexType.Spatial
? []
: [
{ value: '$id', label: '$id' },
{ value: '$createdAt', label: '$createdAt' },
{ value: '$updatedAt', label: '$updatedAt' }
{ value: '$id', label: '$id', leadingIcon: IconFingerPrint },
{
value: '$createdAt',
label: '$createdAt',
leadingIcon: IconCalendar
},
{
value: '$updatedAt',
label: '$updatedAt',
leadingIcon: IconCalendar
}
]),
...columnOptions
]}
@@ -34,8 +34,6 @@
})
)
);
await invalidate(Dependencies.TABLE);
showDelete = false;
addNotification({
type: 'success',
message:
@@ -43,7 +41,13 @@
? 'Index has been deleted'
: `${selectedKeys.length} indexes have been deleted`
});
trackEvent(Submit.IndexDelete);
await invalidate(Dependencies.TABLE);
showDelete = false;
selectedIndex = Array.isArray(selectedIndex) ? [] : null;
} catch (e) {
error = e.message;
trackError(e, Submit.IndexDelete);
@@ -21,6 +21,7 @@
import SpreadsheetContainer from './spreadsheet.svelte';
import { onDestroy, onMount } from 'svelte';
import { debounce } from '$lib/helpers/debounce';
import { columnOptions } from '../columns/store';
type Mode = 'rows' | 'rows-filtered' | 'indexes';
@@ -95,6 +96,7 @@
...col,
width: 180,
hide: false,
icon: columnOptions.find((colOpt) => colOpt.type === col?.type)?.icon,
...baseColProps
}));
@@ -323,20 +325,16 @@
background: linear-gradient(
180deg,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 0.855) 21%,
rgba(255, 255, 255, 0.86) 32.25%,
#ffffff 100%
);
z-index: 20;
display: flex;
justify-content: center;
transition: height 300ms cubic-bezier(0.4, 0, 0.2, 1);
transition: none !important;
height: var(--dynamic-overlay-height, 70.5vh);
&[data-collapsed-tabs='true'] {
height: calc(var(--dynamic-overlay-height, 79.1vh) + 8.6vh);
}
@media (max-width: 1024px) {
height: var(--dynamic-overlay-height, 63.35vh);
}
@@ -349,10 +347,9 @@
:global(.theme-dark) .spreadsheet-fade-bottom {
background: linear-gradient(
180deg,
rgba(25, 25, 28, 0.38) 13%,
rgba(25, 25, 28, 0.7) 21%,
rgba(25, 25, 28, 0.95) 38%,
var(--bgcolor-neutral-default, #19191c) 100%
rgba(29, 29, 33, 0) 0%,
rgba(29, 29, 33, 0.86) 21%,
#1d1d21 100%
);
}
@@ -5,16 +5,20 @@
import { Button, Form } from '$lib/elements/forms';
import { isTabletViewport } from '$lib/stores/viewport';
import { Badge, Divider, Layout, Sheet, Tag, Typography } from '@appwrite.io/pink-svelte';
import type { HTMLAttributes } from 'svelte/elements';
import { beforeNavigate } from '$app/navigation';
let {
show = $bindable(false),
title,
closeOnBlur = false,
submit,
cancel,
children = null,
footer = null,
titleBadge = null,
topAction = null
topAction = null,
...restProps
}: {
show: boolean;
title: string;
@@ -36,17 +40,27 @@
onClick?: () => boolean | void | Promise<boolean | void>;
}
| undefined;
cancel?:
| {
text?: string;
disabled?: boolean;
onClick?: () => void;
}
| undefined;
children?: Snippet;
footer?: Snippet | null;
} = $props();
} & HTMLAttributes<HTMLDivElement> = $props();
let form: Form;
let submitting = $state(writable(false));
let copyText = $state(undefined);
beforeNavigate(() => {
show = false;
});
</script>
<div class="sheet-container">
<div class="sheet-container" data-side-sheet-visible={show} {...restProps}>
<Sheet bind:open={show} {closeOnBlur}>
<div slot="header" style:width="100%">
<Layout.Stack direction="row" justifyContent="space-between" alignItems="center">
@@ -110,8 +124,19 @@
{#if footer}
{@render footer?.()}
{/if}
<Button size="s" secondary on:click={() => (show = false)}
>Cancel</Button>
<Button
size="s"
secondary
disabled={cancel?.disabled}
on:click={() => {
if (cancel?.onClick) {
cancel.onClick();
} else {
show = false;
}
}}>{cancel?.text ?? 'Cancel'}</Button>
<Button
size="s"
submit
@@ -31,5 +31,5 @@
min={column.min}
max={column.max}
required={column.required}
step={column.type === 'double' ? 'any' : 1}
step={column.type === 'double' ? 0.1 : 1}
leadingIcon={!limited ? IconHashtag : undefined} />
@@ -58,7 +58,7 @@
},
{} as Record<string, unknown>
),
permissions: [],
permissions: existingData?.$permissions ?? [],
columns: availableColumns
};
}
@@ -97,7 +97,7 @@
export let data: PageData;
export let showRowCreateSheet: {
show: boolean;
row: Models.Row | null;
row: Partial<Models.Row> | null;
};
$: rows = writable(data.rows);
@@ -297,7 +297,7 @@
switch (type) {
case 'string':
return IconText;
case 'float':
case 'double':
case 'integer':
return IconHashtag;
case 'boolean':
@@ -543,7 +543,13 @@
}
if (action === 'duplicate-row') {
showRowCreateSheet.row = row;
/**
* remove dates because
* console can override timestamps!
*/
const { $createdAt, $updatedAt, ...rowWithoutDates } = row;
showRowCreateSheet.row = rowWithoutDates;
showRowCreateSheet.show = true;
}
@@ -1147,7 +1153,7 @@
</Table.Root>
<Layout.Stack direction="column" gap="m">
<Alert.Inline>To change the selection edit the relationship settings.</Alert.Inline>
<Alert.Inline title="To change the selection edit the relationship settings." />
</Layout.Stack>
{:else}
<p class="u-bold">This action is irreversible.</p>
@@ -1211,6 +1217,10 @@
padding-inline: 8px !important;
}
& :global(.input:has([type^='date'])) {
padding: 12px !important;
}
& :global(.input:focus-within) {
top: 0 !important;
}
@@ -33,9 +33,12 @@ export const tableColumns = writable<Column[]>([]);
export const isCsvImportInProgress = writable(false);
export const columnsOrder = writable<string[]>([]);
export const columnsWidth = writable<{
export type ColumnsWidth = {
[columnId: string]: { fixed: number | { min: number }; resized: number };
}>();
};
export const columnsWidth = writable<ColumnsWidth>();
type DatabaseSheetOptions = {
show: boolean;
@@ -6,7 +6,7 @@
import { Id } from '$lib/components';
import { Dependencies } from '$lib/constants';
import { Button } from '$lib/elements/forms';
import { toLocaleDateTime } from '$lib/helpers/date';
import DualTimeView from '$lib/components/dualTimeView.svelte';
import { addNotification } from '$lib/stores/notifications';
import { canWriteTables } from '$lib/stores/roles';
import { sdk } from '$lib/stores/sdk';
@@ -14,6 +14,7 @@
import type { PageData } from './$types';
import { tableViewColumns } from './store';
import Confirm from '$lib/components/confirm.svelte';
import { subNavigation } from '$lib/stores/database';
export let data: PageData;
const databaseId = page.params.database;
@@ -38,7 +39,8 @@
type: 'success',
message: `${selectedTables.length} table${selectedTables.length > 1 ? 's' : ''} deleted`
});
invalidate(Dependencies.TABLES);
await invalidate(Dependencies.TABLES);
subNavigation.update();
} catch (error) {
addNotification({
type: 'error',
@@ -76,7 +78,7 @@
{:else if column.id === 'name'}
{table.name}
{:else}
{toLocaleDateTime(table[column.id])}
<DualTimeView time={table[column.id]} />
{/if}
</Table.Cell>
{/each}
@@ -11,7 +11,7 @@
import type { ComponentType } from 'svelte';
export let name: string;
export let id: string;
export let id: string | null;
export let entrypoint: string;
export let showEntrypoint = false;
export let runtime: string;
@@ -51,7 +51,7 @@
let isSubmitting = writable(false);
let name = '';
let id: string;
let id: string | null = null;
let runtime: Runtime;
let entrypoint = '';
let buildCommand = '';
@@ -46,7 +46,7 @@
let isSubmitting = writable(false);
let name = '';
let id: string;
let id: string | null = null;
let runtime: Runtime;
let entrypoint = '';
let buildCommand = '';
@@ -49,7 +49,7 @@
let isSubmitting = writable(false);
let name = data.template.name;
let id: string;
let id: string | null = null;
let runtime: Runtime;
let branch = 'main';
let rootDir = './';
@@ -86,10 +86,11 @@
</svelte:fragment>
</Alert.Inline>
{:else}
<Alert.Inline status="info" dismissible on:dismiss={() => (showAlert = false)}>
Some configuration changes are not live yet. Your function is redeploying —
changes will be applied once the build is complete.
</Alert.Inline>
<Alert.Inline
status="info"
dismissible
on:dismiss={() => (showAlert = false)}
title="Some configuration changes are not live yet. Your function is redeploying — changes will be applied once the build is complete." />
{/if}
{/if}
<Layout.Stack gap="xxxl">
@@ -21,6 +21,7 @@
import { Copy } from '$lib/components';
import { logStatusConverter } from './store';
import { LogsRequest, LogsResponse } from '$lib/components/logs';
import { timer } from '$lib/actions/timer';
export let selectedLogId: string;
export let logs: Models.Execution[];
@@ -139,7 +140,11 @@
Duration
</Typography.Text>
<Typography.Text variant="m-400">
{calculateTime(selectedLog.duration)}
{#if ['processing', 'waiting'].includes(selectedLog.status)}
<span use:timer={{ start: selectedLog.$createdAt }}></span>
{:else}
{calculateTime(selectedLog.duration)}
{/if}
</Typography.Text>
</Layout.Stack>
@@ -25,6 +25,7 @@
import { invalidate } from '$app/navigation';
import { Dependencies } from '$lib/constants';
import { Button } from '$lib/elements/forms';
import { timer } from '$lib/actions/timer';
export let columns: Column[];
export let executions: Models.ExecutionList;
@@ -121,7 +122,11 @@
</span>
</Tooltip>
{:else if column.id === 'duration'}
{calculateTime(log.duration)}
{#if ['processing', 'waiting'].includes(log.status)}
<span use:timer={{ start: log.$createdAt }}></span>
{:else}
{calculateTime(log.duration)}
{/if}
{/if}
</Table.Cell>
{/each}
@@ -4,7 +4,7 @@
import { Empty, EmptyFilter, EmptySearch, Id, PaginationWithLimit } from '$lib/components';
import { hasPageQueries } from '$lib/components/filters';
import { Button } from '$lib/elements/forms';
import { toLocaleDateTime } from '$lib/helpers/date';
import DualTimeView from '$lib/components/dualTimeView.svelte';
import { Container, ResponsiveContainerHeader } from '$lib/layout';
import { MessagingProviderType } from '@appwrite.io/console';
import CreateMessageDropdown from './createMessageDropdown.svelte';
@@ -49,10 +49,56 @@
filter: false,
width: { min: 140 }
},
{ id: 'providerType', title: 'Type', type: 'string', width: { min: 100 } },
{ id: 'status', title: 'Status', type: 'string', width: { min: 120 } },
{ id: 'scheduledAt', title: 'Scheduled at', type: 'datetime', width: { min: 120 } },
{ id: 'deliveredAt', title: 'Delivered at', type: 'datetime', width: { min: 120 } }
{
id: 'providerType',
title: 'Type',
type: 'string',
width: { min: 100 },
array: true,
format: 'enum',
elements: [
{ value: 'email', label: 'Email' },
{ value: 'sms', label: 'SMS' },
{ value: 'push', label: 'Push' }
]
},
{
id: 'status',
title: 'Status',
type: 'enum',
width: { min: 120 },
array: true,
format: 'enum',
elements: ['draft', 'scheduled', 'processing', 'sent', 'failed']
},
{
id: 'scheduledAt',
title: 'Scheduled at',
type: 'datetime',
width: { min: 120 },
format: 'datetime',
elements: [
{ value: 5 * 60 * 1000, label: 'last 5 minutes' },
{ value: 60 * 60 * 1000, label: 'last 1 hour' },
{ value: 24 * 60 * 60 * 1000, label: 'last 24 hours' },
{ value: 7 * 24 * 60 * 60 * 1000, label: 'last 7 days' },
{ value: 30 * 24 * 60 * 60 * 1000, label: 'last 30 days' }
]
},
{
id: 'deliveredAt',
title: 'Delivered at',
type: 'datetime',
width: { min: 120 },
format: 'datetime',
elements: [
{ value: 5 * 60 * 1000, label: 'last 5 minutes' },
{ value: 60 * 60 * 1000, label: 'last 1 hour' },
{ value: 24 * 60 * 60 * 1000, label: 'last 24 hours' },
{ value: 7 * 24 * 60 * 60 * 1000, label: 'last 7 days' },
{ value: 30 * 24 * 60 * 60 * 1000, label: 'last 30 days' }
]
}
]);
const region = page.params.region;
@@ -162,7 +208,7 @@
{#if !message[column.id]}
-
{:else}
{toLocaleDateTime(message[column.id])}
<DualTimeView time={message[column.id]} />
{/if}
{:else}
{message[column.id]}
@@ -23,7 +23,7 @@
let formComponent: Form;
let isSubmitting = writable(false);
let showCustomId = false;
let id: string;
let id: string | null = null;
let subject: string;
let content: string;
let topics: string[];
@@ -24,7 +24,7 @@
let formComponent: Form;
let isSubmitting = writable(false);
let showCustomId = false;
let id: string;
let id: string | null = null;
let file: Models.File;
let data: Writable<[string, string][]> = writable([['', '']]);
let title: string;
@@ -22,7 +22,7 @@
let formComponent: Form;
let isSubmitting = writable(false);
let showCustomId = false;
let id: string;
let id: string | null = null;
let content: string;
let topics: string[];
let users: string[];
@@ -1,6 +1,7 @@
<script lang="ts">
import { type Models, MessagingProviderType } from '@appwrite.io/console';
import { CardGrid, Empty, PaginationInline, EmptySearch } from '$lib/components';
import { CardGrid, Empty, PaginationInline } from '$lib/components';
import { Card, Empty as PinkEmpty } from '@appwrite.io/pink-svelte';
import TopicsModal from '../topicsModal.svelte';
import { sdk } from '$lib/stores/sdk';
import { invalidate } from '$app/navigation';
@@ -139,17 +140,21 @@
{:else if message.status === 'draft'}
<Empty on:click={() => (showTopics = true)}>Add a topic</Empty>
{:else}
<EmptySearch hidePagination>
<div class="u-text-center">
No topics have been selected.
<p>
Need a hand? Check out our <Button
href="https://appwrite.io/docs/products/messaging/topics">
documentation</Button
>.
</p>
</div>
</EmptySearch>
<Card.Base padding="none">
<PinkEmpty
type="secondary"
title="No topics were selected"
description="No topics were selected for this message. Need a hand? Check out our documentation.">
<svelte:fragment slot="actions">
<Button
secondary
href="https://appwrite.io/docs/products/messaging/topics"
external>
Documentation
</Button>
</svelte:fragment>
</PinkEmpty>
</Card.Base>
{/if}
</svelte:fragment>
<svelte:fragment slot="actions">
@@ -4,14 +4,14 @@
import { CustomId, LabelCard } from '$lib/components';
import { providers } from '../store';
import { InputText } from '$lib/elements/forms';
import { Badge, Icon } from '@appwrite.io/pink-svelte';
import { Tag, Icon } from '@appwrite.io/pink-svelte';
import { IconPencil } from '@appwrite.io/pink-icons-svelte';
import { Providers } from '../../provider.svelte';
import { SmtpEncryption } from '@appwrite.io/console';
let name = '';
let showCustomId = false;
let id: string = null;
let id: string | null = null;
$: {
name;
@@ -152,12 +152,10 @@
{#if !showCustomId}
<div>
<Badge
variant="secondary"
content="Provider ID"
on:click={() => (showCustomId = !showCustomId)}>
<Icon icon={IconPencil} size="s" slot="start" />
</Badge>
<Tag size="s" on:click={() => (showCustomId = !showCustomId)}>
<Icon icon={IconPencil} size="s" />
Provider ID
</Tag>
</div>
{:else}
<CustomId bind:show={showCustomId} name="Provider" bind:id autofocus={false} />
@@ -14,7 +14,9 @@
const dispatch = createEventDispatcher();
let name: string, id: string, error: string;
let name: string,
id: string | null = null,
error: string | null = null;
let showCustomId = false;
const create = async () => {
@@ -8,7 +8,7 @@
import { invalidate } from '$app/navigation';
import { Dependencies } from '$lib/constants';
import { sdk } from '$lib/stores/sdk';
import { toLocaleDateTime } from '$lib/helpers/date';
import DualTimeView from '$lib/components/dualTimeView.svelte';
import type { Column } from '$lib/helpers/types';
import { page } from '$app/state';
import { canWriteTopics } from '$lib/stores/roles';
@@ -72,7 +72,9 @@
<Id value={topic.$id}>{topic.$id}</Id>
{/key}
{:else if column.type === 'datetime'}
{topic[column.id] ? toLocaleDateTime(topic[column.id]) : '-'}
{#if topic[column.id]}
<DualTimeView time={topic[column.id]} />
{:else}-{/if}
{:else if column.id === 'total'}
{topic.emailTotal + topic.smsTotal + topic.pushTotal}
{:else}
@@ -8,7 +8,7 @@
import { addNotification } from '$lib/stores/notifications';
import type { PageData } from './$types';
import ProviderType from '../../providerType.svelte';
import { toLocaleDateTime } from '$lib/helpers/date';
import DualTimeView from '$lib/components/dualTimeView.svelte';
import { project } from '$routes/(console)/project-[region]-[project]/store';
import { sdk } from '$lib/stores/sdk';
import { page } from '$app/state';
@@ -109,7 +109,7 @@
{:else if column.id === 'type'}
<ProviderType type={subscriber.target.providerType} size="xs" />
{:else if column.id === '$createdAt'}
{toLocaleDateTime(subscriber[column.id])}
<DualTimeView time={subscriber[column.id]} />
{:else}
{subscriber[column.id]}
{/if}
@@ -6,7 +6,8 @@
import { Button } from '$lib/elements/forms';
import { canWriteKeys } from '$lib/stores/roles';
import type { Models } from '@appwrite.io/console';
import { diffDays, toLocaleDate, toLocaleDateTime } from '$lib/helpers/date';
import { diffDays } from '$lib/helpers/date';
import DualTimeView from '$lib/components/dualTimeView.svelte';
import { devKeyColumns, keyColumns, showDevKeysCreateModal } from '../store';
import { Badge, FloatingActionBar, Layout, Table } from '@appwrite.io/pink-svelte';
import DeleteBatch from './deleteBatch.svelte';
@@ -69,12 +70,20 @@
{key.name}
</Table.Cell>
<Table.Cell {root}>
{key.accessedAt ? toLocaleDate(key.accessedAt) : 'never'}
{#if key.accessedAt}
<DualTimeView time={key.accessedAt} />
{:else}
never
{/if}
</Table.Cell>
<Table.Cell {root}>
{@const expiration = getExpiryDetails(key)}
<Layout.Stack gap="s" direction="row">
{key.expire ? toLocaleDateTime(key.expire) : 'never'}
{#if key.expire}
<DualTimeView time={key.expire} />
{:else}
never
{/if}
{#if expiration.status}
<Badge
@@ -89,9 +89,12 @@
</svelte:fragment>
</Alert.Inline>
{:else}
<Alert.Inline status="info" dismissible on:dismiss={() => (showAlert = false)}>
Some configuration changes are not live yet. Your site is redeploying — changes
will be applied once the build is complete.
<Alert.Inline
status="info"
title="Some configuration changes are not live yet. Your site is redeploying — changes
will be applied once the build is complete."
dismissible
on:dismiss={() => (showAlert = false)}>
</Alert.Inline>
{/if}
{/if}
@@ -22,7 +22,10 @@
siteId: selectedDeployment.resourceId,
deploymentId: selectedDeployment.$id
});
await invalidate(Dependencies.SITE);
await Promise.all([
invalidate(Dependencies.DEPLOYMENTS),
invalidate(Dependencies.SITE)
]);
if (page.url.href.includes(`deployment-${selectedDeployment.$id}`)) {
await goto(
`${base}/project-${page.params.region}-${page.params.project}/sites/site-${page.params.site}/deployments`
@@ -60,7 +60,10 @@
trackError(error, Submit.DeploymentDelete);
} finally {
selectedRows = [];
invalidate(Dependencies.DEPLOYMENTS);
await Promise.all([
invalidate(Dependencies.DEPLOYMENTS),
invalidate(Dependencies.SITE)
]);
}
}
</script>
@@ -19,6 +19,7 @@
import { Copy } from '$lib/components';
import { LogsRequest, LogsResponse } from '$lib/components/logs';
import { site } from '../store';
import { timer } from '$lib/actions/timer';
export let open = false;
export let selectedLogId: string;
@@ -112,7 +113,12 @@
Duration
</Typography.Text>
<Typography.Text variant="m-400">
{calculateTime(selectedLog.duration)}
{#if ['processing', 'waiting'].includes(selectedLog.status)}
<span use:timer={{ start: selectedLog.$createdAt }}
></span>
{:else}
{calculateTime(selectedLog.duration)}
{/if}
</Typography.Text>
</Layout.Stack>
<Layout.Stack gap="xs" inline>
@@ -14,6 +14,7 @@
import { calculateTime } from '$lib/helpers/timeConversion';
import { getBadgeTypeFromStatusCode } from '$lib/helpers/httpStatus';
import { Button } from '$lib/elements/forms';
import { timer } from '$lib/actions/timer';
export let columns: Column[];
export let logs: Models.ExecutionList;
@@ -83,7 +84,11 @@
{log.requestMethod}
</Typography.Code>
{:else if column.id === 'duration'}
{calculateTime(log.duration)}
{#if ['processing', 'waiting'].includes(log.status)}
<span use:timer={{ start: log.$createdAt }}></span>
{:else}
{calculateTime(log.duration)}
{/if}
{:else if column.id === 'responseStatusCode'}
<div>
<Badge

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