mirror of
https://github.com/trailbaseio/trailbase.git
synced 2026-04-26 02:29:45 -05:00
Admin UI: split table state from table component. This will make it easier to use more of TanStack/table's more advanced features.
Also split and clean-up some of the table explorer components.
This commit is contained in:
@@ -1,12 +1,4 @@
|
||||
import {
|
||||
For,
|
||||
Match,
|
||||
Switch,
|
||||
createEffect,
|
||||
createMemo,
|
||||
createSignal,
|
||||
splitProps,
|
||||
} from "solid-js";
|
||||
import { For, Match, Show, Switch } from "solid-js";
|
||||
import type { Accessor } from "solid-js";
|
||||
import {
|
||||
flexRender,
|
||||
@@ -19,7 +11,6 @@ import type {
|
||||
ColumnPinningState,
|
||||
PaginationState,
|
||||
Row,
|
||||
RowSelectionState,
|
||||
Table as TableType,
|
||||
} from "@tanstack/solid-table";
|
||||
import { TbPin, TbPinFilled } from "solid-icons/tb";
|
||||
@@ -33,7 +24,7 @@ import {
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import {
|
||||
Table,
|
||||
Table as ShadcnTable,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
@@ -57,37 +48,27 @@ export function safeParseInt(v: string | undefined): number | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
type Props<TData, TValue> = {
|
||||
columns: Accessor<ColumnDef<TData, TValue>[]>;
|
||||
data: Accessor<TData[] | undefined>;
|
||||
type TableOptions<TData, TValue> = {
|
||||
data: TData[] | undefined;
|
||||
columns: ColumnDef<TData, TValue>[];
|
||||
|
||||
rowCount?: number;
|
||||
pagination?: PaginationState;
|
||||
onPaginationChange?: (state: PaginationState) => void;
|
||||
|
||||
onRowSelection?: (rows: Row<TData>[], value: boolean) => void;
|
||||
onRowClick?: (idx: number, row: TData) => void;
|
||||
|
||||
columnPinning?: Accessor<ColumnPinningState>;
|
||||
onColumnPinningChange?: (state: ColumnPinningState) => void;
|
||||
};
|
||||
|
||||
// TODO: This entire implementation is incredibly messy. We should probably just
|
||||
// receive a `createSolidTable` result, i.e. use TableImpl below. This would allow
|
||||
// users direct access to the table state.
|
||||
export function DataTable<TData, TValue>(props: Props<TData, TValue>) {
|
||||
const [local] = splitProps(props, ["columns", "data"]);
|
||||
const [rowSelection, setRowSelection] = createSignal<RowSelectionState>({});
|
||||
createEffect(() => {
|
||||
// NOTE: because we use our own state for row selection, reset it when data changes.
|
||||
const _ = local.data();
|
||||
setRowSelection({});
|
||||
});
|
||||
export function buildTable<TData, TValue>(opts: TableOptions<TData, TValue>) {
|
||||
console.debug("buildTable: ", opts);
|
||||
|
||||
const buildColumns = () => {
|
||||
const onRowSelection = props.onRowSelection;
|
||||
function buildColumns() {
|
||||
const onRowSelection = opts.onRowSelection;
|
||||
if (!onRowSelection) {
|
||||
return local.columns();
|
||||
return opts.columns;
|
||||
}
|
||||
|
||||
const helper = createColumnHelper<TData>();
|
||||
@@ -131,122 +112,110 @@ export function DataTable<TData, TValue>(props: Props<TData, TValue>) {
|
||||
),
|
||||
}),
|
||||
// Custom/Domain-provided columns
|
||||
...local.columns(),
|
||||
...opts.columns,
|
||||
];
|
||||
};
|
||||
}
|
||||
|
||||
const table = createMemo(() => {
|
||||
console.debug("table data rebuild");
|
||||
|
||||
const columns = buildColumns();
|
||||
const enableColumnPinning =
|
||||
props.columnPinning !== undefined && columns.length > 2;
|
||||
|
||||
const buildColumnPinningState = (): ColumnPinningState => {
|
||||
const state = {
|
||||
...props.columnPinning?.(),
|
||||
};
|
||||
if (state.left?.[0] !== "__select__") {
|
||||
state.left = ["__select__", ...(state.left ?? [])];
|
||||
}
|
||||
return state;
|
||||
function buildColumnPinningState(): ColumnPinningState {
|
||||
const state = {
|
||||
...opts.columnPinning?.(),
|
||||
};
|
||||
if (state.left?.[0] !== "__select__") {
|
||||
state.left = ["__select__", ...(state.left ?? [])];
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
const t = createSolidTable({
|
||||
data: local.data() || [],
|
||||
state: {
|
||||
pagination:
|
||||
props.pagination !== undefined
|
||||
? {
|
||||
pageIndex: props.pagination.pageIndex ?? 0,
|
||||
pageSize: props.pagination.pageSize ?? 20,
|
||||
}
|
||||
: undefined,
|
||||
rowSelection: rowSelection(),
|
||||
columnPinning: buildColumnPinningState(),
|
||||
},
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
const columns = buildColumns();
|
||||
const enableColumnPinning =
|
||||
opts.columnPinning !== undefined && columns.length > 2;
|
||||
|
||||
// NOTE: requires setting up the header cells with resize handles.
|
||||
// enableColumnResizing: true,
|
||||
// columnResizeMode: 'onChange',
|
||||
|
||||
// pagination:
|
||||
manualPagination: true,
|
||||
onPaginationChange:
|
||||
props.onPaginationChange !== undefined
|
||||
? (updater) => {
|
||||
const newState =
|
||||
typeof updater === "function"
|
||||
? updater(t.getState().pagination)
|
||||
: updater;
|
||||
|
||||
props.onPaginationChange!(newState);
|
||||
const t = createSolidTable({
|
||||
data: opts.data ?? [],
|
||||
state: {
|
||||
pagination:
|
||||
opts.pagination !== undefined
|
||||
? {
|
||||
pageIndex: opts.pagination.pageIndex ?? 0,
|
||||
pageSize: opts.pagination.pageSize ?? 20,
|
||||
}
|
||||
: undefined,
|
||||
// If set to true, pagination will be reset to the first page when page-altering state changes
|
||||
// eg. data is updated, filters change, grouping changes, etc.
|
||||
//
|
||||
// NOTE: In our current setup this causes infinite reload cycles when paginating.
|
||||
autoResetPageIndex: false,
|
||||
rowCount: props.rowCount,
|
||||
// rowSelection: rowSelection(),
|
||||
columnPinning: buildColumnPinningState(),
|
||||
},
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
|
||||
// Just means, the input data is already filtered.
|
||||
manualFiltering: true,
|
||||
// NOTE: requires setting up the header cells with resize handles.
|
||||
// enableColumnResizing: true,
|
||||
// columnResizeMode: 'onChange',
|
||||
|
||||
enableRowSelection: true,
|
||||
enableMultiRowSelection: props.onRowSelection ? true : false,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
// pagination:
|
||||
manualPagination: true,
|
||||
onPaginationChange:
|
||||
opts.onPaginationChange !== undefined
|
||||
? (updater) => {
|
||||
const newState =
|
||||
typeof updater === "function"
|
||||
? updater(t.getState().pagination)
|
||||
: updater;
|
||||
|
||||
enableColumnPinning,
|
||||
onColumnPinningChange:
|
||||
props.onColumnPinningChange !== undefined
|
||||
? (updater) => {
|
||||
const newState =
|
||||
typeof updater === "function"
|
||||
? updater(t.getState().columnPinning)
|
||||
: updater;
|
||||
opts.onPaginationChange!(newState);
|
||||
}
|
||||
: undefined,
|
||||
// If set to true, pagination will be reset to the first page when page-altering state changes
|
||||
// eg. data is updated, filters change, grouping changes, etc.
|
||||
//
|
||||
// NOTE: In our current setup this causes infinite reload cycles when paginating.
|
||||
autoResetPageIndex: false,
|
||||
rowCount: opts.rowCount,
|
||||
|
||||
props.onColumnPinningChange!(newState);
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
// Just means, the input data is already filtered.
|
||||
manualFiltering: true,
|
||||
|
||||
return t;
|
||||
enableRowSelection: true,
|
||||
enableMultiRowSelection: opts.onRowSelection ? true : false,
|
||||
// onRowSelectionChange: setRowSelection,
|
||||
|
||||
enableColumnPinning,
|
||||
onColumnPinningChange:
|
||||
opts.onColumnPinningChange !== undefined
|
||||
? (updater) => {
|
||||
const newState =
|
||||
typeof updater === "function"
|
||||
? updater(t.getState().columnPinning)
|
||||
: updater;
|
||||
|
||||
opts.onColumnPinningChange!(newState);
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
|
||||
return (
|
||||
<TableImpl
|
||||
table={table()}
|
||||
onRowClick={props.onRowClick}
|
||||
paginationEnabled={props.pagination !== undefined}
|
||||
/>
|
||||
);
|
||||
return t;
|
||||
}
|
||||
|
||||
function TableImpl<TData>(props: {
|
||||
export function Table<TData>(props: {
|
||||
table: TableType<TData>;
|
||||
onRowClick?: (idx: number, row: TData) => void;
|
||||
paginationEnabled: boolean;
|
||||
showPaginationControls: boolean;
|
||||
}) {
|
||||
const paginationEnabled = () => props.paginationEnabled;
|
||||
const paginationEnabled = () => props.showPaginationControls;
|
||||
const paginationState = () => props.table.getState().pagination;
|
||||
const columns = () => props.table.options.columns;
|
||||
const numRows = () => props.table.getRowModel().rows?.length ?? 0;
|
||||
const showPin = () => props.table.options.enableColumnPinning;
|
||||
|
||||
return (
|
||||
<>
|
||||
{paginationEnabled() && (
|
||||
<div>
|
||||
<Show when={paginationEnabled()}>
|
||||
<PaginationControl
|
||||
table={props.table}
|
||||
rowCount={props.table.options.rowCount}
|
||||
/>
|
||||
)}
|
||||
</Show>
|
||||
|
||||
<div class="rounded-md border">
|
||||
<Table>
|
||||
<ShadcnTable>
|
||||
<TableHeader>
|
||||
<For each={props.table.getHeaderGroups()}>
|
||||
{(headerGroup) => (
|
||||
@@ -383,15 +352,9 @@ function TableImpl<TData>(props: {
|
||||
</Match>
|
||||
</Switch>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</ShadcnTable>
|
||||
</div>
|
||||
|
||||
{/*
|
||||
{paginationEnabled() && (
|
||||
<PaginationControl table={table} rowCount={props.rowCount} />
|
||||
)}
|
||||
*/}
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
import { createSignal, Match, Show, Switch, Suspense } from "solid-js";
|
||||
import {
|
||||
createMemo,
|
||||
createSignal,
|
||||
Match,
|
||||
Show,
|
||||
Switch,
|
||||
Suspense,
|
||||
} from "solid-js";
|
||||
import type { Setter } from "solid-js";
|
||||
import { useSearchParams } from "@solidjs/router";
|
||||
import { TbRefresh, TbCrown, TbEdit, TbTrash } from "solid-icons/tb";
|
||||
@@ -26,7 +33,7 @@ import {
|
||||
} from "@/components/ui/sheet";
|
||||
|
||||
import { Header } from "@/components/Header";
|
||||
import { DataTable, safeParseInt } from "@/components/Table";
|
||||
import { Table, buildTable, safeParseInt } from "@/components/Table";
|
||||
import { IconButton } from "@/components/IconButton";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Label } from "@/components/ui/label";
|
||||
@@ -330,7 +337,21 @@ export function AccountsPage() {
|
||||
|
||||
const [editUser, setEditUser] = createSignal<UserJson | undefined>();
|
||||
|
||||
const columns = () => buildColumns(setEditUser, refetch);
|
||||
const accountsTable = createMemo(() => {
|
||||
return buildTable({
|
||||
columns: buildColumns(setEditUser, refetch),
|
||||
data: users.data?.users ?? [],
|
||||
rowCount: Number(users.data?.total_row_count ?? -1),
|
||||
pagination: pagination(),
|
||||
onPaginationChange: (s: PaginationState) => {
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
pageIndex: s.pageIndex,
|
||||
pageSize: s.pageSize,
|
||||
});
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<div class="h-full">
|
||||
@@ -368,18 +389,10 @@ export function AccountsPage() {
|
||||
|
||||
<Match when={users.data}>
|
||||
<div class="w-full space-y-2.5">
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={() => users.data!.users}
|
||||
rowCount={Number(users.data!.total_row_count ?? -1)}
|
||||
pagination={pagination()}
|
||||
onPaginationChange={(s: PaginationState) => {
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
pageIndex: s.pageIndex,
|
||||
pageSize: s.pageSize,
|
||||
});
|
||||
}}
|
||||
<Table
|
||||
table={accountsTable()}
|
||||
showPaginationControls={true}
|
||||
onRowClick={undefined}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
Match,
|
||||
Show,
|
||||
Switch,
|
||||
createMemo,
|
||||
createEffect,
|
||||
createSignal,
|
||||
onCleanup,
|
||||
@@ -63,7 +64,7 @@ import {
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { showToast } from "@/components/ui/toast";
|
||||
import { DataTable } from "@/components/Table";
|
||||
import { Table, buildTable } from "@/components/Table";
|
||||
import { useNavbar, DirtyDialog } from "@/components/Navbar";
|
||||
|
||||
import type { QueryResponse } from "@bindings/QueryResponse";
|
||||
@@ -110,6 +111,32 @@ function ResultView(props: {
|
||||
const isCached = () => props.response === undefined;
|
||||
const response = () => props.response ?? props.script.result;
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
<Match when={response()?.error}>
|
||||
<div class="p-4">Error: {response()?.error?.message}</div>
|
||||
</Match>
|
||||
|
||||
<Match when={response()?.data === undefined}>
|
||||
<div class="p-4">No Data</div>
|
||||
</Match>
|
||||
|
||||
<Match when={response()?.data !== undefined}>
|
||||
<ResultViewImpl
|
||||
data={response()!.data!}
|
||||
timestamp={response()?.timestamp}
|
||||
isCached={isCached()}
|
||||
/>
|
||||
</Match>
|
||||
</Switch>
|
||||
);
|
||||
}
|
||||
|
||||
function ResultViewImpl(props: {
|
||||
data: QueryResponse;
|
||||
isCached: boolean;
|
||||
timestamp?: number;
|
||||
}) {
|
||||
const [columnPinningState, setColumnPinningState] = createSignal({});
|
||||
|
||||
function columnDefs(data: QueryResponse): ColumnDef<ArrayRecord, SqlValue>[] {
|
||||
@@ -126,49 +153,41 @@ function ResultView(props: {
|
||||
});
|
||||
}
|
||||
|
||||
const dataTable = createMemo(() => {
|
||||
// TODO: Enable pagination
|
||||
return buildTable({
|
||||
columns: columnDefs(props.data),
|
||||
data: props.data.rows,
|
||||
columnPinning: columnPinningState,
|
||||
onColumnPinningChange: setColumnPinningState,
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
<Match when={response()?.error}>
|
||||
<div class="p-4">Error: {response()?.error?.message}</div>
|
||||
</Match>
|
||||
<ErrorBoundary
|
||||
fallback={(err, _reset) => {
|
||||
return (
|
||||
<div class="m-4 flex flex-col gap-4">
|
||||
<p>Failed to render query result: {`${err}`}</p>
|
||||
|
||||
<Match when={response()?.data === undefined}>
|
||||
<div class="p-4">No Data</div>
|
||||
</Match>
|
||||
|
||||
<Match when={response()?.data !== undefined}>
|
||||
<ErrorBoundary
|
||||
fallback={(err, _reset) => {
|
||||
return (
|
||||
<div class="m-4 flex flex-col gap-4">
|
||||
<p>Failed to render query result: {`${err}`}</p>
|
||||
|
||||
{isCached() && (
|
||||
<p>
|
||||
The view is trying to show cached data. Maybe the schema has
|
||||
changed. Try to re-execute the query.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
>
|
||||
<div class="flex flex-col gap-2 p-4">
|
||||
<div class="flex justify-end text-sm">
|
||||
<ExecutionTime timestamp={response()?.timestamp} />
|
||||
</div>
|
||||
|
||||
{/* TODO: Enable pagination */}
|
||||
<DataTable
|
||||
columns={() => columnDefs(response()!.data!)}
|
||||
data={() => response()!.data!.rows}
|
||||
columnPinning={columnPinningState}
|
||||
onColumnPinningChange={setColumnPinningState}
|
||||
/>
|
||||
<Show when={props.isCached}>
|
||||
<p>
|
||||
The view is trying to show cached data. Maybe the schema has
|
||||
changed. Try to re-execute the query.
|
||||
</p>
|
||||
</Show>
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
</Match>
|
||||
</Switch>
|
||||
);
|
||||
}}
|
||||
>
|
||||
<div class="flex flex-col gap-2 p-4">
|
||||
<div class="flex justify-end text-sm">
|
||||
<ExecutionTime timestamp={props.timestamp} />
|
||||
</div>
|
||||
|
||||
<Table table={dataTable()} showPaginationControls={false} />
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
createSignal,
|
||||
onCleanup,
|
||||
onMount,
|
||||
createMemo,
|
||||
} from "solid-js";
|
||||
import { useSearchParams } from "@solidjs/router";
|
||||
import { createWritableMemo } from "@solid-primitives/memo";
|
||||
@@ -39,7 +40,7 @@ import {
|
||||
DialogTitle,
|
||||
DialogFooter,
|
||||
} from "@/components/ui/dialog";
|
||||
import { DataTable, safeParseInt } from "@/components/Table";
|
||||
import { Table, buildTable, safeParseInt } from "@/components/Table";
|
||||
import { FilterBar } from "@/components/FilterBar";
|
||||
|
||||
import { getLogs } from "@/lib/api/logs";
|
||||
@@ -203,9 +204,26 @@ export function LogsPage() {
|
||||
|
||||
const [showMap, setShowMap] = createSignal(true);
|
||||
const [showGeoipDialog, setShowGeoipDialog] = createSignal(false);
|
||||
|
||||
const [columnPinningState, setColumnPinningState] = createSignal({});
|
||||
|
||||
const logsTable = createMemo(() => {
|
||||
return buildTable({
|
||||
columns,
|
||||
data: logsFetch.data?.entries ?? [],
|
||||
columnPinning: columnPinningState,
|
||||
onColumnPinningChange: setColumnPinningState,
|
||||
rowCount: Number(logsFetch.data?.total_row_count ?? -1),
|
||||
pagination: pagination(),
|
||||
onPaginationChange: (s: PaginationState) => {
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
pageIndex: s.pageIndex,
|
||||
pageSize: s.pageSize,
|
||||
});
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<div class="h-full">
|
||||
<Header
|
||||
@@ -293,21 +311,7 @@ export function LogsPage() {
|
||||
placeholder={`Filter Query, e.g. '(latency > 2 || status >= 400) && method = "GET"'`}
|
||||
/>
|
||||
|
||||
<DataTable
|
||||
columns={() => columns}
|
||||
data={() => logsFetch.data!.entries}
|
||||
columnPinning={columnPinningState}
|
||||
onColumnPinningChange={setColumnPinningState}
|
||||
rowCount={Number(logsFetch.data!.total_row_count ?? -1)}
|
||||
pagination={pagination()}
|
||||
onPaginationChange={(s: PaginationState) => {
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
pageIndex: s.pageIndex,
|
||||
pageSize: s.pageSize,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Table table={logsTable()} showPaginationControls={true} />
|
||||
</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createSignal, Switch, Match } from "solid-js";
|
||||
import { createSignal, Switch, Match, createMemo } from "solid-js";
|
||||
import { useQueryClient } from "@tanstack/solid-query";
|
||||
import type { Row, ColumnDef } from "@tanstack/solid-table";
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
} from "@/components/ui/card";
|
||||
import { DataTable } from "@/components/Table";
|
||||
import { Table, buildTable } from "@/components/Table";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -70,6 +70,31 @@ function DatabaseSettingsForm(props: {
|
||||
await setConfig(queryClient, newConfig, { throw: false });
|
||||
};
|
||||
|
||||
const dbTable = createMemo(() => {
|
||||
return buildTable({
|
||||
columns: buildColumns(),
|
||||
data: props.config.databases,
|
||||
rowCount: props.config.databases.length,
|
||||
onRowSelection: (rows: Row<DatabaseConfig>[], value: boolean) => {
|
||||
const newSelection = new Set<string>(selectedRows());
|
||||
|
||||
for (const row of rows) {
|
||||
const key = row.original.name;
|
||||
if (!key) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (value) {
|
||||
newSelection.add(key);
|
||||
} else {
|
||||
newSelection.delete(key);
|
||||
}
|
||||
}
|
||||
setSelectedRows(newSelection);
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
let ref: HTMLInputElement | undefined;
|
||||
|
||||
return (
|
||||
@@ -159,31 +184,7 @@ function DatabaseSettingsForm(props: {
|
||||
</p>
|
||||
|
||||
<div class="max-h-[500px] overflow-auto">
|
||||
<DataTable
|
||||
columns={buildColumns}
|
||||
data={() => props.config.databases}
|
||||
rowCount={props.config.databases.length}
|
||||
onRowSelection={(
|
||||
rows: Row<DatabaseConfig>[],
|
||||
value: boolean,
|
||||
) => {
|
||||
const newSelection = new Set<string>(selectedRows());
|
||||
|
||||
for (const row of rows) {
|
||||
const key = row.original.name;
|
||||
if (!key) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (value) {
|
||||
newSelection.add(key);
|
||||
} else {
|
||||
newSelection.delete(key);
|
||||
}
|
||||
}
|
||||
setSelectedRows(newSelection);
|
||||
}}
|
||||
/>
|
||||
<Table table={dbTable()} showPaginationControls={false} />
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
import { createSignal, Switch, Match, Show } from "solid-js";
|
||||
import { useQuery } from "@tanstack/solid-query";
|
||||
import { TbDownload, TbColumnsOff } from "solid-icons/tb";
|
||||
import { TbDownload, TbBug } from "solid-icons/tb";
|
||||
|
||||
import { adminFetch } from "@/lib/fetch";
|
||||
import { showSaveFileDialog, stringToReadableStream } from "@/lib/utils";
|
||||
|
||||
import { RecordApiConfig } from "@proto/config";
|
||||
import type { Table } from "@bindings/Table";
|
||||
import type { TableIndex } from "@bindings/TableIndex";
|
||||
import type { TableTrigger } from "@bindings/TableTrigger";
|
||||
import type { View } from "@bindings/View";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardTitle, CardHeader } from "@/components/ui/card";
|
||||
@@ -130,43 +126,26 @@ export function SchemaCard(props: { api: RecordApiConfig }) {
|
||||
);
|
||||
}
|
||||
|
||||
export function DebugSchemaDialogButton(props: {
|
||||
table: Table | View;
|
||||
indexes: TableIndex[];
|
||||
triggers: TableTrigger[];
|
||||
}) {
|
||||
const indexes = () => props.indexes;
|
||||
const triggers = () => props.triggers;
|
||||
|
||||
export function DebugDialogButton(props: { title: string; data: object }) {
|
||||
return (
|
||||
<Dialog id="schema">
|
||||
<DialogTrigger>
|
||||
<IconButton tooltip="[DEV only]">
|
||||
<TbColumnsOff />
|
||||
<TbBug />
|
||||
</IconButton>
|
||||
</DialogTrigger>
|
||||
|
||||
<DialogContent class="max-w-[80dvw]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>[Debug] Schema</DialogTitle>
|
||||
<DialogTitle>[Debug] {props.title}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div class="max-h-[80dvh] overflow-auto">
|
||||
<div class="mx-2 flex flex-col gap-2">
|
||||
<h3>Schema</h3>
|
||||
<h3>{props.title}</h3>
|
||||
|
||||
<pre class="w-[70vw] overflow-x-hidden text-xs">
|
||||
{JSON.stringify(props.table, null, 2)}
|
||||
</pre>
|
||||
|
||||
<h3>Indexes</h3>
|
||||
<pre class="w-[70vw] overflow-x-hidden text-xs">
|
||||
{JSON.stringify(indexes(), null, 2)}
|
||||
</pre>
|
||||
|
||||
<h3>Triggers</h3>
|
||||
<pre class="w-[70vw] overflow-x-hidden text-xs">
|
||||
{JSON.stringify(triggers(), null, 2)}
|
||||
{JSON.stringify(props.data, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -37,10 +37,14 @@ import {
|
||||
import { SidebarTrigger } from "@/components/ui/sidebar";
|
||||
import { showToast } from "@/components/ui/toast";
|
||||
|
||||
import { DebugSchemaDialogButton } from "@/components/tables/SchemaDownload";
|
||||
import { DebugDialogButton } from "@/components/tables/SchemaDownload";
|
||||
import { CreateAlterTableForm } from "@/components/tables/CreateAlterTable";
|
||||
import { CreateAlterIndexForm } from "@/components/tables/CreateAlterIndex";
|
||||
import { DataTable, safeParseInt } from "@/components/Table";
|
||||
import {
|
||||
Table as TableComponent,
|
||||
buildTable,
|
||||
safeParseInt,
|
||||
} from "@/components/Table";
|
||||
import { FilterBar } from "@/components/FilterBar";
|
||||
import { DestructiveActionButton } from "@/components/DestructiveActionButton";
|
||||
import { IconButton } from "@/components/IconButton";
|
||||
@@ -246,16 +250,17 @@ function TableHeaderRightHandButtons(props: {
|
||||
allTables: Table[];
|
||||
schemaRefetch: () => Promise<void>;
|
||||
}) {
|
||||
const table = () => props.table;
|
||||
const hidden = () => hiddenTable(table());
|
||||
const type = () => tableType(table());
|
||||
const selectedSchema = () => props.table;
|
||||
const hidden = () => hiddenTable(selectedSchema());
|
||||
const type = () => tableType(selectedSchema());
|
||||
const satisfiesRecordApi = createMemo(() =>
|
||||
tableOrViewSatisfiesRecordApiRequirements(props.table, props.allTables),
|
||||
);
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const config = createConfigQuery();
|
||||
const hasRecordApi = () => hasRecordApis(config?.data?.config, table().name);
|
||||
const hasRecordApi = () =>
|
||||
hasRecordApis(config?.data?.config, selectedSchema().name);
|
||||
|
||||
return (
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
@@ -267,7 +272,7 @@ function TableHeaderRightHandButtons(props: {
|
||||
return (async () => {
|
||||
try {
|
||||
await dropTable({
|
||||
name: prettyFormatQualifiedName(table().name),
|
||||
name: prettyFormatQualifiedName(selectedSchema().name),
|
||||
dry_run: null,
|
||||
});
|
||||
} finally {
|
||||
@@ -370,15 +375,13 @@ function TableHeaderRightHandButtons(props: {
|
||||
|
||||
function TableHeader(props: {
|
||||
table: [Table, string] | [View, string];
|
||||
indexes: [TableIndex, string][];
|
||||
triggers: [TableTrigger, string][];
|
||||
allTables: [Table, string][];
|
||||
schemaRefetch: () => Promise<void>;
|
||||
rowsRefetch: () => void;
|
||||
}) {
|
||||
const allTables = createMemo(() => props.allTables.map(([t, _]) => t));
|
||||
const table = () => props.table[0];
|
||||
const type = () => tableType(table());
|
||||
const selectedSchema = () => props.table[0];
|
||||
const type = () => tableType(selectedSchema());
|
||||
|
||||
const headerTitle = () => {
|
||||
switch (type()) {
|
||||
@@ -395,7 +398,7 @@ function TableHeader(props: {
|
||||
<Header
|
||||
leading={<SidebarTrigger />}
|
||||
title={headerTitle()}
|
||||
titleSelect={prettyFormatQualifiedName(table().name)}
|
||||
titleSelect={prettyFormatQualifiedName(selectedSchema().name)}
|
||||
left={
|
||||
<div class="flex items-center">
|
||||
<IconButton tooltip="Refresh Data" onClick={props.rowsRefetch}>
|
||||
@@ -419,19 +422,11 @@ function TableHeader(props: {
|
||||
</span>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Show when={import.meta.env.DEV}>
|
||||
<DebugSchemaDialogButton
|
||||
table={table()}
|
||||
indexes={props.indexes.map(([index, _]) => index)}
|
||||
triggers={props.triggers.map(([trig, _]) => trig)}
|
||||
/>
|
||||
</Show>
|
||||
</div>
|
||||
}
|
||||
right={
|
||||
<TableHeaderRightHandButtons
|
||||
table={table()}
|
||||
table={selectedSchema()}
|
||||
allTables={allTables()}
|
||||
schemaRefetch={props.schemaRefetch}
|
||||
/>
|
||||
@@ -534,8 +529,10 @@ function ArrayRecordTable(props: {
|
||||
new Map<string, SqlValue>(),
|
||||
);
|
||||
|
||||
const table = () => props.state.selected;
|
||||
const mutable = () => tableType(table()) === "table" && !hiddenTable(table());
|
||||
const selectedSchema = () => props.state.selected;
|
||||
const mutable = () =>
|
||||
tableType(selectedSchema()) === "table" && !hiddenTable(selectedSchema());
|
||||
const data = () => props.state.response.rows;
|
||||
|
||||
const rowsRefetch = () => props.rowsRefetch();
|
||||
const columns = (): Column[] => props.state.response.columns;
|
||||
@@ -544,17 +541,49 @@ function ArrayRecordTable(props: {
|
||||
const pkColumnIndex = createMemo(
|
||||
() => findPrimaryKeyColumnIndex(columns()) ?? 0,
|
||||
);
|
||||
const columnDefs = createMemo(() =>
|
||||
buildColumnDefs(
|
||||
table().name,
|
||||
|
||||
const table = createMemo(() => {
|
||||
const columns = buildColumnDefs(
|
||||
selectedSchema().name,
|
||||
pkColumnIndex(),
|
||||
props.state.response.columns,
|
||||
blobEncoding(),
|
||||
),
|
||||
);
|
||||
);
|
||||
|
||||
return buildTable({
|
||||
// NOTE: The cell rendering is constrolled via the columnsDefs.
|
||||
columns,
|
||||
data: data(),
|
||||
columnPinning: props.columnPinningState[0],
|
||||
onColumnPinningChange: props.columnPinningState[1],
|
||||
rowCount: Number(totalRowCount()),
|
||||
pagination: props.pagination[0](),
|
||||
onPaginationChange: (s: PaginationState) => {
|
||||
props.pagination[1](s);
|
||||
},
|
||||
onRowSelection: mutable()
|
||||
? // eslint-disable-next-line solid/reactivity
|
||||
(rows: Row<ArrayRecord>[], value: boolean) => {
|
||||
const newSelection = new Map<string, SqlValue>(selectedRows());
|
||||
|
||||
for (const row of rows) {
|
||||
const pkValue: SqlValue = row.original[pkColumnIndex()];
|
||||
const key = hashSqlValue(pkValue);
|
||||
|
||||
if (value) {
|
||||
newSelection.set(key, pkValue);
|
||||
} else {
|
||||
newSelection.delete(key);
|
||||
}
|
||||
}
|
||||
setSelectedRows(newSelection);
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div id="data">
|
||||
<SafeSheet
|
||||
open={[
|
||||
() => editRow() !== undefined,
|
||||
@@ -569,7 +598,7 @@ function ArrayRecordTable(props: {
|
||||
<>
|
||||
<SheetContent class={sheetMaxWidth}>
|
||||
<InsertUpdateRowForm
|
||||
schema={table() as Table}
|
||||
schema={selectedSchema() as Table}
|
||||
rowsRefetch={rowsRefetch}
|
||||
row={editRow()}
|
||||
{...sheet}
|
||||
@@ -589,17 +618,9 @@ function ArrayRecordTable(props: {
|
||||
/>
|
||||
|
||||
<div class="overflow-x-auto pt-4">
|
||||
<DataTable
|
||||
// NOTE: The formatting is done via the columnsDefs.
|
||||
columns={columnDefs}
|
||||
data={() => props.state.response.rows}
|
||||
columnPinning={props.columnPinningState[0]}
|
||||
onColumnPinningChange={props.columnPinningState[1]}
|
||||
rowCount={Number(totalRowCount())}
|
||||
pagination={props.pagination[0]()}
|
||||
onPaginationChange={(s: PaginationState) => {
|
||||
props.pagination[1](s);
|
||||
}}
|
||||
<TableComponent
|
||||
table={table()}
|
||||
showPaginationControls={true}
|
||||
onRowClick={
|
||||
mutable()
|
||||
? (_idx: number, row: ArrayRecord) => {
|
||||
@@ -607,28 +628,6 @@ function ArrayRecordTable(props: {
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
onRowSelection={
|
||||
mutable()
|
||||
? (rows: Row<ArrayRecord>[], value: boolean) => {
|
||||
const newSelection = new Map<string, SqlValue>(
|
||||
selectedRows(),
|
||||
);
|
||||
|
||||
for (const row of rows) {
|
||||
const pkValue: SqlValue =
|
||||
row.original[pkColumnIndex()];
|
||||
const key = hashSqlValue(pkValue);
|
||||
|
||||
if (value) {
|
||||
newSelection.set(key, pkValue);
|
||||
} else {
|
||||
newSelection.delete(key);
|
||||
}
|
||||
}
|
||||
setSelectedRows(newSelection);
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
@@ -646,7 +645,7 @@ function ArrayRecordTable(props: {
|
||||
<>
|
||||
<SheetContent class={sheetMaxWidth}>
|
||||
<InsertUpdateRowForm
|
||||
schema={table() as Table}
|
||||
schema={selectedSchema() as Table}
|
||||
rowsRefetch={rowsRefetch}
|
||||
{...sheet}
|
||||
/>
|
||||
@@ -676,10 +675,13 @@ function ArrayRecordTable(props: {
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
await deleteRows(prettyFormatQualifiedName(table().name), {
|
||||
primary_key_column: columns()[pkColumnIndex()].name,
|
||||
values: ids,
|
||||
});
|
||||
await deleteRows(
|
||||
prettyFormatQualifiedName(selectedSchema().name),
|
||||
{
|
||||
primary_key_column: columns()[pkColumnIndex()].name,
|
||||
values: ids,
|
||||
},
|
||||
);
|
||||
|
||||
setSelectedRows(new Map<string, SqlValue>());
|
||||
} catch (err) {
|
||||
@@ -723,47 +725,225 @@ function ArrayRecordTable(props: {
|
||||
|
||||
<SelectContent />
|
||||
</Select>
|
||||
|
||||
<Show when={import.meta.env.DEV}>
|
||||
<DebugDialogButton title="Schema" data={data()} />
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function IndexTable(props: {
|
||||
table: Table;
|
||||
schemas: ListSchemasResponse;
|
||||
schemaRefetch: () => Promise<void>;
|
||||
hidden: boolean;
|
||||
}) {
|
||||
const [editIndex, setEditIndex] = createSignal<TableIndex | undefined>();
|
||||
const [selectedIndexes, setSelectedIndexes] = createSignal(new Set<string>());
|
||||
|
||||
const indexes = createMemo(() => {
|
||||
return props.schemas.indexes.filter(([index, _]) =>
|
||||
equalQualifiedNames(props.table.name, {
|
||||
name: index.table_name,
|
||||
database_schema: index.name.database_schema,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
const indexesTable = createMemo(() => {
|
||||
return buildTable({
|
||||
columns: indexColumns,
|
||||
data: indexes().map(([index, _]) => index),
|
||||
onRowSelection: props.hidden
|
||||
? undefined
|
||||
: // eslint-disable-next-line solid/reactivity
|
||||
(rows: Row<TableIndex>[], value: boolean) => {
|
||||
const newSelection = new Set(selectedIndexes());
|
||||
|
||||
for (const row of rows) {
|
||||
const qualifiedName = prettyFormatQualifiedName(
|
||||
row.original.name,
|
||||
);
|
||||
if (value) {
|
||||
newSelection.add(qualifiedName);
|
||||
} else {
|
||||
newSelection.delete(qualifiedName);
|
||||
}
|
||||
}
|
||||
setSelectedIndexes(newSelection);
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<div id="indexes">
|
||||
<h2>
|
||||
Indexes
|
||||
<Show when={import.meta.env.DEV}>
|
||||
<DebugDialogButton title="Indexes" data={indexes()} />
|
||||
</Show>
|
||||
</h2>
|
||||
|
||||
<SafeSheet
|
||||
open={[
|
||||
() => editIndex() !== undefined,
|
||||
(isOpen: boolean | ((value: boolean) => boolean)) => {
|
||||
if (!isOpen) {
|
||||
setEditIndex(undefined);
|
||||
}
|
||||
},
|
||||
]}
|
||||
>
|
||||
{(sheet) => {
|
||||
return (
|
||||
<>
|
||||
<SheetContent class={sheetMaxWidth}>
|
||||
<CreateAlterIndexForm
|
||||
schema={editIndex()}
|
||||
table={props.table}
|
||||
schemaRefetch={props.schemaRefetch}
|
||||
{...sheet}
|
||||
/>
|
||||
</SheetContent>
|
||||
|
||||
<div class="space-y-2.5 overflow-x-auto">
|
||||
<TableComponent
|
||||
table={indexesTable()}
|
||||
showPaginationControls={false}
|
||||
onRowClick={
|
||||
props.hidden
|
||||
? undefined
|
||||
: (_idx: number, index: TableIndex) => {
|
||||
setEditIndex(index);
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</SafeSheet>
|
||||
|
||||
<Show when={!props.hidden}>
|
||||
<div class="mt-2 flex gap-2">
|
||||
<SafeSheet>
|
||||
{(sheet) => {
|
||||
return (
|
||||
<>
|
||||
<SheetContent class={sheetMaxWidth}>
|
||||
<CreateAlterIndexForm
|
||||
schemaRefetch={props.schemaRefetch}
|
||||
table={props.table}
|
||||
{...sheet}
|
||||
/>
|
||||
</SheetContent>
|
||||
|
||||
<SheetTrigger
|
||||
as={(props: DialogTriggerProps) => (
|
||||
<Button variant="default" {...props}>
|
||||
Add Index
|
||||
</Button>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</SafeSheet>
|
||||
|
||||
<Button
|
||||
variant="destructive"
|
||||
disabled={selectedIndexes().size == 0}
|
||||
onClick={() => {
|
||||
const names = Array.from(selectedIndexes());
|
||||
if (names.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
for (const name of names) {
|
||||
await dropIndex({ name, dry_run: null });
|
||||
}
|
||||
|
||||
setSelectedIndexes(new Set<string>());
|
||||
} catch (err) {
|
||||
showToast({
|
||||
title: "Deletion Error",
|
||||
description: `${err}`,
|
||||
variant: "error",
|
||||
});
|
||||
} finally {
|
||||
props.schemaRefetch();
|
||||
}
|
||||
})();
|
||||
}}
|
||||
>
|
||||
Delete indexes
|
||||
</Button>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TriggerTable(props: { table: Table; schemas: ListSchemasResponse }) {
|
||||
const triggers = createMemo(() => {
|
||||
return props.schemas.triggers.filter(([trig, _]) =>
|
||||
equalQualifiedNames(props.table.name, {
|
||||
name: trig.table_name,
|
||||
database_schema: trig.name.database_schema,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
const triggersTable = createMemo(() => {
|
||||
return buildTable({
|
||||
columns: triggerColumns,
|
||||
data: triggers().map(([trig, sql]) => ({
|
||||
...trig,
|
||||
sql,
|
||||
})),
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<div id="triggers">
|
||||
<h2>
|
||||
Triggers
|
||||
<Show when={import.meta.env.DEV}>
|
||||
<DebugDialogButton title="Triggers" data={triggers()} />
|
||||
</Show>
|
||||
</h2>
|
||||
|
||||
<p class="text-sm">
|
||||
The admin dashboard currently does not support modifying triggers.
|
||||
Please use the editor to{" "}
|
||||
<a href="https://www.sqlite.org/lang_createtrigger.html">create</a> new
|
||||
triggers or <a href="https://sqlite.org/lang_droptrigger.html">drop</a>{" "}
|
||||
existing ones.
|
||||
</p>
|
||||
|
||||
<div class="mt-4">
|
||||
<TableComponent
|
||||
table={triggersTable()}
|
||||
showPaginationControls={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function TablePane(props: {
|
||||
selectedTable: [Table, string] | [View, string];
|
||||
schemas: ListSchemasResponse;
|
||||
schemaRefetch: () => Promise<void>;
|
||||
}) {
|
||||
const [editIndex, setEditIndex] = createSignal<TableIndex | undefined>();
|
||||
const [selectedIndexes, setSelectedIndexes] = createSignal(new Set<string>());
|
||||
|
||||
const table = () => props.selectedTable[0];
|
||||
const indexes = createMemo(() => {
|
||||
return props.schemas.indexes.filter(([index, _]) =>
|
||||
equalQualifiedNames(
|
||||
{
|
||||
name: index.table_name,
|
||||
database_schema: index.name.database_schema,
|
||||
},
|
||||
table().name,
|
||||
),
|
||||
);
|
||||
});
|
||||
const triggers = createMemo(() => {
|
||||
return props.schemas.triggers.filter(([trig, _]) =>
|
||||
equalQualifiedNames(
|
||||
{
|
||||
name: trig.table_name,
|
||||
database_schema: trig.name.database_schema,
|
||||
},
|
||||
table().name,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
// Derived table() props.
|
||||
const type = () => tableType(table());
|
||||
const hidden = () => hiddenTable(table());
|
||||
const selectedSchema = () => props.selectedTable[0];
|
||||
const type = () => tableType(selectedSchema());
|
||||
const hidden = () => hiddenTable(selectedSchema());
|
||||
|
||||
const [searchParams, setSearchParams] = useSearchParams<{
|
||||
filter?: string;
|
||||
@@ -862,8 +1042,6 @@ export function TablePane(props: {
|
||||
<>
|
||||
<TableHeader
|
||||
table={props.selectedTable}
|
||||
indexes={indexes()}
|
||||
triggers={triggers()}
|
||||
allTables={props.schemas.tables}
|
||||
schemaRefetch={schemaRefetch}
|
||||
rowsRefetch={rowsRefetch}
|
||||
@@ -893,158 +1071,21 @@ export function TablePane(props: {
|
||||
</Match>
|
||||
</Switch>
|
||||
|
||||
{type() === "table" && (
|
||||
<div id="indexes">
|
||||
<h2>Indexes</h2>
|
||||
<Show when={type() === "table"}>
|
||||
<IndexTable
|
||||
table={selectedSchema() as Table}
|
||||
schemas={props.schemas}
|
||||
schemaRefetch={props.schemaRefetch}
|
||||
hidden={hidden()}
|
||||
/>
|
||||
</Show>
|
||||
|
||||
<SafeSheet
|
||||
open={[
|
||||
() => editIndex() !== undefined,
|
||||
(isOpen: boolean | ((value: boolean) => boolean)) => {
|
||||
if (!isOpen) {
|
||||
setEditIndex(undefined);
|
||||
}
|
||||
},
|
||||
]}
|
||||
children={(sheet) => {
|
||||
return (
|
||||
<>
|
||||
<SheetContent class={sheetMaxWidth}>
|
||||
<CreateAlterIndexForm
|
||||
schema={editIndex()}
|
||||
table={table() as Table}
|
||||
schemaRefetch={props.schemaRefetch}
|
||||
{...sheet}
|
||||
/>
|
||||
</SheetContent>
|
||||
|
||||
<div class="space-y-2.5 overflow-x-auto">
|
||||
<DataTable
|
||||
columns={() => indexColumns}
|
||||
data={() => indexes().map(([index, _]) => index)}
|
||||
onRowClick={
|
||||
hidden()
|
||||
? undefined
|
||||
: (_idx: number, index: TableIndex) => {
|
||||
setEditIndex(index);
|
||||
}
|
||||
}
|
||||
onRowSelection={
|
||||
hidden()
|
||||
? undefined
|
||||
: (rows: Row<TableIndex>[], value: boolean) => {
|
||||
const newSelection = new Set(selectedIndexes());
|
||||
|
||||
for (const row of rows) {
|
||||
const qualifiedName =
|
||||
prettyFormatQualifiedName(
|
||||
row.original.name,
|
||||
);
|
||||
if (value) {
|
||||
newSelection.add(qualifiedName);
|
||||
} else {
|
||||
newSelection.delete(qualifiedName);
|
||||
}
|
||||
}
|
||||
setSelectedIndexes(newSelection);
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
{!hidden() && (
|
||||
<div class="mt-2 flex gap-2">
|
||||
<SafeSheet
|
||||
children={(sheet) => {
|
||||
return (
|
||||
<>
|
||||
<SheetContent class={sheetMaxWidth}>
|
||||
<CreateAlterIndexForm
|
||||
schemaRefetch={props.schemaRefetch}
|
||||
table={table() as Table}
|
||||
{...sheet}
|
||||
/>
|
||||
</SheetContent>
|
||||
|
||||
<SheetTrigger
|
||||
as={(props: DialogTriggerProps) => (
|
||||
<Button variant="default" {...props}>
|
||||
Add Index
|
||||
</Button>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
variant="destructive"
|
||||
disabled={selectedIndexes().size == 0}
|
||||
onClick={() => {
|
||||
const names = Array.from(selectedIndexes());
|
||||
if (names.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
for (const name of names) {
|
||||
await dropIndex({ name, dry_run: null });
|
||||
}
|
||||
|
||||
setSelectedIndexes(new Set<string>());
|
||||
} catch (err) {
|
||||
showToast({
|
||||
title: "Deletion Error",
|
||||
description: `${err}`,
|
||||
variant: "error",
|
||||
});
|
||||
} finally {
|
||||
props.schemaRefetch();
|
||||
}
|
||||
})();
|
||||
}}
|
||||
>
|
||||
Delete indexes
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{type() === "table" && (
|
||||
<div id="triggers">
|
||||
<h2>Triggers</h2>
|
||||
|
||||
<p class="text-sm">
|
||||
The admin dashboard currently does not support modifying triggers.
|
||||
Please use the editor to{" "}
|
||||
<a href="https://www.sqlite.org/lang_createtrigger.html">
|
||||
create
|
||||
</a>{" "}
|
||||
new triggers or{" "}
|
||||
<a href="https://sqlite.org/lang_droptrigger.html">drop</a>{" "}
|
||||
existing ones.
|
||||
</p>
|
||||
|
||||
<div class="mt-4">
|
||||
<DataTable
|
||||
columns={() => triggerColumns}
|
||||
data={() =>
|
||||
triggers().map(([trig, sql]) => ({
|
||||
...trig,
|
||||
sql,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<Show when={type() === "table"}>
|
||||
<TriggerTable
|
||||
table={selectedSchema() as Table}
|
||||
schemas={props.schemas}
|
||||
/>
|
||||
</Show>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user