feat: gate multi-db UI behind multi-db flag, fix stale HNSW order in createIndex

This commit is contained in:
Prem
2026-04-17 06:36:25 +01:00
parent 83b7668cc2
commit 13181eead3
6 changed files with 245 additions and 42 deletions
+3 -2
View File
@@ -5,7 +5,6 @@ import type { Models } from '@appwrite.io/console';
// Parse feature flags from env as a string array (exact match only)
const flagsRaw = (env.PUBLIC_CONSOLE_FEATURE_FLAGS ?? '').split(',');
// @ts-expect-error: unused method!
function isFlagEnabled(name: string) {
// loose generic to allow safe access while retaining type safety
return <T extends { account?: Account; organization?: Models.Organization }>(data: T) => {
@@ -19,4 +18,6 @@ function isFlagEnabled(name: string) {
};
}
export const flags = {};
export const flags = {
multiDb: isFlagEnabled('multi-db')
};
@@ -1,10 +1,12 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { page } from '$app/state';
import { PaginationWithLimit } from '$lib/components';
import { Empty, PaginationWithLimit } from '$lib/components';
import { Button } from '$lib/elements/forms';
import { Container, ResponsiveContainerHeader } from '$lib/layout';
import type { Models } from '@appwrite.io/console';
import Create from './create.svelte';
import Grid from './grid.svelte';
import { columns } from './store';
import Table from './table.svelte';
@@ -23,23 +25,47 @@
import { resolveRoute, withPath } from '$lib/stores/navigation';
import EmptyDatabaseCloud from './empty.svelte';
import { flags } from '$lib/flags';
import { user } from '$lib/stores/user';
import { project } from '../store';
const { data }: PageProps = $props();
let showCreate = $state(false);
const isMultiDb = $derived(flags.multiDb({ account: $user, organization: $organization }));
const isLimited = $derived(isServiceLimited('databases', $organization, data.databases.total));
async function handleCreate(event: CustomEvent<Models.Database>) {
showCreate = false;
await goto(
resolveRoute('/(console)/project-[region]-[project]/databases/database-[database]', {
...page.params,
database: event.detail.$id
})
);
}
async function goToCreateDatabaseWizard() {
await goto(
resolveRoute('/(console)/project-[region]-[project]/databases/create', page.params)
);
}
function triggerCreate() {
if (isMultiDb) {
goToCreateDatabaseWizard();
} else {
showCreate = true;
}
}
$effect(() => {
$registerCommands([
{
label: 'Create database',
callback: () => {
goToCreateDatabaseWizard();
triggerCreate();
},
keys: ['c'],
disabled: !$canWriteDatabases || isLimited,
@@ -56,7 +82,7 @@
{#if data.databases.total}
{#if data.view === 'grid'}
<Grid {data} onCreateDatabaseClick={goToCreateDatabaseWizard} />
<Grid {data} onCreateDatabaseClick={triggerCreate} />
{:else}
<Table
entities={data.entities}
@@ -77,7 +103,7 @@
size="s"
secondary>Clear Search</Button>
</EmptySearch>
{:else}
{:else if isMultiDb}
<EmptyDatabaseCloud
disabled={$canWriteDatabases}
onDatabaseTypeSelected={async (type) => {
@@ -91,9 +117,17 @@
)
);
}} />
{:else}
<Empty
single
target="database"
allowCreate={$canWriteDatabases}
on:click={() => (showCreate = true)} />
{/if}
</Container>
<Create bind:showCreate project={$project} on:created={handleCreate} />
{#snippet containerHeader()}
<ResponsiveContainerHeader
hasSearch
@@ -103,10 +137,7 @@
{#if $canWriteDatabases}
<Tooltip disabled={!isLimited} maxWidth={BODY_TOOLTIP_MAX_WIDTH}>
<div>
<Button
disabled={isLimited}
event="create_database"
on:click={goToCreateDatabaseWizard}>
<Button disabled={isLimited} event="create_database" on:click={triggerCreate}>
<Icon icon={IconPlus} slot="start" size="s" />
Create database
</Button>
@@ -0,0 +1,154 @@
<script lang="ts">
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { CustomId, Modal } from '$lib/components';
import { Button, InputText } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
import { BackupServices, ID, type Models } from '@appwrite.io/console';
import { createEventDispatcher } from 'svelte';
import { isCloud } from '$lib/system';
import { currentPlan } from '$lib/stores/organization';
import { getChangePlanUrl } from '$lib/stores/billing';
import CreatePolicy from './database-[database]/backups/createPolicy.svelte';
import { cronExpression, type UserBackupPolicy } from '$lib/helpers/backups';
import { Alert, Icon, Tag } from '@appwrite.io/pink-svelte';
import { IconPencil } from '@appwrite.io/pink-icons-svelte';
import { page } from '$app/state';
export let showCreate = false;
export let project: Models.Project;
let totalPolicies: UserBackupPolicy[] = [];
const dispatch = createEventDispatcher();
let name = '';
let id: string = null;
let showCustomId = false;
const trackEvents = (policies: UserBackupPolicy[]) => {
policies.forEach((policy: UserBackupPolicy) => {
let actualDay = null;
const monthlyBackupFrequency = policy.monthlyBackupFrequency;
switch (monthlyBackupFrequency) {
case 'first':
actualDay = '1st';
break;
case 'middle':
actualDay = '15th';
break;
case 'end':
default:
actualDay = '28th';
break;
}
const message = {
keepFor: `${policy.retained} days`,
frequency: policy.plainTextFrequency,
policy: policy.default ? 'preset' : 'custom'
};
if (actualDay) {
message['monthlyInterval'] = actualDay;
}
trackEvent('submit_policy_submit', message);
});
};
const createPolicies = async (resourceId: string) => {
if (!totalPolicies.length) return;
const totalPoliciesPromise = totalPolicies.map((policy) => {
cronExpression(policy);
return sdk.forProject(page.params.region, page.params.project).backups.createPolicy({
policyId: ID.unique(),
services: [BackupServices.Databases],
retention: policy.retained,
schedule: policy.schedule,
name: policy.label,
resourceId
});
});
await Promise.all(totalPoliciesPromise);
trackEvents(totalPolicies);
};
const create = async () => {
try {
const databaseId = id ? id : ID.unique();
const database = await sdk
.forProject(page.params.region, page.params.project)
.tablesDB.create({
databaseId,
name
});
await createPolicies(databaseId);
showCreate = false;
dispatch('created', database);
addNotification({
type: 'success',
message: `${name} has been created`
});
trackEvent(Submit.DatabaseCreate, {
customId: !!id
});
name = id = null;
} catch (error) {
addNotification({
type: 'error',
message: error.message
});
trackError(error, Submit.DatabaseCreate);
}
};
</script>
<Modal title="Create database" onSubmit={create} bind:show={showCreate}>
<InputText
id="name"
label="Name"
placeholder="Enter database name"
bind:value={name}
autofocus
required />
{#if !showCustomId}
<div>
<Tag
size="s"
on:click={() => {
showCustomId = true;
}}><Icon icon={IconPencil} /> Database ID</Tag>
</div>
{/if}
<CustomId bind:show={showCustomId} name="Database" bind:id autofocus={false} />
{#if isCloud}
{#if !$currentPlan?.backupsEnabled}
<Alert.Inline title="This database won't be backed up" status="warning">
Upgrade your plan to ensure your data stays safe and backed up.
<svelte:fragment slot="actions">
<Button compact href={getChangePlanUrl(project.teamId)}>Upgrade plan</Button>
</svelte:fragment>
</Alert.Inline>
{:else}
<CreatePolicy
{project}
bind:totalPolicies
bind:isShowing={showCreate}
title="Backup policies"
subtitle="Protect your data and ensure quick recovery by adding backup policies." />
{/if}
{/if}
<svelte:fragment slot="footer">
<Button secondary on:click={() => (showCreate = false)}>Cancel</Button>
<Button submit>Create</Button>
</svelte:fragment>
</Modal>
@@ -24,6 +24,9 @@
import type { PageProps } from './$types';
import { createDatabaseStore } from './store';
import { isTabletViewport } from '$lib/stores/viewport';
import { flags } from '$lib/flags';
import { user } from '$lib/stores/user';
import { organization } from '$lib/stores/organization';
const { data }: PageProps = $props();
@@ -42,30 +45,36 @@
const isDark = $derived($app.themeInUse === 'dark');
const backupsImg = $derived(isDark ? EmptyDarkMobile : EmptyLightMobile);
const isMultiDb = $derived(flags.multiDb({ account: $user, organization: $organization }));
const databaseTypes: Array<{
type: DatabaseType;
title: string;
subtitle: string;
}> = [
}> = $derived([
{
type: 'tablesdb',
type: 'tablesdb' as DatabaseType,
title: 'TablesDB',
subtitle:
'Structure your data in rows and columns. Best for relational data and advanced querying.'
},
{
type: 'documentsdb',
title: 'DocumentsDB',
subtitle:
'Store flexible data without a fixed schema. Best for unstructured data and simple querying.'
},
{
type: 'vectorsdb',
title: 'VectorsDB',
subtitle:
'Store data as vectors to find similar results. Best for semantic search and recommendations.'
}
];
...(isMultiDb
? [
{
type: 'documentsdb' as DatabaseType,
title: 'DocumentsDB',
subtitle:
'Store flexible data without a fixed schema. Best for unstructured data and simple querying.'
},
{
type: 'vectorsdb' as DatabaseType,
title: 'VectorsDB',
subtitle:
'Store data as vectors to find similar results. Best for semantic search and recommendations.'
}
]
: [])
]);
afterNavigate(({ from }) => (previousPage = from?.url?.pathname || previousPage));
@@ -132,10 +132,13 @@
}
});
// HNSW indexes: auto-fill embeddings, single field only
// HNSW indexes: auto-fill embeddings, single field only.
// When switching away to Key/Unique, reset stale `order: null` left over from HNSW.
$effect(() => {
if (isHnswType) {
fieldList = [{ value: 'embeddings', order: null, length: null }];
} else if (!isObjectType && fieldList.some((f) => f.order === null)) {
fieldList = [{ value: '', order: OrderBy.Asc, length: null }];
}
});
@@ -19,6 +19,9 @@
import { isSmallViewport } from '$lib/stores/viewport';
import type { DatabaseType } from '$database/(entity)';
import { flags } from '$lib/flags';
import { user } from '$lib/stores/user';
import { organization } from '$lib/stores/organization';
const {
disabled,
@@ -62,23 +65,25 @@
image: tablesDbImage
})}
<!-- documentsDB -->
{@render databaseTypeCard({
type: 'documentsdb',
title: 'DocumentsDB',
subtitle:
'Store flexible data without a fixed schema. Best for unstructured data and simple querying.',
image: documentsDbImage
})}
{#if flags.multiDb({ account: $user, organization: $organization })}
<!-- documentsDB -->
{@render databaseTypeCard({
type: 'documentsdb',
title: 'DocumentsDB',
subtitle:
'Store flexible data without a fixed schema. Best for unstructured data and simple querying.',
image: documentsDbImage
})}
<!-- vectorsDB -->
{@render databaseTypeCard({
type: 'vectorsdb',
title: 'VectorsDB',
subtitle:
'Store data as vectors to find similar results. Best for semantic search and recommendations.',
image: vectorsDbImage
})}
<!-- vectorsDB -->
{@render databaseTypeCard({
type: 'vectorsdb',
title: 'VectorsDB',
subtitle:
'Store data as vectors to find similar results. Best for semantic search and recommendations.',
image: vectorsDbImage
})}
{/if}
</Layout.Grid>
</Layout.Stack>
{/snippet}