mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
update: abstracted indexes for base views.
This commit is contained in:
@@ -12,13 +12,21 @@ type ResolveArgs<T extends RouteId | Pathname> = T extends RouteId
|
||||
: [route: T, params: RouteParams<T>]
|
||||
: [route: T];
|
||||
|
||||
export function withPath(base: string, ...parts: string[]) {
|
||||
return [base.replace(/\/+$/, ''), ...parts].join('/');
|
||||
}
|
||||
|
||||
export function resolveRoute<T extends RouteId>(route: T, params?: Record<string, string>) {
|
||||
// type cast is necessary here!
|
||||
const resolveArgs = params ? ([route, params] as [T, RouteParams<T>]) : [route];
|
||||
|
||||
return resolve(...(resolveArgs as ResolveArgs<T>));
|
||||
}
|
||||
|
||||
export function navigate<T extends RouteId>(
|
||||
route: T,
|
||||
params?: Record<string, string>
|
||||
): Promise<void> {
|
||||
// type cast is necessary here!
|
||||
const resolveArgs = params ? ([route, params] as [T, RouteParams<T>]) : [route];
|
||||
const resolvedPathname = resolve(...(resolveArgs as ResolveArgs<T>));
|
||||
|
||||
return goto(resolvedPathname);
|
||||
return goto(resolveRoute(route, params));
|
||||
}
|
||||
|
||||
+1
@@ -9,6 +9,7 @@ import type { Term, TerminologyResult, TerminologyShape } from '$database/(entit
|
||||
export type DatabaseType = 'legacy' | 'tablesdb' | 'documentsdb' | 'vectordb';
|
||||
export type Entity = Partial<Models.Table>;
|
||||
export type Field = Partial<Columns>;
|
||||
export type Index = Partial<Models.Index | Models.ColumnIndex>;
|
||||
|
||||
export const baseTerminology = {
|
||||
tablesdb: {
|
||||
|
||||
+1
@@ -1,4 +1,5 @@
|
||||
export * from './helpers';
|
||||
export * from './views/indexes';
|
||||
export * from './views/settings';
|
||||
export { default as Usage } from './views/usage/view.svelte';
|
||||
export { default as EntityContainer } from './views/container.svelte';
|
||||
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
export { default as SideSheet } from './sidesheet.svelte';
|
||||
export { default as SpreadsheetContainer } from './spreadsheet.svelte';
|
||||
+174
@@ -0,0 +1,174 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
import { Copy } from '$lib/components';
|
||||
import { writable } from 'svelte/store';
|
||||
import { Button, Form } from '$lib/elements/forms';
|
||||
import { isTabletViewport } from '$lib/stores/viewport';
|
||||
import { Badge, Divider, Layout, Sheet, Tag, Typography } from '@appwrite.io/pink-svelte';
|
||||
|
||||
let {
|
||||
show = $bindable(false),
|
||||
title,
|
||||
closeOnBlur = false,
|
||||
submit,
|
||||
children = null,
|
||||
footer = null,
|
||||
titleBadge = null,
|
||||
topAction = null
|
||||
}: {
|
||||
show: boolean;
|
||||
title: string;
|
||||
titleBadge?: string;
|
||||
closeOnBlur?: boolean;
|
||||
topAction?:
|
||||
| {
|
||||
text: string;
|
||||
value: string;
|
||||
show?: boolean;
|
||||
mode?: 'copy-tag' | 'plaintext';
|
||||
onClick?: () => string | Promise<string>;
|
||||
}
|
||||
| undefined;
|
||||
submit?:
|
||||
| {
|
||||
text: string;
|
||||
disabled?: boolean;
|
||||
onClick?: () => boolean | void | Promise<boolean | void>;
|
||||
}
|
||||
| undefined;
|
||||
children?: Snippet;
|
||||
footer?: Snippet | null;
|
||||
} = $props();
|
||||
|
||||
let form: Form;
|
||||
let submitting = $state(writable(false));
|
||||
|
||||
let copyText = $state(undefined);
|
||||
</script>
|
||||
|
||||
<div class="sheet-container" data-side-sheet-visible={show}>
|
||||
<Sheet bind:open={show} {closeOnBlur}>
|
||||
<div slot="header" style:width="100%">
|
||||
<Layout.Stack direction="row" justifyContent="space-between" alignItems="center">
|
||||
<Layout.Stack direction="row" gap="m" alignItems="center">
|
||||
<Typography.Text variant="m-400">{title}</Typography.Text>
|
||||
{#if titleBadge}
|
||||
<Badge variant="secondary" content={titleBadge} size="s" />
|
||||
{/if}
|
||||
|
||||
{#if topAction && topAction.text && topAction.show}
|
||||
{#if topAction.mode === 'copy-tag'}
|
||||
<Copy value={topAction.value} {copyText}>
|
||||
<Tag size="xs" variant="code">
|
||||
{topAction.text}
|
||||
</Tag>
|
||||
</Copy>
|
||||
{:else}
|
||||
<Button
|
||||
extraCompact
|
||||
text
|
||||
size="xs"
|
||||
on:click={topAction.onClick ?? undefined}>
|
||||
{topAction.text}
|
||||
</Button>
|
||||
{/if}
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
</Layout.Stack>
|
||||
</div>
|
||||
|
||||
<Layout.Stack direction="column" justifyContent="space-evenly">
|
||||
<Form
|
||||
bind:this={form}
|
||||
bind:isSubmitting={submitting}
|
||||
onSubmit={async () => {
|
||||
try {
|
||||
const keepOpen = await submit?.onClick?.();
|
||||
if (!keepOpen) {
|
||||
show = false;
|
||||
}
|
||||
} catch (error) {
|
||||
// error occurred, dont close the sidebar
|
||||
}
|
||||
}}>
|
||||
<Layout.Stack gap="xl" class="sheet-content">
|
||||
{@render children?.()}
|
||||
</Layout.Stack>
|
||||
</Form>
|
||||
|
||||
{#if submit}
|
||||
<div class="sheet-footer">
|
||||
<Layout.Stack gap="l">
|
||||
<Divider />
|
||||
|
||||
<div class="sheet-footer-actions">
|
||||
<Layout.Stack
|
||||
gap="m"
|
||||
direction="row"
|
||||
justifyContent="flex-end"
|
||||
alignItems="center">
|
||||
{#if footer}
|
||||
{@render footer?.()}
|
||||
{/if}
|
||||
<Button size="s" secondary on:click={() => (show = false)}
|
||||
>Cancel</Button>
|
||||
<Button
|
||||
size="s"
|
||||
submit
|
||||
disabled={submit.disabled || $submitting}
|
||||
forceShowLoader={$submitting && $isTabletViewport}
|
||||
submissionLoader={$submitting && $isTabletViewport}
|
||||
on:click={() => form?.triggerSubmit()}>
|
||||
{submit.text}
|
||||
</Button>
|
||||
</Layout.Stack>
|
||||
</div>
|
||||
</Layout.Stack>
|
||||
</div>
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
</Sheet>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.sheet-container {
|
||||
top: 0;
|
||||
position: absolute;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
& :global(aside header) {
|
||||
margin-top: 6rem;
|
||||
}
|
||||
}
|
||||
|
||||
& :global(.sheet-content) {
|
||||
// overflow-y: auto;
|
||||
padding-bottom: 5rem;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
/*
|
||||
* different mobile browsers handle bottom spaces differently,
|
||||
* therefore, having extra bottom space doesn't hurt anyone imo!
|
||||
*/
|
||||
padding-bottom: 15rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sheet-footer {
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
position: absolute;
|
||||
background: var(--bgcolor-neutral-primary);
|
||||
|
||||
@media (max-width: 768px) {
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
& .sheet-footer-actions {
|
||||
padding-inline: var(--space-8);
|
||||
padding-block-end: var(--space-6);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
<script lang="ts">
|
||||
import { debounce } from '$lib/helpers/debounce';
|
||||
import { scrollStore, sheetHeightStore } from './store';
|
||||
import { onMount, onDestroy, type Snippet, tick } from 'svelte';
|
||||
|
||||
let {
|
||||
children
|
||||
}: {
|
||||
children: Snippet;
|
||||
} = $props();
|
||||
|
||||
let spreadsheetWrapper: HTMLDivElement;
|
||||
let spreadsheetGridContainer: HTMLDivElement;
|
||||
|
||||
/** resizing logic variables */
|
||||
let resizeObserver: ResizeObserver;
|
||||
let mutationObserver: MutationObserver;
|
||||
|
||||
/** to avoid querySelector for perf! */
|
||||
let cachedElements = new Set<Element>();
|
||||
|
||||
/** writable store to prevent jumps when changing views */
|
||||
let spreadsheetHeight = $state($sheetHeightStore);
|
||||
|
||||
const handleResize = debounce(() => resizeSheet(), 125);
|
||||
|
||||
function observeElement(selector: string) {
|
||||
const element = document.querySelector(selector);
|
||||
if (element && !cachedElements.has(element)) {
|
||||
cachedElements.add(element);
|
||||
resizeObserver.observe(element);
|
||||
}
|
||||
}
|
||||
|
||||
/** get the actual spreadsheet-container */
|
||||
function initSpreadsheetGridContainer(): boolean {
|
||||
if (spreadsheetGridContainer) return true;
|
||||
|
||||
spreadsheetGridContainer = spreadsheetWrapper?.querySelector('.spreadsheet-container');
|
||||
return !!spreadsheetGridContainer;
|
||||
}
|
||||
|
||||
/** adjust height to fill remaining viewport space */
|
||||
function resizeSheet(): void {
|
||||
if (!spreadsheetWrapper) return;
|
||||
const wrapperRect = spreadsheetWrapper.getBoundingClientRect();
|
||||
const wrapperTop = wrapperRect.top;
|
||||
const viewportHeight = window.innerHeight;
|
||||
const availableHeight = viewportHeight - wrapperTop;
|
||||
const finalHeight = Math.max(100, availableHeight);
|
||||
|
||||
const currentHeight = parseFloat(spreadsheetHeight);
|
||||
const heightChanged = Math.abs(currentHeight - finalHeight) > 1;
|
||||
|
||||
if (heightChanged) {
|
||||
const newHeight = `${finalHeight}px`;
|
||||
spreadsheetHeight = newHeight;
|
||||
sheetHeightStore.set(newHeight);
|
||||
}
|
||||
}
|
||||
|
||||
function addObservers() {
|
||||
/** grab the sheet container */
|
||||
initSpreadsheetGridContainer();
|
||||
|
||||
resizeObserver = new ResizeObserver(handleResize);
|
||||
|
||||
/** banners */
|
||||
observeElement('.top-banner');
|
||||
|
||||
/** expand / collapse tabs */
|
||||
observeElement('.layout-header');
|
||||
|
||||
/** just in case */
|
||||
resizeObserver.observe(document.body);
|
||||
|
||||
/** add an observer when a banner pops-in */
|
||||
mutationObserver = new MutationObserver(() => {
|
||||
observeElement('.top-banner');
|
||||
});
|
||||
|
||||
mutationObserver.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
}
|
||||
|
||||
/** save grid sheet scroll for restore */
|
||||
export function saveGridSheetScroll(): void {
|
||||
if (initSpreadsheetGridContainer()) {
|
||||
scrollStore.set(spreadsheetGridContainer.scrollLeft || 0);
|
||||
}
|
||||
}
|
||||
|
||||
/** restore grid sheet scroll from before */
|
||||
export function restoreGridSheetScroll(): void {
|
||||
if (initSpreadsheetGridContainer() && spreadsheetGridContainer.scrollWidth > 0) {
|
||||
spreadsheetGridContainer.scrollTop = 0;
|
||||
spreadsheetGridContainer.scrollLeft = $scrollStore;
|
||||
}
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
await tick();
|
||||
addObservers();
|
||||
resizeSheet();
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
resizeObserver?.disconnect();
|
||||
mutationObserver?.disconnect();
|
||||
});
|
||||
</script>
|
||||
|
||||
<div bind:this={spreadsheetWrapper} class="spreadsheet-wrapper" style:height={spreadsheetHeight}>
|
||||
{@render children()}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.spreadsheet-wrapper {
|
||||
transition: height 300ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
</style>
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export const scrollStore = writable(null);
|
||||
|
||||
export const sheetHeightStore = writable('74.5vh');
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
<script lang="ts">
|
||||
import Modal from '$lib/components/modal.svelte';
|
||||
import Button from '$lib/elements/forms/button.svelte';
|
||||
import { Icon } from '@appwrite.io/pink-svelte';
|
||||
import { IconExclamationCircle } from '@appwrite.io/pink-icons-svelte';
|
||||
|
||||
let {
|
||||
show = $bindable(),
|
||||
error,
|
||||
title,
|
||||
header
|
||||
}: {
|
||||
show: boolean;
|
||||
error: string;
|
||||
title: string;
|
||||
header: string;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<!-- TODO: @itznotabug, Stacks please -->
|
||||
<Modal {title} bind:show>
|
||||
<div class="box u-flex-vertical u-gap-24">
|
||||
<p class="u-inline-flex u-cross-center u-gap-8">
|
||||
<Icon icon={IconExclamationCircle} size="m" color="--color-danger-100" />
|
||||
|
||||
{header}
|
||||
</p>
|
||||
<p>{error}</p>
|
||||
</div>
|
||||
|
||||
<svelte:fragment slot="footer">
|
||||
<Button secondary on:click={() => (show = false)}>Close</Button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
+87
-61
@@ -1,5 +1,14 @@
|
||||
<script module lang="ts">
|
||||
export type CreateIndexesCallbackType = {
|
||||
key: string;
|
||||
type: IndexType;
|
||||
fields: string[];
|
||||
lengths: number[];
|
||||
orders: string[];
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/state';
|
||||
import { goto, invalidate } from '$app/navigation';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
@@ -7,40 +16,49 @@
|
||||
import { Button, InputNumber, InputSelect, InputText } from '$lib/elements/forms';
|
||||
import { remove } from '$lib/helpers/array';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { IndexType } from '@appwrite.io/console';
|
||||
import { isRelationship, isSpatialType } from '../rows/store';
|
||||
import { table, indexes } from '../store';
|
||||
import { isRelationship, isSpatialType } from '$database/table-[table]/rows/store';
|
||||
import { Icon, Layout } from '@appwrite.io/pink-svelte';
|
||||
import { IconPlus, IconX } from '@appwrite.io/pink-icons-svelte';
|
||||
import { isSmallViewport } from '$lib/stores/viewport';
|
||||
import type { DependenciesResult, Entity, TerminologyResult } from '$database/(entity)';
|
||||
import { resolveRoute, withPath } from '$lib/stores/navigation';
|
||||
import { IndexType } from '@appwrite.io/console';
|
||||
|
||||
let {
|
||||
entity,
|
||||
terminology,
|
||||
dependencies,
|
||||
showCreateIndex = $bindable(false),
|
||||
externalColumnKey = null
|
||||
externalFieldKey = null,
|
||||
onCreateIndex
|
||||
}: {
|
||||
entity: Entity;
|
||||
terminology: TerminologyResult;
|
||||
dependencies: DependenciesResult;
|
||||
showCreateIndex: boolean;
|
||||
externalColumnKey?: string;
|
||||
externalFieldKey?: string;
|
||||
onCreateIndex: (index: CreateIndexesCallbackType) => Promise<void>;
|
||||
} = $props();
|
||||
|
||||
const databaseId = page.params.database;
|
||||
|
||||
let key = $state('');
|
||||
|
||||
let selectedType = $state<IndexType>(IndexType.Key);
|
||||
|
||||
let columnOptions = $derived(
|
||||
$table.columns
|
||||
.filter((column) => {
|
||||
const fieldOptions = $derived(
|
||||
// TODO: could be columns or attributes!
|
||||
entity.columns
|
||||
.filter((field) => {
|
||||
if (selectedType === IndexType.Spatial) {
|
||||
return isSpatialType(column); // keep only spatial
|
||||
// keep only spatial
|
||||
return isSpatialType(field);
|
||||
}
|
||||
return !isRelationship(column) && !isSpatialType(column); // keep non-relationship and non-spatial
|
||||
// keep non-relationship and non-spatial
|
||||
return !isRelationship(field) && !isSpatialType(field);
|
||||
})
|
||||
.map((column) => ({ value: column.key, label: column.key }))
|
||||
.map((field) => ({ value: field.key, label: field.key }))
|
||||
);
|
||||
|
||||
let columnList = $state([{ value: '', order: '', length: null }]);
|
||||
let fieldList = $state([{ value: '', order: '', length: null }]);
|
||||
|
||||
const types = [
|
||||
{ value: IndexType.Key, label: 'Key' },
|
||||
@@ -63,16 +81,16 @@
|
||||
]
|
||||
);
|
||||
|
||||
// spatial type selected -> reset column list to single empty column
|
||||
// and the column already is not spatial type
|
||||
// spatial type selected -> reset field list to single empty field
|
||||
// and the field already is not spatial type
|
||||
$effect(() => {
|
||||
if (selectedType === IndexType.Spatial && !columnList.at(0).value) {
|
||||
columnList = [{ value: '', order: null, length: null }];
|
||||
if (selectedType === IndexType.Spatial && !fieldList.at(0).value) {
|
||||
fieldList = [{ value: '', order: null, length: null }];
|
||||
}
|
||||
});
|
||||
|
||||
function generateIndexKey() {
|
||||
let indexKeys = $indexes.map((index) => index.key);
|
||||
let indexKeys = entity.indexes.map((index) => index.key);
|
||||
|
||||
let highestIndex = indexKeys.reduce((max, key) => {
|
||||
const match = key.match(/^index_(\d+)$/);
|
||||
@@ -83,26 +101,33 @@
|
||||
}
|
||||
|
||||
function initialize() {
|
||||
const column = $table.columns.filter((column) => externalColumnKey === column.key);
|
||||
const isSpatial = column.length && isSpatialType(column[0]);
|
||||
// TODO: could be columns or attributes or something else!
|
||||
const field = entity.columns.filter((field) => externalFieldKey === field.key);
|
||||
const isSpatial = field.length && isSpatialType(field[0]);
|
||||
const order = isSpatial ? null : 'ASC';
|
||||
selectedType = isSpatial ? IndexType.Spatial : IndexType.Key;
|
||||
columnList = externalColumnKey
|
||||
? [{ value: externalColumnKey, order, length: null }]
|
||||
fieldList = externalFieldKey
|
||||
? [{ value: externalFieldKey, order, length: null }]
|
||||
: [{ value: '', order, length: null }];
|
||||
key = `index_${$indexes.length + 1}`;
|
||||
key = `index_${entity.indexes.length + 1}`;
|
||||
}
|
||||
|
||||
const addColumnDisabled = $derived(
|
||||
const addFieldDisabled = $derived(
|
||||
selectedType === IndexType.Spatial ||
|
||||
!columnList.at(-1)?.value ||
|
||||
(!columnList.at(-1)?.order && columnList.at(-1)?.order !== null)
|
||||
!fieldList.at(-1)?.value ||
|
||||
(!fieldList.at(-1)?.order && fieldList.at(-1)?.order !== null)
|
||||
);
|
||||
|
||||
const isOnIndexesPage = $derived(page.route.id?.endsWith('/indexes'));
|
||||
const navigatorPathToIndexes = $derived(
|
||||
`${base}/project-${page.params.region}-${page.params.project}/databases/database-${databaseId}/table-${$table?.$id}/indexes`
|
||||
);
|
||||
const navigatorPathToIndexes = $derived.by(() => {
|
||||
const type = terminology.entity.lower.singular;
|
||||
const base = resolveRoute(
|
||||
'/(console)/project-[region]-[project]/databases/database-[database]',
|
||||
page.params
|
||||
);
|
||||
|
||||
return withPath(base, `${type}-${entity.$id}`, 'indexes');
|
||||
});
|
||||
|
||||
let initializedForOpen = $state(false);
|
||||
$effect(() => {
|
||||
@@ -117,28 +142,27 @@
|
||||
});
|
||||
|
||||
export async function create() {
|
||||
if (!key || !selectedType || (selectedType !== IndexType.Spatial && addColumnDisabled)) {
|
||||
const fieldType = terminology.field.lower.singular;
|
||||
if (!key || !selectedType || (selectedType !== IndexType.Spatial && addFieldDisabled)) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: 'Selected column key or type invalid'
|
||||
message: `Selected ${fieldType} key or type invalid`
|
||||
});
|
||||
throw new Error('Selected column key or type invalid');
|
||||
throw new Error(`Selected ${fieldType} key or type invalid`);
|
||||
}
|
||||
|
||||
try {
|
||||
const orders = columnList.map((a) => a.order).filter((order) => order !== null);
|
||||
await sdk.forProject(page.params.region, page.params.project).tablesDB.createIndex({
|
||||
databaseId,
|
||||
tableId: $table.$id,
|
||||
const orders = fieldList.map((a) => a.order).filter((order) => order !== null);
|
||||
await onCreateIndex({
|
||||
key,
|
||||
type: selectedType,
|
||||
columns: columnList.map((a) => a.value),
|
||||
lengths: columnList.map((a) => (a.length ? Number(a.length) : null)),
|
||||
...(orders.length ? { orders } : {})
|
||||
fields: fieldList.map((a) => a.value),
|
||||
lengths: fieldList.map((a) => (a.length ? Number(a.length) : null)),
|
||||
orders: orders.length ? orders : []
|
||||
});
|
||||
|
||||
await Promise.allSettled([
|
||||
invalidate(Dependencies.TABLE),
|
||||
invalidate(dependencies.entity.singular),
|
||||
invalidate(Dependencies.DATABASE)
|
||||
]);
|
||||
|
||||
@@ -166,11 +190,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
function addColumn() {
|
||||
if (addColumnDisabled) return;
|
||||
function addField() {
|
||||
if (addFieldDisabled) return;
|
||||
|
||||
// We assign instead of pushing to trigger Svelte's reactivity
|
||||
columnList = [...columnList, { value: '', order: '', length: null }];
|
||||
fieldList = [...fieldList, { value: '', order: '', length: null }];
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -183,10 +207,12 @@
|
||||
bind:value={key}
|
||||
autofocus />
|
||||
|
||||
<InputSelect required options={types} id="type" label="Index type" bind:value={selectedType} />
|
||||
<InputSelect required id="type" options={types} label="Index type" bind:value={selectedType} />
|
||||
|
||||
<Layout.Stack gap="s">
|
||||
{#each columnList as column, index}
|
||||
{@const fieldType = terminology.field.title.singular}
|
||||
{@const fieldTypeLower = terminology.field.lower.singular}
|
||||
{#each fieldList as field, index}
|
||||
{@const direction = $isSmallViewport ? 'column' : 'row'}
|
||||
<Layout.Stack {direction}>
|
||||
<InputSelect
|
||||
@@ -200,19 +226,19 @@
|
||||
{ value: '$createdAt', label: '$createdAt' },
|
||||
{ value: '$updatedAt', label: '$updatedAt' }
|
||||
]),
|
||||
...columnOptions
|
||||
...fieldOptions
|
||||
]}
|
||||
id={`column-${index}`}
|
||||
label={index === 0 ? 'Column' : undefined}
|
||||
placeholder="Select column"
|
||||
bind:value={column.value} />
|
||||
id={`field-${index}`}
|
||||
label={index === 0 ? fieldType : undefined}
|
||||
placeholder="Select {fieldType}"
|
||||
bind:value={field.value} />
|
||||
|
||||
<InputSelect
|
||||
options={orderOptions}
|
||||
required
|
||||
id={`order-${index}`}
|
||||
label={index === 0 ? 'Order' : undefined}
|
||||
bind:value={column.order}
|
||||
bind:value={field.order}
|
||||
placeholder="Select order" />
|
||||
|
||||
{#if selectedType === IndexType.Key}
|
||||
@@ -220,7 +246,7 @@
|
||||
id={`length-${index}`}
|
||||
label={index === 0 ? 'Length' : undefined}
|
||||
placeholder="Enter length"
|
||||
bind:value={column.length} />
|
||||
bind:value={field.length} />
|
||||
{/if}
|
||||
|
||||
{#if $isSmallViewport}
|
||||
@@ -228,9 +254,9 @@
|
||||
<Button
|
||||
text
|
||||
secondary
|
||||
disabled={columnList.length <= 1}
|
||||
disabled={fieldList.length <= 1}
|
||||
on:click={() => {
|
||||
columnList = remove(columnList, index);
|
||||
fieldList = remove(fieldList, index);
|
||||
}}>
|
||||
Remove
|
||||
</Button>
|
||||
@@ -241,9 +267,9 @@
|
||||
icon
|
||||
size="s"
|
||||
secondary
|
||||
disabled={columnList.length <= 1}
|
||||
disabled={fieldList.length <= 1}
|
||||
on:click={() => {
|
||||
columnList = remove(columnList, index);
|
||||
fieldList = remove(fieldList, index);
|
||||
}}>
|
||||
<Icon icon={IconX} size="s" />
|
||||
</Button>
|
||||
@@ -252,9 +278,9 @@
|
||||
</Layout.Stack>
|
||||
{/each}
|
||||
<div>
|
||||
<Button compact on:click={addColumn} disabled={addColumnDisabled}>
|
||||
<Button compact on:click={addField} disabled={addFieldDisabled}>
|
||||
<Icon icon={IconPlus} slot="start" size="s" />
|
||||
Add column
|
||||
Add {fieldTypeLower}
|
||||
</Button>
|
||||
</div>
|
||||
</Layout.Stack>
|
||||
+32
-24
@@ -1,49 +1,57 @@
|
||||
<script lang="ts">
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { page } from '$app/state';
|
||||
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import Confirm from '$lib/components/confirm.svelte';
|
||||
import type { DependenciesResult } from '$database/(entity)';
|
||||
|
||||
let {
|
||||
dependencies,
|
||||
showDelete = $bindable(false),
|
||||
selectedIndex = $bindable(null)
|
||||
selectedIndex = $bindable(null),
|
||||
onDeleteIndexes
|
||||
}: {
|
||||
dependencies: DependenciesResult;
|
||||
showDelete: boolean;
|
||||
selectedIndex: Models.ColumnIndex | string[];
|
||||
selectedIndex: Models.ColumnIndex | string[] | null;
|
||||
onDeleteIndexes: (selectedKeys: string[]) => Promise<void>;
|
||||
} = $props();
|
||||
|
||||
let error: string = $state(null);
|
||||
let selectedKeys = $derived(getKeys(selectedIndex));
|
||||
|
||||
function getKeys(selected: Models.ColumnIndex | string[]): string[] {
|
||||
console.log(`getKeys`, selected);
|
||||
return Array.isArray(selected) ? selected : [selected.key];
|
||||
}
|
||||
|
||||
async function cleanup() {
|
||||
// reset selection!
|
||||
selectedIndex = [];
|
||||
|
||||
showDelete = false; // hide.
|
||||
|
||||
// events and notif!
|
||||
trackEvent(Submit.IndexDelete);
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message:
|
||||
selectedKeys.length === 1
|
||||
? 'Index has been deleted'
|
||||
: `${selectedKeys.length} indexes have been deleted`
|
||||
});
|
||||
|
||||
console.log(`cleanup > selectedIndex`, selectedIndex, selectedKeys);
|
||||
|
||||
// invalidate proper dependency.
|
||||
await invalidate(dependencies.entity.singular);
|
||||
}
|
||||
|
||||
async function handleDelete() {
|
||||
try {
|
||||
await Promise.all(
|
||||
selectedKeys.map((key) =>
|
||||
sdk.forProject(page.params.region, page.params.project).tablesDB.deleteIndex({
|
||||
databaseId: page.params.database,
|
||||
tableId: page.params.table,
|
||||
key
|
||||
})
|
||||
)
|
||||
);
|
||||
await invalidate(Dependencies.TABLE);
|
||||
showDelete = false;
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message:
|
||||
selectedKeys.length === 1
|
||||
? 'Index has been deleted'
|
||||
: `${selectedKeys.length} indexes have been deleted`
|
||||
});
|
||||
trackEvent(Submit.IndexDelete);
|
||||
await onDeleteIndexes(selectedKeys);
|
||||
await cleanup();
|
||||
} catch (e) {
|
||||
error = e.message;
|
||||
trackError(e, Submit.IndexDelete);
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
export { default as Indexes } from './view.svelte';
|
||||
export { default as CreateIndex, type CreateIndexesCallbackType } from './create.svelte';
|
||||
+7
-1
@@ -2,12 +2,17 @@
|
||||
import { InputText } from '$lib/elements/forms';
|
||||
import { Layout } from '@appwrite.io/pink-svelte';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import type { TerminologyResult } from '$database/(entity)';
|
||||
|
||||
let {
|
||||
terminology,
|
||||
selectedIndex = null
|
||||
}: {
|
||||
terminology: TerminologyResult;
|
||||
selectedIndex: Models.ColumnIndex;
|
||||
} = $props();
|
||||
|
||||
const entityType = terminology.entity.title.singular;
|
||||
</script>
|
||||
|
||||
<InputText
|
||||
@@ -25,12 +30,13 @@
|
||||
value={selectedIndex.type}
|
||||
readonly />
|
||||
|
||||
<!-- TODO: could be attributes or columns -->
|
||||
{#if selectedIndex?.columns?.length}
|
||||
{#each selectedIndex.columns as column, i}
|
||||
<Layout.Stack direction="row">
|
||||
<InputText
|
||||
required
|
||||
label={i === 0 ? 'Column' : ''}
|
||||
label={i === 0 ? entityType : ''}
|
||||
id={`value-${column}`}
|
||||
value={column}
|
||||
readonly />
|
||||
+2
@@ -3,6 +3,8 @@
|
||||
import { Icon } from '@appwrite.io/pink-svelte';
|
||||
import { IconChevronDown } from '@appwrite.io/pink-icons-svelte';
|
||||
|
||||
// TODO: @itznotabug: svelte5
|
||||
|
||||
export let value: string;
|
||||
export let id: string;
|
||||
export let label: string;
|
||||
+308
@@ -0,0 +1,308 @@
|
||||
<script lang="ts">
|
||||
import { Container } from '$lib/layout';
|
||||
import Delete from './delete.svelte';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import Overview from './overview.svelte';
|
||||
import CreateIndex from './create.svelte';
|
||||
import FailedModal from '../failedModal.svelte';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { canWriteTables } from '$lib/stores/roles';
|
||||
import {
|
||||
ActionMenu,
|
||||
Badge,
|
||||
Divider,
|
||||
FloatingActionBar,
|
||||
Icon,
|
||||
Layout,
|
||||
Link,
|
||||
Popover,
|
||||
Spreadsheet,
|
||||
Typography
|
||||
} from '@appwrite.io/pink-svelte';
|
||||
import {
|
||||
IconDotsHorizontal,
|
||||
IconEye,
|
||||
IconPlus,
|
||||
IconTrash
|
||||
} from '@appwrite.io/pink-icons-svelte';
|
||||
import { type ComponentProps, onDestroy, type Snippet } from 'svelte';
|
||||
import { Click, trackEvent } from '$lib/actions/analytics';
|
||||
import { isSmallViewport } from '$lib/stores/viewport';
|
||||
import { type Entity, EntityContainer } from '$database/(entity)';
|
||||
import { SpreadsheetContainer, SideSheet } from '$database/(entity)/views/(layouts)';
|
||||
import type { CreateIndexesCallbackType } from '$database/(entity)/views/indexes/create.svelte';
|
||||
|
||||
// TODO: change `column` to `entity`!
|
||||
|
||||
let {
|
||||
entity,
|
||||
showCreateColumnSheet = $bindable(),
|
||||
onCreateIndex,
|
||||
onDeleteIndexes,
|
||||
emptyIndexesSheetView,
|
||||
emptyColumnsSheetView
|
||||
}: {
|
||||
entity: Entity;
|
||||
showCreateColumnSheet: boolean;
|
||||
onCreateIndex: (index: CreateIndexesCallbackType) => Promise<void>;
|
||||
onDeleteIndexes: (indexKeys: string[]) => Promise<void>;
|
||||
emptyIndexesSheetView: Snippet<[() => void]>;
|
||||
emptyColumnsSheetView?: Snippet<[() => void]>;
|
||||
} = $props();
|
||||
|
||||
let showCreateIndex = $state(false);
|
||||
let selectedIndex: Models.ColumnIndex = $state(null);
|
||||
|
||||
let createIndex: CreateIndex;
|
||||
let selectedIndexes = $state([]);
|
||||
|
||||
let error = $state('');
|
||||
let showFailed = $state(false);
|
||||
let showDelete = $state(false);
|
||||
let showOverview = $state(false);
|
||||
|
||||
let columns = $state([
|
||||
{ id: 'key' },
|
||||
{ id: 'type' },
|
||||
{ id: 'columns' },
|
||||
// { id: 'orders' }, // design doesn't have orders atm
|
||||
{ id: 'lengths' },
|
||||
{ id: 'actions', width: 40, isAction: true }
|
||||
]);
|
||||
|
||||
function getColumnStatusBadge(status: string): ComponentProps<Badge>['type'] {
|
||||
switch (status) {
|
||||
case 'processing':
|
||||
return 'warning';
|
||||
case 'deleting':
|
||||
case 'stuck':
|
||||
case 'failed':
|
||||
return 'error';
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
onDestroy(() => (showCreateColumnSheet = false));
|
||||
|
||||
const emptyCellsLimit = $derived($isSmallViewport ? 14 : 17);
|
||||
const emptyCellsCount = $derived(
|
||||
entity.indexes.length >= emptyCellsLimit ? 0 : emptyCellsLimit - entity.indexes.length
|
||||
);
|
||||
|
||||
$effect(() => {
|
||||
console.log(`$effect > selectedIndexes`, $state.snapshot(selectedIndexes));
|
||||
});
|
||||
</script>
|
||||
|
||||
<EntityContainer>
|
||||
{#snippet children(_, dependencies, terminology)}
|
||||
<Container
|
||||
expanded
|
||||
expandHeightButton={$isSmallViewport}
|
||||
style="background: var(--bgcolor-neutral-primary)">
|
||||
<Layout.Stack direction="row" justifyContent="flex-end">
|
||||
{#if $canWriteTables}
|
||||
<Button
|
||||
secondary
|
||||
event="create_index"
|
||||
disabled={!entity.columns?.length}
|
||||
on:click={() => (showCreateIndex = true)}>
|
||||
<Icon icon={IconPlus} slot="start" size="s" />
|
||||
Create index
|
||||
</Button>
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
</Container>
|
||||
|
||||
<div class="databases-spreadsheet">
|
||||
{#if entity.columns?.length}
|
||||
{#if entity.indexes.length}
|
||||
<SpreadsheetContainer>
|
||||
<Spreadsheet.Root
|
||||
let:root
|
||||
{columns}
|
||||
height="100%"
|
||||
allowSelection
|
||||
emptyCells={emptyCellsCount}
|
||||
bind:selectedRows={selectedIndexes}
|
||||
bottomActionClick={() => (showCreateIndex = true)}>
|
||||
<svelte:fragment slot="header" let:root>
|
||||
<Spreadsheet.Header.Cell column="key" {root}
|
||||
>Key</Spreadsheet.Header.Cell>
|
||||
<Spreadsheet.Header.Cell column="type" {root}
|
||||
>Type</Spreadsheet.Header.Cell>
|
||||
<Spreadsheet.Header.Cell column="columns" {root}
|
||||
>Columns</Spreadsheet.Header.Cell>
|
||||
<!-- <Spreadsheet.Header.Cell column="orders" {root}-->
|
||||
<!-- >Orders</Spreadsheet.Header.Cell>-->
|
||||
<Spreadsheet.Header.Cell column="lengths" {root}
|
||||
>Lengths</Spreadsheet.Header.Cell>
|
||||
<Spreadsheet.Header.Cell column="actions" {root} />
|
||||
</svelte:fragment>
|
||||
|
||||
{#each entity.indexes as index (index.key)}
|
||||
<Spreadsheet.Row.Base {root} id={index.key}>
|
||||
<Spreadsheet.Cell column="key" {root} isEditable={false}>
|
||||
<Layout.Stack direction="row" alignItems="center">
|
||||
{index.key}
|
||||
{#if index.status !== 'available'}
|
||||
<Badge
|
||||
size="s"
|
||||
variant="secondary"
|
||||
content={index.status}
|
||||
type={getColumnStatusBadge(index.status)} />
|
||||
{#if index.error}
|
||||
<Link.Button
|
||||
variant="muted"
|
||||
on:click={(e) => {
|
||||
e.preventDefault();
|
||||
error = index.error;
|
||||
showFailed = true;
|
||||
}}>Details</Link.Button>
|
||||
{/if}
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
</Spreadsheet.Cell>
|
||||
<Spreadsheet.Cell column="type" {root} isEditable={false}
|
||||
>{index.type}</Spreadsheet.Cell>
|
||||
<Spreadsheet.Cell column="columns" {root} isEditable={false}>
|
||||
{index.columns.join(', ')}
|
||||
</Spreadsheet.Cell>
|
||||
<!-- <Spreadsheet.Cell column="orders" {root} isEditable={false}>-->
|
||||
<!-- {index.orders}-->
|
||||
<!-- </Spreadsheet.Cell>-->
|
||||
<Spreadsheet.Cell column="lengths" {root} isEditable={false}>
|
||||
{index.lengths}
|
||||
</Spreadsheet.Cell>
|
||||
<Spreadsheet.Cell column="actions" {root}>
|
||||
<Popover
|
||||
let:toggle
|
||||
padding="none"
|
||||
placement="bottom-end"
|
||||
portal>
|
||||
<Button
|
||||
text
|
||||
icon
|
||||
ariaLabel="more options"
|
||||
on:click={toggle}>
|
||||
<Icon icon={IconDotsHorizontal} size="s" />
|
||||
</Button>
|
||||
<ActionMenu.Root slot="tooltip" let:toggle>
|
||||
<ActionMenu.Item.Button
|
||||
leadingIcon={IconEye}
|
||||
on:click={() => {
|
||||
toggle();
|
||||
selectedIndex = index;
|
||||
showOverview = true;
|
||||
}}>Overview</ActionMenu.Item.Button>
|
||||
|
||||
<div style:padding-block="0.25rem">
|
||||
<Divider />
|
||||
</div>
|
||||
|
||||
<ActionMenu.Item.Button
|
||||
status="danger"
|
||||
leadingIcon={IconTrash}
|
||||
on:click={() => {
|
||||
toggle();
|
||||
showDelete = true;
|
||||
selectedIndex = index;
|
||||
trackEvent(Click.DatabaseIndexDelete);
|
||||
}}>Delete</ActionMenu.Item.Button>
|
||||
</ActionMenu.Root>
|
||||
</Popover>
|
||||
</Spreadsheet.Cell>
|
||||
</Spreadsheet.Row.Base>
|
||||
{/each}
|
||||
|
||||
<svelte:fragment slot="footer">
|
||||
<Layout.Stack
|
||||
direction="row"
|
||||
alignContent="center"
|
||||
alignItems="center"
|
||||
justifyContent="space-between">
|
||||
<Typography.Text
|
||||
variant="m-400"
|
||||
color="--fgcolor-neutral-secondary">
|
||||
{@const length = entity.indexes.length}
|
||||
{length}
|
||||
{length === 1 ? 'index' : 'indexes'}
|
||||
</Typography.Text>
|
||||
</Layout.Stack>
|
||||
</svelte:fragment>
|
||||
</Spreadsheet.Root>
|
||||
</SpreadsheetContainer>
|
||||
{:else}
|
||||
{@render emptyIndexesSheetView(() => (showCreateIndex = true))}
|
||||
{/if}
|
||||
{:else}
|
||||
{@render emptyColumnsSheetView(() => (showCreateColumnSheet = true))}
|
||||
{/if}
|
||||
|
||||
{#if selectedIndexes.length > 0}
|
||||
<div class="floating-action-bar">
|
||||
<FloatingActionBar>
|
||||
<svelte:fragment slot="start">
|
||||
<div style:width="max-content">
|
||||
<Layout.Stack direction="row" alignItems="center" gap="m">
|
||||
<Badge content={selectedIndexes.length.toString()} />
|
||||
<span style:font-size="14px">
|
||||
{selectedIndexes.length > 1 ? 'indexes' : 'index'}
|
||||
selected
|
||||
</span>
|
||||
</Layout.Stack>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="end">
|
||||
<Button text on:click={() => (selectedIndexes = [])}>Cancel</Button>
|
||||
<Button secondary on:click={() => (showDelete = true)}>Delete</Button>
|
||||
</svelte:fragment>
|
||||
</FloatingActionBar>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<SideSheet
|
||||
title="Create index"
|
||||
bind:show={showCreateIndex}
|
||||
submit={{
|
||||
text: 'Create',
|
||||
onClick: async () => await createIndex.create()
|
||||
}}>
|
||||
<CreateIndex
|
||||
{entity}
|
||||
{terminology}
|
||||
{dependencies}
|
||||
{onCreateIndex}
|
||||
{showCreateIndex}
|
||||
bind:this={createIndex} />
|
||||
</SideSheet>
|
||||
|
||||
{#if selectedIndex}
|
||||
<Delete bind:showDelete {onDeleteIndexes} {dependencies} bind:selectedIndex />
|
||||
{:else if selectedIndexes && selectedIndexes.length}
|
||||
<Delete
|
||||
bind:showDelete
|
||||
{dependencies}
|
||||
{onDeleteIndexes}
|
||||
bind:selectedIndex={selectedIndexes} />
|
||||
{/if}
|
||||
|
||||
<SideSheet title="Preview index" bind:show={showOverview}>
|
||||
<Overview {selectedIndex} {terminology} />
|
||||
</SideSheet>
|
||||
|
||||
<FailedModal {error} bind:show={showFailed} title="Create index" header="Creation failed" />
|
||||
{/snippet}
|
||||
</EntityContainer>
|
||||
|
||||
<style lang="scss">
|
||||
.floating-action-bar {
|
||||
left: 50%;
|
||||
width: 100%;
|
||||
z-index: 14;
|
||||
position: absolute;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
</style>
|
||||
+29
-5
@@ -57,13 +57,15 @@
|
||||
import { generateFakeRecords, generateColumns } from '$lib/helpers/faker';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { sleep } from '$lib/helpers/promises';
|
||||
import CreateIndex from './indexes/createIndex.svelte';
|
||||
import { hash } from '$lib/helpers/string';
|
||||
import { preferences } from '$lib/stores/preferences';
|
||||
import { buildRowUrl, isRelationship } from './rows/store';
|
||||
import { chunks } from '$lib/helpers/array';
|
||||
import { Submit, trackEvent } from '$lib/actions/analytics';
|
||||
|
||||
import { CreateIndex } from '$database/(entity)';
|
||||
import { EntityContainer } from '$database/(entity)/index.js';
|
||||
|
||||
let editRow: EditRow;
|
||||
let editRelatedRow: EditRelatedRow;
|
||||
let editRowPermissions: EditRowPermissions;
|
||||
@@ -431,10 +433,32 @@
|
||||
await createIndex.create();
|
||||
}
|
||||
}}>
|
||||
<CreateIndex
|
||||
bind:this={createIndex}
|
||||
bind:showCreateIndex={$showCreateIndexSheet.show}
|
||||
externalColumnKey={$showCreateIndexSheet.column} />
|
||||
<EntityContainer>
|
||||
<!-- TODO: @itznotabug, not the best way, see what we can do. -->
|
||||
<!-- Maybe a better setContext logic would suffice! would also avoid using snippets! -->
|
||||
{#snippet children(_, dependencies, terminology)}
|
||||
<CreateIndex
|
||||
entity={$table}
|
||||
bind:this={createIndex}
|
||||
bind:showCreateIndex={$showCreateIndexSheet.show}
|
||||
externalFieldKey={$showCreateIndexSheet.column}
|
||||
{dependencies}
|
||||
{terminology}
|
||||
onCreateIndex={async (index) => {
|
||||
await sdk
|
||||
.forProject(page.params.region, page.params.project)
|
||||
.tablesDB.createIndex({
|
||||
databaseId: page.params.database,
|
||||
tableId: page.params.table,
|
||||
key: index.key,
|
||||
type: index.type,
|
||||
columns: index.fields,
|
||||
lengths: index.lengths,
|
||||
orders: index.orders
|
||||
});
|
||||
}} />
|
||||
{/snippet}
|
||||
</EntityContainer>
|
||||
</SideSheet>
|
||||
|
||||
<SideSheet
|
||||
|
||||
+1
-1
@@ -474,5 +474,5 @@
|
||||
</SideSheet>
|
||||
|
||||
{#if showFailed}
|
||||
<FailedModal bind:show={showFailed} title="Create attribute" header="Creation failed" {error} />
|
||||
<FailedModal bind:show={showFailed} title="Create column" header="Creation failed" {error} />
|
||||
{/if}
|
||||
|
||||
+49
-269
@@ -1,288 +1,68 @@
|
||||
<script lang="ts">
|
||||
import { Container } from '$lib/layout';
|
||||
import { table } from '../store';
|
||||
import Delete from './deleteIndex.svelte';
|
||||
import CreateIndex from './createIndex.svelte';
|
||||
import Overview from './overviewIndex.svelte';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import FailedModal from '../failedModal.svelte';
|
||||
import { canWriteTables } from '$lib/stores/roles';
|
||||
import {
|
||||
ActionMenu,
|
||||
Badge,
|
||||
Divider,
|
||||
FloatingActionBar,
|
||||
Icon,
|
||||
Layout,
|
||||
Link,
|
||||
Popover,
|
||||
Spreadsheet,
|
||||
Typography
|
||||
} from '@appwrite.io/pink-svelte';
|
||||
import {
|
||||
IconDotsHorizontal,
|
||||
IconEye,
|
||||
IconPlus,
|
||||
IconTrash
|
||||
} from '@appwrite.io/pink-icons-svelte';
|
||||
import { type ComponentProps, onDestroy } from 'svelte';
|
||||
import { Click, trackEvent } from '$lib/actions/analytics';
|
||||
import { page } from '$app/state';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import type { PageProps } from './$types';
|
||||
import EmptySheet from '../layout/emptySheet.svelte';
|
||||
import SpreadsheetContainer from '../layout/spreadsheet.svelte';
|
||||
import SideSheet from '../layout/sidesheet.svelte';
|
||||
import type { PageData } from './$types';
|
||||
import { showCreateColumnSheet } from '../store';
|
||||
import { isSmallViewport } from '$lib/stores/viewport';
|
||||
import { showCreateColumnSheet } from '$database/table-[table]/store';
|
||||
import { type CreateIndexesCallbackType, Indexes } from '$database/(entity)';
|
||||
|
||||
let {
|
||||
data
|
||||
}: {
|
||||
data: PageData;
|
||||
} = $props();
|
||||
let { data }: PageProps = $props();
|
||||
|
||||
let showCreateIndex = $state(false);
|
||||
let selectedIndex: Models.ColumnIndex = $state(null);
|
||||
const params = $derived({
|
||||
databaseId: page.params.database,
|
||||
tableId: page.params.table
|
||||
});
|
||||
|
||||
let selectedIndexes = $state([]);
|
||||
let createIndex: CreateIndex;
|
||||
const tablesDB = $derived(sdk.forProject(page.params.region, page.params.project).tablesDB);
|
||||
|
||||
let error = $state('');
|
||||
let showFailed = $state(false);
|
||||
let showDelete = $state(false);
|
||||
let showOverview = $state(false);
|
||||
|
||||
let columns = $state([
|
||||
{ id: 'key' },
|
||||
{ id: 'type' },
|
||||
{ id: 'columns' },
|
||||
// { id: 'orders' }, // design doesn't have orders atm
|
||||
{ id: 'lengths' },
|
||||
{ id: 'actions', width: 40, isAction: true }
|
||||
]);
|
||||
|
||||
function getColumnStatusBadge(status: string): ComponentProps<Badge>['type'] {
|
||||
switch (status) {
|
||||
case 'processing':
|
||||
return 'warning';
|
||||
case 'deleting':
|
||||
case 'stuck':
|
||||
case 'failed':
|
||||
return 'error';
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
async function onCreateIndex(index: CreateIndexesCallbackType) {
|
||||
await tablesDB.createIndex({
|
||||
...params,
|
||||
key: index.key,
|
||||
type: index.type,
|
||||
columns: index.fields,
|
||||
lengths: index.lengths,
|
||||
orders: index.orders
|
||||
});
|
||||
}
|
||||
|
||||
onDestroy(() => ($showCreateColumnSheet.show = false));
|
||||
|
||||
const emptyCellsLimit = $derived($isSmallViewport ? 14 : 17);
|
||||
const emptyCellsCount = $derived(
|
||||
data.table.indexes.length >= emptyCellsLimit
|
||||
? 0
|
||||
: emptyCellsLimit - data.table.indexes.length
|
||||
);
|
||||
async function onDeleteIndexes(selectedKeys: string[]) {
|
||||
await Promise.all(
|
||||
selectedKeys.map((key) =>
|
||||
tablesDB.deleteIndex({
|
||||
...params,
|
||||
key
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
<Container
|
||||
expanded
|
||||
expandHeightButton={$isSmallViewport}
|
||||
style="background: var(--bgcolor-neutral-primary)">
|
||||
<Layout.Stack direction="row" justifyContent="flex-end">
|
||||
{#if $canWriteTables}
|
||||
<Button
|
||||
secondary
|
||||
event="create_index"
|
||||
disabled={!$table?.columns?.length}
|
||||
on:click={() => (showCreateIndex = true)}>
|
||||
<Icon icon={IconPlus} slot="start" size="s" />
|
||||
Create index
|
||||
</Button>
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
</Container>
|
||||
<Indexes
|
||||
{onCreateIndex}
|
||||
{onDeleteIndexes}
|
||||
entity={data.table}
|
||||
showCreateColumnSheet={$showCreateColumnSheet.show}>
|
||||
{#snippet emptyIndexesSheetView(toggle)}
|
||||
<EmptySheet
|
||||
mode="indexes"
|
||||
actions={{
|
||||
primary: {
|
||||
onClick: toggle,
|
||||
disabled: !data.table.columns?.length
|
||||
}
|
||||
}} />
|
||||
{/snippet}
|
||||
|
||||
<div class="databases-spreadsheet">
|
||||
{#if data.table?.columns?.length}
|
||||
{#if data.table.indexes.length}
|
||||
<SpreadsheetContainer>
|
||||
<Spreadsheet.Root
|
||||
let:root
|
||||
{columns}
|
||||
height="100%"
|
||||
allowSelection
|
||||
emptyCells={emptyCellsCount}
|
||||
bind:selectedRows={selectedIndexes}
|
||||
bottomActionClick={() => (showCreateIndex = true)}>
|
||||
<svelte:fragment slot="header" let:root>
|
||||
<Spreadsheet.Header.Cell column="key" {root}>Key</Spreadsheet.Header.Cell>
|
||||
<Spreadsheet.Header.Cell column="type" {root}>Type</Spreadsheet.Header.Cell>
|
||||
<Spreadsheet.Header.Cell column="columns" {root}
|
||||
>Columns</Spreadsheet.Header.Cell>
|
||||
<!-- <Spreadsheet.Header.Cell column="orders" {root}-->
|
||||
<!-- >Orders</Spreadsheet.Header.Cell>-->
|
||||
<Spreadsheet.Header.Cell column="lengths" {root}
|
||||
>Lengths</Spreadsheet.Header.Cell>
|
||||
<Spreadsheet.Header.Cell column="actions" {root} />
|
||||
</svelte:fragment>
|
||||
|
||||
{#each data.table.indexes as index}
|
||||
<Spreadsheet.Row.Base {root} id={index.key}>
|
||||
<Spreadsheet.Cell column="key" {root} isEditable={false}>
|
||||
<Layout.Stack direction="row" alignItems="center">
|
||||
{index.key}
|
||||
{#if index.status !== 'available'}
|
||||
<Badge
|
||||
size="s"
|
||||
variant="secondary"
|
||||
content={index.status}
|
||||
type={getColumnStatusBadge(index.status)} />
|
||||
{#if index.error}
|
||||
<Link.Button
|
||||
variant="muted"
|
||||
on:click={(e) => {
|
||||
e.preventDefault();
|
||||
error = index.error;
|
||||
showFailed = true;
|
||||
}}>Details</Link.Button>
|
||||
{/if}
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
</Spreadsheet.Cell>
|
||||
<Spreadsheet.Cell column="type" {root} isEditable={false}
|
||||
>{index.type}</Spreadsheet.Cell>
|
||||
<Spreadsheet.Cell column="columns" {root} isEditable={false}>
|
||||
{index.columns.join(', ')}
|
||||
</Spreadsheet.Cell>
|
||||
<!-- <Spreadsheet.Cell column="orders" {root} isEditable={false}>-->
|
||||
<!-- {index.orders}-->
|
||||
<!-- </Spreadsheet.Cell>-->
|
||||
<Spreadsheet.Cell column="lengths" {root} isEditable={false}>
|
||||
{index.lengths}
|
||||
</Spreadsheet.Cell>
|
||||
<Spreadsheet.Cell column="actions" {root}>
|
||||
<Popover let:toggle padding="none" placement="bottom-end" portal>
|
||||
<Button text icon ariaLabel="more options" on:click={toggle}>
|
||||
<Icon icon={IconDotsHorizontal} size="s" />
|
||||
</Button>
|
||||
<ActionMenu.Root slot="tooltip" let:toggle>
|
||||
<ActionMenu.Item.Button
|
||||
leadingIcon={IconEye}
|
||||
on:click={() => {
|
||||
toggle();
|
||||
selectedIndex = index;
|
||||
showOverview = true;
|
||||
}}>Overview</ActionMenu.Item.Button>
|
||||
|
||||
<div style:padding-block="0.25rem">
|
||||
<Divider />
|
||||
</div>
|
||||
|
||||
<ActionMenu.Item.Button
|
||||
status="danger"
|
||||
leadingIcon={IconTrash}
|
||||
on:click={() => {
|
||||
toggle();
|
||||
showDelete = true;
|
||||
selectedIndex = index;
|
||||
trackEvent(Click.DatabaseIndexDelete);
|
||||
}}>Delete</ActionMenu.Item.Button>
|
||||
</ActionMenu.Root>
|
||||
</Popover>
|
||||
</Spreadsheet.Cell>
|
||||
</Spreadsheet.Row.Base>
|
||||
{/each}
|
||||
|
||||
<svelte:fragment slot="footer">
|
||||
<Layout.Stack
|
||||
direction="row"
|
||||
alignContent="center"
|
||||
alignItems="center"
|
||||
justifyContent="space-between">
|
||||
<Typography.Text variant="m-400" color="--fgcolor-neutral-secondary">
|
||||
{@const length = data.table.indexes.length}
|
||||
{length}
|
||||
{length === 1 ? 'index' : 'indexes'}
|
||||
</Typography.Text>
|
||||
</Layout.Stack>
|
||||
</svelte:fragment>
|
||||
</Spreadsheet.Root>
|
||||
</SpreadsheetContainer>
|
||||
{:else}
|
||||
<EmptySheet
|
||||
mode="indexes"
|
||||
actions={{
|
||||
primary: {
|
||||
onClick: () => (showCreateIndex = true),
|
||||
disabled: !$table?.columns?.length
|
||||
}
|
||||
}} />
|
||||
{/if}
|
||||
{:else}
|
||||
{#snippet emptyColumnsSheetView(toggle)}
|
||||
<EmptySheet
|
||||
mode="indexes"
|
||||
title="You have no columns yet"
|
||||
actions={{
|
||||
primary: {
|
||||
text: 'Create columns',
|
||||
onClick: async () => {
|
||||
$showCreateColumnSheet.show = true;
|
||||
}
|
||||
onClick: toggle
|
||||
}
|
||||
}} />
|
||||
{/if}
|
||||
|
||||
{#if selectedIndexes.length > 0}
|
||||
<div class="floating-action-bar">
|
||||
<FloatingActionBar>
|
||||
<svelte:fragment slot="start">
|
||||
<div style:width="max-content">
|
||||
<Layout.Stack direction="row" alignItems="center" gap="m">
|
||||
<Badge content={selectedIndexes.length.toString()} />
|
||||
<span style:font-size="14px">
|
||||
{selectedIndexes.length > 1 ? 'indexes' : 'index'}
|
||||
selected
|
||||
</span>
|
||||
</Layout.Stack>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="end">
|
||||
<Button text on:click={() => (selectedIndexes = [])}>Cancel</Button>
|
||||
<Button secondary on:click={() => (showDelete = true)}>Delete</Button>
|
||||
</svelte:fragment>
|
||||
</FloatingActionBar>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<SideSheet
|
||||
title="Create index"
|
||||
bind:show={showCreateIndex}
|
||||
submit={{
|
||||
text: 'Create',
|
||||
onClick: async () => await createIndex.create()
|
||||
}}>
|
||||
<CreateIndex {showCreateIndex} bind:this={createIndex} />
|
||||
</SideSheet>
|
||||
|
||||
{#if selectedIndex}
|
||||
<Delete bind:showDelete {selectedIndex} />
|
||||
{:else if selectedIndexes && selectedIndexes.length}
|
||||
<Delete bind:showDelete bind:selectedIndex={selectedIndexes} />
|
||||
{/if}
|
||||
|
||||
<SideSheet title="Preview index" bind:show={showOverview}>
|
||||
<Overview {selectedIndex} />
|
||||
</SideSheet>
|
||||
|
||||
<FailedModal bind:show={showFailed} title="Create index" header="Creation failed" {error} />
|
||||
|
||||
<style lang="scss">
|
||||
.floating-action-bar {
|
||||
left: 50%;
|
||||
width: 100%;
|
||||
z-index: 14;
|
||||
position: absolute;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
</style>
|
||||
{/snippet}
|
||||
</Indexes>
|
||||
|
||||
+2
-3
@@ -12,7 +12,6 @@
|
||||
UpdateStatus
|
||||
} from '$database/(entity)';
|
||||
|
||||
const tablesDB = sdk.forProject(page.params.region, page.params.project).tablesDB;
|
||||
const params = $derived.by(() => {
|
||||
return {
|
||||
name: $table.name,
|
||||
@@ -22,7 +21,7 @@
|
||||
});
|
||||
|
||||
async function deleteTable() {
|
||||
await tablesDB.deleteTable({ ...params });
|
||||
await sdk.forProject(page.params.region, page.params.project).tablesDB.deleteTable({ ...params });
|
||||
}
|
||||
|
||||
async function updateTable(
|
||||
@@ -33,7 +32,7 @@
|
||||
rowSecurity: boolean;
|
||||
}>
|
||||
) {
|
||||
await tablesDB.updateTable({ ...params, ...updates });
|
||||
await sdk.forProject(page.params.region, page.params.project).tablesDB.updateTable({ ...params, ...updates });
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user