Merge branch 'feat-documentsdb' of https://github.com/appwrite/console into feat-documentsdb
@@ -93,7 +93,7 @@
|
||||
"flatted": "^3.4.2",
|
||||
"immutable": "^5.1.5",
|
||||
"minimatch": "10.2.3",
|
||||
"picomatch": "^2.3.2",
|
||||
"picomatch": "^4.0.4",
|
||||
"vite": "npm:rolldown-vite@latest",
|
||||
"yaml": "^1.10.3",
|
||||
},
|
||||
@@ -1184,7 +1184,7 @@
|
||||
|
||||
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
|
||||
"picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
|
||||
"picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
|
||||
|
||||
"playwright": ["playwright@1.58.2", "", { "dependencies": { "playwright-core": "1.58.2" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A=="],
|
||||
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
"immutable": "^5.1.5",
|
||||
"flatted": "^3.4.2",
|
||||
"yaml": "^1.10.3",
|
||||
"picomatch": "^2.3.2",
|
||||
"picomatch": "^4.0.4",
|
||||
"cookie": "^0.7.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
};
|
||||
|
||||
|
Before Width: | Height: | Size: 450 KiB After Width: | Height: | Size: 205 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 3.8 MiB |
|
Before Width: | Height: | Size: 450 KiB After Width: | Height: | Size: 205 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 3.8 MiB |
@@ -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,24 +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.'
|
||||
}
|
||||
];
|
||||
...(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));
|
||||
|
||||
@@ -276,7 +291,7 @@
|
||||
{/snippet}
|
||||
|
||||
{#snippet selectDatabaseType(disabled = false)}
|
||||
<Layout.Grid columns={2} columnsS={1}>
|
||||
<Layout.Grid columns={3} columnsS={1}>
|
||||
{#each databaseTypes as databaseType}
|
||||
<div class="card-selector">
|
||||
<Card.Selector
|
||||
|
||||
@@ -17,7 +17,8 @@ import type {
|
||||
Models,
|
||||
OrderBy,
|
||||
TablesDBIndexType,
|
||||
DocumentsDBIndexType
|
||||
DocumentsDBIndexType,
|
||||
VectorsDBIndexType
|
||||
} from '@appwrite.io/console';
|
||||
|
||||
export type DatabaseSdkResult = {
|
||||
@@ -35,6 +36,7 @@ export type DatabaseSdkResult = {
|
||||
entityId: string;
|
||||
name: string;
|
||||
databaseType?: DatabaseType;
|
||||
dimension?: number /* vectorsDB specific */;
|
||||
}) => Promise<Entity>;
|
||||
getEntity: (params: {
|
||||
databaseId: string;
|
||||
@@ -53,6 +55,15 @@ export type DatabaseSdkResult = {
|
||||
entityId: string;
|
||||
databaseType?: DatabaseType;
|
||||
}) => Promise<{}>;
|
||||
updateEntity: (params: {
|
||||
databaseId: string;
|
||||
entityId: string;
|
||||
name?: string;
|
||||
permissions?: string[];
|
||||
documentSecurity?: boolean;
|
||||
enabled?: boolean;
|
||||
databaseType?: DatabaseType;
|
||||
}) => Promise<Entity>;
|
||||
createRecord: (params: {
|
||||
databaseId: string;
|
||||
entityId: string;
|
||||
@@ -98,8 +109,30 @@ export type DatabaseSdkResult = {
|
||||
orders?: OrderBy[];
|
||||
databaseType?: DatabaseType;
|
||||
}) => Promise<Index>;
|
||||
deleteIndex: (params: {
|
||||
databaseId: string;
|
||||
entityId: string;
|
||||
key: string;
|
||||
databaseType?: DatabaseType;
|
||||
}) => Promise<{}>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the raw DocumentsDB or VectorsDB SDK service for a given database type.
|
||||
* Use in load functions (.ts) where Svelte runes aren't available.
|
||||
*/
|
||||
export function getCollectionService(region: string, project: string, type: DatabaseType) {
|
||||
const projectSdk = sdk.forProject(region, project);
|
||||
switch (type) {
|
||||
case 'documentsdb':
|
||||
return projectSdk.documentsDB;
|
||||
case 'vectorsdb':
|
||||
return projectSdk.vectorsDB;
|
||||
default:
|
||||
throw new Error(`Unsupported collection database type: ${type}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function useDatabaseSdk(
|
||||
regionOrPage: string | Page,
|
||||
projectOrTerminology: string | TerminologyResult,
|
||||
@@ -131,8 +164,9 @@ export function useDatabaseSdk(
|
||||
case 'documentsdb': {
|
||||
return await baseSdk.documentsDB.create(params);
|
||||
}
|
||||
case 'vectorsdb':
|
||||
throw new Error('Database type not supported yet');
|
||||
case 'vectorsdb': {
|
||||
return await baseSdk.vectorsDB.create(params);
|
||||
}
|
||||
default:
|
||||
throw new Error('Unknown database type');
|
||||
}
|
||||
@@ -141,7 +175,8 @@ export function useDatabaseSdk(
|
||||
async list(params): Promise<Models.DatabaseList> {
|
||||
const results = await Promise.all([
|
||||
baseSdk.tablesDB.list(params),
|
||||
baseSdk.documentsDB.list(params)
|
||||
baseSdk.documentsDB.list(params),
|
||||
baseSdk.vectorsDB.list(params)
|
||||
]);
|
||||
|
||||
return results.reduce(
|
||||
@@ -171,8 +206,15 @@ export function useDatabaseSdk(
|
||||
|
||||
return toSupportiveEntity(table);
|
||||
}
|
||||
case 'vectorsdb':
|
||||
throw new Error('Database type not supported yet');
|
||||
case 'vectorsdb': {
|
||||
const collection = await baseSdk.vectorsDB.createCollection({
|
||||
...params,
|
||||
dimension: params.dimension,
|
||||
collectionId: params.entityId
|
||||
});
|
||||
|
||||
return toSupportiveEntity(collection);
|
||||
}
|
||||
default:
|
||||
throw new Error('Unknown database type');
|
||||
}
|
||||
@@ -190,8 +232,10 @@ export function useDatabaseSdk(
|
||||
await baseSdk.documentsDB.listCollections(params);
|
||||
return { total, entities: collections.map(toSupportiveEntity) };
|
||||
}
|
||||
case 'vectorsdb':
|
||||
throw new Error(`Database type not supported yet`);
|
||||
case 'vectorsdb': {
|
||||
const { total, collections } = await baseSdk.vectorsDB.listCollections(params);
|
||||
return { total, entities: collections.map(toSupportiveEntity) };
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unknown database type`);
|
||||
}
|
||||
@@ -215,8 +259,14 @@ export function useDatabaseSdk(
|
||||
|
||||
return toSupportiveEntity(collection);
|
||||
}
|
||||
case 'vectorsdb':
|
||||
throw new Error(`Database type not supported yet`);
|
||||
case 'vectorsdb': {
|
||||
const collection = await baseSdk.vectorsDB.getCollection({
|
||||
databaseId: params.databaseId,
|
||||
collectionId: params.entityId
|
||||
});
|
||||
|
||||
return toSupportiveEntity(collection);
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unknown database type`);
|
||||
}
|
||||
@@ -230,7 +280,7 @@ export function useDatabaseSdk(
|
||||
case 'documentsdb':
|
||||
return await baseSdk.documentsDB.delete(params);
|
||||
case 'vectorsdb':
|
||||
throw new Error('Database type not supported yet');
|
||||
return await baseSdk.vectorsDB.delete(params);
|
||||
default:
|
||||
throw new Error(`Unknown database type`);
|
||||
}
|
||||
@@ -250,7 +300,51 @@ export function useDatabaseSdk(
|
||||
collectionId: params.entityId
|
||||
});
|
||||
case 'vectorsdb':
|
||||
throw new Error('Database type not supported yet');
|
||||
return await baseSdk.vectorsDB.deleteCollection({
|
||||
databaseId: params.databaseId,
|
||||
collectionId: params.entityId
|
||||
});
|
||||
default:
|
||||
throw new Error(`Unknown database type`);
|
||||
}
|
||||
},
|
||||
|
||||
async updateEntity(params) {
|
||||
switch (type ?? params.databaseType) {
|
||||
case 'legacy': /* databases api */
|
||||
case 'tablesdb':
|
||||
return toSupportiveEntity(
|
||||
await baseSdk.tablesDB.updateTable({
|
||||
databaseId: params.databaseId,
|
||||
tableId: params.entityId,
|
||||
name: params.name,
|
||||
permissions: params.permissions,
|
||||
rowSecurity: params.documentSecurity,
|
||||
enabled: params.enabled
|
||||
})
|
||||
);
|
||||
case 'documentsdb':
|
||||
return toSupportiveEntity(
|
||||
await baseSdk.documentsDB.updateCollection({
|
||||
databaseId: params.databaseId,
|
||||
collectionId: params.entityId,
|
||||
name: params.name,
|
||||
permissions: params.permissions,
|
||||
documentSecurity: params.documentSecurity,
|
||||
enabled: params.enabled
|
||||
})
|
||||
);
|
||||
case 'vectorsdb':
|
||||
return toSupportiveEntity(
|
||||
await baseSdk.vectorsDB.updateCollection({
|
||||
databaseId: params.databaseId,
|
||||
collectionId: params.entityId,
|
||||
name: params.name,
|
||||
permissions: params.permissions,
|
||||
documentSecurity: params.documentSecurity,
|
||||
enabled: params.enabled
|
||||
})
|
||||
);
|
||||
default:
|
||||
throw new Error(`Unknown database type`);
|
||||
}
|
||||
@@ -275,8 +369,15 @@ export function useDatabaseSdk(
|
||||
data: params.data,
|
||||
permissions: params.permissions
|
||||
});
|
||||
case 'vectorsdb':
|
||||
throw new Error('Database type not supported yet');
|
||||
case 'vectorsdb': {
|
||||
return await baseSdk.vectorsDB.createDocument({
|
||||
databaseId: params.databaseId,
|
||||
collectionId: params.entityId,
|
||||
documentId: params.recordId,
|
||||
data: params.data,
|
||||
permissions: params.permissions
|
||||
});
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unknown database type`);
|
||||
}
|
||||
@@ -301,8 +402,15 @@ export function useDatabaseSdk(
|
||||
data: params.data,
|
||||
permissions: params.permissions
|
||||
});
|
||||
case 'vectorsdb':
|
||||
throw new Error('Database type not supported yet');
|
||||
case 'vectorsdb': {
|
||||
return await baseSdk.vectorsDB.upsertDocument({
|
||||
databaseId: params.databaseId,
|
||||
collectionId: params.entityId,
|
||||
documentId: params.recordId,
|
||||
data: params.data,
|
||||
permissions: params.permissions
|
||||
});
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unknown database type`);
|
||||
}
|
||||
@@ -325,8 +433,14 @@ export function useDatabaseSdk(
|
||||
documentId: params.recordId,
|
||||
permissions: params.permissions
|
||||
});
|
||||
case 'vectorsdb':
|
||||
throw new Error('Database type not supported yet');
|
||||
case 'vectorsdb': {
|
||||
return await baseSdk.vectorsDB.upsertDocument({
|
||||
databaseId: params.databaseId,
|
||||
collectionId: params.entityId,
|
||||
documentId: params.recordId,
|
||||
permissions: params.permissions
|
||||
});
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unknown database type`);
|
||||
}
|
||||
@@ -351,8 +465,19 @@ export function useDatabaseSdk(
|
||||
});
|
||||
return toSupportiveRecord(document);
|
||||
}
|
||||
case 'vectorsdb':
|
||||
throw new Error('Database type not supported yet');
|
||||
case 'vectorsdb': {
|
||||
if (!params.recordId) {
|
||||
throw new Error('Record ID is required to delete a VectorsDB document');
|
||||
}
|
||||
|
||||
const document = await baseSdk.vectorsDB.deleteDocument({
|
||||
databaseId: params.databaseId,
|
||||
collectionId: params.entityId,
|
||||
documentId: params.recordId
|
||||
});
|
||||
|
||||
return toSupportiveRecord(document);
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unknown database type`);
|
||||
}
|
||||
@@ -377,8 +502,15 @@ export function useDatabaseSdk(
|
||||
});
|
||||
return { total, records: documents.map(toSupportiveRecord) };
|
||||
}
|
||||
case 'vectorsdb':
|
||||
throw new Error('Database type not supported yet');
|
||||
case 'vectorsdb': {
|
||||
const { total, documents } = await baseSdk.vectorsDB.deleteDocuments({
|
||||
databaseId: params.databaseId,
|
||||
collectionId: params.entityId,
|
||||
queries: params.queries
|
||||
});
|
||||
|
||||
return { total, records: documents.map(toSupportiveRecord) };
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unknown database type`);
|
||||
}
|
||||
@@ -411,8 +543,45 @@ export function useDatabaseSdk(
|
||||
});
|
||||
return toSupportiveIndex(index);
|
||||
}
|
||||
case 'vectorsdb': {
|
||||
const index = await baseSdk.vectorsDB.createIndex({
|
||||
databaseId: params.databaseId,
|
||||
collectionId: params.entityId,
|
||||
key: params.key,
|
||||
type: params.type as VectorsDBIndexType,
|
||||
attributes: params.attributes,
|
||||
lengths: params.lengths,
|
||||
orders: params.orders
|
||||
});
|
||||
|
||||
return toSupportiveIndex(index);
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unknown database type`);
|
||||
}
|
||||
},
|
||||
|
||||
async deleteIndex(params) {
|
||||
switch (type ?? params.databaseType) {
|
||||
case 'legacy': /* databases api */
|
||||
case 'tablesdb':
|
||||
return await baseSdk.tablesDB.deleteIndex({
|
||||
databaseId: params.databaseId,
|
||||
tableId: params.entityId,
|
||||
key: params.key
|
||||
});
|
||||
case 'documentsdb':
|
||||
return await baseSdk.documentsDB.deleteIndex({
|
||||
databaseId: params.databaseId,
|
||||
collectionId: params.entityId,
|
||||
key: params.key
|
||||
});
|
||||
case 'vectorsdb':
|
||||
throw new Error('Database type not supported yet');
|
||||
return await baseSdk.vectorsDB.deleteIndex({
|
||||
databaseId: params.databaseId,
|
||||
collectionId: params.entityId,
|
||||
key: params.key
|
||||
});
|
||||
default:
|
||||
throw new Error(`Unknown database type`);
|
||||
}
|
||||
|
||||
@@ -6,10 +6,13 @@ import type { Attributes, Collection, Columns, Table } from '$database/store';
|
||||
import type { Term, TerminologyResult, TerminologyShape } from '$database/(entity)/helpers/types';
|
||||
|
||||
type BaseTerminology = typeof baseTerminology;
|
||||
type ImplementedDBTypes = Omit<BaseTerminology, 'vectorsdb' | 'legacy'>;
|
||||
type ImplementedDBTypes = Omit<BaseTerminology, 'legacy'>;
|
||||
|
||||
/* manual type for the time being because vectorsdb is pending */
|
||||
export type DatabaseType = 'legacy' | 'tablesdb' | 'documentsdb' | 'vectorsdb';
|
||||
export type CollectionDatabaseType = Extract<DatabaseType, 'documentsdb' | 'vectorsdb'>;
|
||||
|
||||
export const DEFAULT_VECTOR_DIMENSION = 768;
|
||||
|
||||
export type RecordType = ImplementedDBTypes[keyof ImplementedDBTypes]['record'];
|
||||
|
||||
@@ -18,6 +21,7 @@ export type Entity = Partial<Collection | Table> & {
|
||||
indexes?: Index[];
|
||||
fields?: (Attributes | Columns)[];
|
||||
recordSecurity?: Models.Collection['documentSecurity'] | Models.Table['rowSecurity'];
|
||||
dimension?: number;
|
||||
};
|
||||
|
||||
export type Field = Partial<Attributes> | Partial<Columns>;
|
||||
@@ -62,7 +66,11 @@ export const baseTerminology = {
|
||||
field: 'attribute',
|
||||
record: 'document'
|
||||
},
|
||||
vectorsdb: {}
|
||||
vectorsdb: {
|
||||
entity: 'collection',
|
||||
field: 'attribute',
|
||||
record: 'document'
|
||||
}
|
||||
} as const;
|
||||
|
||||
const createTerm = (singular: string, pluralForm: string): Term => {
|
||||
|
||||
@@ -4,14 +4,14 @@
|
||||
import { Modal, CustomId } from '$lib/components';
|
||||
import { subNavigation } from '$lib/stores/database';
|
||||
import { ID } from '@appwrite.io/console';
|
||||
import { Button, InputText } from '$lib/elements/forms';
|
||||
import { Button, InputNumber, InputText } from '$lib/elements/forms';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import {
|
||||
Input as SuggestionsInput,
|
||||
entityColumnSuggestions
|
||||
} from '$database/(suggestions)/index';
|
||||
|
||||
import { getTerminologies } from '../helpers';
|
||||
import { getTerminologies, DEFAULT_VECTOR_DIMENSION } from '$database/(entity)';
|
||||
import { resetSampleFieldsConfig } from '$database/store';
|
||||
|
||||
let {
|
||||
@@ -21,7 +21,7 @@
|
||||
}: {
|
||||
show: boolean;
|
||||
useSuggestions?: boolean;
|
||||
onCreateEntity: (id: string, name: string) => Promise<void>;
|
||||
onCreateEntity: (id: string, name: string, dimension?: number) => Promise<void>;
|
||||
} = $props();
|
||||
|
||||
const { analytics, terminology } = getTerminologies();
|
||||
@@ -29,12 +29,14 @@
|
||||
const lower = terminology.entity.lower.singular;
|
||||
const title = terminology.entity.title.singular;
|
||||
const analyticsCreateSubmit = analytics.submit.entity('Create');
|
||||
const isVectorsDb = terminology.type === 'vectorsdb';
|
||||
|
||||
// example - `table-[table]`, `collection-[collection]`
|
||||
const isOnEntitiesPage = $derived(page.route?.id.endsWith(`${lower}-[${lower}]`));
|
||||
|
||||
let name = $state('');
|
||||
let id = $state(null);
|
||||
let dimension = $state(DEFAULT_VECTOR_DIMENSION);
|
||||
let error = $state(null);
|
||||
let creatingEntity = $state(false);
|
||||
|
||||
@@ -64,7 +66,7 @@
|
||||
enableThinkingModeForSuggestions(finalId, name);
|
||||
|
||||
// create entity.
|
||||
await onCreateEntity(finalId, name);
|
||||
await onCreateEntity(finalId, name, isVectorsDb ? dimension : undefined);
|
||||
|
||||
// cleanup
|
||||
updateAndCleanup();
|
||||
@@ -121,6 +123,16 @@
|
||||
|
||||
<CustomId show bind:id required={false} autofocus={false} name={title} syncFrom={name} />
|
||||
|
||||
{#if isVectorsDb}
|
||||
<InputNumber
|
||||
id="dimension"
|
||||
label="Vector dimension"
|
||||
bind:value={dimension}
|
||||
min={1}
|
||||
max={4096}
|
||||
required />
|
||||
{/if}
|
||||
|
||||
{#if useSuggestions}
|
||||
<SuggestionsInput showSampleCountPicker={!terminology.schema} />
|
||||
{/if}
|
||||
|
||||
@@ -292,7 +292,7 @@
|
||||
|
||||
const spreadsheetColumns = $derived.by(() => {
|
||||
return isRecordMode
|
||||
? type !== 'documentsdb'
|
||||
? type === 'tablesdb' || type === 'legacy'
|
||||
? getRowColumns()
|
||||
: getDocumentsDbColumns()
|
||||
: getIndexesColumns();
|
||||
@@ -382,7 +382,7 @@
|
||||
|
||||
{#snippet noSqlEditor()}
|
||||
{#if showNoSqlEditor}
|
||||
{#if type === 'documentsdb' && mode === 'records'}
|
||||
{#if (type === 'documentsdb' || type === 'vectorsdb') && mode === 'records'}
|
||||
<NoSqlEditor loading />
|
||||
{/if}
|
||||
{/if}
|
||||
@@ -411,7 +411,8 @@
|
||||
</Layout.Stack>
|
||||
|
||||
{#if showActions && actions}
|
||||
{@const isOnlyIndexes = mode === 'indexes' && type === 'documentsdb'}
|
||||
{@const isOnlyIndexes =
|
||||
mode === 'indexes' && (type === 'documentsdb' || type === 'vectorsdb')}
|
||||
{@const inline = mode === 'records-filtered' || isOnlyIndexes}
|
||||
<div class="controlled-width" class:single-mode={isOnlyIndexes}>
|
||||
<Layout.Stack
|
||||
@@ -504,7 +505,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
&[data-mode='records'][data-type='documentsdb'] {
|
||||
&[data-mode='records'][data-type='documentsdb'],
|
||||
&[data-mode='records'][data-type='vectorsdb'] {
|
||||
position: unset;
|
||||
// disable animation when not loading!
|
||||
&[data-loading='false'] :global(.skeleton) {
|
||||
|
||||
@@ -35,6 +35,8 @@
|
||||
const record = terminology.record.lower;
|
||||
const entity = terminology.entity.lower.singular;
|
||||
|
||||
const isSchemaless = type === 'documentsdb' || type === 'vectorsdb';
|
||||
|
||||
const title = $derived.by(() => {
|
||||
switch (type) {
|
||||
default:
|
||||
@@ -45,20 +47,19 @@
|
||||
: `Smart ${field.singular} suggestions available on Cloud`;
|
||||
|
||||
case 'documentsdb':
|
||||
case 'vectorsdb':
|
||||
return featureActive ? `Sample Data` : `Sample Data available on Cloud`;
|
||||
}
|
||||
});
|
||||
|
||||
const subtitle = $derived.by(() => {
|
||||
const isDocs = type === 'documentsdb';
|
||||
|
||||
if (featureActive) {
|
||||
return isDocs
|
||||
return isSchemaless
|
||||
? `Generate sample ${record.plural} based on your ${entity} name`
|
||||
: `Enable AI to suggest useful ${field.plural} based on your ${entity} name`;
|
||||
}
|
||||
|
||||
return isDocs
|
||||
return isSchemaless
|
||||
? `Sign up for Cloud to generate sample documents based on your ${entity} name`
|
||||
: `Sign up for Cloud to generate ${field.plural} based on your ${entity} name`;
|
||||
});
|
||||
|
||||
@@ -42,11 +42,12 @@
|
||||
|
||||
$registerSearchers(tablesSearcher);
|
||||
|
||||
async function createEntity(entityId: string, name: string) {
|
||||
async function createEntity(entityId: string, name: string, dimension?: number) {
|
||||
const entity = await databaseSdk.createEntity({
|
||||
databaseId,
|
||||
entityId,
|
||||
name
|
||||
name,
|
||||
dimension
|
||||
});
|
||||
|
||||
await invalidate(Dependencies.DATABASE);
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import { Modal } from '$lib/components';
|
||||
import { Button, InputTextarea } from '$lib/elements/forms';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { Layout, Icon } from '@appwrite.io/pink-svelte';
|
||||
import { IconInfo } from '@appwrite.io/pink-icons-svelte';
|
||||
|
||||
let {
|
||||
show = $bindable(false),
|
||||
onGenerate
|
||||
}: {
|
||||
show: boolean;
|
||||
onGenerate: (embeddings: number[]) => void;
|
||||
} = $props();
|
||||
|
||||
let content = $state('');
|
||||
let generating = $state(false);
|
||||
|
||||
const MAX_LENGTH = 25000;
|
||||
|
||||
async function generate() {
|
||||
if (!content.trim()) return;
|
||||
|
||||
generating = true;
|
||||
try {
|
||||
const response = await sdk
|
||||
.forProject(page.params.region, page.params.project)
|
||||
.vectorsDB.createTextEmbeddings({ texts: [content.trim()] });
|
||||
|
||||
const embedding = response?.embeddings?.[0]?.embedding;
|
||||
if (embedding?.length) {
|
||||
onGenerate(embedding);
|
||||
content = '';
|
||||
show = false;
|
||||
} else {
|
||||
const error = response?.embeddings?.[0]?.error;
|
||||
throw new Error(error || 'Failed to generate embeddings');
|
||||
}
|
||||
} catch (e) {
|
||||
addNotification({ type: 'error', message: e instanceof Error ? e.message : String(e) });
|
||||
} finally {
|
||||
generating = false;
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (!show) {
|
||||
content = '';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<Modal size="m" bind:show title="Text for embedding" onSubmit={generate}>
|
||||
<p class="u-margin-block-end-8">
|
||||
Enter the content you want to convert into a vector. This will allow semantic search and
|
||||
similarity matching.
|
||||
</p>
|
||||
|
||||
<InputTextarea
|
||||
id="embedding-content"
|
||||
label="Content to embed"
|
||||
placeholder="Paste or type your text here..."
|
||||
bind:value={content}
|
||||
maxlength={MAX_LENGTH}
|
||||
autofocus
|
||||
required />
|
||||
|
||||
<Layout.Stack direction="row" gap="xs" alignItems="center">
|
||||
<Icon icon={IconInfo} size="s" />
|
||||
<span class="u-color-text-offline"
|
||||
>Embeddings are generated using Embedding Gemma model</span>
|
||||
</Layout.Stack>
|
||||
|
||||
<svelte:fragment slot="footer">
|
||||
<Button secondary disabled={generating} on:click={() => (show = false)}>Cancel</Button>
|
||||
<Button
|
||||
submit
|
||||
disabled={generating || !content.trim()}
|
||||
submissionLoader
|
||||
forceShowLoader={generating}>Generate</Button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
@@ -15,6 +15,7 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
EditorView,
|
||||
ViewPlugin,
|
||||
keymap,
|
||||
lineNumbers,
|
||||
highlightActiveLine,
|
||||
@@ -44,11 +45,12 @@
|
||||
import type { Text as CmText } from '@codemirror/state';
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import Id, { truncateId } from '$lib/components/id.svelte';
|
||||
import { Icon, Layout, Skeleton, Tooltip } from '@appwrite.io/pink-svelte';
|
||||
import { Badge, Icon, Layout, Skeleton, Tooltip, Typography } from '@appwrite.io/pink-svelte';
|
||||
import { IconDuplicate } from '@appwrite.io/pink-icons-svelte';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { copy } from '$lib/helpers/copy';
|
||||
import { isSmallViewport } from '$lib/stores/viewport';
|
||||
import { isMac } from '$lib/helpers/platform';
|
||||
|
||||
import { makeErrorMessage } from './helpers/errorMessages';
|
||||
import { customTheme, customSyntaxHighlighting } from './helpers/theme';
|
||||
@@ -81,6 +83,7 @@
|
||||
import { sleep } from '$lib/helpers/promises';
|
||||
import { toLocaleDateTime } from '$lib/helpers/date';
|
||||
import { Suggestions, Error as ErrorSonner, Save as SavingSonner } from '../sonners';
|
||||
import HintBadge from '../hintBadge.svelte';
|
||||
import { json5, json5ParseCache, json5ParseLinter } from 'codemirror-json5';
|
||||
import { SvelteMap } from 'svelte/reactivity';
|
||||
|
||||
@@ -100,6 +103,8 @@
|
||||
showSuggestions?: boolean;
|
||||
suggestedAttributes?: string[];
|
||||
showMockSuggestions?: boolean;
|
||||
suggestedDefaults?: Record<string, unknown>;
|
||||
onGenerateEmbedding?: () => void;
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -117,7 +122,9 @@
|
||||
showHeaderActions = true,
|
||||
showSuggestions = false,
|
||||
suggestedAttributes = [],
|
||||
showMockSuggestions = false
|
||||
showMockSuggestions = false,
|
||||
suggestedDefaults,
|
||||
onGenerateEmbedding
|
||||
}: Props = $props();
|
||||
|
||||
let editorContainer: HTMLDivElement = $state(null);
|
||||
@@ -143,6 +150,9 @@
|
||||
|
||||
let tooltipMessage = $state('Copy document');
|
||||
|
||||
// Cache embeddings values for the fold placeholder (survives fold state changes)
|
||||
let cachedEmbeddingsPreview = '';
|
||||
|
||||
// Store the original data to preserve system values
|
||||
let originalData = $state<JsonValue>(data);
|
||||
|
||||
@@ -197,7 +207,7 @@
|
||||
|
||||
if (filteredEntries.length === 0) return '{}';
|
||||
|
||||
// Sort entries: $id first, user fields in middle, timestamps last
|
||||
// Sort entries: $id first, metadata before embeddings, timestamps last
|
||||
const sortedEntries = filteredEntries.sort(([keyA], [keyB]) => {
|
||||
// $id always comes first
|
||||
if (keyA === '$id') return -1;
|
||||
@@ -210,6 +220,10 @@
|
||||
if (isKeyATimestamp && !isKeyBTimestamp) return 1;
|
||||
if (!isKeyATimestamp && isKeyBTimestamp) return -1;
|
||||
|
||||
// metadata before embeddings
|
||||
if (keyA === 'metadata' && keyB === 'embeddings') return -1;
|
||||
if (keyA === 'embeddings' && keyB === 'metadata') return 1;
|
||||
|
||||
return 0;
|
||||
});
|
||||
|
||||
@@ -228,6 +242,11 @@
|
||||
const items = value as JsonArray;
|
||||
if (items.length === 0) return '[]';
|
||||
|
||||
// Render embeddings as 3 lines: [ / values / ] — enables native fold gutter
|
||||
if (key === 'embeddings' && items.length > 0 && typeof items[0] === 'number') {
|
||||
return `[\n${items.join(',')}\n${indentStr}]`;
|
||||
}
|
||||
|
||||
const elements = items.map((item, index) => {
|
||||
const isLast = index === items.length - 1;
|
||||
const formattedValue = dataToString(item, indent + 1);
|
||||
@@ -278,6 +297,27 @@
|
||||
return serialized;
|
||||
}
|
||||
|
||||
export function replaceData(newData: JsonValue) {
|
||||
if (!editorView) return;
|
||||
data = newData;
|
||||
const content = dataToString(newData);
|
||||
lastExpectedContent = content;
|
||||
lastSerializedData = null;
|
||||
isUpdatingFromEditor = true;
|
||||
const currentContent = editorView.state.doc.toString();
|
||||
editorView.dispatch({
|
||||
changes: { from: 0, to: currentContent.length, insert: content },
|
||||
annotations: [Transaction.addToHistory.of(false)]
|
||||
});
|
||||
editorView.requestMeasure({
|
||||
read() {},
|
||||
write(_measure, view) {
|
||||
foldEmbeddings(view);
|
||||
}
|
||||
});
|
||||
queueMicrotask(() => (isUpdatingFromEditor = false));
|
||||
}
|
||||
|
||||
function findNewDocCursorPos(state: EditorState): number | null {
|
||||
const maxLines = Math.min(state.doc.lines, 12);
|
||||
for (let ln = 1; ln <= maxLines; ln += 1) {
|
||||
@@ -940,10 +980,10 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// Create an object with suggested attributes as empty strings
|
||||
const suggestedObject: Record<string, string> = {};
|
||||
// Create an object with suggested attributes
|
||||
const suggestedObject: Record<string, unknown> = {};
|
||||
for (const attr of suggestedAttributes) {
|
||||
suggestedObject[attr] = '';
|
||||
suggestedObject[attr] = suggestedDefaults?.[attr] ?? '';
|
||||
}
|
||||
|
||||
// Merge with existing data (keeping system fields)
|
||||
@@ -1005,6 +1045,40 @@
|
||||
undo(editorView);
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-fold the embeddings array. Uses multi-line format so the
|
||||
* native fold gutter handles expand/collapse with the same chevron as metadata.
|
||||
*/
|
||||
function foldEmbeddings(view: EditorView) {
|
||||
const doc = view.state.doc;
|
||||
for (let ln = 1; ln <= doc.lines; ln++) {
|
||||
const line = doc.line(ln);
|
||||
const match = line.text.match(/^(\s*embeddings:\s*\[)/);
|
||||
if (!match) continue;
|
||||
// Find closing ] line and cache values for fold placeholder
|
||||
for (let endLn = ln + 1; endLn <= doc.lines; endLn++) {
|
||||
const endLine = doc.line(endLn);
|
||||
if (endLine.text.trim().startsWith(']')) {
|
||||
// Cache the values line before folding hides it
|
||||
const valuesLn = ln + 1;
|
||||
if (valuesLn <= doc.lines && valuesLn < endLn) {
|
||||
cachedEmbeddingsPreview = doc.line(valuesLn).text.trim();
|
||||
}
|
||||
const from = line.from + match[1].length; // after [
|
||||
const to = endLine.from + endLine.text.indexOf(']'); // before ]
|
||||
if (to > from) {
|
||||
view.dispatch({
|
||||
effects: [foldEffect.of({ from, to })],
|
||||
annotations: [Transaction.addToHistory.of(false)]
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (!editorContainer) return;
|
||||
|
||||
@@ -1015,6 +1089,76 @@
|
||||
lineNumbers(),
|
||||
highlightActiveLine(),
|
||||
highlightActiveLineGutter(),
|
||||
// Replace embeddings fold placeholder with truncated value preview
|
||||
ViewPlugin.define((view) => {
|
||||
function updatePlaceholder() {
|
||||
if (!cachedEmbeddingsPreview) return;
|
||||
|
||||
// Get char width and styles from a number element, or fall back to cm-content
|
||||
const numberEl = view.dom.querySelector('.cm-number');
|
||||
const refEl = numberEl || view.dom.querySelector('.cm-content');
|
||||
if (!refEl) return;
|
||||
|
||||
const charWidth = numberEl
|
||||
? numberEl.getBoundingClientRect().width /
|
||||
(numberEl.textContent?.length || 1)
|
||||
: 7.8;
|
||||
const numberStyles = getComputedStyle(refEl);
|
||||
|
||||
for (const p of view.dom.querySelectorAll('.cm-foldPlaceholder')) {
|
||||
const lineEl = p.closest('.cm-line');
|
||||
if (!lineEl?.textContent?.match(/embeddings:\s*\[/)) continue;
|
||||
|
||||
const pos = view.posAtDOM(lineEl);
|
||||
const line = view.state.doc.lineAt(pos);
|
||||
const match = line.text.match(/^(\s*embeddings:\s*\[)/);
|
||||
if (!match) continue;
|
||||
|
||||
const contentWidth =
|
||||
view.dom.querySelector('.cm-content')?.clientWidth ?? 600;
|
||||
const available =
|
||||
Math.floor(contentWidth / charWidth) - match[1].length - 8;
|
||||
if (available <= 10) continue;
|
||||
|
||||
const truncated = cachedEmbeddingsPreview.slice(0, available - 3);
|
||||
const lastComma = truncated.lastIndexOf(',');
|
||||
const el = p as HTMLElement;
|
||||
el.textContent =
|
||||
(lastComma > 0 ? truncated.slice(0, lastComma) : truncated) + '...';
|
||||
el.classList.add('cm-embeddings-fold');
|
||||
|
||||
Object.assign(el.style, {
|
||||
fontFamily: numberStyles.fontFamily,
|
||||
fontSize: numberStyles.fontSize,
|
||||
fontWeight: numberStyles.fontWeight,
|
||||
lineHeight: numberStyles.lineHeight,
|
||||
letterSpacing: numberStyles.letterSpacing
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleUpdate() {
|
||||
requestAnimationFrame(() => updatePlaceholder());
|
||||
}
|
||||
|
||||
const observer = new ResizeObserver(scheduleUpdate);
|
||||
observer.observe(view.dom);
|
||||
|
||||
return {
|
||||
update(update: ViewUpdate) {
|
||||
if (
|
||||
update.transactions.some((tr) =>
|
||||
tr.effects.some((e) => e.is(foldEffect))
|
||||
)
|
||||
) {
|
||||
scheduleUpdate();
|
||||
}
|
||||
},
|
||||
destroy() {
|
||||
observer.disconnect();
|
||||
}
|
||||
};
|
||||
}),
|
||||
// Use fold gutter, hide default glyphs (we style via CSS)
|
||||
foldGutter({ openText: ' ', closedText: ' ' }),
|
||||
indentUnit.of(' '), // Use 2 spaces for indentation
|
||||
@@ -1077,6 +1221,17 @@
|
||||
return false;
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'Mod-g',
|
||||
preventDefault: true,
|
||||
run: () => {
|
||||
if (onGenerateEmbedding) {
|
||||
onGenerateEmbedding();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'Escape',
|
||||
run: () => {
|
||||
@@ -1193,6 +1348,8 @@
|
||||
state: startState,
|
||||
parent: editorContainer
|
||||
});
|
||||
|
||||
foldEmbeddings(editorView);
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
@@ -1259,6 +1416,7 @@
|
||||
extensions: baseExtensions
|
||||
});
|
||||
editorView.setState(newState);
|
||||
foldEmbeddings(editorView);
|
||||
|
||||
if (isNew && !readonly) {
|
||||
const pos = findNewDocCursorPos(editorView.state);
|
||||
@@ -1294,6 +1452,7 @@
|
||||
changes: { from: 0, to: currentContent.length, insert: expectedContent },
|
||||
annotations: [Transaction.addToHistory.of(false)]
|
||||
});
|
||||
foldEmbeddings(editorView);
|
||||
queueMicrotask(() => (isUpdatingFromEditor = false));
|
||||
}
|
||||
});
|
||||
@@ -1424,6 +1583,30 @@
|
||||
applySuggestedAttributes();
|
||||
}} />
|
||||
{/if}
|
||||
|
||||
{#if onGenerateEmbedding && !$isSmallViewport}
|
||||
<div class="embedding-hint">
|
||||
<HintBadge>
|
||||
<Layout.Stack inline gap="xs" direction="row" alignItems="center">
|
||||
<Typography.Caption variant="400" color="--fgcolor-neutral-secondary">
|
||||
Press
|
||||
</Typography.Caption>
|
||||
<Layout.Stack
|
||||
direction="row"
|
||||
inline
|
||||
gap="xxxs"
|
||||
alignItems="center"
|
||||
style="height: fit-content">
|
||||
<Badge content={isMac() ? '⌘' : 'Ctrl'} variant="secondary" size="xs" />
|
||||
<Badge content="G" variant="secondary" size="xs" />
|
||||
</Layout.Stack>
|
||||
<Typography.Caption variant="400" color="--fgcolor-neutral-secondary">
|
||||
to generate embeddings
|
||||
</Typography.Caption>
|
||||
</Layout.Stack>
|
||||
</HintBadge>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if !loading && (errorMessage || warningMessage)}
|
||||
@@ -1715,5 +1898,18 @@
|
||||
padding: 0 4px;
|
||||
background: var(--bgcolor-neutral-secondary);
|
||||
}
|
||||
|
||||
:global(.cm-foldPlaceholder.cm-embeddings-fold) {
|
||||
color: var(--brand-mint-600);
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.embedding-hint {
|
||||
bottom: 12px;
|
||||
right: 12px;
|
||||
z-index: 50;
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
let { children }: { children: Snippet } = $props();
|
||||
</script>
|
||||
|
||||
<div class="hint-badge">
|
||||
{@render children()}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.hint-badge {
|
||||
height: 44px;
|
||||
width: max-content;
|
||||
gap: var(--gap-xxs);
|
||||
align-items: center;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
padding: var(--space-5);
|
||||
border-radius: var(--border-radius-m);
|
||||
background: var(--bgcolor-neutral-primary);
|
||||
border: var(--border-width-s) solid var(--border-neutral);
|
||||
box-shadow:
|
||||
0 1px 3px 0 rgba(0, 0, 0, 0.03),
|
||||
0 4px 4px 0 rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Badge, FloatingActionBar, Layout, Typography } from '@appwrite.io/pink-svelte';
|
||||
import { isSmallViewport } from '$lib/stores/viewport';
|
||||
import HintBadge from '../hintBadge.svelte';
|
||||
|
||||
let {
|
||||
show = true,
|
||||
@@ -29,7 +30,7 @@
|
||||
</button>
|
||||
{:else}
|
||||
<div class="suggestions-wrapper" style:transform={`translateX(${translateX})`}>
|
||||
<div class="popover-content">
|
||||
<HintBadge>
|
||||
<Layout.Stack inline gap="xs" direction="row" alignItems="center">
|
||||
<Typography.Caption variant="400" color="--fgcolor-neutral-secondary">
|
||||
Press
|
||||
@@ -49,7 +50,7 @@
|
||||
{suffix}
|
||||
</Typography.Caption>
|
||||
</Layout.Stack>
|
||||
</div>
|
||||
</HintBadge>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
@@ -62,22 +63,6 @@
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.popover-content {
|
||||
height: 44px;
|
||||
width: max-content;
|
||||
gap: var(--gap-xxs);
|
||||
align-items: center;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
padding: var(--space-5);
|
||||
border-radius: var(--border-radius-m);
|
||||
background: var(--bgcolor-neutral-primary);
|
||||
border: var(--border-width-s) solid var(--border-neutral);
|
||||
box-shadow:
|
||||
0 1px 3px 0 rgba(0, 0, 0, 0.03),
|
||||
0 4px 4px 0 rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.floating-action-bar {
|
||||
left: 50%;
|
||||
bottom: 5%;
|
||||
|
||||
@@ -51,11 +51,12 @@
|
||||
SideSheet,
|
||||
EditRecordPermissions,
|
||||
RecordActivity,
|
||||
CreateIndex,
|
||||
useDatabaseSdk,
|
||||
DEFAULT_VECTOR_DIMENSION,
|
||||
type Field,
|
||||
type Index
|
||||
} from '$database/(entity)';
|
||||
import CreateIndex from './indexes/createIndex.svelte';
|
||||
import {
|
||||
entityColumnSuggestions,
|
||||
type ColumnInput,
|
||||
@@ -98,7 +99,10 @@
|
||||
}
|
||||
|
||||
return realtime.forProject(page.params.region, ['project', 'console'], (response) => {
|
||||
if (response.events.includes('documentsdb.*.collections.*.indexes.*')) {
|
||||
if (
|
||||
response.events.includes('documentsdb.*.collections.*.indexes.*') ||
|
||||
response.events.includes('vectorsdb.*.collections.*.indexes.*')
|
||||
) {
|
||||
if (!isWaterfallFromFaker && !$entityColumnSuggestions.entity) {
|
||||
invalidate(Dependencies.COLLECTION);
|
||||
}
|
||||
@@ -205,7 +209,11 @@
|
||||
});
|
||||
|
||||
async function handleCreateIndex(index: Index) {
|
||||
const databaseSdk = useDatabaseSdk(page.params.region, page.params.project);
|
||||
const databaseSdk = useDatabaseSdk(
|
||||
page.params.region,
|
||||
page.params.project,
|
||||
data.database.type
|
||||
);
|
||||
|
||||
await databaseSdk.createIndex({
|
||||
databaseId: page.params.database,
|
||||
@@ -271,13 +279,33 @@
|
||||
const { rows, ids } = generateFakeRecords($randomDataModalState.value, fields);
|
||||
documentIds = ids;
|
||||
|
||||
await sdk
|
||||
.forProject(page.params.region, page.params.project)
|
||||
.documentsDB.createDocuments({
|
||||
const dbType = data.database?.type;
|
||||
const isVectorsDb = dbType === 'vectorsdb';
|
||||
const dimension = collection?.dimension ?? DEFAULT_VECTOR_DIMENSION;
|
||||
|
||||
// For vectorsdb, wrap fields in metadata and add empty embeddings
|
||||
const documents = isVectorsDb
|
||||
? rows.map((row) => {
|
||||
const { $id, ...rest } = row;
|
||||
return { $id, metadata: rest, embeddings: new Array(dimension).fill(0) };
|
||||
})
|
||||
: rows;
|
||||
|
||||
const projectSdk = sdk.forProject(page.params.region, page.params.project);
|
||||
|
||||
if (isVectorsDb) {
|
||||
await projectSdk.vectorsDB.createDocuments({
|
||||
databaseId: page.params.database,
|
||||
collectionId: page.params.collection,
|
||||
documents: rows
|
||||
documents
|
||||
});
|
||||
} else {
|
||||
await projectSdk.documentsDB.createDocuments({
|
||||
databaseId: page.params.database,
|
||||
collectionId: page.params.collection,
|
||||
documents
|
||||
});
|
||||
}
|
||||
|
||||
await invalidate(Dependencies.DOCUMENTS);
|
||||
} catch (e) {
|
||||
@@ -340,6 +368,7 @@
|
||||
}}>
|
||||
<CreateIndex
|
||||
entity={collection}
|
||||
databaseType={data.database.type}
|
||||
bind:this={createIndex}
|
||||
bind:showCreateIndex={$showCreateIndexSheet.show}
|
||||
externalFieldKey={$showCreateIndexSheet.column}
|
||||
|
||||
@@ -304,7 +304,7 @@
|
||||
{/snippet}
|
||||
</EmptySheet>
|
||||
{:else}
|
||||
<EmptySheet mode="records" type="documentsdb" showActions={$canWriteRows}>
|
||||
<EmptySheet mode="records" type={data.database.type} showActions={$canWriteRows}>
|
||||
{#snippet actions()}
|
||||
<EmptySheetCards
|
||||
icon={IconViewBoards}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Dependencies, SPREADSHEET_PAGE_LIMIT } from '$lib/constants';
|
||||
import { getLimit, getPage, getQuery, getView, pageToOffset, View } from '$lib/helpers/load';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import type { PageLoad } from './$types';
|
||||
import { queries, queryParamToMap } from '$lib/components/filters';
|
||||
import { buildGridQueries, extractSortFromQueries } from '$database/store';
|
||||
import { getCollectionService } from '$database/(entity)';
|
||||
|
||||
export const load: PageLoad = async ({ params, depends, url, route, parent }) => {
|
||||
const { collection } = await parent();
|
||||
const { collection, database } = await parent();
|
||||
depends(Dependencies.DOCUMENTS);
|
||||
|
||||
const page = getPage(url);
|
||||
@@ -20,6 +20,7 @@ export const load: PageLoad = async ({ params, depends, url, route, parent }) =>
|
||||
queries.set(parsedQueries);
|
||||
|
||||
const currentSort = extractSortFromQueries(parsedQueries);
|
||||
const collectionSdk = getCollectionService(params.region, params.project, database.type);
|
||||
|
||||
return {
|
||||
offset,
|
||||
@@ -28,7 +29,7 @@ export const load: PageLoad = async ({ params, depends, url, route, parent }) =>
|
||||
query,
|
||||
currentSort,
|
||||
parsedQueries,
|
||||
documents: await sdk.forProject(params.region, params.project).documentsDB.listDocuments({
|
||||
documents: await collectionSdk.listDocuments({
|
||||
databaseId: params.database,
|
||||
collectionId: params.collection,
|
||||
queries: buildGridQueries(limit, offset, parsedQueries, collection)
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { type DocumentsDBIndexType } from '@appwrite.io/console';
|
||||
import type { PageProps } from './$types';
|
||||
import {
|
||||
type CreateIndexesCallbackType,
|
||||
Indexes,
|
||||
EmptySheet,
|
||||
EmptySheetCards
|
||||
EmptySheetCards,
|
||||
useDatabaseSdk
|
||||
} from '$database/(entity)';
|
||||
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
|
||||
import CreateIndexForm from './createIndex.svelte';
|
||||
@@ -16,20 +15,14 @@
|
||||
|
||||
let createIndexRef: CreateIndexForm;
|
||||
|
||||
const params = $derived({
|
||||
databaseId: page.params.database,
|
||||
collectionId: page.params.collection
|
||||
});
|
||||
|
||||
const documentsDB = $derived(
|
||||
sdk.forProject(page.params.region, page.params.project).documentsDB
|
||||
);
|
||||
const databaseSdk = useDatabaseSdk(page.params.region, page.params.project, data.database.type);
|
||||
|
||||
async function onCreateIndex(index: CreateIndexesCallbackType) {
|
||||
await documentsDB.createIndex({
|
||||
...params,
|
||||
await databaseSdk.createIndex({
|
||||
databaseId: page.params.database,
|
||||
entityId: page.params.collection,
|
||||
key: index.key,
|
||||
type: index.type as DocumentsDBIndexType,
|
||||
type: index.type,
|
||||
attributes: index.fields,
|
||||
lengths: index.lengths,
|
||||
orders: index.orders
|
||||
@@ -39,8 +32,9 @@
|
||||
async function onDeleteIndexes(selectedKeys: string[]) {
|
||||
await Promise.all(
|
||||
selectedKeys.map((key) =>
|
||||
documentsDB.deleteIndex({
|
||||
...params,
|
||||
databaseSdk.deleteIndex({
|
||||
databaseId: page.params.database,
|
||||
entityId: page.params.collection,
|
||||
key
|
||||
})
|
||||
)
|
||||
@@ -52,6 +46,7 @@
|
||||
{#snippet createIndexForm()}
|
||||
<CreateIndexForm
|
||||
entity={data.collection}
|
||||
databaseType={data.database.type}
|
||||
{onCreateIndex}
|
||||
showCreateIndex={true}
|
||||
bind:this={createIndexRef} />
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script module lang="ts">
|
||||
import { DocumentsDBIndexType, OrderBy } from '@appwrite.io/console';
|
||||
import { DocumentsDBIndexType, VectorsDBIndexType, OrderBy } from '@appwrite.io/console';
|
||||
import type { CreateIndexesCallbackType } from '$database/(entity)';
|
||||
import type { DatabaseType } from '$database/(entity)/helpers/terminology';
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
@@ -12,28 +13,60 @@
|
||||
import { remove } from '$lib/helpers/array';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { Icon, Layout } from '@appwrite.io/pink-svelte';
|
||||
import { IconCalendar, IconFingerPrint, IconPlus, IconX } from '@appwrite.io/pink-icons-svelte';
|
||||
import { IconPlus, IconX } from '@appwrite.io/pink-icons-svelte';
|
||||
import { isSmallViewport } from '$lib/stores/viewport';
|
||||
import { type Entity, getTerminologies } from '$database/(entity)';
|
||||
import { resolveRoute, withPath } from '$lib/stores/navigation';
|
||||
|
||||
let {
|
||||
entity,
|
||||
databaseType,
|
||||
showCreateIndex = $bindable(false),
|
||||
externalFieldKey = null,
|
||||
onCreateIndex
|
||||
}: {
|
||||
entity: Entity;
|
||||
databaseType: DatabaseType;
|
||||
showCreateIndex: boolean;
|
||||
externalFieldKey?: string | null;
|
||||
onCreateIndex: (index: CreateIndexesCallbackType) => Promise<void>;
|
||||
} = $props();
|
||||
|
||||
let key = $state('');
|
||||
let initializedForOpen = $state(false);
|
||||
let selectedType = $state<DocumentsDBIndexType>(DocumentsDBIndexType.Key);
|
||||
let selectedType = $state<string>(
|
||||
databaseType === 'vectorsdb' ? VectorsDBIndexType.Key : DocumentsDBIndexType.Key
|
||||
);
|
||||
|
||||
const { dependencies, terminology } = getTerminologies();
|
||||
|
||||
const hasAttributes = $derived(entity.fields?.length > 0);
|
||||
const types = $derived.by(() => {
|
||||
if (databaseType === 'vectorsdb') {
|
||||
return [
|
||||
{ value: VectorsDBIndexType.Key, label: 'Key' },
|
||||
{ value: VectorsDBIndexType.Unique, label: 'Unique' },
|
||||
{ value: VectorsDBIndexType.HnswEuclidean, label: 'HNSW Euclidean' },
|
||||
{ value: VectorsDBIndexType.HnswDot, label: 'HNSW Dot' },
|
||||
{ value: VectorsDBIndexType.HnswCosine, label: 'HNSW Cosine' },
|
||||
{ value: VectorsDBIndexType.Object, label: 'Object' }
|
||||
];
|
||||
}
|
||||
return [
|
||||
{ value: DocumentsDBIndexType.Key, label: 'Key' },
|
||||
{ value: DocumentsDBIndexType.Unique, label: 'Unique' },
|
||||
{ value: DocumentsDBIndexType.Fulltext, label: 'Fulltext' }
|
||||
];
|
||||
});
|
||||
|
||||
const isHnswType = $derived(
|
||||
[
|
||||
VectorsDBIndexType.HnswEuclidean,
|
||||
VectorsDBIndexType.HnswDot,
|
||||
VectorsDBIndexType.HnswCosine
|
||||
].includes(selectedType as VectorsDBIndexType)
|
||||
);
|
||||
|
||||
const isObjectType = $derived(selectedType === VectorsDBIndexType.Object);
|
||||
|
||||
const fieldOptions = $derived(
|
||||
(entity.fields ?? []).map((field) => ({
|
||||
@@ -44,16 +77,10 @@
|
||||
|
||||
let fieldList: Array<{
|
||||
value: string;
|
||||
order: OrderBy;
|
||||
order: OrderBy | null;
|
||||
length: number | null;
|
||||
}> = $state([{ value: '', order: OrderBy.Asc, length: null }]);
|
||||
|
||||
const types = [
|
||||
{ value: DocumentsDBIndexType.Key, label: 'Key' },
|
||||
{ value: DocumentsDBIndexType.Unique, label: 'Unique' },
|
||||
{ value: DocumentsDBIndexType.Fulltext, label: 'Fulltext' }
|
||||
];
|
||||
|
||||
const orderOptions = [
|
||||
{ value: OrderBy.Asc, label: 'ASC' },
|
||||
{ value: OrderBy.Desc, label: 'DESC' }
|
||||
@@ -71,12 +98,20 @@
|
||||
}
|
||||
|
||||
function initialize() {
|
||||
selectedType = DocumentsDBIndexType.Key;
|
||||
fieldList = [{ value: '', order: OrderBy.Asc, length: null }];
|
||||
selectedType =
|
||||
databaseType === 'vectorsdb' ? VectorsDBIndexType.Key : DocumentsDBIndexType.Key;
|
||||
fieldList = externalFieldKey
|
||||
? [{ value: externalFieldKey, order: OrderBy.Asc, length: null }]
|
||||
: [{ value: '', order: OrderBy.Asc, length: null }];
|
||||
key = `index_${entity.indexes.length + 1}`;
|
||||
}
|
||||
|
||||
const addFieldDisabled = $derived(!fieldList.at(-1)?.value || !fieldList.at(-1)?.order);
|
||||
const addFieldDisabled = $derived(
|
||||
isHnswType ||
|
||||
isObjectType ||
|
||||
!fieldList.at(-1)?.value ||
|
||||
(!isObjectType && !fieldList.at(-1)?.order)
|
||||
);
|
||||
|
||||
const isOnIndexesPage = $derived(page.route.id?.endsWith('/indexes'));
|
||||
const navigatorPathToIndexes = $derived.by(() => {
|
||||
@@ -101,10 +136,25 @@
|
||||
}
|
||||
});
|
||||
|
||||
// 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 }];
|
||||
}
|
||||
});
|
||||
|
||||
export async function create() {
|
||||
const fieldType = terminology.field.lower.singular;
|
||||
|
||||
if (!key || !selectedType || addFieldDisabled) {
|
||||
if (
|
||||
!key ||
|
||||
!selectedType ||
|
||||
(!isHnswType && !isObjectType && addFieldDisabled) ||
|
||||
((isHnswType || isObjectType) && !fieldList.at(0)?.value)
|
||||
) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: `Selected ${fieldType} key or type invalid`
|
||||
@@ -117,8 +167,16 @@
|
||||
key,
|
||||
type: selectedType,
|
||||
fields: fieldList.map((a) => a.value),
|
||||
lengths: fieldList.map((a) => (a.length ? Number(a.length) : null)),
|
||||
orders: fieldList.map((a) => a.order)
|
||||
lengths:
|
||||
isHnswType || isObjectType
|
||||
? []
|
||||
: fieldList.map((a) => (a.length ? Number(a.length) : null)),
|
||||
orders:
|
||||
isHnswType || isObjectType
|
||||
? []
|
||||
: fieldList
|
||||
.map((a) => a.order)
|
||||
.filter((order): order is OrderBy => order !== null)
|
||||
});
|
||||
|
||||
await Promise.allSettled([
|
||||
@@ -173,21 +231,20 @@
|
||||
{#each fieldList as field, index}
|
||||
{@const direction = $isSmallViewport ? 'column' : 'row'}
|
||||
<Layout.Stack {direction}>
|
||||
{#if hasAttributes}
|
||||
{#if isHnswType}
|
||||
<InputText
|
||||
required
|
||||
disabled
|
||||
id={`field-${index}`}
|
||||
label={index === 0 ? fieldType : undefined}
|
||||
bind:value={field.value} />
|
||||
{:else if fieldOptions.length}
|
||||
<InputSelect
|
||||
required
|
||||
options={[
|
||||
{ value: '$id', label: '$id', leadingIcon: IconFingerPrint },
|
||||
{
|
||||
value: '$createdAt',
|
||||
label: '$createdAt',
|
||||
leadingIcon: IconCalendar
|
||||
},
|
||||
{
|
||||
value: '$updatedAt',
|
||||
label: '$updatedAt',
|
||||
leadingIcon: IconCalendar
|
||||
},
|
||||
{ value: '$id', label: '$id' },
|
||||
{ value: '$createdAt', label: '$createdAt' },
|
||||
{ value: '$updatedAt', label: '$updatedAt' },
|
||||
...fieldOptions
|
||||
]}
|
||||
id={`field-${index}`}
|
||||
@@ -203,20 +260,22 @@
|
||||
bind:value={field.value} />
|
||||
{/if}
|
||||
|
||||
<InputSelect
|
||||
options={orderOptions}
|
||||
required
|
||||
id={`order-${index}`}
|
||||
label={index === 0 ? 'Order' : undefined}
|
||||
bind:value={field.order}
|
||||
placeholder="Select order" />
|
||||
{#if !isHnswType && !isObjectType}
|
||||
<InputSelect
|
||||
options={orderOptions}
|
||||
required
|
||||
id={`order-${index}`}
|
||||
label={index === 0 ? 'Order' : undefined}
|
||||
bind:value={field.order}
|
||||
placeholder="Select order" />
|
||||
|
||||
{#if selectedType === DocumentsDBIndexType.Key}
|
||||
<InputNumber
|
||||
id={`length-${index}`}
|
||||
label={index === 0 ? 'Length' : undefined}
|
||||
placeholder="Enter length"
|
||||
bind:value={field.length} />
|
||||
{#if selectedType === DocumentsDBIndexType.Key || selectedType === VectorsDBIndexType.Key}
|
||||
<InputNumber
|
||||
id={`length-${index}`}
|
||||
label={index === 0 ? 'Length' : undefined}
|
||||
placeholder="Enter length"
|
||||
bind:value={field.length} />
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
{#if $isSmallViewport}
|
||||
@@ -247,12 +306,14 @@
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
{/each}
|
||||
<div>
|
||||
<Button compact on:click={addField} disabled={addFieldDisabled}>
|
||||
<Icon icon={IconPlus} slot="start" size="s" />
|
||||
Add {fieldTypeLower}
|
||||
</Button>
|
||||
</div>
|
||||
{#if !isHnswType && !isObjectType}
|
||||
<div>
|
||||
<Button compact on:click={addField} disabled={addFieldDisabled}>
|
||||
<Icon icon={IconPlus} slot="start" size="s" />
|
||||
Add {fieldTypeLower}
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { Container } from '$lib/layout';
|
||||
import {
|
||||
DangerZone,
|
||||
UpdateName,
|
||||
UpdatePermissions,
|
||||
UpdateSecurity,
|
||||
UpdateStatus
|
||||
UpdateStatus,
|
||||
useDatabaseSdk
|
||||
} from '$database/(entity)';
|
||||
import type { PageProps } from './$types';
|
||||
import DisplayName from './displayName.svelte';
|
||||
@@ -17,18 +17,15 @@
|
||||
|
||||
const collection = $derived(data.collection);
|
||||
|
||||
const params = $derived.by(() => {
|
||||
return {
|
||||
name: collection.name,
|
||||
collectionId: page.params.collection,
|
||||
databaseId: page.params.database
|
||||
};
|
||||
const databaseSdk = useDatabaseSdk(page.params.region, page.params.project, data.database.type);
|
||||
|
||||
const entityParams = $derived({
|
||||
databaseId: page.params.database,
|
||||
entityId: page.params.collection
|
||||
});
|
||||
|
||||
async function deleteCollection() {
|
||||
await sdk
|
||||
.forProject(page.params.region, page.params.project)
|
||||
.documentsDB.deleteCollection({ ...params });
|
||||
await databaseSdk.deleteEntity(entityParams);
|
||||
}
|
||||
|
||||
async function updateCollection(
|
||||
@@ -39,9 +36,11 @@
|
||||
documentSecurity: boolean;
|
||||
}>
|
||||
) {
|
||||
await sdk
|
||||
.forProject(page.params.region, page.params.project)
|
||||
.documentsDB.updateCollection({ ...params, ...updates });
|
||||
await databaseSdk.updateEntity({
|
||||
...entityParams,
|
||||
name: collection.name,
|
||||
...updates
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
type JsonValue,
|
||||
NoSqlEditor
|
||||
} from '$database/collection-[collection]/(components)/editor';
|
||||
import EmbeddingModal from '$database/collection-[collection]/(components)/editor/embeddingModal.svelte';
|
||||
import { buildFieldUrl } from '$database/(entity)/helpers/navigation';
|
||||
import {
|
||||
SpreadsheetOptions,
|
||||
@@ -75,13 +76,15 @@
|
||||
$: if ($documents) {
|
||||
paginatedDocuments.clear();
|
||||
|
||||
const docs = $documents.documents;
|
||||
|
||||
// If we have a new document, add it at the start
|
||||
if ($noSqlDocument.isDirty && $noSqlDocument.isNew) {
|
||||
const tempDoc = $noSqlDocument.document as Models.DefaultDocument;
|
||||
const docsWithTemp = [tempDoc, ...$documents.documents];
|
||||
const docsWithTemp = [tempDoc, ...docs];
|
||||
paginatedDocuments.setPage(1, docsWithTemp);
|
||||
} else {
|
||||
paginatedDocuments.setPage(1, $documents.documents);
|
||||
paginatedDocuments.setPage(1, docs);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,6 +92,25 @@
|
||||
const collectionId = page.params.collection;
|
||||
const databaseSdk = useDatabaseSdk(page.params.region, page.params.project, data.database.type);
|
||||
|
||||
const isVectorsDb = data.database.type === 'vectorsdb';
|
||||
let showEmbeddingModal = false;
|
||||
let editorRef: { replaceData: (data: JsonValue) => void } | undefined;
|
||||
|
||||
const projectSdk = sdk.forProject(page.params.region, page.params.project);
|
||||
const listDocumentsFn = isVectorsDb
|
||||
? projectSdk.vectorsDB.listDocuments.bind(projectSdk.vectorsDB)
|
||||
: projectSdk.documentsDB.listDocuments.bind(projectSdk.documentsDB);
|
||||
const getDocumentFn = isVectorsDb
|
||||
? projectSdk.vectorsDB.getDocument.bind(projectSdk.vectorsDB)
|
||||
: projectSdk.documentsDB.getDocument.bind(projectSdk.documentsDB);
|
||||
|
||||
function handleEmbeddingGenerated(embeddings: number[]) {
|
||||
if ($noSqlDocument.document && typeof $noSqlDocument.document === 'object') {
|
||||
const updated = { ...$noSqlDocument.document, embeddings };
|
||||
editorRef?.replaceData(updated);
|
||||
}
|
||||
}
|
||||
|
||||
const emptyCellsLimit = $spreadsheetLoading
|
||||
? 30
|
||||
: $isSmallViewport
|
||||
@@ -115,13 +137,11 @@
|
||||
const documentId = $noSqlDocument.documentId;
|
||||
noSqlDocument.update({ documentId: null }); // reset for later!
|
||||
|
||||
const loadedDocument = await sdk
|
||||
.forProject(page.params.region, page.params.project)
|
||||
.documentsDB.getDocument({
|
||||
databaseId: page.params.database,
|
||||
collectionId: page.params.collection,
|
||||
documentId
|
||||
});
|
||||
const loadedDocument = await getDocumentFn({
|
||||
databaseId: page.params.database,
|
||||
collectionId: page.params.collection,
|
||||
documentId
|
||||
});
|
||||
|
||||
if (loadedDocument) {
|
||||
noSqlDocument.edit(loadedDocument);
|
||||
@@ -462,7 +482,9 @@
|
||||
spreadsheetRenderKey.set(hash(Date.now().toString()));
|
||||
const firstDocument = $documents?.documents?.[0];
|
||||
if (firstDocument) {
|
||||
noSqlDocument.update({ document: firstDocument });
|
||||
noSqlDocument.update({
|
||||
document: firstDocument
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
@@ -492,19 +514,17 @@
|
||||
const filterQueries = parsedQueries.size ? data.parsedQueries.values() : [];
|
||||
|
||||
$paginatedDocumentsLoading = true;
|
||||
const loadedRows = await sdk
|
||||
.forProject(page.params.region, page.params.project)
|
||||
.documentsDB.listDocuments({
|
||||
databaseId,
|
||||
collectionId,
|
||||
queries: [
|
||||
getCorrectOrderQuery(),
|
||||
Query.limit(SPREADSHEET_PAGE_LIMIT),
|
||||
Query.offset(pageToOffset(pageNumber, SPREADSHEET_PAGE_LIMIT)),
|
||||
...filterQueries /* filter queries */,
|
||||
...buildWildcardEntitiesQuery(collection)
|
||||
]
|
||||
});
|
||||
const loadedRows = await listDocumentsFn({
|
||||
databaseId,
|
||||
collectionId,
|
||||
queries: [
|
||||
getCorrectOrderQuery(),
|
||||
Query.limit(SPREADSHEET_PAGE_LIMIT),
|
||||
Query.offset(pageToOffset(pageNumber, SPREADSHEET_PAGE_LIMIT)),
|
||||
...filterQueries /* filter queries */,
|
||||
...buildWildcardEntitiesQuery(collection)
|
||||
]
|
||||
});
|
||||
|
||||
paginatedDocuments.setPage(pageNumber, loadedRows.documents);
|
||||
$paginatedDocumentsLoading = false;
|
||||
@@ -520,18 +540,16 @@
|
||||
paginatedDocuments.setMaxPage(targetPageNum);
|
||||
$paginatedDocumentsLoading = true;
|
||||
|
||||
const loadedRows = await sdk
|
||||
.forProject(page.params.region, page.params.project)
|
||||
.documentsDB.listDocuments({
|
||||
databaseId,
|
||||
collectionId,
|
||||
queries: [
|
||||
getCorrectOrderQuery(),
|
||||
Query.limit(SPREADSHEET_PAGE_LIMIT),
|
||||
Query.offset(pageToOffset(targetPageNum, SPREADSHEET_PAGE_LIMIT)),
|
||||
...buildWildcardEntitiesQuery(collection)
|
||||
]
|
||||
});
|
||||
const loadedRows = await listDocumentsFn({
|
||||
databaseId,
|
||||
collectionId,
|
||||
queries: [
|
||||
getCorrectOrderQuery(),
|
||||
Query.limit(SPREADSHEET_PAGE_LIMIT),
|
||||
Query.offset(pageToOffset(targetPageNum, SPREADSHEET_PAGE_LIMIT)),
|
||||
...buildWildcardEntitiesQuery(collection)
|
||||
]
|
||||
});
|
||||
|
||||
paginatedDocuments.setPage(targetPageNum, loadedRows.documents);
|
||||
$paginatedDocumentsLoading = false;
|
||||
@@ -553,14 +571,29 @@
|
||||
const MIN_DOCS_FOR_FUZZY_SUGGESTIONS = 5;
|
||||
|
||||
$: useMockSuggestions =
|
||||
!isVectorsDb &&
|
||||
$noSqlDocument.isNew &&
|
||||
($documents?.documents?.length ?? 0) < MIN_DOCS_FOR_FUZZY_SUGGESTIONS;
|
||||
|
||||
$: metadataKeys =
|
||||
isVectorsDb && $documents?.documents
|
||||
? (fuzzySearchKeys(
|
||||
$documents.documents.map((d) => d.metadata ?? {}),
|
||||
{ minOccurrences: 2 }
|
||||
) ?? [])
|
||||
: [];
|
||||
|
||||
$: vectorsDbMetadataDefaults = isVectorsDb
|
||||
? Object.fromEntries(metadataKeys.map((key) => [key, '']))
|
||||
: {};
|
||||
|
||||
$: suggestedAttributes =
|
||||
$noSqlDocument.isNew && $documents?.documents
|
||||
? useMockSuggestions
|
||||
? mockSuggestions.columns.map((column) => column.name)
|
||||
: (fuzzySearchKeys($documents.documents, { minOccurrences: 2 }) ?? [])
|
||||
? isVectorsDb
|
||||
? ['metadata', 'embeddings']
|
||||
: useMockSuggestions
|
||||
? mockSuggestions.columns.map((column) => column.name)
|
||||
: (fuzzySearchKeys($documents.documents, { minOccurrences: 2 }) ?? [])
|
||||
: [];
|
||||
|
||||
$: showSuggestions = $noSqlDocument.isNew && suggestedAttributes.length > 0;
|
||||
@@ -863,6 +896,7 @@
|
||||
|
||||
{#snippet noSqlEditor()}
|
||||
<NoSqlEditor
|
||||
bind:this={editorRef}
|
||||
ctrlSave
|
||||
isNew={$noSqlDocument.isNew}
|
||||
loading={$noSqlDocument.loading}
|
||||
@@ -872,6 +906,12 @@
|
||||
{showSuggestions}
|
||||
{suggestedAttributes}
|
||||
showMockSuggestions={useMockSuggestions}
|
||||
suggestedDefaults={isVectorsDb
|
||||
? {
|
||||
metadata: vectorsDbMetadataDefaults,
|
||||
embeddings: []
|
||||
}
|
||||
: undefined}
|
||||
onDiscard={() => {
|
||||
const firstDocument = $documents?.documents?.[0];
|
||||
if (firstDocument) {
|
||||
@@ -881,7 +921,8 @@
|
||||
}
|
||||
}}
|
||||
onSave={async (document) => await createOrUpdateDocument(document)}
|
||||
onChange={(_, hasDataChanged) => noSqlDocument.update({ hasDataChanged })} />
|
||||
onChange={(_, hasDataChanged) => noSqlDocument.update({ hasDataChanged })}
|
||||
onGenerateEmbedding={isVectorsDb ? () => (showEmbeddingModal = true) : undefined} />
|
||||
{/snippet}
|
||||
|
||||
{#snippet sideSheetHeaderAction()}
|
||||
@@ -981,6 +1022,10 @@
|
||||
</Confirm>
|
||||
{/if}
|
||||
|
||||
{#if isVectorsDb}
|
||||
<EmbeddingModal bind:show={showEmbeddingModal} onGenerate={handleEmbeddingGenerated} />
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.floating-action-bar {
|
||||
left: 50%;
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
<script lang="ts" module>
|
||||
export type HeaderCellAction =
|
||||
| 'update'
|
||||
| 'column-left'
|
||||
| 'column-right'
|
||||
| 'duplicate-header'
|
||||
| 'create-index'
|
||||
| 'sort-asc'
|
||||
| 'sort-desc'
|
||||
| 'delete';
|
||||
|
||||
export type RowCellAction =
|
||||
| 'update'
|
||||
| 'duplicate-row'
|
||||
| 'permissions'
|
||||
| 'activity'
|
||||
| 'copy-url'
|
||||
| 'copy-json'
|
||||
// | 'copy-snippet'
|
||||
| 'delete';
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import type { ComponentType, Snippet } from 'svelte';
|
||||
import { Divider, Popover, ActionMenu } from '@appwrite.io/pink-svelte';
|
||||
import {
|
||||
IconArrowLeft,
|
||||
IconArrowRight,
|
||||
IconChartBar,
|
||||
IconClipboardCopy,
|
||||
IconDuplicate,
|
||||
IconKey,
|
||||
IconLink,
|
||||
IconPencil,
|
||||
IconSortAscending,
|
||||
IconSortDescending,
|
||||
IconTrash
|
||||
} from '@appwrite.io/pink-icons-svelte';
|
||||
import { databaseColumnSheetOptions } from './store';
|
||||
import type { Columns } from '$database/store';
|
||||
import { isRelationship } from './rows/store';
|
||||
|
||||
interface MenuItem {
|
||||
label?: string;
|
||||
action?: HeaderCellAction | RowCellAction;
|
||||
danger?: boolean;
|
||||
divider?: boolean;
|
||||
icon?: ComponentType;
|
||||
}
|
||||
|
||||
// Only allow sort for these columns
|
||||
const internalColumns = ['$sequence', '$id', '$createdAt', '$updatedAt'];
|
||||
|
||||
const headerMenuItems: MenuItem[] = [
|
||||
{ label: 'Update', icon: IconPencil, action: 'update' },
|
||||
{ label: 'Create column left', icon: IconArrowLeft, action: 'column-left' },
|
||||
{ label: 'Create column right', icon: IconArrowRight, action: 'column-right' },
|
||||
{ label: 'Duplicate', icon: IconDuplicate, action: 'duplicate-header' },
|
||||
{ divider: true },
|
||||
{ label: 'Create index', icon: IconPencil, action: 'create-index' },
|
||||
{ label: 'Sort ascending', icon: IconSortAscending, action: 'sort-asc' },
|
||||
{ label: 'Sort descending', icon: IconSortDescending, action: 'sort-desc' },
|
||||
{ divider: true },
|
||||
{ label: 'Delete', icon: IconTrash, action: 'delete', danger: true }
|
||||
];
|
||||
|
||||
const rowMenuItems: MenuItem[] = [
|
||||
{ label: 'Update', icon: IconPencil, action: 'update' },
|
||||
{ label: 'Duplicate', icon: IconDuplicate, action: 'duplicate-row' },
|
||||
{ divider: true },
|
||||
{ label: 'Manage permissions', icon: IconKey, action: 'permissions' },
|
||||
{ label: 'View activity', icon: IconChartBar, action: 'activity' },
|
||||
{ divider: true },
|
||||
{ label: 'Copy URL', icon: IconLink, action: 'copy-url' },
|
||||
{ label: 'Copy as JSON', icon: IconClipboardCopy, action: 'copy-json' },
|
||||
{ divider: true },
|
||||
{ label: 'Delete', icon: IconTrash, action: 'delete', danger: true }
|
||||
];
|
||||
|
||||
let {
|
||||
column,
|
||||
columnId = null,
|
||||
children,
|
||||
onSelect,
|
||||
onVisibilityChanged,
|
||||
type
|
||||
}: {
|
||||
column: Columns;
|
||||
columnId?: string;
|
||||
type: 'header' | 'row';
|
||||
onVisibilityChanged?: (visible: boolean) => void;
|
||||
onSelect: (option: HeaderCellAction | RowCellAction, columnId: string) => void;
|
||||
children: Snippet<[toggle: (event: Event) => void]>;
|
||||
} = $props();
|
||||
|
||||
function handleSelect(action: HeaderCellAction | RowCellAction, hide: () => void) {
|
||||
hide();
|
||||
$databaseColumnSheetOptions.column = column;
|
||||
|
||||
if (action === 'column-left') {
|
||||
$databaseColumnSheetOptions.direction = {
|
||||
neighbour: columnId,
|
||||
to: 'left'
|
||||
};
|
||||
} else if (action === 'column-right') {
|
||||
$databaseColumnSheetOptions.direction = {
|
||||
neighbour: columnId,
|
||||
to: 'right'
|
||||
};
|
||||
}
|
||||
|
||||
onSelect(action, columnId);
|
||||
}
|
||||
|
||||
function shouldShow(item: MenuItem): boolean {
|
||||
const isSequence = columnId === '$sequence';
|
||||
const isSystemColumn = internalColumns.includes(columnId);
|
||||
|
||||
if (type === 'header') {
|
||||
if (isSequence) {
|
||||
return ['sort-asc', 'sort-desc'].includes(item?.action);
|
||||
}
|
||||
|
||||
if (['delete', 'update', 'duplicate-header'].includes(item?.action) && isSystemColumn) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// hide column-left and create-index for $id (first column, already indexed)
|
||||
if (columnId === '$id' && ['column-left', 'create-index'].includes(item?.action)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// hide sort options for relationship columns
|
||||
if (isRelationship(column) && ['sort-asc', 'sort-desc'].includes(item?.action)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function cleanMenu(items: MenuItem[]): MenuItem[] {
|
||||
const visible = items.filter((item) => item.divider || shouldShow(item));
|
||||
|
||||
return visible.filter((item, i, arr) => {
|
||||
const prev = arr[i - 1];
|
||||
const next = arr[i + 1];
|
||||
|
||||
if (item.divider) {
|
||||
return prev && !prev.divider && next && !next.divider;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
let htmlSpanElement = $state<HTMLSpanElement | null>(null);
|
||||
|
||||
$effect(() => {
|
||||
const visible = htmlSpanElement !== null;
|
||||
onVisibilityChanged?.(visible);
|
||||
});
|
||||
</script>
|
||||
|
||||
<Popover let:toggle padding="none" placement="bottom-start" portal>
|
||||
{@render children(toggle)}
|
||||
|
||||
<svelte:fragment slot="tooltip" let:hide>
|
||||
{@const menuItems = cleanMenu(type === 'header' ? headerMenuItems : rowMenuItems)}
|
||||
|
||||
<!-- hacky, i know! -->
|
||||
<span bind:this={htmlSpanElement}> </span>
|
||||
|
||||
<div class="action-menu-root">
|
||||
<ActionMenu.Root width="180px">
|
||||
{#each menuItems as item, index (index)}
|
||||
{#if item.divider}
|
||||
{@const isLastDivider = index === menuItems.length - 2}
|
||||
<div
|
||||
style:margin-inline="-1rem"
|
||||
style:padding-block-start="0.5rem"
|
||||
style:padding-block-end={isLastDivider ? '0.25rem' : '0.5rem'}>
|
||||
<Divider />
|
||||
</div>
|
||||
{:else if shouldShow(item)}
|
||||
<ActionMenu.Item.Button
|
||||
leadingIcon={item.icon}
|
||||
status={item.danger ? 'danger' : undefined}
|
||||
on:click={() => item.action && handleSelect(item.action, hide)}>
|
||||
{item.label}
|
||||
</ActionMenu.Item.Button>
|
||||
{/if}
|
||||
{/each}
|
||||
</ActionMenu.Root>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</Popover>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
@@ -14,8 +14,14 @@
|
||||
import DocumentsDB from './(assets)/documents-db.svg';
|
||||
import DocumentsDBDark from './(assets)/dark/documents-db.svg';
|
||||
|
||||
import { isSmallViewport, isViewPortWidthInRange } from '$lib/stores/viewport';
|
||||
import VectorsDB from './(assets)/vectors-db.svg';
|
||||
import VectorsDBDark from './(assets)/dark/vectors-db.svg';
|
||||
|
||||
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,
|
||||
@@ -29,8 +35,7 @@
|
||||
/*const mongoDbImage = $derived(isDark ? MongoDBDark : MongoDB);*/
|
||||
const tablesDbImage = $derived(isDark ? TablesDBDark : TablesDB);
|
||||
const documentsDbImage = $derived(isDark ? DocumentsDBDark : DocumentsDB);
|
||||
|
||||
const inRangeStore = isViewPortWidthInRange(1024, 1280);
|
||||
const vectorsDbImage = $derived(isDark ? VectorsDBDark : VectorsDB);
|
||||
</script>
|
||||
|
||||
{#if $isSmallViewport}
|
||||
@@ -50,7 +55,7 @@
|
||||
>Store, organize, and manage your app data</Typography.Text>
|
||||
</Layout.Stack>
|
||||
|
||||
<Layout.Grid columns={2} columnsS={1} gap="xl">
|
||||
<Layout.Grid columns={3} columnsS={2} columnsXS={1} gap="xl">
|
||||
<!-- legacy, tablesDB -->
|
||||
{@render databaseTypeCard({
|
||||
type: 'tablesdb',
|
||||
@@ -60,14 +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
|
||||
})}
|
||||
{/if}
|
||||
</Layout.Grid>
|
||||
</Layout.Stack>
|
||||
{/snippet}
|
||||
@@ -79,25 +95,16 @@
|
||||
padding="none"
|
||||
{disabled}
|
||||
on:click={() => onDatabaseTypeSelected?.(type)}>
|
||||
{@const direction = $isSmallViewport || $inRangeStore ? 'column' : 'row'}
|
||||
<Layout.Stack gap="none" {direction}>
|
||||
<img
|
||||
src={image}
|
||||
class="database-image"
|
||||
class:adaptive-height={$inRangeStore}
|
||||
alt="database type artwork" />
|
||||
<Layout.Stack gap="none" direction="column">
|
||||
<img src={image} class="database-image adaptive-height" alt="database type artwork" />
|
||||
|
||||
{#if $isSmallViewport || $inRangeStore}
|
||||
<Divider />
|
||||
{/if}
|
||||
<Divider />
|
||||
|
||||
<Layout.Stack
|
||||
gap="xxs"
|
||||
direction="column"
|
||||
justifyContent="space-between"
|
||||
style="padding: var(--gap-xl); flex: 1; {!$isSmallViewport && !$inRangeStore
|
||||
? 'border-inline-start: 1px solid var(--border-neutral);'
|
||||
: ''}">
|
||||
style="padding: var(--gap-xl); flex: 1;">
|
||||
<Layout.Stack direction="column" gap="xxs">
|
||||
<Layout.Stack
|
||||
inline
|
||||
|
||||
|
After Width: | Height: | Size: 223 KiB |
|
After Width: | Height: | Size: 223 KiB |