Skip to content

Commit 61ecff5

Browse files
NicolappsConvex, Inc.
authored andcommitted
dash: Allow users to use search indexes on the Data page (#40435)
This adds full-text search indexes to the Data page of the dashboard. Not supported yet: * adding filter fields that are part of the search index definition (ENG-9734) * adding arbitrary filters (ENG-9733) This shouldn’t be deployed before Funrun is deployed with #40468 GitOrigin-RevId: eec64fbbf1124e32cfb9e48b3db3eff5db01c544
1 parent 4039efc commit 61ecff5

File tree

13 files changed

+707
-267
lines changed

13 files changed

+707
-267
lines changed

npm-packages/dashboard-common/src/features/data/components/DataContent.tsx

Lines changed: 25 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,7 @@ import udfs from "@common/udfs";
1414
import classNames from "classnames";
1515
import {
1616
Filter,
17-
FilterByIndex,
18-
FilterByIndexRange,
17+
FilterExpression,
1918
SchemaJson,
2019
} from "system-udfs/convex/_system/frontend/lib/filters";
2120
import { Shape } from "shapes";
@@ -55,6 +54,7 @@ import { getDefaultIndex } from "@common/features/data/components/DataFilters/In
5554
import { api } from "system-udfs/convex/_generated/api";
5655
import { useNents } from "@common/lib/useNents";
5756
import omit from "lodash/omit";
57+
import { clearFilters } from "./DataFilters/clearFilters";
5858

5959
export function DataContent({
6060
tableName,
@@ -364,32 +364,28 @@ export function DataContent({
364364
numRowsInTable={numRowsInTable}
365365
/>
366366
</Sheet>
367+
) : isEmptySearchFilter(filters) ? (
368+
<div className="flex h-full flex-1 flex-col items-center gap-2 rounded-t-none border bg-background-secondary pt-8">
369+
<div className="text-content-secondary">
370+
Enter a search term to find matching documents.
371+
</div>
372+
<Button
373+
onClick={() =>
374+
applyFiltersWithHistory(clearFilters(filters))
375+
}
376+
size="xs"
377+
>
378+
Clear filters
379+
</Button>
380+
</div>
367381
) : (
368382
<div className="flex h-full flex-1 flex-col items-center gap-2 rounded-t-none border bg-background-secondary pt-8">
369383
<div className="text-content-secondary">
370384
No documents match the selected filters.
371385
</div>
372386
<Button
373387
onClick={() =>
374-
applyFiltersWithHistory({
375-
clauses: [],
376-
index: {
377-
name: filters?.index?.name || "_creationTime",
378-
clauses: (filters?.index?.clauses.map((clause) => ({
379-
...clause,
380-
enabled: false,
381-
})) as
382-
| FilterByIndex[]
383-
| [...FilterByIndex[], FilterByIndexRange]) || [
384-
{
385-
enabled: false,
386-
lowerOp: "lte",
387-
lowerValue: new Date().getTime(),
388-
},
389-
],
390-
},
391-
order: filters?.order,
392-
})
388+
applyFiltersWithHistory(clearFilters(filters))
393389
}
394390
size="xs"
395391
>
@@ -450,3 +446,11 @@ export function DataContentSkeleton() {
450446
</div>
451447
);
452448
}
449+
450+
function isEmptySearchFilter(filters: FilterExpression | undefined) {
451+
return (
452+
filters?.index &&
453+
"search" in filters.index &&
454+
filters.index.search.trim() === ""
455+
);
456+
}

npm-packages/dashboard-common/src/features/data/components/DataFilters/DataFilters.tsx

Lines changed: 32 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,7 @@ import {
1010
import { GenericDocument } from "convex/server";
1111
import {
1212
Filter,
13-
FilterByIndex,
14-
FilterByIndexRange,
15-
DatabaseFilterExpression,
13+
FilterExpression,
1614
FilterValidationError,
1715
} from "system-udfs/convex/_system/frontend/lib/filters";
1816
import {
@@ -47,6 +45,7 @@ import { api } from "system-udfs/convex/_generated/api";
4745
import { Index } from "@common/features/data/lib/api";
4846
import { IndexFilterState } from "./IndexFilterEditor";
4947
import { IndexFilters, getDefaultIndex } from "./IndexFilters";
48+
import { clearFilters } from "./clearFilters";
5049

5150
export function DataFilters({
5251
defaultDocument,
@@ -69,11 +68,11 @@ export function DataFilters({
6968
tableName: string;
7069
tableFields: string[];
7170
componentId: string | null;
72-
filters?: DatabaseFilterExpression;
73-
onFiltersChange(next: DatabaseFilterExpression): void;
71+
filters?: FilterExpression;
72+
onFiltersChange(next: FilterExpression): void;
7473
dataFetchErrors?: FilterValidationError[];
75-
draftFilters?: DatabaseFilterExpression;
76-
setDraftFilters(next: DatabaseFilterExpression): void;
74+
draftFilters?: FilterExpression;
75+
setDraftFilters(next: FilterExpression): void;
7776
activeSchema: SchemaJson | null;
7877
numRows?: number;
7978
numRowsLoaded: number;
@@ -123,6 +122,8 @@ export function DataFilters({
123122
[onError],
124123
);
125124

125+
const isSearchIndex = shownFilters.index && "search" in shownFilters.index;
126+
126127
return (
127128
<form
128129
className="flex w-full flex-col gap-2 rounded-t-lg border border-b-0 bg-background-secondary/50 p-2"
@@ -306,18 +307,21 @@ export function DataFilters({
306307
</div>
307308
)}
308309
<div className="mt-2 flex items-center gap-1">
309-
<Button
310-
variant="neutral"
311-
size="xs"
312-
className="text-xs"
313-
icon={<PlusIcon />}
314-
onClick={() => {
315-
onAddFilter(shownFilters.clauses.length);
316-
log("add filter");
317-
}}
318-
>
319-
Add filter
320-
</Button>
310+
{/* TODO(ENG-9733) Support arbitrary filters in search queries */}
311+
{!isSearchIndex && (
312+
<Button
313+
variant="neutral"
314+
size="xs"
315+
className="text-xs"
316+
icon={<PlusIcon />}
317+
onClick={() => {
318+
onAddFilter(shownFilters.clauses.length);
319+
log("add filter");
320+
}}
321+
>
322+
Add filter
323+
</Button>
324+
)}
321325
{isDirty || (dataFetchErrors && dataFetchErrors.length > 0) ? (
322326
<Button
323327
type="submit"
@@ -345,22 +349,7 @@ export function DataFilters({
345349
variant="neutral"
346350
className="ml-auto text-xs"
347351
onClick={() => {
348-
onFiltersChange({
349-
clauses: [],
350-
index: shownFilters.index
351-
? {
352-
name: shownFilters.index.name,
353-
clauses: shownFilters.index.clauses.map(
354-
(clause) => ({
355-
...clause,
356-
enabled: false,
357-
}),
358-
) as
359-
| FilterByIndex[]
360-
| [...FilterByIndex[], FilterByIndexRange],
361-
}
362-
: undefined,
363-
});
352+
onFiltersChange(clearFilters(shownFilters));
364353
}}
365354
>
366355
Clear filters
@@ -480,10 +469,10 @@ function useDataFilters({
480469
}: {
481470
tableName: string;
482471
componentId: string | null;
483-
filters?: DatabaseFilterExpression;
484-
onFiltersChange(next: DatabaseFilterExpression): void;
485-
draftFilters?: DatabaseFilterExpression;
486-
setDraftFilters(next: DatabaseFilterExpression): void;
472+
filters?: FilterExpression;
473+
onFiltersChange(next: FilterExpression): void;
474+
draftFilters?: FilterExpression;
475+
setDraftFilters(next: FilterExpression): void;
487476
activeSchema: SchemaJson | null;
488477
}) {
489478
const { useLogDeploymentEvent } = useContext(DeploymentInfoContext);
@@ -511,7 +500,7 @@ function useDataFilters({
511500
({
512501
clauses: [],
513502
index: getDefaultIndex(),
514-
} as DatabaseFilterExpression),
503+
} satisfies FilterExpression),
515504
[draftFilters],
516505
);
517506

@@ -586,7 +575,7 @@ function useDataFilters({
586575
filterType: "index",
587576
filterIndex: idx,
588577
});
589-
} else if (oldFilter.type !== filter.type) {
578+
} else if ("type" in oldFilter && oldFilter.type !== filter.type) {
590579
log("index filter type change", {
591580
oldType: oldFilter.type,
592581
newType: filter.type,
@@ -615,7 +604,7 @@ function useDataFilters({
615604
...shownFilters.clauses.slice(idx + 1),
616605
],
617606
index: shownFilters.index || getDefaultIndex(),
618-
} as DatabaseFilterExpression;
607+
} satisfies FilterExpression;
619608
setDraftFilters(newFilters);
620609
},
621610
[shownFilters, setDraftFilters, setInvalidFilters, log],
@@ -635,7 +624,7 @@ function useDataFilters({
635624
...shownFilters.clauses.slice(idx),
636625
],
637626
index: shownFilters.index || getDefaultIndex(),
638-
} as DatabaseFilterExpression;
627+
} satisfies FilterExpression;
639628
setDraftFilters(newFilters);
640629
},
641630
[shownFilters, setDraftFilters, log],

npm-packages/dashboard-common/src/features/data/components/DataFilters/FilterButton.tsx

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { ChevronDownIcon, MixerHorizontalIcon } from "@radix-ui/react-icons";
2-
import { DatabaseFilterExpression } from "system-udfs/convex/_system/frontend/lib/filters";
2+
import { FilterExpression } from "system-udfs/convex/_system/frontend/lib/filters";
33
import { Button } from "@ui/Button";
44
import { cn } from "@ui/cn";
55

@@ -10,7 +10,7 @@ export function FilterButton({
1010
onClick,
1111
open,
1212
}: {
13-
filters?: DatabaseFilterExpression;
13+
filters?: FilterExpression;
1414
onClick(): void;
1515
open: boolean;
1616
}) {
@@ -23,23 +23,20 @@ export function FilterButton({
2323
.map((filter) => filter.field),
2424
)
2525
: new Set([]);
26-
const indexFilters = filters?.index?.clauses.filter(
27-
(clause) => clause.enabled,
28-
);
26+
const indexFiltersCount = countIndexFilters(filters);
2927

3028
const regularFilters = filters?.clauses.filter(
3129
(filter) => filter.enabled !== false,
3230
);
3331

34-
const hasAnyEnabledFilters =
35-
indexFilters?.length || validFilterNames.size > 0;
32+
const hasAnyEnabledFilters = indexFiltersCount || validFilterNames.size > 0;
3633

3734
const filterButtonContent = (
3835
<div className="flex items-center gap-2">
3936
<span>Filter & Sort</span>
4037
{hasAnyEnabledFilters && (
4138
<span className="rounded-full border border-content-primary px-1 py-0 text-xs leading-[14px] tabular-nums">
42-
{(indexFilters?.length || 0) + (regularFilters?.length || 0)}
39+
{indexFiltersCount + (regularFilters?.length || 0)}
4340
</span>
4441
)}
4542
</div>
@@ -71,3 +68,15 @@ export function FilterButton({
7168
</Button>
7269
);
7370
}
71+
72+
function countIndexFilters(filters?: FilterExpression) {
73+
if (filters === undefined) return 0;
74+
75+
if (!filters.index) return 0;
76+
77+
return (
78+
filters.index.clauses.filter((c) => c.enabled).length +
79+
// the search filter always counts as one
80+
("search" in filters.index ? 1 : 0)
81+
);
82+
}

npm-packages/dashboard-common/src/features/data/components/DataFilters/IndexFilters.stories.tsx

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { mockDeploymentInfo } from "@common/lib/mockDeploymentInfo";
44
import { mockConvexReactClient } from "@common/lib/mockConvexReactClient";
55
import { ConvexProvider } from "convex/react";
66
import udfs from "@common/udfs";
7+
import { SearchIndexFilter } from "system-udfs/convex/_system/frontend/lib/filters";
78
import { IndexFilters } from "./IndexFilters";
89

910
const mockClient = mockConvexReactClient()
@@ -52,6 +53,28 @@ const meta: Meta<typeof IndexFilters> = {
5253
state: "done",
5354
},
5455
},
56+
{
57+
name: "by_name",
58+
fields: {
59+
searchField: "name",
60+
filterFields: [],
61+
},
62+
staged: false,
63+
backfill: {
64+
state: "done",
65+
},
66+
},
67+
{
68+
name: "by_name_filtered_by_status",
69+
fields: {
70+
searchField: "name",
71+
filterFields: ["status"],
72+
},
73+
staged: false,
74+
backfill: {
75+
state: "done",
76+
},
77+
},
5578
],
5679
tableName: "users",
5780
activeSchema: {
@@ -69,7 +92,18 @@ const meta: Meta<typeof IndexFilters> = {
6992
fields: ["name", "status"],
7093
},
7194
],
72-
searchIndexes: [],
95+
searchIndexes: [
96+
{
97+
indexDescriptor: "by_name",
98+
searchField: "name",
99+
filterFields: [],
100+
},
101+
{
102+
indexDescriptor: "by_name_filtered_by_status",
103+
searchField: "name",
104+
filterFields: ["status"],
105+
},
106+
],
73107
documentType: null,
74108
},
75109
],
@@ -184,6 +218,40 @@ export const DatabaseIndexPartialFilter: Story = {
184218
},
185219
};
186220

221+
export const SearchIndexWithoutFilters: Story = {
222+
args: {
223+
shownFilters: {
224+
clauses: [],
225+
index: {
226+
name: "by_name",
227+
search: "Hello world",
228+
clauses: [],
229+
} satisfies SearchIndexFilter,
230+
order: "asc",
231+
},
232+
},
233+
};
234+
235+
export const SearchIndexWithFilters: Story = {
236+
args: {
237+
shownFilters: {
238+
clauses: [],
239+
index: {
240+
name: "by_name_filtered_by_status",
241+
search: "Hello world",
242+
clauses: [
243+
{
244+
field: "status",
245+
value: "active",
246+
enabled: false,
247+
},
248+
],
249+
} satisfies SearchIndexFilter,
250+
order: "asc",
251+
},
252+
},
253+
};
254+
187255
export const WithError: Story = {
188256
args: {
189257
hasInvalidFilters: true,

0 commit comments

Comments
 (0)