New filter bar and filter edit form.

This commit is contained in:
Mike Cao 2025-04-09 21:15:12 -07:00
parent 47e89afcb4
commit bfdd3f9525
19 changed files with 300 additions and 150 deletions

View file

@ -78,7 +78,7 @@
"@react-spring/web": "^9.7.5", "@react-spring/web": "^9.7.5",
"@tanstack/react-query": "^5.71.10", "@tanstack/react-query": "^5.71.10",
"@umami/prisma-client": "^0.16.0", "@umami/prisma-client": "^0.16.0",
"@umami/react-zen": "^0.79.0", "@umami/react-zen": "^0.81.0",
"@umami/redis-client": "^0.27.0", "@umami/redis-client": "^0.27.0",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"chalk": "^4.1.2", "chalk": "^4.1.2",

10
pnpm-lock.yaml generated
View file

@ -45,8 +45,8 @@ importers:
specifier: ^0.16.0 specifier: ^0.16.0
version: 0.16.0(@prisma/client@6.5.0(prisma@6.5.0(typescript@5.8.3))(typescript@5.8.3))(@prisma/extension-read-replicas@0.4.1(@prisma/client@6.5.0(prisma@6.5.0(typescript@5.8.3))(typescript@5.8.3))) version: 0.16.0(@prisma/client@6.5.0(prisma@6.5.0(typescript@5.8.3))(typescript@5.8.3))(@prisma/extension-read-replicas@0.4.1(@prisma/client@6.5.0(prisma@6.5.0(typescript@5.8.3))(typescript@5.8.3)))
'@umami/react-zen': '@umami/react-zen':
specifier: ^0.79.0 specifier: ^0.81.0
version: 0.79.0(@babel/core@7.26.9)(@types/react@19.1.0)(immer@9.0.21)(use-sync-external-store@1.5.0(react@19.1.0)) version: 0.81.0(@babel/core@7.26.9)(@types/react@19.1.0)(immer@9.0.21)(use-sync-external-store@1.5.0(react@19.1.0))
'@umami/redis-client': '@umami/redis-client':
specifier: ^0.27.0 specifier: ^0.27.0
version: 0.27.0 version: 0.27.0
@ -2946,8 +2946,8 @@ packages:
'@prisma/client': ^4.8.0 '@prisma/client': ^4.8.0
'@prisma/extension-read-replicas': ^0.3.0 '@prisma/extension-read-replicas': ^0.3.0
'@umami/react-zen@0.79.0': '@umami/react-zen@0.81.0':
resolution: {integrity: sha512-qumZSV/dWvtq7iR7QwxEO5emE/jzgI5uPP5Y1E9S+MMGnJRhD5gQ1TvURf+jYAlTuiPubLysu6U3nKYmPNJxUg==} resolution: {integrity: sha512-DmwttqG+rhllcyqdCusxZ0MLVPSjIv4BXPsz7M1CED8I6wrz2MzG7euZgkJY1GMLtLPscisTooCqSG/RzwhhjQ==}
'@umami/redis-client@0.27.0': '@umami/redis-client@0.27.0':
resolution: {integrity: sha512-SbHTpxhgeZyTBUSp2zdZM+XUtpsaSL4Tad8QXIEhEtjWhvvfoornyT5kLuyYCVtzSAT4daALeGmOO1z6EE1KcA==} resolution: {integrity: sha512-SbHTpxhgeZyTBUSp2zdZM+XUtpsaSL4Tad8QXIEhEtjWhvvfoornyT5kLuyYCVtzSAT4daALeGmOO1z6EE1KcA==}
@ -10572,7 +10572,7 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@umami/react-zen@0.79.0(@babel/core@7.26.9)(@types/react@19.1.0)(immer@9.0.21)(use-sync-external-store@1.5.0(react@19.1.0))': '@umami/react-zen@0.81.0(@babel/core@7.26.9)(@types/react@19.1.0)(immer@9.0.21)(use-sync-external-store@1.5.0(react@19.1.0))':
dependencies: dependencies:
'@fontsource/jetbrains-mono': 5.2.5 '@fontsource/jetbrains-mono': 5.2.5
'@react-aria/focus': 3.20.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@react-aria/focus': 3.20.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)

View file

@ -28,7 +28,7 @@ export function LanguageSetting() {
return ( return (
<Row gap="3"> <Row gap="3">
<Select <Select
value={locale} selectedKey={locale}
onChange={val => saveLocale(val as string)} onChange={val => saveLocale(val as string)}
allowSearch allowSearch
onSearch={setSearch} onSearch={setSearch}

View file

@ -26,7 +26,7 @@ export function TimezoneSetting() {
<Row gap="3"> <Row gap="3">
<Select <Select
className={styles.dropdown} className={styles.dropdown}
value={timezone} selectedKey={timezone}
onChange={(value: any) => saveTimezone(value)} onChange={(value: any) => saveTimezone(value)}
allowSearch={true} allowSearch={true}
onSearch={setSearch} onSearch={setSearch}

View file

@ -7,7 +7,6 @@ import {
Column, Column,
Row, Row,
Select, Select,
Flexbox,
Icon, Icon,
Icons, Icons,
Loading, Loading,
@ -130,15 +129,17 @@ export function FieldFilterEditForm({
window.setTimeout(() => setShowMenu(false), 500); window.setTimeout(() => setShowMenu(false), 500);
}; };
const items = filterDropdownItems(name);
return ( return (
<Column> <Column>
<Row className={styles.filter}> <Row className={styles.filter}>
<Label>{label}</Label> <Label>{label}</Label>
<Flexbox gap="3"> <Row gap="3">
{allowFilterSelect && ( {allowFilterSelect && (
<Select <Select
className={styles.dropdown} className={styles.dropdown}
items={filterDropdownItems(name)} items={items}
value={operator} value={operator}
onChange={handleOperatorChange} onChange={handleOperatorChange}
> >
@ -183,7 +184,7 @@ export function FieldFilterEditForm({
onChange={e => setValue(e.target.value)} onChange={e => setValue(e.target.value)}
/> />
)} )}
</Flexbox> </Row>
<Button variant="primary" onPress={handleAdd} isDisabled={isDisabled}> <Button variant="primary" onPress={handleAdd} isDisabled={isDisabled}>
{formatMessage(isNew ? labels.add : labels.update)} {formatMessage(isNew ? labels.add : labels.update)}
</Button> </Button>

View file

@ -11,11 +11,11 @@ export function FieldSelectForm({ fields = [], onSelect, showType = true }: Fiel
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
return ( return (
<Menu> <Menu onAction={value => onSelect?.(value)}>
<MenuSection title={formatMessage(labels.fields)}> <MenuSection title={formatMessage(labels.fields)} selectionMode="multiple">
{fields.map(({ name, label, type }) => { {fields.map(({ name, label, type }) => {
return ( return (
<MenuItem key={name} id={name} onAction={() => onSelect(name)}> <MenuItem key={name} id={name}>
<Row alignItems="center" justifyContent="space-between"> <Row alignItems="center" justifyContent="space-between">
<Text>{label || name}</Text> <Text>{label || name}</Text>
{showType && type && <Text color="muted">{type}</Text>} {showType && type && <Text color="muted">{type}</Text>}

View file

@ -30,7 +30,7 @@ export function FilterSelectForm({
return ( return (
<FieldFilterEditForm <FieldFilterEditForm
websiteId={websiteId} websiteId={websiteId}
name={name} name={name || 'url'}
label={label} label={label}
type={type} type={type}
startDate={startDate} startDate={startDate}

View file

@ -1,7 +1,7 @@
import { Button, Icon, Icons, MenuTrigger, Popover, Text } from '@umami/react-zen'; import { Button, Icon, Icons, DialogTrigger, Dialog, Modal, Text } from '@umami/react-zen';
import { FilterSelectForm } from '@/app/(main)/reports/[reportId]/FilterSelectForm'; import { FilterEditForm } from '@/components/common/FilterEditForm';
import { useFields, useMessages, useNavigation, useDateRange } from '@/components/hooks'; import { useMessages, useNavigation, useFilters } from '@/components/hooks';
import { OPERATOR_PREFIXES } from '@/lib/constants'; import { OPERATORS } from '@/lib/constants';
export function WebsiteFilterButton({ export function WebsiteFilterButton({
websiteId, websiteId,
@ -14,41 +14,44 @@ export function WebsiteFilterButton({
}) { }) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { renderUrl, router } = useNavigation(); const { renderUrl, router } = useNavigation();
const { fields } = useFields(); const { filters } = useFilters();
const {
dateRange: { startDate, endDate },
} = useDateRange(websiteId);
const handleAddFilter = ({ name, operator, value }) => { const handleChange = (filters: any[]) => {
const prefix = OPERATOR_PREFIXES[operator]; const params = filters.reduce((obj, filter) => {
const { name, operator, value } = filter;
router.push(renderUrl({ [name]: prefix + value })); obj[name] = operator === OPERATORS.equals ? value : `${operator}~${value}`;
return obj;
}, {});
const url = renderUrl(params);
router.push(url);
}; };
return ( return (
<MenuTrigger> <DialogTrigger>
<Button variant="quiet"> <Button variant="quiet">
<Icon> <Icon>
<Icons.Plus /> <Icons.Plus />
</Icon> </Icon>
{showText && <Text>{formatMessage(labels.filter)}</Text>} {showText && <Text>{formatMessage(labels.filter)}</Text>}
</Button> </Button>
<Popover placement="bottom start"> <Modal>
{({ close }: any) => { <Dialog>
return ( {({ close }) => {
<FilterSelectForm return (
websiteId={websiteId} <FilterEditForm
fields={fields} websiteId={websiteId}
startDate={startDate} data={filters}
endDate={endDate} onChange={handleChange}
onChange={value => { onClose={close}
handleAddFilter(value); />
close(); );
}} }}
/> </Dialog>
); </Modal>
}} </DialogTrigger>
</Popover>
</MenuTrigger>
); );
} }

View file

@ -22,7 +22,7 @@ export function EventsPage({ websiteId }) {
<EventsMetricsBar websiteId={websiteId} /> <EventsMetricsBar websiteId={websiteId} />
</Panel> </Panel>
<GridRow layout="two-one"> <GridRow layout="two-one">
<Panel> <Panel gridColumn="span 2">
<EventsChart websiteId={websiteId} /> <EventsChart websiteId={websiteId} />
</Panel> </Panel>
<Panel> <Panel>

View file

@ -22,7 +22,7 @@ export function SessionsPage({ websiteId }) {
<SessionsMetricsBar websiteId={websiteId} /> <SessionsMetricsBar websiteId={websiteId} />
</Panel> </Panel>
<GridRow layout="two-one"> <GridRow layout="two-one">
<Panel padding="0"> <Panel padding="0" gridColumn="span 2">
<WorldMap websiteId={websiteId} /> <WorldMap websiteId={websiteId} />
</Panel> </Panel>
<Panel> <Panel>

View file

@ -1,5 +1,5 @@
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import { Icon, Text, Flexbox } from '@umami/react-zen'; import { Icon, Text, Column } from '@umami/react-zen';
import { Icons } from '@/components/icons'; import { Icons } from '@/components/icons';
export interface EmptyPlaceholderProps { export interface EmptyPlaceholderProps {
@ -9,12 +9,12 @@ export interface EmptyPlaceholderProps {
export function EmptyPlaceholder({ message, children }: EmptyPlaceholderProps) { export function EmptyPlaceholder({ message, children }: EmptyPlaceholderProps) {
return ( return (
<Flexbox direction="column" alignItems="center" justifyContent="center" gap={60} height={600}> <Column alignItems="center" justifyContent="center" gap="5" height="100%" width="100%">
<Icon size="xl"> <Icon size="xl">
<Icons.Logo /> <Icons.Logo />
</Icon> </Icon>
<Text size="lg">{message}</Text> <Text size="lg">{message}</Text>
<div>{children}</div> <div>{children}</div>
</Flexbox> </Column>
); );
} }

View file

@ -0,0 +1,86 @@
import { useState, Key } from 'react';
import { Grid, Row, Column, Label, List, ListItem, Button, Heading } from '@umami/react-zen';
import { useFilters, useMessages } from '@/components/hooks';
import { EmptyPlaceholder } from '@/components/common/EmptyPlaceholder';
import { FilterRecord } from '@/components/common/FilterRecord';
export interface FilterEditFormProps {
websiteId?: string;
data: any[];
onChange?: (filters: { name: string; type: string; operator: string; value: string }[]) => void;
onClose?: () => void;
}
export function FilterEditForm({ data = [], onChange, onClose }: FilterEditFormProps) {
const { formatMessage, labels } = useMessages();
const [filters, setFilters] = useState(data);
const { fields } = useFilters();
const updateFilter = (name: string, props: { [key: string]: any }) => {
setFilters(filters =>
filters.map(filter => (filter.name === name ? { ...filter, ...props } : filter)),
);
};
const handleAdd = (name: Key) => {
setFilters(filters.concat({ name, operator: 'eq', value: '' }));
};
const handleChange = (name: string, value: Key) => {
updateFilter(name, { value });
};
const handleSelect = (name: string, operator: Key) => {
updateFilter(name, { operator });
};
const handleRemove = (name: string) => {
setFilters(filters.filter(filter => filter.name !== name));
};
const handleApply = () => {
onChange?.(filters.filter(f => f.value));
onClose?.();
};
return (
<Grid columns="160px 1fr" width="760px" gapY="6">
<Row gridColumn="span 2">
<Heading>{formatMessage(labels.filters)}</Heading>
</Row>
<Column border="right" paddingRight="3">
<Label>Fields</Label>
<List onAction={handleAdd}>
{fields.map((field: any) => {
const isDisabled = filters.find(({ name }) => name === field.name);
return (
<ListItem key={field.name} id={field.name} isDisabled={isDisabled}>
{field.label}
</ListItem>
);
})}
</List>
</Column>
<Column paddingLeft="6" overflow="auto" gapY="4">
{filters.map(filter => {
return (
<FilterRecord
key={filter.name}
{...filter}
onSelect={handleSelect}
onRemove={handleRemove}
onChange={handleChange}
/>
);
})}
{!filters.length && <EmptyPlaceholder message="No filters selected." />}
</Column>
<Row alignItems="center" justifyContent="flex-end" gridColumn="span 2" gap>
<Button onPress={onClose}>{formatMessage(labels.cancel)}</Button>
<Button variant="primary" onPress={handleApply}>
{formatMessage(labels.apply)}
</Button>
</Row>
</Grid>
);
}

View file

@ -0,0 +1,64 @@
import {
Grid,
Row,
Column,
TextField,
Label,
ListItem,
Select,
Icon,
Icons,
Button,
} from '@umami/react-zen';
import { useFilters } from '@/components/hooks';
export interface FilterRecordProps {
name: string;
operator: string;
value: string;
onSelect?: (name: string, value: any) => void;
onRemove?: (name: string) => void;
onChange?: (name: string, value: string) => void;
}
export function FilterRecord({
name,
operator,
value,
onSelect,
onRemove,
onChange,
}: FilterRecordProps) {
const { fields, operators } = useFilters();
return (
<Grid columns="1fr auto">
<Column>
<Label>{fields.find(f => f.name === name)?.label}</Label>
<Row gap alignItems="center">
<Select
items={operators.filter(({ type }) => type === 'string')}
selectedKey={operator}
onSelectionChange={value => onSelect?.(name, value)}
>
{({ name, label }: any) => {
return (
<ListItem key={name} id={name}>
{label}
</ListItem>
);
}}
</Select>
<TextField value={value} onChange={e => onChange?.(name, e.target.value)} />
</Row>
</Column>
<Column justifyContent="flex-end">
<Button variant="quiet" onPress={() => onRemove?.(name)}>
<Icon>
<Icons.Close />
</Icon>
</Button>
</Column>
</Grid>
);
}

View file

@ -1,8 +1,46 @@
import { useMessages } from './useMessages'; import { useMessages } from './useMessages';
import { OPERATORS } from '@/lib/constants'; import { useNavigation } from '@/components/hooks/useNavigation';
import { FILTER_COLUMNS, OPERATORS } from '@/lib/constants';
import { safeDecodeURIComponent } from '@/lib/url';
export function useFilters() { export function useFilters() {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { query } = useNavigation();
const fields = [
{ name: 'url', type: 'string', label: formatMessage(labels.url) },
{ name: 'title', type: 'string', label: formatMessage(labels.pageTitle) },
{ name: 'referrer', type: 'string', label: formatMessage(labels.referrer) },
{ name: 'query', type: 'string', label: formatMessage(labels.query) },
{ name: 'browser', type: 'string', label: formatMessage(labels.browser) },
{ name: 'os', type: 'string', label: formatMessage(labels.os) },
{ name: 'device', type: 'string', label: formatMessage(labels.device) },
{ name: 'country', type: 'string', label: formatMessage(labels.country) },
{ name: 'region', type: 'string', label: formatMessage(labels.region) },
{ name: 'city', type: 'string', label: formatMessage(labels.city) },
{ name: 'host', type: 'string', label: formatMessage(labels.host) },
{ name: 'tag', type: 'string', label: formatMessage(labels.tag) },
];
const operators = [
{ name: 'eq', type: 'string', label: 'Is' },
{ name: 'neq', type: 'string', label: 'Is not' },
{ name: 'c', type: 'string', label: 'Contains' },
{ name: 'dnc', type: 'string', label: 'Does not contain' },
{ name: 'c', type: 'array', label: 'Contains' },
{ name: 'dnc', type: 'array', label: 'Does not contain' },
{ name: 't', type: 'boolean', label: 'True' },
{ name: 'f', type: 'boolean', label: 'False' },
{ name: 'eq', type: 'number', label: 'Is' },
{ name: 'neq', type: 'number', label: 'Is not' },
{ name: 'gt', type: 'number', label: 'Greater than' },
{ name: 'lt', type: 'number', label: 'Less than' },
{ name: 'gte', type: 'number', label: 'Greater than or equals' },
{ name: 'lte', type: 'number', label: 'Less than or equals' },
{ name: 'bf', type: 'date', label: 'Before' },
{ name: 'af', type: 'date', label: 'After' },
{ name: 'eq', type: 'uuid', label: 'Is' },
];
const operatorLabels = { const operatorLabels = {
[OPERATORS.equals]: formatMessage(labels.is), [OPERATORS.equals]: formatMessage(labels.is),
@ -37,15 +75,38 @@ export function useFilters() {
uuid: [OPERATORS.equals], uuid: [OPERATORS.equals],
}; };
const filters = Object.keys(typeFilters).flatMap(key => { const filters = Object.keys(query).reduce((arr, key) => {
return ( if (FILTER_COLUMNS[key]) {
typeFilters[key]?.map(value => ({ type: key, value, label: operatorLabels[value] })) ?? [] let operator = 'eq';
); let value = safeDecodeURIComponent(query[key]);
}); const label = fields.find(({ name }) => name === key)?.label;
const getFilters = type => { const match = value.match(/^([a-z]+)~(.*)/);
return typeFilters[type]?.map(key => ({ type, value: key, label: operatorLabels[key] })) ?? [];
if (match) {
operator = match[1];
value = match[2];
}
return arr.concat({
name: key,
operator,
value,
label,
});
}
return arr;
}, []);
const getFilters = (type: string) => {
return (
typeFilters[type]?.map((key: string | number) => ({
type,
value: key,
label: operatorLabels[key],
})) ?? []
);
}; };
return { filters, operatorLabels, typeFilters, getFilters }; return { fields, operators, filters, operatorLabels, typeFilters, getFilters };
} }

View file

@ -18,8 +18,8 @@ export function useNavigation() {
return obj; return obj;
}, [params]); }, [params]);
function renderUrl(params: any, reset?: boolean) { function renderUrl(params: any) {
return reset ? pathname : buildUrl(pathname, { ...query, ...params }); return !params ? pathname : buildUrl(pathname, { ...query, ...params });
} }
function renderTeamUrl(url: string) { function renderTeamUrl(url: string) {

View file

@ -299,6 +299,7 @@ export const labels = defineMessages({
grouped: { id: 'label.grouped', defaultMessage: 'Grouped' }, grouped: { id: 'label.grouped', defaultMessage: 'Grouped' },
other: { id: 'label.other', defaultMessage: 'Other' }, other: { id: 'label.other', defaultMessage: 'Other' },
boards: { id: 'label.boards', defaultMessage: 'Boards' }, boards: { id: 'label.boards', defaultMessage: 'Boards' },
apply: { id: 'label.apply', defaultMessage: 'Apply' },
}); });
export const messages = defineMessages({ export const messages = defineMessages({

View file

@ -1,52 +1,14 @@
import { MouseEvent } from 'react'; import { MouseEvent } from 'react';
import { import { Button, Icon, Icons, Text, Row, TooltipTrigger, Tooltip } from '@umami/react-zen';
Button, import { useNavigation, useMessages, useFormat, useFilters } from '@/components/hooks';
Icon, import { isSearchOperator } from '@/lib/params';
Icons,
Popover,
MenuTrigger,
Text,
Row,
TooltipTrigger,
Tooltip,
} from '@umami/react-zen';
import {
useDateRange,
useFields,
useNavigation,
useMessages,
useFormat,
useFilters,
} from '@/components/hooks';
import { FieldFilterEditForm } from '@/app/(main)/reports/[reportId]/FieldFilterEditForm';
import { FILTER_COLUMNS, OPERATOR_PREFIXES } from '@/lib/constants';
import { isSearchOperator, parseParameterValue } from '@/lib/params';
import { WebsiteFilterButton } from '@/app/(main)/websites/[websiteId]/WebsiteFilterButton'; import { WebsiteFilterButton } from '@/app/(main)/websites/[websiteId]/WebsiteFilterButton';
export function FilterBar({ websiteId }: { websiteId: string }) { export function FilterBar({ websiteId }: { websiteId: string }) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { formatValue } = useFormat(); const { formatValue } = useFormat();
const { dateRange } = useDateRange(websiteId); const { router, renderUrl } = useNavigation();
const { const { filters, operatorLabels } = useFilters();
router,
renderUrl,
query: { view },
} = useNavigation();
const { fields } = useFields();
const { operatorLabels } = useFilters();
const { startDate, endDate } = dateRange;
const { query } = useNavigation();
const params = Object.keys(query).reduce((obj, key) => {
if (FILTER_COLUMNS[key]) {
obj[key] = query[key];
}
return obj;
}, {});
if (Object.keys(params).filter(key => params[key]).length === 0) {
return null;
}
const handleCloseFilter = (param: string, e: MouseEvent) => { const handleCloseFilter = (param: string, e: MouseEvent) => {
e.stopPropagation(); e.stopPropagation();
@ -54,25 +16,17 @@ export function FilterBar({ websiteId }: { websiteId: string }) {
}; };
const handleResetFilter = () => { const handleResetFilter = () => {
router.push(renderUrl({ view }, true)); router.push(renderUrl(false));
}; };
const handleChangeFilter = ( if (!filters.length) {
values: { name: string; operator: string; value: string }, return null;
close: () => void, }
) => {
const { name, operator, value } = values;
const prefix = OPERATOR_PREFIXES[operator];
router.push(renderUrl({ [name]: prefix + value }));
close();
};
return ( return (
<Row <Row
className="dark-theme"
gap="3" gap="3"
backgroundColor="3" backgroundColor="2"
alignItems="center" alignItems="center"
justifyContent="space-between" justifyContent="space-between"
paddingY="3" paddingY="3"
@ -85,46 +39,26 @@ export function FilterBar({ websiteId }: { websiteId: string }) {
<Text color="11" weight="bold"> <Text color="11" weight="bold">
{formatMessage(labels.filters)} {formatMessage(labels.filters)}
</Text> </Text>
{Object.keys(params).map(key => { {Object.keys(filters).map(key => {
if (!params[key]) { const filter = filters[key];
return null; const { name, label, operator, value } = filter;
}
const label = fields.find(f => f.name === key)?.label;
const { operator, value } = parseParameterValue(params[key]);
const paramValue = isSearchOperator(operator) ? value : formatValue(value, key); const paramValue = isSearchOperator(operator) ? value : formatValue(value, key);
return ( return (
<MenuTrigger key={key}> <Button key={name} variant="outline">
<Button variant="outline"> <Row alignItems="center" gap="6">
<Row alignItems="center" gap="2"> <Row alignItems="center" gap="2">
<Text weight="bold">{label}</Text> <Text weight="bold">{label}</Text>
<Text transform="uppercase" color="muted"> <Text transform="uppercase" color="11" size="1">
{operatorLabels[operator]} {operatorLabels[operator]}
</Text> </Text>
<Text weight="bold">{paramValue}</Text> <Text weight="bold">{paramValue}</Text>
<Icon onClick={e => handleCloseFilter(key, e)}>
<Icons.Close />
</Icon>
</Row> </Row>
</Button> <Icon onClick={e => handleCloseFilter(name, e)}>
<Popover placement="start"> <Icons.Close />
{({ close }: any) => { </Icon>
return ( </Row>
<FieldFilterEditForm </Button>
label={label}
type="string"
websiteId={websiteId}
name={key}
operator={operator}
defaultValue={value}
startDate={startDate}
endDate={endDate}
onChange={values => handleChangeFilter(values, close)}
/>
);
}}
</Popover>
</MenuTrigger>
); );
})} })}
<WebsiteFilterButton websiteId={websiteId} alignment="center" showText={false} /> <WebsiteFilterButton websiteId={websiteId} alignment="center" showText={false} />

View file

@ -20,7 +20,7 @@ export function PagesTable({ allowFilter, ...props }: PagesTableProps) {
const { domain } = useContext(WebsiteContext); const { domain } = useContext(WebsiteContext);
const handleSelect = (key: any) => { const handleSelect = (key: any) => {
router.push(renderUrl({ view: key }), { scroll: false }); router.push(renderUrl({ view: key }));
}; };
const buttons = [ const buttons = [

View file

@ -20,7 +20,7 @@ export function ReferrersTable({ allowFilter, ...props }: ReferrersTableProps) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const handleSelect = (key: any) => { const handleSelect = (key: any) => {
router.push(renderUrl({ view: key }), { scroll: false }); router.push(renderUrl({ view: key }));
}; };
const buttons = [ const buttons = [