Skip to content

Commit 338295b

Browse files
committed
feat: Implement most search operators
https://harperdb.atlassian.net/browse/STUDIO-451
1 parent 8bfa6cb commit 338295b

File tree

8 files changed

+374
-68
lines changed

8 files changed

+374
-68
lines changed

src/features/instance/databases/DatabaseTableView.tsx

Lines changed: 74 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { ConfirmDeletionModal } from '@/components/ConfirmDeletionModal';
22
import { Button } from '@/components/ui/button';
33
import { useInstanceClientIdParams } from '@/config/useInstanceClient';
4+
import { ColumnFiltersSchema } from '@/features/instance/databases/components/ColumnFilters';
45
import { PickColumnsDropdown } from '@/features/instance/databases/components/PickColumnsDropdown';
56
import { TableView } from '@/features/instance/databases/components/TableView';
67
import { formatBrowseDataTableHeader } from '@/features/instance/databases/functions/formatBrowseDataTableHeader';
@@ -12,18 +13,28 @@ import { useDeleteTableRecords } from '@/features/instance/operations/mutations/
1213
import { useInsertTableRecords } from '@/features/instance/operations/mutations/insertTableRecords';
1314
import { useUpdateTableRecords } from '@/features/instance/operations/mutations/updateTableRecords';
1415
import { getDescribeTableQueryOptions } from '@/features/instance/operations/queries/getDescribeTable';
16+
import {
17+
getSearchByConditionsOptions,
18+
SearchCondition,
19+
translateColumnFilterToSearchCondition,
20+
} from '@/features/instance/operations/queries/getSearchByConditions';
1521
import { getSearchByIdOptions } from '@/features/instance/operations/queries/getSearchById';
1622
import { getSearchByValueOptions } from '@/features/instance/operations/queries/getSearchByValue';
23+
import { useDebounce } from '@/hooks/useDebounce';
1724
import { useEffectedState } from '@/hooks/useEffectedState';
1825
import { useInstanceBrowseManagePermission, useInstanceSchemaTablePermission } from '@/hooks/usePermissions';
1926
import { useRefreshClick } from '@/hooks/useRefreshClick';
2027
import { useSessionStorage } from '@/hooks/useSessionStorage';
28+
import { useToggleCallback } from '@/hooks/useToggleCallback';
29+
import { keyBy } from '@/lib/keyBy';
2130
import { queryClient } from '@/react-query/queryClient';
31+
import { zodResolver } from '@hookform/resolvers/zod';
2232
import { useQuery, useSuspenseQuery } from '@tanstack/react-query';
2333
import { useNavigate, useParams } from '@tanstack/react-router';
2434
import { Row, VisibilityState } from '@tanstack/react-table';
25-
import { ImportIcon, PlusIcon, RefreshCwIcon, Trash } from 'lucide-react';
26-
import { useCallback, useEffect, useState } from 'react';
35+
import { ImportIcon, PlusIcon, RefreshCwIcon, SearchIcon, Trash } from 'lucide-react';
36+
import { useCallback, useEffect, useMemo, useState } from 'react';
37+
import { useForm } from 'react-hook-form';
2738
import { toast } from 'sonner';
2839

2940
export function DatabaseTableView() {
@@ -48,52 +59,85 @@ export function DatabaseTableView() {
4859
...instanceParams,
4960
databaseName,
5061
tableName,
51-
})
62+
}),
5263
);
64+
const attributesMap = useMemo(() => keyBy(describeTableData.attributes, 'attribute'), [describeTableData])
5365
const [selectedIds, setSelectedIds] = useEffectedState<null | unknown[]>(null, allParams);
5466
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
5567

56-
const { data: searchByIdData } = useQuery(
57-
getSearchByIdOptions({
58-
...instanceParams,
59-
isEditModalOpen: isEditModalOpen,
60-
databaseName: databaseName,
61-
tableName: tableName,
62-
ids: selectedIds,
63-
})
64-
);
68+
const columnFiltersForm = useForm({
69+
resolver: zodResolver(ColumnFiltersSchema),
70+
});
71+
const columnFiltersValues = columnFiltersForm.watch();
72+
const rawSearchConditions: SearchCondition[] | null = useMemo(() => {
73+
const conditions: SearchCondition[] = [];
74+
for (const key in columnFiltersValues) {
75+
if (columnFiltersValues[key]?.length) {
76+
conditions.push(translateColumnFilterToSearchCondition(key, columnFiltersValues[key], attributesMap[key]));
77+
}
78+
}
79+
return conditions.length ? conditions : null;
80+
}, [attributesMap, columnFiltersValues])
81+
const searchConditions = useDebounce(rawSearchConditions, 500, JSON.stringify);
6582

6683
const { dataTableColumns, hashAttribute } = formatBrowseDataTableHeader(describeTableData);
6784
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
6885
const [isImportCSVModalOpen, setIsImportCSVModalOpen] = useState(false);
69-
const [sortTableDataParams, setSortTableDataParams] = useEffectedState(
86+
const [sort, setSort] = useEffectedState(
7087
{
7188
attribute: hashAttribute,
7289
descending: false,
7390
},
74-
allParams
91+
allParams,
7592
);
7693

7794
const [totalRecords, setTotalRecords] = useState(describeTableData.record_count);
7895
const [pageIndex, setPageIndex] = useEffectedState(0, [databaseName, tableName]);
7996
const [pageSize, setPageSize] = useState(20);
8097
const [totalPages, setTotalPages] = useState(Math.ceil(describeTableData.record_count / pageSize));
8198

99+
// Full list
82100
const {
83-
data: tableData,
101+
data: fullTableData,
84102
refetch: refetchSearchByValueOptions,
85103
isFetching: tableDataFetching,
86104
} = useQuery(
87105
getSearchByValueOptions({
106+
enabled: !searchConditions,
88107
...instanceParams,
89108
databaseName,
90109
tableName,
91110
searchAttribute: hashAttribute,
92-
sortTableDataParams,
111+
sort,
112+
pageSize,
113+
pageIndex,
114+
}),
115+
);
116+
// Filtered list
117+
const { data: filteredTableData } = useQuery(
118+
getSearchByConditionsOptions({
119+
enabled: !!searchConditions,
120+
...instanceParams,
121+
databaseName,
122+
tableName,
123+
conditions: searchConditions,
124+
sort,
93125
pageSize,
94126
pageIndex,
95-
})
127+
}),
96128
);
129+
const tableData = searchConditions ? filteredTableData : fullTableData;
130+
// One by id
131+
const { data: searchByIdData } = useQuery(
132+
getSearchByIdOptions({
133+
...instanceParams,
134+
enabled: isEditModalOpen,
135+
databaseName: databaseName,
136+
tableName: tableName,
137+
ids: selectedIds,
138+
}),
139+
);
140+
97141
const { mutate: addTableRecords, isPending: isAddTableRecordsPending } = useInsertTableRecords();
98142
const { mutate: updateTableRecords, isPending: isUpdateTableRecordsPending } = useUpdateTableRecords();
99143
const { mutate: deleteTableRecords, isPending: isDeleteTableRecordsPending } = useDeleteTableRecords();
@@ -119,7 +163,7 @@ export function DatabaseTableView() {
119163
setIsAddModalOpen(false);
120164
toast.success('Record added successfully');
121165
},
122-
}
166+
},
123167
);
124168
};
125169
const onRecordUpdate = (data: Record<string, unknown>[]) => {
@@ -137,7 +181,7 @@ export function DatabaseTableView() {
137181
setIsEditModalOpen(false);
138182
toast.success('Record updated successfully');
139183
},
140-
}
184+
},
141185
);
142186
};
143187

@@ -156,7 +200,7 @@ export function DatabaseTableView() {
156200
setIsEditModalOpen(false);
157201
toast.success('Record deleted successfully');
158202
},
159-
}
203+
},
160204
);
161205
};
162206

@@ -176,7 +220,7 @@ export function DatabaseTableView() {
176220
setIsEditModalOpen(!isEditModalOpen);
177221
};
178222
const onColumnClick = (accessorKey: string, isAscending: boolean) => {
179-
setSortTableDataParams({
223+
setSort({
180224
attribute: accessorKey,
181225
descending: !isAscending,
182226
});
@@ -215,10 +259,10 @@ export function DatabaseTableView() {
215259
void navigate({ to: '../' });
216260
}
217261
},
218-
}
262+
},
219263
);
220264
},
221-
[deleteTable, instanceParams, navigate, tableName]
265+
[deleteTable, instanceParams, navigate, tableName],
222266
);
223267

224268
const onDeletionConfirmed = useCallback(() => {
@@ -231,6 +275,7 @@ export function DatabaseTableView() {
231275
`ColumnDisplayed/${databaseName}}/${tableName}` as 'ColumnDisplayed/{database}/{table}',
232276
{} satisfies VisibilityState,
233277
);
278+
const [showSearch, toggleShowSearch] = useToggleCallback(false);
234279

235280
return (
236281
<>
@@ -268,6 +313,10 @@ export function DatabaseTableView() {
268313
</div>
269314

270315
<div className="flex space-x-2">
316+
<Button variant="ghost" onClick={toggleShowSearch}>
317+
<SearchIcon className="inline-block " />
318+
{showSearch ? 'Clear Search' : 'Search'}
319+
</Button>
271320
<PickColumnsDropdown
272321
columns={dataTableColumns}
273322
columnVisibility={columnVisibility}
@@ -285,6 +334,7 @@ export function DatabaseTableView() {
285334
<TableView<Record<string, unknown>, unknown>
286335
data={tableData?.data || []}
287336
isFetching={tableDataFetching}
337+
showSearch={showSearch}
288338
columns={dataTableColumns}
289339
columnVisibility={columnVisibility}
290340
onRowClick={onRowClick}
@@ -293,6 +343,7 @@ export function DatabaseTableView() {
293343
totalRecords={totalRecords}
294344
pageIndex={pageIndex}
295345
pageSize={pageSize}
346+
columnFiltersForm={columnFiltersForm}
296347
setPageIndex={setPageIndex}
297348
setPageSize={setPageSize}
298349
/>
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { Form } from '@/components/ui/form/Form';
2+
import { FormControl } from '@/components/ui/form/FormControl';
3+
import { FormField } from '@/components/ui/form/FormField';
4+
import { FormItem } from '@/components/ui/form/FormItem';
5+
import { FormMessage } from '@/components/ui/form/FormMessage';
6+
import { Input } from '@/components/ui/input';
7+
import { TableCell, TableHeader, TableRow } from '@/components/ui/table';
8+
import { HeaderGroup } from '@tanstack/react-table';
9+
import { UseFormReturn } from 'react-hook-form';
10+
import { z } from 'zod';
11+
12+
export const ColumnFiltersSchema = z.record(z.string(), z.string());
13+
14+
export function ColumnFilters<TData>({
15+
columnFiltersForm,
16+
headerGroups,
17+
}: {
18+
columnFiltersForm: UseFormReturn<z.infer<typeof ColumnFiltersSchema>>,
19+
headerGroups: HeaderGroup<TData>[]
20+
}) {
21+
return (<TableHeader>
22+
<Form {...columnFiltersForm}>
23+
{headerGroups.map((headerGroup) => (
24+
<TableRow key={headerGroup.id} className="border-none">
25+
{headerGroup.headers.map((header) =>
26+
<TableCell key={header.id} style={{ width: `${header.column.getSize()}px` }}>
27+
{header.column.columnDef.enableColumnFilter && (<FormField
28+
control={columnFiltersForm.control}
29+
name={header.id}
30+
render={({ field }) => (
31+
<FormItem className="border-r-1 border-r-black">
32+
<FormControl>
33+
<Input
34+
{...field}
35+
type="text"
36+
autoCapitalize="none"
37+
autoComplete="off"
38+
className="rounded-none"
39+
value={field.value ?? ''}
40+
/>
41+
</FormControl>
42+
<FormMessage />
43+
</FormItem>
44+
)}
45+
/>)}
46+
</TableCell>)}
47+
</TableRow>))}
48+
</Form>
49+
</TableHeader>);
50+
}

src/features/instance/databases/components/TableView.tsx

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Loading } from '@/components/Loading';
44
import { Button } from '@/components/ui/button';
55
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
66
import { Table, TableBody, TableCell, TableHeader, TableHeadSortable, TableRow } from '@/components/ui/table';
7+
import { ColumnFilters, ColumnFiltersSchema } from '@/features/instance/databases/components/ColumnFilters';
78
import { addCommasToNumbers } from '@/lib/addCommasToNumbers';
89
import { cn } from '@/lib/cn';
910
import {
@@ -17,35 +18,41 @@ import {
1718
} from '@tanstack/react-table';
1819
import { ArrowLeftIcon, ArrowRightIcon } from 'lucide-react';
1920
import { Dispatch, SetStateAction, useCallback } from 'react';
21+
import { UseFormReturn } from 'react-hook-form';
22+
import { z } from 'zod';
2023

2124
interface BrowseDataTableProps<TData, TValue> {
22-
columns: ColumnDef<TData, TValue>[];
23-
columnVisibility: VisibilityState;
24-
data: TData[];
25-
isFetching?: boolean;
26-
totalPages: number;
27-
totalRecords: number;
28-
onRowClick?: (row: Row<TData>) => void;
29-
onColumnClick?: (accessorKey: string, isDescending: boolean) => void;
30-
pageIndex: number;
31-
pageSize: number;
32-
setPageIndex: Dispatch<SetStateAction<number>>;
33-
setPageSize: Dispatch<SetStateAction<number>>;
25+
columnFiltersForm: UseFormReturn<z.infer<typeof ColumnFiltersSchema>>,
26+
columns: ColumnDef<TData, TValue>[],
27+
columnVisibility: VisibilityState,
28+
data: TData[],
29+
isFetching?: boolean,
30+
onColumnClick?: (accessorKey: string, isDescending: boolean) => void,
31+
onRowClick?: (row: Row<TData>) => void,
32+
pageIndex: number,
33+
pageSize: number,
34+
setPageIndex: Dispatch<SetStateAction<number>>,
35+
setPageSize: Dispatch<SetStateAction<number>>,
36+
showSearch: boolean,
37+
totalPages: number,
38+
totalRecords: number,
3439
}
3540

3641
export function TableView<TData, TValue>({
3742
columns,
3843
columnVisibility,
44+
columnFiltersForm,
3945
data,
4046
isFetching,
41-
totalPages,
42-
totalRecords,
43-
onRowClick,
4447
onColumnClick,
48+
onRowClick,
4549
pageIndex,
46-
setPageIndex,
4750
pageSize,
51+
setPageIndex,
4852
setPageSize,
53+
showSearch,
54+
totalPages,
55+
totalRecords,
4956
}: BrowseDataTableProps<TData, TValue>) {
5057
const table = useReactTable({
5158
data,
@@ -69,7 +76,7 @@ export function TableView<TData, TValue>({
6976
setPageIndex(pageIndex - 1);
7077
}, [pageIndex, setPageIndex]);
7178
const nextPage = useCallback(() => {
72-
setPageIndex(pageIndex + 1);
79+
setPageIndex(pageIndex + 1);
7380
}, [pageIndex, setPageIndex]);
7481

7582
return (<>
@@ -80,6 +87,9 @@ export function TableView<TData, TValue>({
8087
<TableHeadSortable key={header.id} header={header} onColumnClick={onColumnClick} />)}
8188
</TableRow>))}
8289
</TableHeader>
90+
{showSearch && (
91+
<ColumnFilters columnFiltersForm={columnFiltersForm} headerGroups={table.getHeaderGroups()} />
92+
)}
8393
<TableBody className="bg-black border border-grey-700">
8494
{table.getRowModel().rows?.length ? (table.getRowModel().rows.map((row) => (
8595
<TableRow key={row.id} data-state={row.getIsSelected() && 'selected'}

src/features/instance/databases/functions/formatBrowseDataTableHeader.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export function formatBrowseDataTableHeader(instanceTable: InstanceTable): {
1717
header: attribute,
1818
accessorKey: attribute,
1919
enableSorting: Boolean(is_primary_key || indexed),
20+
enableColumnFilter: Boolean(is_primary_key || indexed),
2021
enableResizing: true,
2122
size: sizeByAttributeType(type),
2223
};

0 commit comments

Comments
 (0)