fix: improve overview filters
This commit is contained in:
@@ -1,10 +1,8 @@
|
|||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import { useAppParams } from '@/hooks/use-app-params';
|
import { useAppParams } from '@/hooks/use-app-params';
|
||||||
import { useEventQueryFilters } from '@/hooks/use-event-query-filters';
|
import { useEventQueryFilters } from '@/hooks/use-event-query-filters';
|
||||||
import { useTRPC } from '@/integrations/trpc/react';
|
import { useTRPC } from '@/integrations/trpc/react';
|
||||||
import { cn } from '@/utils/cn';
|
import { cn } from '@/utils/cn';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { GlobeIcon } from 'lucide-react';
|
|
||||||
|
|
||||||
export function OriginFilter() {
|
export function OriginFilter() {
|
||||||
const { projectId } = useAppParams();
|
const { projectId } = useAppParams();
|
||||||
@@ -14,12 +12,8 @@ export function OriginFilter() {
|
|||||||
|
|
||||||
const { data } = useQuery(
|
const { data } = useQuery(
|
||||||
trpc.event.origin.queryOptions(
|
trpc.event.origin.queryOptions(
|
||||||
{
|
{ projectId },
|
||||||
projectId: projectId,
|
{ staleTime: 1000 * 60 * 60 },
|
||||||
},
|
|
||||||
{
|
|
||||||
staleTime: 1000 * 60 * 60,
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -28,20 +22,23 @@ export function OriginFilter() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-1.5">
|
||||||
{data?.map((item) => {
|
{data.map((item) => {
|
||||||
|
const active = originFilter?.value.includes(item.origin);
|
||||||
return (
|
return (
|
||||||
<Button
|
<button
|
||||||
key={item.origin}
|
key={item.origin}
|
||||||
variant="outline"
|
type="button"
|
||||||
icon={GlobeIcon}
|
|
||||||
className={cn(
|
|
||||||
originFilter?.value.includes(item.origin) && 'border-foreground',
|
|
||||||
)}
|
|
||||||
onClick={() => setFilter('origin', [item.origin], 'is')}
|
onClick={() => setFilter('origin', [item.origin], 'is')}
|
||||||
|
className={cn(
|
||||||
|
'rounded-md border px-2.5 py-1 text-sm transition-colors cursor-pointer truncate max-w-56',
|
||||||
|
active
|
||||||
|
? 'bg-foreground text-background border-foreground font-medium'
|
||||||
|
: 'text-muted-foreground hover:text-foreground hover:border-foreground/30',
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{item.origin}
|
{item.origin}
|
||||||
</Button>
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,13 +1,21 @@
|
|||||||
import { Button } from '@/components/ui/button';
|
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 {
|
import {
|
||||||
useEventQueryFilters,
|
useEventQueryFilters,
|
||||||
useEventQueryNamesFilter,
|
useEventQueryNamesFilter,
|
||||||
} from '@/hooks/use-event-query-filters';
|
} from '@/hooks/use-event-query-filters';
|
||||||
|
import { usePropertyValues } from '@/hooks/use-property-values';
|
||||||
import { pushModal } from '@/modals';
|
import { pushModal } from '@/modals';
|
||||||
import type { OverviewFiltersProps } from '@/modals/overview-filters';
|
import type { OverviewFiltersProps } from '@/modals/overview-filters';
|
||||||
import { getPropertyLabel } from '@/translations/properties';
|
import { getPropertyLabel } from '@/translations/properties';
|
||||||
import { cn } from '@/utils/cn';
|
import { cn } from '@/utils/cn';
|
||||||
import { operators } from '@openpanel/constants';
|
import { operators } from '@openpanel/constants';
|
||||||
|
import type {
|
||||||
|
IChartEventFilter,
|
||||||
|
IChartEventFilterOperator,
|
||||||
|
} from '@openpanel/validation';
|
||||||
import { FilterIcon, X } from 'lucide-react';
|
import { FilterIcon, X } from 'lucide-react';
|
||||||
import type { Options as NuqsOptions } from 'nuqs';
|
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 (
|
||||||
|
<div className="flex items-stretch text-sm border rounded-md overflow-hidden h-8">
|
||||||
|
{/* Key — opens modal to change the property */}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => pushModal('OverviewFilters', { nuqsOptions })}
|
||||||
|
className="px-2 hover:bg-accent transition-colors cursor-pointer"
|
||||||
|
>
|
||||||
|
{getPropertyLabel(filter.name)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Operator dropdown */}
|
||||||
|
<FilterOperatorSelect value={filter.operator} onChange={onChangeOperator}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="px-2 opacity-50 lowercase hover:opacity-100 hover:bg-accent transition-colors border-l cursor-pointer"
|
||||||
|
>
|
||||||
|
{operators[filter.operator]}
|
||||||
|
</button>
|
||||||
|
</FilterOperatorSelect>
|
||||||
|
|
||||||
|
{/* Value picker — only when operator needs a value */}
|
||||||
|
{!noValueNeeded && (
|
||||||
|
<ComboboxAdvanced
|
||||||
|
items={potentialValues.map((v) => ({ value: v, label: v }))}
|
||||||
|
value={filter.value}
|
||||||
|
onChange={onChangeValue}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="px-2 font-semibold hover:bg-accent transition-colors border-l cursor-pointer max-w-40 truncate"
|
||||||
|
>
|
||||||
|
{filter.value.length > 0 ? (
|
||||||
|
filter.value.join(', ')
|
||||||
|
) : (
|
||||||
|
<span className="opacity-40 font-normal italic">pick value</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</ComboboxAdvanced>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Remove */}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onRemove}
|
||||||
|
className="px-2 hover:bg-destructive hover:text-destructive-foreground transition-colors border-l cursor-pointer"
|
||||||
|
aria-label="Remove filter"
|
||||||
|
>
|
||||||
|
<X className="size-3" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function OverviewFiltersButtons({
|
export function OverviewFiltersButtons({
|
||||||
className,
|
className,
|
||||||
nuqsOptions,
|
nuqsOptions,
|
||||||
}: OverviewFiltersButtonsProps) {
|
}: OverviewFiltersButtonsProps) {
|
||||||
const [events, setEvents] = useEventQueryNamesFilter(nuqsOptions);
|
const [events, setEvents] = useEventQueryNamesFilter(nuqsOptions);
|
||||||
const [filters, _setFilter, _setFilters, removeFilter] =
|
const [filters, setFilter, _setFilters, removeFilter] =
|
||||||
useEventQueryFilters(nuqsOptions);
|
useEventQueryFilters(nuqsOptions);
|
||||||
|
|
||||||
if (filters.length === 0 && events.length === 0) return null;
|
if (filters.length === 0 && events.length === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn('flex flex-wrap gap-2', className)}>
|
<div className={cn('flex flex-wrap gap-2', className)}>
|
||||||
{events.map((event) => (
|
{events.map((event) => (
|
||||||
@@ -54,27 +143,20 @@ export function OverviewFiltersButtons({
|
|||||||
<strong className="font-semibold">{event}</strong>
|
<strong className="font-semibold">{event}</strong>
|
||||||
</Button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
{filters.map((filter) => {
|
{filters.map((filter) => (
|
||||||
return (
|
<FilterPill
|
||||||
<Button
|
key={filter.name}
|
||||||
key={filter.name}
|
filter={filter}
|
||||||
size="sm"
|
nuqsOptions={nuqsOptions}
|
||||||
variant="outline"
|
onRemove={() => removeFilter(filter.name)}
|
||||||
icon={X}
|
onChangeOperator={(operator) =>
|
||||||
onClick={() => removeFilter(filter.name)}
|
setFilter(filter.name, filter.value, operator)
|
||||||
>
|
}
|
||||||
<span>{getPropertyLabel(filter.name)}</span>
|
onChangeValue={(value) =>
|
||||||
<span className="opacity-40 ml-2 lowercase">
|
setFilter(filter.name, value, filter.operator)
|
||||||
{operators[filter.operator]}
|
}
|
||||||
</span>
|
/>
|
||||||
{filter.value.length > 0 && (
|
))}
|
||||||
<strong className="font-semibold ml-2">
|
|
||||||
{filter.value.join(', ')}
|
|
||||||
</strong>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,19 @@
|
|||||||
import { ColorSquare } from '@/components/color-square';
|
import { ColorSquare } from '@/components/color-square';
|
||||||
|
import { FilterOperatorSelect } from '@/components/report/sidebar/filters/FilterOperatorSelect';
|
||||||
import { RenderDots } from '@/components/ui/RenderDots';
|
import { RenderDots } from '@/components/ui/RenderDots';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { ComboboxAdvanced } from '@/components/ui/combobox-advanced';
|
import { ComboboxAdvanced } from '@/components/ui/combobox-advanced';
|
||||||
import { DropdownMenuComposed } from '@/components/ui/dropdown-menu';
|
|
||||||
import { InputEnter } from '@/components/ui/input-enter';
|
import { InputEnter } from '@/components/ui/input-enter';
|
||||||
import { useAppParams } from '@/hooks/use-app-params';
|
import { useAppParams } from '@/hooks/use-app-params';
|
||||||
import { usePropertyValues } from '@/hooks/use-property-values';
|
import { usePropertyValues } from '@/hooks/use-property-values';
|
||||||
import { useDispatch } from '@/redux';
|
import { useDispatch } from '@/redux';
|
||||||
import { operators } from '@openpanel/constants';
|
|
||||||
import type {
|
import type {
|
||||||
IChartEvent,
|
IChartEvent,
|
||||||
IChartEventFilter,
|
IChartEventFilter,
|
||||||
IChartEventFilterOperator,
|
IChartEventFilterOperator,
|
||||||
IChartEventFilterValue,
|
IChartEventFilterValue,
|
||||||
} from '@openpanel/validation';
|
} from '@openpanel/validation';
|
||||||
import { mapKeys } from '@openpanel/validation';
|
|
||||||
import { SlidersHorizontal, Trash } from 'lucide-react';
|
import { SlidersHorizontal, Trash } from 'lucide-react';
|
||||||
import { changeEvent } from '../../reportSlice';
|
import { changeEvent } from '../../reportSlice';
|
||||||
|
|
||||||
@@ -155,18 +154,10 @@ export function PureFilterItem({
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-1">
|
<div className="flex gap-1">
|
||||||
<DropdownMenuComposed
|
<FilterOperatorSelect
|
||||||
|
value={filter.operator}
|
||||||
onChange={changeFilterOperator}
|
onChange={changeFilterOperator}
|
||||||
items={mapKeys(operators).map((key) => ({
|
/>
|
||||||
value: key,
|
|
||||||
label: operators[key],
|
|
||||||
}))}
|
|
||||||
label="Operator"
|
|
||||||
>
|
|
||||||
<Button variant={'outline'} className="whitespace-nowrap">
|
|
||||||
{operators[filter.operator]}
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuComposed>
|
|
||||||
{filter.operator === 'is' || filter.operator === 'isNot' ? (
|
{filter.operator === 'is' || filter.operator === 'isNot' ? (
|
||||||
<ComboboxAdvanced
|
<ComboboxAdvanced
|
||||||
items={valuesCombobox}
|
items={valuesCombobox}
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { DropdownMenuComposed } from '@/components/ui/dropdown-menu';
|
||||||
|
import { operators } from '@openpanel/constants';
|
||||||
|
import type { IChartEventFilterOperator } from '@openpanel/validation';
|
||||||
|
import { mapKeys } from '@openpanel/validation';
|
||||||
|
|
||||||
|
interface FilterOperatorSelectProps {
|
||||||
|
value: IChartEventFilterOperator;
|
||||||
|
onChange: (operator: IChartEventFilterOperator) => void;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FilterOperatorSelect({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
children,
|
||||||
|
}: FilterOperatorSelectProps) {
|
||||||
|
const trigger = children ?? (
|
||||||
|
<Button variant="outline" className="whitespace-nowrap">
|
||||||
|
{operators[value]}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenuComposed
|
||||||
|
onChange={onChange}
|
||||||
|
items={mapKeys(operators).map((key) => ({
|
||||||
|
value: key,
|
||||||
|
label: operators[key],
|
||||||
|
}))}
|
||||||
|
label="Operator"
|
||||||
|
>
|
||||||
|
{trigger}
|
||||||
|
</DropdownMenuComposed>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,13 +1,11 @@
|
|||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Command, CommandInput, CommandItem } from '@/components/ui/command';
|
import { Command, CommandInput, CommandItem } from '@/components/ui/command';
|
||||||
import { cn } from '@/utils/cn';
|
|
||||||
import { ChevronsUpDownIcon } from 'lucide-react';
|
import { ChevronsUpDownIcon } from 'lucide-react';
|
||||||
import VirtualList from 'rc-virtual-list';
|
import VirtualList from 'rc-virtual-list';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { useOnClickOutside } from 'usehooks-ts';
|
|
||||||
|
|
||||||
import { Button, type ButtonProps } from './button';
|
import { Button, type ButtonProps } from './button';
|
||||||
import { Checkbox, DumpCheckbox } from './checkbox';
|
import { DumpCheckbox } from './checkbox';
|
||||||
import {
|
import {
|
||||||
Popover,
|
Popover,
|
||||||
PopoverContent,
|
PopoverContent,
|
||||||
@@ -30,9 +28,10 @@ interface ComboboxAdvancedProps {
|
|||||||
value: IValue[];
|
value: IValue[];
|
||||||
onChange: (value: IValue[]) => void;
|
onChange: (value: IValue[]) => void;
|
||||||
items: IItem[];
|
items: IItem[];
|
||||||
placeholder: string;
|
placeholder?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
size?: ButtonProps['size'];
|
size?: ButtonProps['size'];
|
||||||
|
children?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ComboboxAdvanced({
|
export function ComboboxAdvanced({
|
||||||
@@ -42,11 +41,10 @@ export function ComboboxAdvanced({
|
|||||||
placeholder,
|
placeholder,
|
||||||
className,
|
className,
|
||||||
size,
|
size,
|
||||||
|
children,
|
||||||
}: ComboboxAdvancedProps) {
|
}: ComboboxAdvancedProps) {
|
||||||
const [open, setOpen] = React.useState(false);
|
const [open, setOpen] = React.useState(false);
|
||||||
const [inputValue, setInputValue] = React.useState('');
|
const [inputValue, setInputValue] = React.useState('');
|
||||||
const ref = React.useRef<HTMLDivElement>(null);
|
|
||||||
useOnClickOutside(ref as React.RefObject<HTMLElement>, () => setOpen(false));
|
|
||||||
|
|
||||||
const selectables = items
|
const selectables = items
|
||||||
.filter((item) => !value.find((s) => s === item.value))
|
.filter((item) => !value.find((s) => s === item.value))
|
||||||
@@ -96,42 +94,32 @@ export function ComboboxAdvanced({
|
|||||||
...value.map((val) => {
|
...value.map((val) => {
|
||||||
const item = items.find((item) => item.value === val);
|
const item = items.find((item) => item.value === val);
|
||||||
return item
|
return item
|
||||||
? {
|
? { value: val, label: item.label }
|
||||||
value: val,
|
: { value: val, label: val };
|
||||||
label: item.label,
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
value: val,
|
|
||||||
label: val,
|
|
||||||
};
|
|
||||||
}),
|
}),
|
||||||
...selectables,
|
...selectables,
|
||||||
].filter((item) => item.value);
|
].filter((item) => item.value);
|
||||||
}, [inputValue, selectables, items]);
|
}, [inputValue, selectables, items]);
|
||||||
|
|
||||||
|
const trigger = children ?? (
|
||||||
|
<Button variant={'outline'} className={className} size={size} autoHeight>
|
||||||
|
<div className="flex w-full flex-wrap gap-1">
|
||||||
|
{value.length === 0 && placeholder}
|
||||||
|
{value.map((val) => {
|
||||||
|
const item = items.find((item) => item.value === val) ?? {
|
||||||
|
value: val,
|
||||||
|
label: val,
|
||||||
|
};
|
||||||
|
return <Badge key={String(item.value)}>{item.label}</Badge>;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<ChevronsUpDownIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover open={open} onOpenChange={setOpen}>
|
<Popover open={open} onOpenChange={setOpen}>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>{trigger}</PopoverTrigger>
|
||||||
<Button
|
|
||||||
variant={'outline'}
|
|
||||||
onClick={() => setOpen((prev) => !prev)}
|
|
||||||
className={className}
|
|
||||||
size={size}
|
|
||||||
autoHeight
|
|
||||||
>
|
|
||||||
<div className="flex w-full flex-wrap gap-1">
|
|
||||||
{value.length === 0 && placeholder}
|
|
||||||
{value.map((value) => {
|
|
||||||
const item = items.find((item) => item.value === value) ?? {
|
|
||||||
value,
|
|
||||||
label: value,
|
|
||||||
};
|
|
||||||
return <Badge key={String(item.value)}>{item.label}</Badge>;
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
<ChevronsUpDownIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
|
||||||
</Button>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverPortal>
|
<PopoverPortal>
|
||||||
<PopoverContent className="w-full max-w-md p-0" align="start">
|
<PopoverContent className="w-full max-w-md p-0" align="start">
|
||||||
<Command shouldFilter={false}>
|
<Command shouldFilter={false}>
|
||||||
|
|||||||
@@ -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
|
'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
|
'mt-auto', // Add margin-top: auto for all screen sizes
|
||||||
'focus:outline-none focus:ring-0 transition-none',
|
'focus:outline-none focus:ring-0 transition-none',
|
||||||
'border bg-background shadow-lg sm:rounded-lg md:max-h-[90vh]',
|
'border bg-def-100 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',
|
'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,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -29,16 +29,16 @@ const SheetOverlay = React.forwardRef<
|
|||||||
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
|
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
|
||||||
|
|
||||||
const sheetVariants = cva(
|
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: {
|
variants: {
|
||||||
side: {
|
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:
|
bottom:
|
||||||
'inset-x-4 bottom-4 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom',
|
'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-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm',
|
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:
|
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: {
|
defaultVariants: {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
useEventQueryNamesFilter,
|
useEventQueryNamesFilter,
|
||||||
} from '@/hooks/use-event-query-filters';
|
} from '@/hooks/use-event-query-filters';
|
||||||
import { useProfileValues } from '@/hooks/use-profile-values';
|
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 { Options as NuqsOptions } from 'nuqs';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
@@ -30,6 +30,16 @@ export interface OverviewFiltersProps {
|
|||||||
mode?: 'events' | 'profile';
|
mode?: 'events' | 'profile';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const Seperator = () => <div className="h-px bg-border -mx-6" />
|
||||||
|
const Heading = ({ title, icon: Icon }: { title: string, icon: LucideIcon }) => (
|
||||||
|
<div className="row items-center gap-2">
|
||||||
|
<Icon className="size-4" />
|
||||||
|
<h2 className="text-sm font-medium">
|
||||||
|
{title}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
export default function OverviewFilters({
|
export default function OverviewFilters({
|
||||||
nuqsOptions,
|
nuqsOptions,
|
||||||
enableEventsFilter,
|
enableEventsFilter,
|
||||||
@@ -44,8 +54,12 @@ export default function OverviewFilters({
|
|||||||
<SheetContent className="[&>button.absolute]:hidden">
|
<SheetContent className="[&>button.absolute]:hidden">
|
||||||
<ModalHeader title="Filters" />
|
<ModalHeader title="Filters" />
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
|
<Heading icon={GlobeIcon} title="Origins" />
|
||||||
<OriginFilter />
|
<OriginFilter />
|
||||||
|
<Seperator />
|
||||||
{enableEventsFilter && (
|
{enableEventsFilter && (
|
||||||
|
<>
|
||||||
|
<Heading icon={GanttChartIcon} title="Events" />
|
||||||
<ComboboxEvents
|
<ComboboxEvents
|
||||||
size="lg"
|
size="lg"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
@@ -56,16 +70,23 @@ export default function OverviewFilters({
|
|||||||
placeholder="Select event"
|
placeholder="Select event"
|
||||||
maxDisplayItems={2}
|
maxDisplayItems={2}
|
||||||
searchable
|
searchable
|
||||||
/>
|
/>
|
||||||
|
<Seperator />
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<Heading icon={SlidersHorizontal} title="Filters" />
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'bg-def-200 rounded-lg border',
|
'bg-card rounded-lg border',
|
||||||
selectedFilters.length === 0 && 'hidden',
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
{selectedFilters.length === 0 && (
|
||||||
|
<div className="p-4 text-center text-sm text-muted-foreground">
|
||||||
|
No filters selected
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{selectedFilters.map((filter) => {
|
{selectedFilters.map((filter) => {
|
||||||
return (
|
return (
|
||||||
<PureFilterItem
|
<PureFilterItem
|
||||||
|
|||||||
@@ -351,6 +351,6 @@ export const eventRouter = createTRPCRouter({
|
|||||||
)} AND origin IS NOT NULL AND origin != '' AND toDate(created_at) > now() - INTERVAL 30 DAY GROUP BY origin ORDER BY count DESC LIMIT 3`,
|
)} AND origin IS NOT NULL AND origin != '' AND toDate(created_at) > 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:'));
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user