Skip to content

Commit b3ae141

Browse files
NicolappsConvex, Inc.
authored andcommitted
Support search index filters on the dashboard Data page (#40933)
GitOrigin-RevId: c30f0d0bd8dab227263ce9fadec0a82241e534c7
1 parent 475902e commit b3ae141

File tree

11 files changed

+474
-111
lines changed

11 files changed

+474
-111
lines changed

npm-packages/common/config/rush/pnpm-lock.yaml

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

npm-packages/dashboard-common/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@
108108
"next-router-mock": "~0.9.11",
109109
"postcss": "^8.4.19",
110110
"prettier": "3.6.2",
111+
"storybook": "^9.1.2",
111112
"prettier-plugin-tailwindcss": "~0.6.11",
112113
"tailwindcss": "^4.1.11",
113114
"tailwind-scrollbar": "^4.0.2",

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

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@ import {
99
} from "@radix-ui/react-icons";
1010
import { GenericDocument } from "convex/server";
1111
import {
12+
DatabaseIndexFilterClause,
1213
Filter,
1314
FilterExpression,
1415
FilterValidationError,
16+
SearchIndexFilterClause,
1517
} from "system-udfs/convex/_system/frontend/lib/filters";
1618
import {
1719
FilterEditor,
@@ -43,7 +45,6 @@ import { useNents } from "@common/lib/useNents";
4345
import { useQuery } from "convex/react";
4446
import { api } from "system-udfs/convex/_generated/api";
4547
import { Index } from "@common/features/data/lib/api";
46-
import { IndexFilterState } from "./IndexFilterEditor";
4748
import { IndexFilters, getDefaultIndex } from "./IndexFilters";
4849
import { clearFilters } from "./clearFilters";
4950

@@ -560,7 +561,10 @@ function useDataFilters({
560561
);
561562

562563
const onChangeIndexFilter = useCallback(
563-
(filter: IndexFilterState, idx: number) => {
564+
(
565+
filter: DatabaseIndexFilterClause | SearchIndexFilterClause,
566+
idx: number,
567+
) => {
564568
const newFilters = cloneDeep(shownFilters);
565569
if (!newFilters.index) {
566570
throw new Error("Index not found");
@@ -575,7 +579,11 @@ function useDataFilters({
575579
filterType: "index",
576580
filterIndex: idx,
577581
});
578-
} else if ("type" in oldFilter && oldFilter.type !== filter.type) {
582+
} else if (
583+
"type" in oldFilter &&
584+
"type" in filter &&
585+
oldFilter.type !== filter.type
586+
) {
579587
log("index filter type change", {
580588
oldType: oldFilter.type,
581589
newType: filter.type,

npm-packages/dashboard-common/src/features/data/components/DataFilters/IndexFilterEditor.tsx renamed to npm-packages/dashboard-common/src/features/data/components/DataFilters/DatabaseIndexFilterEditor.tsx

Lines changed: 21 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,17 @@
11
import { ValidatorJSON, Value, convexToJson } from "convex/values";
22
import React, { useCallback, useState } from "react";
33
import {
4+
DatabaseIndexFilterClause,
45
FilterByIndex,
56
FilterByIndexRange,
67
} from "system-udfs/convex/_system/frontend/lib/filters";
78
import { Checkbox } from "@ui/Checkbox";
8-
import { ObjectEditor } from "@common/elements/ObjectEditor/ObjectEditor";
99
import { Combobox, Option } from "@ui/Combobox";
1010
import { DateTimePicker } from "@common/features/data/components/FilterEditor/DateTimePicker";
1111
import { cn } from "@ui/cn";
12-
import { UNDEFINED_PLACEHOLDER } from "system-udfs/convex/_system/frontend/lib/values";
1312
import { Tooltip } from "@ui/Tooltip";
1413
import { ExclamationTriangleIcon } from "@radix-ui/react-icons";
15-
16-
export type IndexFilterState = FilterByIndex | FilterByIndexRange;
17-
18-
export type IndexFilterEditorProps = {
19-
idx: number;
20-
field: string;
21-
error: string | undefined;
22-
onChange(filter: IndexFilterState, idx: number): void;
23-
onApplyFilters(): void;
24-
onError(idx: number, errors: string[]): void;
25-
filter: IndexFilterState;
26-
autoFocusValueEditor?: boolean;
27-
documentValidator?: ValidatorJSON;
28-
shouldSurfaceValidatorErrors?: boolean;
29-
previousFiltersEnabled: boolean[];
30-
nextFiltersEnabled?: boolean[];
31-
};
14+
import { ObjectEditorWithPlaceholder } from "./ObjectEditorWithPlaceholder";
3215

3316
// Options for the filter type combobox
3417
const filterTypeOptions: Option<string>[] = [
@@ -44,7 +27,7 @@ const filterTypeOptions: Option<string>[] = [
4427
const RANGE_ERROR_MESSAGE =
4528
"The lower bound of this range is currently set to a value that is higher than the upper bound. This filter would never match any documents.";
4629

47-
export function IndexFilterEditor({
30+
export function DatabaseIndexFilterEditor({
4831
idx,
4932
field,
5033
error,
@@ -57,7 +40,20 @@ export function IndexFilterEditor({
5740
shouldSurfaceValidatorErrors,
5841
previousFiltersEnabled,
5942
nextFiltersEnabled = [],
60-
}: IndexFilterEditorProps) {
43+
}: {
44+
idx: number;
45+
field: string;
46+
error: string | undefined;
47+
onChange(filter: DatabaseIndexFilterClause, idx: number): void;
48+
onApplyFilters(): void;
49+
onError(idx: number, errors: string[]): void;
50+
filter: DatabaseIndexFilterClause;
51+
autoFocusValueEditor?: boolean;
52+
documentValidator?: ValidatorJSON;
53+
shouldSurfaceValidatorErrors?: boolean;
54+
previousFiltersEnabled: boolean[];
55+
nextFiltersEnabled?: boolean[];
56+
}) {
6157
const [prevIsLastEnabledFilter, setPrevIsLastEnabledFilter] = useState<
6258
boolean | null
6359
>(null);
@@ -475,7 +471,7 @@ export function IndexFilterEditor({
475471
path={`indexFilter${idx}-${field}-${filter.type}`}
476472
autoFocus={autoFocusValueEditor}
477473
className="rounded-l-none rounded-r"
478-
filter={filter}
474+
enabled={filter.enabled}
479475
onApplyFilters={onApplyFilters}
480476
handleError={handleError}
481477
documentValidator={documentValidator}
@@ -516,7 +512,7 @@ export function IndexFilterEditor({
516512
path={`indexFilter${idx}-${field}-${filter.type}`}
517513
autoFocus={autoFocusValueEditor}
518514
className="rounded-l-none rounded-r"
519-
filter={filter}
515+
enabled={filter.enabled}
520516
onApplyFilters={onApplyFilters}
521517
handleError={handleError}
522518
documentValidator={documentValidator}
@@ -546,7 +542,7 @@ export function IndexFilterEditor({
546542
path={`indexFilterLower${idx}-${field}-${filter.type}`}
547543
autoFocus={autoFocusValueEditor}
548544
className="rounded-l-none rounded-tr rounded-br-none"
549-
filter={filter}
545+
enabled={filter.enabled}
550546
onApplyFilters={onApplyFilters}
551547
handleError={handleError}
552548
documentValidator={documentValidator}
@@ -569,7 +565,7 @@ export function IndexFilterEditor({
569565
onChangeHandler={handleUpperValueChange}
570566
path={`indexFilterUpper${idx}-${field}-${filter.type}`}
571567
className="rounded-l-none rounded-tr-none rounded-br border-t-0"
572-
filter={filter}
568+
enabled={filter.enabled}
573569
onApplyFilters={onApplyFilters}
574570
handleError={handleError}
575571
documentValidator={documentValidator}
@@ -684,77 +680,3 @@ export function IndexFilterEditor({
684680
</div>
685681
);
686682
}
687-
688-
// Create a separate component for ObjectEditor with placeholder
689-
function ObjectEditorWithPlaceholder({
690-
value,
691-
onChangeHandler,
692-
path,
693-
autoFocus = false,
694-
className = "",
695-
filter,
696-
onApplyFilters,
697-
handleError,
698-
documentValidator,
699-
shouldSurfaceValidatorErrors,
700-
}: {
701-
value: any;
702-
onChangeHandler: (value?: Value) => void;
703-
path: string;
704-
autoFocus?: boolean;
705-
className?: string;
706-
filter: IndexFilterState;
707-
onApplyFilters: () => void;
708-
handleError: (errors: string[]) => void;
709-
documentValidator?: ValidatorJSON;
710-
shouldSurfaceValidatorErrors?: boolean;
711-
}) {
712-
const [innerText, setInnerText] = useState("");
713-
714-
return (
715-
<>
716-
{filter.enabled &&
717-
innerText === "" &&
718-
value === UNDEFINED_PLACEHOLDER && (
719-
<div
720-
className="pointer-events-none absolute z-50 font-mono text-xs text-content-secondary italic"
721-
data-testid="undefined-placeholder"
722-
style={{
723-
marginTop: "5px",
724-
marginLeft: "11px",
725-
}}
726-
>
727-
unset
728-
</div>
729-
)}
730-
<ObjectEditor
731-
key={path}
732-
className={cn(
733-
"w-full min-w-4 border focus-within:border focus-within:border-border-selected",
734-
filter.enabled && "border-l-transparent",
735-
className,
736-
)}
737-
editorClassname={cn(
738-
"mt-0 rounded-sm bg-background-secondary px-2 py-1 text-xs",
739-
className,
740-
)}
741-
allowTopLevelUndefined
742-
size="sm"
743-
disableFolding
744-
defaultValue={value === UNDEFINED_PLACEHOLDER ? undefined : value}
745-
onChange={onChangeHandler}
746-
onError={handleError}
747-
path={path}
748-
autoFocus={autoFocus}
749-
disableFind
750-
saveAction={onApplyFilters}
751-
enterSaves
752-
mode="editField"
753-
validator={documentValidator}
754-
shouldSurfaceValidatorErrors={shouldSurfaceValidatorErrors}
755-
disabled={!filter.enabled}
756-
onChangeInnerText={setInnerText}
757-
/>
758-
</>
759-
);
760-
}

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

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@ import { GenericDocument } from "convex/server";
1010
import { convexToJson, ValidatorJSON } from "convex/values";
1111
import {
1212
DatabaseIndexFilter,
13+
DatabaseIndexFilterClause,
1314
FilterByIndexRange,
1415
FilterExpression,
1516
SearchIndexFilter,
17+
SearchIndexFilterClause,
1618
} from "system-udfs/convex/_system/frontend/lib/filters";
1719
import { Button } from "@ui/Button";
1820
import { Combobox } from "@ui/Combobox";
@@ -21,8 +23,9 @@ import { SchemaJson } from "@common/lib/format";
2123
import { DeploymentInfoContext } from "@common/lib/deploymentContext";
2224
import { Index } from "@common/features/data/lib/api";
2325
import { cn } from "@ui/cn";
24-
import { IndexFilterEditor, IndexFilterState } from "./IndexFilterEditor";
26+
import { DatabaseIndexFilterEditor } from "./DatabaseIndexFilterEditor";
2527
import { SearchValueEditor } from "./SearchValueEditor";
28+
import { SearchIndexFilterEditor } from "./SearchIndexFilterEditor";
2629

2730
export function getDefaultIndex(): {
2831
name: string;
@@ -97,7 +100,10 @@ type IndexFiltersProps = {
97100
applyFiltersWithHistory: (next: FilterExpression) => Promise<void>;
98101
setDraftFilters: (next: FilterExpression) => void;
99102
onChangeOrder: (newOrder: "asc" | "desc") => void;
100-
onChangeIndexFilter: (filter: IndexFilterState, idx: number) => void;
103+
onChangeIndexFilter: (
104+
filter: DatabaseIndexFilterClause | SearchIndexFilterClause,
105+
idx: number,
106+
) => void;
101107
onError: (idx: number, errors: string[]) => void;
102108
hasInvalidFilters: boolean;
103109
invalidFilters: Record<string, string>;
@@ -345,7 +351,7 @@ export function IndexFilters({
345351
.map((c) => c.enabled) || [];
346352

347353
return (
348-
<IndexFilterEditor
354+
<DatabaseIndexFilterEditor
349355
key={idx}
350356
idx={idx}
351357
field={fieldName}
@@ -393,14 +399,33 @@ export function IndexFilters({
393399
}
394400
await applyFiltersWithHistory(shownFilters);
395401
}}
402+
indented={searchIndex.clauses.length > 0}
396403
/>
397404

398-
{/* TODO(ENG-9734) Support index filters in search queries */}
399-
{searchIndex.clauses.length > 0 && (
400-
<p className="text-xs text-content-secondary">
401-
Using search index filters in the dashboard isn’t supported yet.
402-
</p>
403-
)}
405+
{searchIndex.clauses.map((clause, idx) => (
406+
<SearchIndexFilterEditor
407+
key={clause.field}
408+
idx={idx}
409+
field={clause.field}
410+
error={
411+
clause.enabled ? invalidFilters[`index/${idx}`] : undefined
412+
}
413+
onChange={onChangeIndexFilter}
414+
onApplyFilters={async () => {
415+
if (hasInvalidFilters) {
416+
return;
417+
}
418+
await applyFiltersWithHistory(shownFilters);
419+
}}
420+
onError={onError}
421+
filter={clause}
422+
autoFocusValueEditor={
423+
idx === (shownFilters.index?.clauses.length || 0) - 1
424+
}
425+
documentValidator={getValidatorForField(clause.field)}
426+
shouldSurfaceValidatorErrors={activeSchema?.schemaValidation}
427+
/>
428+
))}
404429
</>
405430
)}
406431
</div>
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { Meta, StoryObj } from "@storybook/nextjs";
2+
import { fn } from "storybook/test";
3+
import { UNDEFINED_PLACEHOLDER } from "system-udfs/convex/_system/frontend/lib/values";
4+
import udfs from "@common/udfs";
5+
import { mockConvexReactClient } from "@common/lib/mockConvexReactClient";
6+
import { ConvexProvider } from "convex/react";
7+
import { DeploymentInfoContext } from "@common/lib/deploymentContext";
8+
import { mockDeploymentInfo } from "@common/lib/mockDeploymentInfo";
9+
import { ObjectEditorWithPlaceholder } from "./ObjectEditorWithPlaceholder";
10+
11+
const mockClient = mockConvexReactClient()
12+
.registerQueryFake(udfs.components.list, () => [])
13+
.registerQueryFake(udfs.getTableMapping.default, () => ({}));
14+
15+
const meta: Meta<typeof ObjectEditorWithPlaceholder> = {
16+
component: ObjectEditorWithPlaceholder,
17+
args: {
18+
onChangeHandler: fn(),
19+
onApplyFilters: fn(),
20+
handleError: fn(),
21+
path: "test-path",
22+
className: "",
23+
documentValidator: undefined,
24+
shouldSurfaceValidatorErrors: false,
25+
},
26+
render: (args) => (
27+
<ConvexProvider client={mockClient}>
28+
<DeploymentInfoContext.Provider value={mockDeploymentInfo}>
29+
<ObjectEditorWithPlaceholder {...args} />
30+
</DeploymentInfoContext.Provider>
31+
</ConvexProvider>
32+
),
33+
};
34+
export default meta;
35+
36+
type Story = StoryObj<typeof ObjectEditorWithPlaceholder>;
37+
38+
export const Disabled: Story = {
39+
args: {
40+
enabled: false,
41+
value: UNDEFINED_PLACEHOLDER,
42+
},
43+
};
44+
45+
export const EnabledWithoutValue: Story = {
46+
args: {
47+
enabled: true,
48+
value: UNDEFINED_PLACEHOLDER,
49+
},
50+
};
51+
52+
export const EnabledWithValue: Story = {
53+
args: {
54+
enabled: true,
55+
value: { foo: "bar" },
56+
},
57+
};

0 commit comments

Comments
 (0)