From 1d3063b2c9e52c8d470e55a76f78a5fad2323558 Mon Sep 17 00:00:00 2001 From: Justin Torre Date: Tue, 15 Oct 2024 13:23:32 -0700 Subject: [PATCH] filters fe revamp (#2725) --- .../shared/themed/FilterTreeEditor.tsx | 77 +++--- .../shared/themed/table/themedTableHeader.tsx | 140 ++++++++-- .../shared/themed/themedAdvancedFilters.tsx | 198 ++++++------- .../shared/themed/themedNumberDropdown.tsx | 98 +++++-- .../shared/themed/themedTextDropDown.tsx | 137 +++++---- .../templates/dashboard/saveFilterButton.tsx | 19 +- web/components/ui/button.tsx | 2 +- web/components/ui/command.tsx | 153 ++++++++++ web/components/ui/input.tsx | 2 +- web/package.json | 3 +- web/yarn.lock | 261 +++++++++++++++++- 11 files changed, 816 insertions(+), 274 deletions(-) create mode 100644 web/components/ui/command.tsx diff --git a/web/components/shared/themed/FilterTreeEditor.tsx b/web/components/shared/themed/FilterTreeEditor.tsx index cf598a557b..c3a9ddc70d 100644 --- a/web/components/shared/themed/FilterTreeEditor.tsx +++ b/web/components/shared/themed/FilterTreeEditor.tsx @@ -1,6 +1,5 @@ import React from "react"; -import { PlusIcon } from "@heroicons/react/24/outline"; -import { Button } from "@tremor/react"; + import { Result } from "../../../lib/result"; import { SingleFilterDef } from "../../../services/lib/filters/frontendFilterDefs"; import { AdvancedFilterRow, UIFilterRow } from "./themedAdvancedFilters"; @@ -10,6 +9,15 @@ import { } from "../../../services/lib/filters/uiFilterRowTree"; import SaveFilterButton from "../../templates/dashboard/saveFilterButton"; import { OrganizationFilter } from "../../../services/lib/organization_layout/organization_layout"; +import { Button } from "@/components/ui/button"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { PlusSquareIcon } from "lucide-react"; interface FilterTreeEditorProps { uiFilterRowTree: UIFilterRowTree; @@ -119,8 +127,8 @@ const FilterTreeEditor: React.FC = ({ return null; }; - const handleOperatorToggle = (node: UIFilterRowNode) => { - node.operator = node.operator === "and" ? "or" : "and"; + const handleOperatorChange = (node: UIFilterRowNode, value: string) => { + node.operator = value as "and" | "or"; onUpdate({ ...uiFilterRowTree }); }; @@ -133,40 +141,27 @@ const FilterTreeEditor: React.FC = ({ const content = ( <> {node.rows.length > 1 && ( -
handleOperatorChange(node, value)} + defaultValue="and" > - -
+ + + + + And + Or + + )} {node.rows.map((childNode: UIFilterRowTree, childIndex: number) => ( -
+
{renderNode(childNode, [...path, childIndex], false)}
))} {isRoot && ( -
- +
{onSaveFilterCallback && ( = ({ layoutPage={layoutPage} /> )} +
)} @@ -184,7 +193,7 @@ const FilterTreeEditor: React.FC = ({ return isRoot ? (
{content}
) : ( -
+
{content}
); @@ -230,7 +239,7 @@ const FilterTreeEditor: React.FC = ({ ); return path.length === 1 ? ( -
+
{filterRow}
) : ( @@ -267,7 +276,7 @@ const FilterTreeEditor: React.FC = ({ }; return ( -
{renderNode(uiFilterRowTree, [], true)}
+
{renderNode(uiFilterRowTree, [], true)}
); }; diff --git a/web/components/shared/themed/table/themedTableHeader.tsx b/web/components/shared/themed/table/themedTableHeader.tsx index 359c176b6c..cb3b3135ec 100644 --- a/web/components/shared/themed/table/themedTableHeader.tsx +++ b/web/components/shared/themed/table/themedTableHeader.tsx @@ -22,6 +22,12 @@ import FiltersButton from "./filtersButton"; import { DragColumnItem } from "./columns/DragList"; import { UIFilterRowTree } from "../../../../services/lib/filters/uiFilterRowTree"; import { Button } from "@/components/ui/button"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { PinIcon } from "lucide-react"; import { Input } from "@/components/ui/input"; import clsx from "clsx"; @@ -93,6 +99,11 @@ export default function ThemedTableHeader(props: ThemedTableHeaderProps) { const [isSearchExpanded, setIsSearchExpanded] = useState(false); const searchInputRef = useRef(null); + // Add state variables to manage the popover's open state and pin status + const [isFiltersPopoverOpen, setIsFiltersPopoverOpen] = useState(false); + const [isFiltersPinned, setIsFiltersPinned] = useState(false); + const popoverContentRef = useRef(null); + useEffect(() => { const displayFilters = window.sessionStorage.getItem("showFilters") || null; setShowFilters(displayFilters ? JSON.parse(displayFilters) : false); @@ -119,6 +130,10 @@ export default function ThemedTableHeader(props: ThemedTableHeaderProps) { } }; + const handlePopoverInteraction = (e: React.MouseEvent) => { + e.stopPropagation(); + }; + return (
@@ -139,17 +154,87 @@ export default function ThemedTableHeader(props: ThemedTableHeaderProps) { )}
{advancedFilters && ( - + {!isFiltersPinned ? ( + + + + ) : ( + + )} + {}} + onClick={handlePopoverInteraction} + > + +
+ +
+
+ )} {savedFilters && ( @@ -245,16 +330,31 @@ export default function ThemedTableHeader(props: ThemedTableHeaderProps) {
- {advancedFilters && showFilters && ( - + {advancedFilters && showFilters && isFiltersPinned && ( +
+
+ +
+ +
)}
); diff --git a/web/components/shared/themed/themedAdvancedFilters.tsx b/web/components/shared/themed/themedAdvancedFilters.tsx index 390d512c6f..dbd935f676 100644 --- a/web/components/shared/themed/themedAdvancedFilters.tsx +++ b/web/components/shared/themed/themedAdvancedFilters.tsx @@ -1,27 +1,27 @@ +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; import { - InformationCircleIcon, - PlusIcon, - TrashIcon, -} from "@heroicons/react/24/outline"; + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Plus, Trash2Icon } from "lucide-react"; import { Result } from "../../../lib/result"; import { ColumnType, SingleFilterDef, } from "../../../services/lib/filters/frontendFilterDefs"; -import { ThemedTextDropDown } from "./themedTextDropDown"; -import { - NumberInput, - SearchSelect, - SearchSelectItem, - Select, - SelectItem, - TextInput, -} from "@tremor/react"; -import ThemedNumberDropdown from "./themedNumberDropdown"; import { OrganizationFilter } from "../../../services/lib/organization_layout/organization_layout"; -import { styled } from "@mui/material/styles"; import FilterTreeEditor from "./FilterTreeEditor"; -import Tooltip, { TooltipProps, tooltipClasses } from "@mui/material/Tooltip"; +import ThemedNumberDropdown from "./themedNumberDropdown"; +import { ThemedTextDropDown } from "./themedTextDropDown"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip"; interface UIFilterRowNode { operator: "and" | "or"; @@ -53,14 +53,14 @@ export function AdvancedFilters({ const [filterTree, setFilterTree] = [filters, setAdvancedFilters]; return ( -
-
-

Filters

+
+
+

Filters

@@ -103,25 +103,24 @@ function AdvancedFilterInput({ switch (type) { case "text": return ( - { onChange(removeUnicodeCharacters(e.target.value)); }} - placeholder={"text..."} + className="text-xs text-slate-900 dark:text-slate-100" + placeholder="text..." value={value} /> ); case "number": return ( - { onChange(e.target.value); }} - placeholder={"number..."} + placeholder="number..." value={value} - enableStepper={false} /> ); case "timestamp": @@ -139,7 +138,7 @@ function AdvancedFilterInput({ return `${year}-${month}-${day}T${hours}:${minutes}`; }; - const handleChange = (e: any) => { + const handleChange = (e: React.ChangeEvent) => { const localDateTime = e.target.value; if (localDateTime) { @@ -148,14 +147,14 @@ function AdvancedFilterInput({ onChange(""); } }; + return ( - ); case "text-with-suggestions": @@ -177,14 +176,15 @@ function AdvancedFilterInput({ ); case "bool": return ( - + ); } } @@ -215,21 +215,10 @@ export function AdvancedFilterRow({ onAddFilter: () => void; showAddFilter?: boolean; }) { - const BlackTooltip = styled(({ className, ...props }: TooltipProps) => ( - - ))(({ theme }) => ({ - [`& .${tooltipClasses.arrow}`]: { - color: theme.palette.common.black, - }, - [`& .${tooltipClasses.tooltip}`]: { - backgroundColor: theme.palette.common.black, - fontSize: "0.8rem", - }, - })); return ( -
-
- +
+
-
-
-
-
-
- + + Add Filter + + )} + + + -
- {showAddFilter && showAddFilter === true && ( - - )} -
- {(filterMap[filter.filterMapIdx]?.column === "request_body" || - filterMap[filter.filterMapIdx]?.column === "response_body") && ( - - - - )} - {filterMap[filter.filterMapIdx]?.column === - "helicone-score-feedback" && ( - - - - )} + + + + Delete Filter +
); diff --git a/web/components/shared/themed/themedNumberDropdown.tsx b/web/components/shared/themed/themedNumberDropdown.tsx index e1b338e6b7..bf1879de1b 100644 --- a/web/components/shared/themed/themedNumberDropdown.tsx +++ b/web/components/shared/themed/themedNumberDropdown.tsx @@ -1,5 +1,18 @@ -import { SearchSelect, SearchSelectItem } from "@tremor/react"; -import { useState } from "react"; +import { useState, useEffect } from "react"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { Button } from "@/components/ui/button"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command"; interface ThemedNumberDropdownProps { options: { @@ -12,28 +25,71 @@ interface ThemedNumberDropdownProps { const ThemedNumberDropdown = (props: ThemedNumberDropdownProps) => { const { options, onChange, value } = props; + const [query, setQuery] = useState(""); + const [open, setOpen] = useState(false); - const [selected, setSelected] = useState(value); + useEffect(() => { + setQuery(value); + }, [value]); + + const handleValueChange = (currentValue: string) => { + setQuery(currentValue); + onChange(options.find((o) => o.param === currentValue)?.key ?? null); + setOpen(false); + }; + + const filteredOptions = Array.from( + new Set([...options.map((o) => o.param), query]) + ) + .filter(Boolean) + .sort() + .filter((option) => option.toLowerCase().includes(query.toLowerCase())); return ( -
- { - setSelected(value); - onChange(value); - }} - enableClear={false} - > - {options.map((option, i) => ( - -

- {option.param.charAt(0).toUpperCase() + option.param.slice(1)} -

-
- ))} -
+
+ + + + + + + { + setQuery(value); + }} + className="text-xs h-6" + /> + + {filteredOptions.length === 0 && ( + No results found. + )} + + {filteredOptions.map((option, i) => ( + handleValueChange(option)} + className="text-xs" + > + {option} + + ))} + + + + +
); }; diff --git a/web/components/shared/themed/themedTextDropDown.tsx b/web/components/shared/themed/themedTextDropDown.tsx index 8f5662689c..0fa30e586b 100644 --- a/web/components/shared/themed/themedTextDropDown.tsx +++ b/web/components/shared/themed/themedTextDropDown.tsx @@ -1,14 +1,19 @@ +import { Button } from "@/components/ui/button"; import { - SearchSelect, - SearchSelectItem, - Tab, - TabGroup, - TabList, - TextInput, -} from "@tremor/react"; + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; import { useState } from "react"; import { Result } from "../../../lib/result"; -import clsx from "clsx"; interface ThemedTextDropDownProps { options: string[]; @@ -29,85 +34,79 @@ export function ThemedTextDropDown(props: ThemedTextDropDownProps) { const [query, setQuery] = useState(""); const [tabMode, setTabMode] = useState<"smart" | "raw">("smart"); + const [open, setOpen] = useState(false); const handleValueChange = (value: string) => { setQuery(value); - onChange(value); + setOpen(false); }; + const filteredOptions = Array.from(new Set([...parentOptions, query])) + .filter(Boolean) + .sort() + .filter((option) => option.toLowerCase().includes(query.toLowerCase())); + return (
{!hideTabModes && (
-
- - - { - setTabMode("smart"); - }} - > - smart - - { - setTabMode("raw"); - }} - > - raw - - - -
+
{/* Your Tab implementation goes here */}
)} {tabMode === "smart" ? ( - { - setQuery(value); - }} - value={value} - onValueChange={(value) => { - handleValueChange(value || query); - }} - enableClear={true} - placeholder="Select or enter a value" - onSelect={async () => { - await onSearchHandler?.(query); - }} - > - {Array.from(new Set([value, ...parentOptions, query])) - .filter(Boolean) - .sort() - .map((option, i) => ( - - {option} - - ))} - + + + + + + + { + setQuery(value); + onSearchHandler?.(value); + }} + className="text-xs h-6" + /> + + {filteredOptions.length === 0 && ( + No results found. + )} + + {filteredOptions.map((option, i) => ( + handleValueChange(option)} + className="text-xs" + > + {option} + + ))} + + + + + ) : ( - { onChange(e.target.value); }} + className="border border-gray-300 rounded-md px-2 py-1 text-sm w-full" + placeholder="Enter a value" /> )}
diff --git a/web/components/templates/dashboard/saveFilterButton.tsx b/web/components/templates/dashboard/saveFilterButton.tsx index 5c489b49a3..3eb5e9604b 100644 --- a/web/components/templates/dashboard/saveFilterButton.tsx +++ b/web/components/templates/dashboard/saveFilterButton.tsx @@ -15,7 +15,8 @@ import { UIFilterRowTree, isFilterRowNode, } from "../../../services/lib/filters/uiFilterRowTree"; -import { BookmarkIcon } from "@heroicons/react/24/outline"; +import { Button } from "@/components/ui/button"; +import { SaveIcon } from "lucide-react"; interface SaveFilterButtonProps { filters: UIFilterRowTree; @@ -156,19 +157,19 @@ const SaveFilterButton = (props: SaveFilterButtonProps) => { return ( <> - + setIsSaveFiltersModalOpen(false)} diff --git a/web/components/ui/button.tsx b/web/components/ui/button.tsx index 5311800b66..71d3f5d5b0 100644 --- a/web/components/ui/button.tsx +++ b/web/components/ui/button.tsx @@ -11,7 +11,7 @@ const buttonVariants = cva( default: "bg-slate-900 text-slate-50 hover:bg-slate-900/90 dark:bg-slate-50 dark:text-slate-900 dark:hover:bg-slate-50/90", destructive: - "bg-red-500 text-slate-50 hover:bg-red-500/90 dark:bg-red-900 dark:text-slate-50 dark:hover:bg-red-900/90", + "bg-red-400 text-white hover:bg-red-500 dark:bg-red-700 dark:text-slate-100 dark:hover:bg-red-800", outline: "border border-slate-200 bg-white hover:bg-slate-100 hover:text-slate-900 dark:border-slate-800 dark:bg-slate-950 dark:hover:bg-slate-800 dark:hover:text-slate-50", secondary: diff --git a/web/components/ui/command.tsx b/web/components/ui/command.tsx new file mode 100644 index 0000000000..7e22ad038e --- /dev/null +++ b/web/components/ui/command.tsx @@ -0,0 +1,153 @@ +import * as React from "react"; +import { type DialogProps } from "@radix-ui/react-dialog"; +import { Command as CommandPrimitive } from "cmdk"; +import { Search } from "lucide-react"; + +import { cn } from "@/lib/utils"; +import { Dialog, DialogContent } from "@/components/ui/dialog"; + +const Command = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +Command.displayName = CommandPrimitive.displayName; + +interface CommandDialogProps extends DialogProps {} + +const CommandDialog = ({ children, ...props }: CommandDialogProps) => { + return ( + + + + {children} + + + + ); +}; + +const CommandInput = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( +
+ + +
+)); + +CommandInput.displayName = CommandPrimitive.Input.displayName; + +const CommandList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); + +CommandList.displayName = CommandPrimitive.List.displayName; + +const CommandEmpty = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>((props, ref) => ( + +)); + +CommandEmpty.displayName = CommandPrimitive.Empty.displayName; + +const CommandGroup = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); + +CommandGroup.displayName = CommandPrimitive.Group.displayName; + +const CommandSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +CommandSeparator.displayName = CommandPrimitive.Separator.displayName; + +const CommandItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); + +CommandItem.displayName = CommandPrimitive.Item.displayName; + +const CommandShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ); +}; +CommandShortcut.displayName = "CommandShortcut"; + +export { + Command, + CommandDialog, + CommandInput, + CommandList, + CommandEmpty, + CommandGroup, + CommandItem, + CommandShortcut, + CommandSeparator, +}; diff --git a/web/components/ui/input.tsx b/web/components/ui/input.tsx index c95ce62dd9..a0babc5e7f 100644 --- a/web/components/ui/input.tsx +++ b/web/components/ui/input.tsx @@ -11,7 +11,7 @@ const Input = React.forwardRef(