mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
feat: gate multi-db UI behind multi-db flag, fix stale HNSW order in createIndex
This commit is contained in:
+3
-2
@@ -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));
|
||||
|
||||
|
||||
+4
-1
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user