mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
Merge branch 'main' into fix-usage-layout-shifts
This commit is contained in:
+3
-1
@@ -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
|
||||
@@ -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 }}"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
Generated
+15
-15
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -27,10 +27,12 @@
|
||||
id = null;
|
||||
}
|
||||
|
||||
if (!id?.length) {
|
||||
if (id !== null && !id.length) {
|
||||
id = null;
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (show) {
|
||||
trackEvent(Click.ShowCustomIdClick);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 |
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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}
|
||||
|
||||
+2
-2
@@ -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
-2
@@ -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]}
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+6
-8
@@ -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} />
|
||||
|
||||
+2
-2
@@ -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'}
|
||||
|
||||
+1259
File diff suppressed because it is too large
Load Diff
+82
@@ -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>
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
<script lang="ts">
|
||||
import IconAI from './ai.svelte';
|
||||
</script>
|
||||
|
||||
<IconAI notification />
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
export * from './store';
|
||||
export { default as Empty } from './empty.svelte';
|
||||
export { default as Input } from './input.svelte';
|
||||
+567
@@ -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>
|
||||
+81
@@ -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>
|
||||
+61
@@ -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}
|
||||
+151
@@ -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);
|
||||
+12
-11
@@ -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}`
|
||||
);
|
||||
}} />
|
||||
|
||||
+2
-1
@@ -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,
|
||||
|
||||
+6
@@ -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} />
|
||||
+59
-19
@@ -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>
|
||||
|
||||
+4
-2
@@ -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>
|
||||
|
||||
+20
-8
@@ -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>
|
||||
|
||||
+16
-1
@@ -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 />
|
||||
|
||||
+36
-16
@@ -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"
|
||||
|
||||
+159
-37
@@ -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>
|
||||
|
||||
+1
@@ -68,6 +68,7 @@
|
||||
array: false,
|
||||
...data
|
||||
});
|
||||
|
||||
$: listen(data);
|
||||
|
||||
$: handleDefaultState($required || $array);
|
||||
|
||||
+1
-1
@@ -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);
|
||||
|
||||
+3
-3
@@ -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"
|
||||
|
||||
+2
-1
@@ -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();
|
||||
|
||||
+88
-10
@@ -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>
|
||||
|
||||
+19
-6
@@ -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
|
||||
]}
|
||||
|
||||
+6
-2
@@ -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);
|
||||
|
||||
+7
-10
@@ -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%
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+30
-5
@@ -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
|
||||
|
||||
+1
-1
@@ -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} />
|
||||
|
||||
+1
-1
@@ -58,7 +58,7 @@
|
||||
},
|
||||
{} as Record<string, unknown>
|
||||
),
|
||||
permissions: [],
|
||||
permissions: existingData?.$permissions ?? [],
|
||||
columns: availableColumns
|
||||
};
|
||||
}
|
||||
|
||||
+14
-4
@@ -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;
|
||||
}
|
||||
|
||||
+5
-2
@@ -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;
|
||||
|
||||
+5
-3
@@ -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}
|
||||
|
||||
+1
-1
@@ -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;
|
||||
|
||||
+1
-1
@@ -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 = '';
|
||||
|
||||
+1
-1
@@ -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 = '';
|
||||
|
||||
+1
-1
@@ -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 = './';
|
||||
|
||||
+5
-4
@@ -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">
|
||||
|
||||
+6
-1
@@ -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>
|
||||
|
||||
|
||||
+6
-1
@@ -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]}
|
||||
|
||||
+1
-1
@@ -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[];
|
||||
|
||||
+1
-1
@@ -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;
|
||||
|
||||
+1
-1
@@ -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[];
|
||||
|
||||
+17
-12
@@ -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">
|
||||
|
||||
+6
-8
@@ -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}
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+6
-3
@@ -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}
|
||||
|
||||
+4
-1
@@ -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`
|
||||
|
||||
+4
-1
@@ -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
Reference in New Issue
Block a user