From d7e6e737c91476f1339cf13dc5da5fdad478c747 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carl-Gerhard=20Lindesva=CC=88rd?= Date: Wed, 18 Feb 2026 10:43:55 +0100 Subject: [PATCH] fix: improve overview filters --- .../overview/filters/origin-filter.tsx | 31 ++--- .../filters/overview-filters-buttons.tsx | 126 +++++++++++++++--- .../report/sidebar/filters/FilterItem.tsx | 19 +-- .../sidebar/filters/FilterOperatorSelect.tsx | 36 +++++ .../src/components/ui/combobox-advanced.tsx | 58 ++++---- apps/start/src/components/ui/dialog.tsx | 4 +- apps/start/src/components/ui/sheet.tsx | 10 +- apps/start/src/modals/overview-filters.tsx | 29 +++- packages/trpc/src/routers/event.ts | 2 +- 9 files changed, 215 insertions(+), 100 deletions(-) create mode 100644 apps/start/src/components/report/sidebar/filters/FilterOperatorSelect.tsx diff --git a/apps/start/src/components/overview/filters/origin-filter.tsx b/apps/start/src/components/overview/filters/origin-filter.tsx index a1251985..248250cf 100644 --- a/apps/start/src/components/overview/filters/origin-filter.tsx +++ b/apps/start/src/components/overview/filters/origin-filter.tsx @@ -1,10 +1,8 @@ -import { Button } from '@/components/ui/button'; import { useAppParams } from '@/hooks/use-app-params'; import { useEventQueryFilters } from '@/hooks/use-event-query-filters'; import { useTRPC } from '@/integrations/trpc/react'; import { cn } from '@/utils/cn'; import { useQuery } from '@tanstack/react-query'; -import { GlobeIcon } from 'lucide-react'; export function OriginFilter() { const { projectId } = useAppParams(); @@ -14,12 +12,8 @@ export function OriginFilter() { const { data } = useQuery( trpc.event.origin.queryOptions( - { - projectId: projectId, - }, - { - staleTime: 1000 * 60 * 60, - }, + { projectId }, + { staleTime: 1000 * 60 * 60 }, ), ); @@ -28,20 +22,23 @@ export function OriginFilter() { } return ( -
- {data?.map((item) => { +
+ {data.map((item) => { + const active = originFilter?.value.includes(item.origin); return ( - + ); })}
diff --git a/apps/start/src/components/overview/filters/overview-filters-buttons.tsx b/apps/start/src/components/overview/filters/overview-filters-buttons.tsx index 27a27741..966a30dc 100644 --- a/apps/start/src/components/overview/filters/overview-filters-buttons.tsx +++ b/apps/start/src/components/overview/filters/overview-filters-buttons.tsx @@ -1,13 +1,21 @@ import { Button } from '@/components/ui/button'; +import { ComboboxAdvanced } from '@/components/ui/combobox-advanced'; +import { FilterOperatorSelect } from '@/components/report/sidebar/filters/FilterOperatorSelect'; +import { useAppParams } from '@/hooks/use-app-params'; import { useEventQueryFilters, useEventQueryNamesFilter, } from '@/hooks/use-event-query-filters'; +import { usePropertyValues } from '@/hooks/use-property-values'; import { pushModal } from '@/modals'; import type { OverviewFiltersProps } from '@/modals/overview-filters'; import { getPropertyLabel } from '@/translations/properties'; import { cn } from '@/utils/cn'; import { operators } from '@openpanel/constants'; +import type { + IChartEventFilter, + IChartEventFilterOperator, +} from '@openpanel/validation'; import { FilterIcon, X } from 'lucide-react'; import type { Options as NuqsOptions } from 'nuqs'; @@ -33,14 +41,95 @@ export function OverviewFilterButton(props: OverviewFiltersProps) { ); } +interface FilterPillProps { + filter: IChartEventFilter; + nuqsOptions?: NuqsOptions; + onRemove: () => void; + onChangeOperator: (operator: IChartEventFilterOperator) => void; + onChangeValue: (value: string[]) => void; +} + +function FilterPill({ + filter, + nuqsOptions, + onRemove, + onChangeOperator, + onChangeValue, +}: FilterPillProps) { + const { projectId } = useAppParams(); + const potentialValues = usePropertyValues({ + event: '*', + property: filter.name, + projectId, + }); + + const noValueNeeded = + filter.operator === 'isNull' || filter.operator === 'isNotNull'; + + return ( +
+ {/* Key — opens modal to change the property */} + + + {/* Operator dropdown */} + + + + + {/* Value picker — only when operator needs a value */} + {!noValueNeeded && ( + ({ value: v, label: v }))} + value={filter.value} + onChange={onChangeValue} + > + + + )} + + {/* Remove */} + +
+ ); +} + export function OverviewFiltersButtons({ className, nuqsOptions, }: OverviewFiltersButtonsProps) { const [events, setEvents] = useEventQueryNamesFilter(nuqsOptions); - const [filters, _setFilter, _setFilters, removeFilter] = + const [filters, setFilter, _setFilters, removeFilter] = useEventQueryFilters(nuqsOptions); + if (filters.length === 0 && events.length === 0) return null; + return (
{events.map((event) => ( @@ -54,27 +143,20 @@ export function OverviewFiltersButtons({ {event} ))} - {filters.map((filter) => { - return ( - - ); - })} + {filters.map((filter) => ( + removeFilter(filter.name)} + onChangeOperator={(operator) => + setFilter(filter.name, filter.value, operator) + } + onChangeValue={(value) => + setFilter(filter.name, value, filter.operator) + } + /> + ))}
); } diff --git a/apps/start/src/components/report/sidebar/filters/FilterItem.tsx b/apps/start/src/components/report/sidebar/filters/FilterItem.tsx index 1a2d6be2..e9a3dc57 100644 --- a/apps/start/src/components/report/sidebar/filters/FilterItem.tsx +++ b/apps/start/src/components/report/sidebar/filters/FilterItem.tsx @@ -1,20 +1,19 @@ import { ColorSquare } from '@/components/color-square'; +import { FilterOperatorSelect } from '@/components/report/sidebar/filters/FilterOperatorSelect'; import { RenderDots } from '@/components/ui/RenderDots'; import { Button } from '@/components/ui/button'; import { ComboboxAdvanced } from '@/components/ui/combobox-advanced'; -import { DropdownMenuComposed } from '@/components/ui/dropdown-menu'; import { InputEnter } from '@/components/ui/input-enter'; import { useAppParams } from '@/hooks/use-app-params'; import { usePropertyValues } from '@/hooks/use-property-values'; import { useDispatch } from '@/redux'; -import { operators } from '@openpanel/constants'; import type { IChartEvent, IChartEventFilter, IChartEventFilterOperator, IChartEventFilterValue, } from '@openpanel/validation'; -import { mapKeys } from '@openpanel/validation'; + import { SlidersHorizontal, Trash } from 'lucide-react'; import { changeEvent } from '../../reportSlice'; @@ -155,18 +154,10 @@ export function PureFilterItem({
- ({ - value: key, - label: operators[key], - }))} - label="Operator" - > - - + /> {filter.operator === 'is' || filter.operator === 'isNot' ? ( void; + children?: React.ReactNode; +} + +export function FilterOperatorSelect({ + value, + onChange, + children, +}: FilterOperatorSelectProps) { + const trigger = children ?? ( + + ); + + return ( + ({ + value: key, + label: operators[key], + }))} + label="Operator" + > + {trigger} + + ); +} diff --git a/apps/start/src/components/ui/combobox-advanced.tsx b/apps/start/src/components/ui/combobox-advanced.tsx index b8751ab6..e1912556 100644 --- a/apps/start/src/components/ui/combobox-advanced.tsx +++ b/apps/start/src/components/ui/combobox-advanced.tsx @@ -1,13 +1,11 @@ import { Badge } from '@/components/ui/badge'; import { Command, CommandInput, CommandItem } from '@/components/ui/command'; -import { cn } from '@/utils/cn'; import { ChevronsUpDownIcon } from 'lucide-react'; import VirtualList from 'rc-virtual-list'; import * as React from 'react'; -import { useOnClickOutside } from 'usehooks-ts'; import { Button, type ButtonProps } from './button'; -import { Checkbox, DumpCheckbox } from './checkbox'; +import { DumpCheckbox } from './checkbox'; import { Popover, PopoverContent, @@ -30,9 +28,10 @@ interface ComboboxAdvancedProps { value: IValue[]; onChange: (value: IValue[]) => void; items: IItem[]; - placeholder: string; + placeholder?: string; className?: string; size?: ButtonProps['size']; + children?: React.ReactNode; } export function ComboboxAdvanced({ @@ -42,11 +41,10 @@ export function ComboboxAdvanced({ placeholder, className, size, + children, }: ComboboxAdvancedProps) { const [open, setOpen] = React.useState(false); const [inputValue, setInputValue] = React.useState(''); - const ref = React.useRef(null); - useOnClickOutside(ref as React.RefObject, () => setOpen(false)); const selectables = items .filter((item) => !value.find((s) => s === item.value)) @@ -96,42 +94,32 @@ export function ComboboxAdvanced({ ...value.map((val) => { const item = items.find((item) => item.value === val); return item - ? { - value: val, - label: item.label, - } - : { - value: val, - label: val, - }; + ? { value: val, label: item.label } + : { value: val, label: val }; }), ...selectables, ].filter((item) => item.value); }, [inputValue, selectables, items]); + const trigger = children ?? ( + + ); + return ( - - - + {trigger} diff --git a/apps/start/src/components/ui/dialog.tsx b/apps/start/src/components/ui/dialog.tsx index bbe6bd50..55b1f502 100644 --- a/apps/start/src/components/ui/dialog.tsx +++ b/apps/start/src/components/ui/dialog.tsx @@ -62,8 +62,8 @@ function DialogContent({ 'max-h-screen overflow-y-auto overflow-x-hidden', // Ensure the dialog is scrollable if it exceeds the screen height 'mt-auto', // Add margin-top: auto for all screen sizes 'focus:outline-none focus:ring-0 transition-none', - 'border bg-background shadow-lg sm:rounded-lg md:max-h-[90vh]', - 'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg', + 'border bg-def-100 shadow-lg sm:rounded-lg md:max-h-[90vh]', + 'bg-def-100 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg', className, )} {...props} diff --git a/apps/start/src/components/ui/sheet.tsx b/apps/start/src/components/ui/sheet.tsx index 7b662906..49b3ec63 100644 --- a/apps/start/src/components/ui/sheet.tsx +++ b/apps/start/src/components/ui/sheet.tsx @@ -29,16 +29,16 @@ const SheetOverlay = React.forwardRef< SheetOverlay.displayName = SheetPrimitive.Overlay.displayName; const sheetVariants = cva( - 'fixed z-50 flex flex-col gap-4 overflow-y-auto rounded-lg bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out max-sm:w-[calc(100%-theme(spacing.8))]', + 'fixed z-50 flex flex-col gap-4 overflow-y-auto rounded-lg bg-def-100 p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out max-sm:w-[calc(100%-theme(spacing.8))]', { variants: { side: { - top: 'inset-x-4 top-4 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top', + top: 'inset-x-4 top-4 border data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top', bottom: - 'inset-x-4 bottom-4 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom', - left: 'bottom-4 left-4 top-4 w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm', + 'inset-x-4 bottom-4 border data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom', + left: 'bottom-4 left-4 top-4 w-3/4 border data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm', right: - 'bottom-4 right-4 top-4 w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm', + 'bottom-4 right-4 top-4 w-3/4 border data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm', }, }, defaultVariants: { diff --git a/apps/start/src/modals/overview-filters.tsx b/apps/start/src/modals/overview-filters.tsx index 574b1d17..9ca7bf4a 100644 --- a/apps/start/src/modals/overview-filters.tsx +++ b/apps/start/src/modals/overview-filters.tsx @@ -8,7 +8,7 @@ import { useEventQueryNamesFilter, } from '@/hooks/use-event-query-filters'; import { useProfileValues } from '@/hooks/use-profile-values'; -import { FilterIcon, XIcon } from 'lucide-react'; +import { FilterIcon, GanttChartIcon, GlobeIcon, LucideIcon, SlidersHorizontal, XIcon } from 'lucide-react'; import type { Options as NuqsOptions } from 'nuqs'; import type { @@ -30,6 +30,16 @@ export interface OverviewFiltersProps { mode?: 'events' | 'profile'; } +const Seperator = () =>
+const Heading = ({ title, icon: Icon }: { title: string, icon: LucideIcon }) => ( +
+ +

+ {title} +

+
+); + export default function OverviewFilters({ nuqsOptions, enableEventsFilter, @@ -44,8 +54,12 @@ export default function OverviewFilters({
+ + {enableEventsFilter && ( + <> + + /> + + )}
+
+ {selectedFilters.length === 0 && ( +
+ No filters selected +
+ )} {selectedFilters.map((filter) => { return ( now() - INTERVAL 30 DAY GROUP BY origin ORDER BY count DESC LIMIT 3`, ); - return res; + return res.filter((item) => item.origin && !item.origin.includes('localhost:')); }), });