mirror of
https://github.com/umami-software/umami.git
synced 2026-02-04 04:37:11 +01:00
New filter bar and filter edit form.
This commit is contained in:
parent
47e89afcb4
commit
bfdd3f9525
19 changed files with 300 additions and 150 deletions
|
|
@ -78,7 +78,7 @@
|
|||
"@react-spring/web": "^9.7.5",
|
||||
"@tanstack/react-query": "^5.71.10",
|
||||
"@umami/prisma-client": "^0.16.0",
|
||||
"@umami/react-zen": "^0.79.0",
|
||||
"@umami/react-zen": "^0.81.0",
|
||||
"@umami/redis-client": "^0.27.0",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"chalk": "^4.1.2",
|
||||
|
|
|
|||
10
pnpm-lock.yaml
generated
10
pnpm-lock.yaml
generated
|
|
@ -45,8 +45,8 @@ importers:
|
|||
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)))
|
||||
'@umami/react-zen':
|
||||
specifier: ^0.79.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))
|
||||
specifier: ^0.81.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':
|
||||
specifier: ^0.27.0
|
||||
version: 0.27.0
|
||||
|
|
@ -2946,8 +2946,8 @@ packages:
|
|||
'@prisma/client': ^4.8.0
|
||||
'@prisma/extension-read-replicas': ^0.3.0
|
||||
|
||||
'@umami/react-zen@0.79.0':
|
||||
resolution: {integrity: sha512-qumZSV/dWvtq7iR7QwxEO5emE/jzgI5uPP5Y1E9S+MMGnJRhD5gQ1TvURf+jYAlTuiPubLysu6U3nKYmPNJxUg==}
|
||||
'@umami/react-zen@0.81.0':
|
||||
resolution: {integrity: sha512-DmwttqG+rhllcyqdCusxZ0MLVPSjIv4BXPsz7M1CED8I6wrz2MzG7euZgkJY1GMLtLPscisTooCqSG/RzwhhjQ==}
|
||||
|
||||
'@umami/redis-client@0.27.0':
|
||||
resolution: {integrity: sha512-SbHTpxhgeZyTBUSp2zdZM+XUtpsaSL4Tad8QXIEhEtjWhvvfoornyT5kLuyYCVtzSAT4daALeGmOO1z6EE1KcA==}
|
||||
|
|
@ -10572,7 +10572,7 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- 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:
|
||||
'@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)
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ export function LanguageSetting() {
|
|||
return (
|
||||
<Row gap="3">
|
||||
<Select
|
||||
value={locale}
|
||||
selectedKey={locale}
|
||||
onChange={val => saveLocale(val as string)}
|
||||
allowSearch
|
||||
onSearch={setSearch}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ export function TimezoneSetting() {
|
|||
<Row gap="3">
|
||||
<Select
|
||||
className={styles.dropdown}
|
||||
value={timezone}
|
||||
selectedKey={timezone}
|
||||
onChange={(value: any) => saveTimezone(value)}
|
||||
allowSearch={true}
|
||||
onSearch={setSearch}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import {
|
|||
Column,
|
||||
Row,
|
||||
Select,
|
||||
Flexbox,
|
||||
Icon,
|
||||
Icons,
|
||||
Loading,
|
||||
|
|
@ -130,15 +129,17 @@ export function FieldFilterEditForm({
|
|||
window.setTimeout(() => setShowMenu(false), 500);
|
||||
};
|
||||
|
||||
const items = filterDropdownItems(name);
|
||||
|
||||
return (
|
||||
<Column>
|
||||
<Row className={styles.filter}>
|
||||
<Label>{label}</Label>
|
||||
<Flexbox gap="3">
|
||||
<Row gap="3">
|
||||
{allowFilterSelect && (
|
||||
<Select
|
||||
className={styles.dropdown}
|
||||
items={filterDropdownItems(name)}
|
||||
items={items}
|
||||
value={operator}
|
||||
onChange={handleOperatorChange}
|
||||
>
|
||||
|
|
@ -183,7 +184,7 @@ export function FieldFilterEditForm({
|
|||
onChange={e => setValue(e.target.value)}
|
||||
/>
|
||||
)}
|
||||
</Flexbox>
|
||||
</Row>
|
||||
<Button variant="primary" onPress={handleAdd} isDisabled={isDisabled}>
|
||||
{formatMessage(isNew ? labels.add : labels.update)}
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -11,11 +11,11 @@ export function FieldSelectForm({ fields = [], onSelect, showType = true }: Fiel
|
|||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
return (
|
||||
<Menu>
|
||||
<MenuSection title={formatMessage(labels.fields)}>
|
||||
<Menu onAction={value => onSelect?.(value)}>
|
||||
<MenuSection title={formatMessage(labels.fields)} selectionMode="multiple">
|
||||
{fields.map(({ name, label, type }) => {
|
||||
return (
|
||||
<MenuItem key={name} id={name} onAction={() => onSelect(name)}>
|
||||
<MenuItem key={name} id={name}>
|
||||
<Row alignItems="center" justifyContent="space-between">
|
||||
<Text>{label || name}</Text>
|
||||
{showType && type && <Text color="muted">{type}</Text>}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ export function FilterSelectForm({
|
|||
return (
|
||||
<FieldFilterEditForm
|
||||
websiteId={websiteId}
|
||||
name={name}
|
||||
name={name || 'url'}
|
||||
label={label}
|
||||
type={type}
|
||||
startDate={startDate}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { Button, Icon, Icons, MenuTrigger, Popover, Text } from '@umami/react-zen';
|
||||
import { FilterSelectForm } from '@/app/(main)/reports/[reportId]/FilterSelectForm';
|
||||
import { useFields, useMessages, useNavigation, useDateRange } from '@/components/hooks';
|
||||
import { OPERATOR_PREFIXES } from '@/lib/constants';
|
||||
import { Button, Icon, Icons, DialogTrigger, Dialog, Modal, Text } from '@umami/react-zen';
|
||||
import { FilterEditForm } from '@/components/common/FilterEditForm';
|
||||
import { useMessages, useNavigation, useFilters } from '@/components/hooks';
|
||||
import { OPERATORS } from '@/lib/constants';
|
||||
|
||||
export function WebsiteFilterButton({
|
||||
websiteId,
|
||||
|
|
@ -14,41 +14,44 @@ export function WebsiteFilterButton({
|
|||
}) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { renderUrl, router } = useNavigation();
|
||||
const { fields } = useFields();
|
||||
const {
|
||||
dateRange: { startDate, endDate },
|
||||
} = useDateRange(websiteId);
|
||||
const { filters } = useFilters();
|
||||
|
||||
const handleAddFilter = ({ name, operator, value }) => {
|
||||
const prefix = OPERATOR_PREFIXES[operator];
|
||||
const handleChange = (filters: any[]) => {
|
||||
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 (
|
||||
<MenuTrigger>
|
||||
<DialogTrigger>
|
||||
<Button variant="quiet">
|
||||
<Icon>
|
||||
<Icons.Plus />
|
||||
</Icon>
|
||||
{showText && <Text>{formatMessage(labels.filter)}</Text>}
|
||||
</Button>
|
||||
<Popover placement="bottom start">
|
||||
{({ close }: any) => {
|
||||
return (
|
||||
<FilterSelectForm
|
||||
websiteId={websiteId}
|
||||
fields={fields}
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
onChange={value => {
|
||||
handleAddFilter(value);
|
||||
close();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</Popover>
|
||||
</MenuTrigger>
|
||||
<Modal>
|
||||
<Dialog>
|
||||
{({ close }) => {
|
||||
return (
|
||||
<FilterEditForm
|
||||
websiteId={websiteId}
|
||||
data={filters}
|
||||
onChange={handleChange}
|
||||
onClose={close}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</Dialog>
|
||||
</Modal>
|
||||
</DialogTrigger>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ export function EventsPage({ websiteId }) {
|
|||
<EventsMetricsBar websiteId={websiteId} />
|
||||
</Panel>
|
||||
<GridRow layout="two-one">
|
||||
<Panel>
|
||||
<Panel gridColumn="span 2">
|
||||
<EventsChart websiteId={websiteId} />
|
||||
</Panel>
|
||||
<Panel>
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ export function SessionsPage({ websiteId }) {
|
|||
<SessionsMetricsBar websiteId={websiteId} />
|
||||
</Panel>
|
||||
<GridRow layout="two-one">
|
||||
<Panel padding="0">
|
||||
<Panel padding="0" gridColumn="span 2">
|
||||
<WorldMap websiteId={websiteId} />
|
||||
</Panel>
|
||||
<Panel>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
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';
|
||||
|
||||
export interface EmptyPlaceholderProps {
|
||||
|
|
@ -9,12 +9,12 @@ export interface EmptyPlaceholderProps {
|
|||
|
||||
export function EmptyPlaceholder({ message, children }: EmptyPlaceholderProps) {
|
||||
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">
|
||||
<Icons.Logo />
|
||||
</Icon>
|
||||
<Text size="lg">{message}</Text>
|
||||
<div>{children}</div>
|
||||
</Flexbox>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
86
src/components/common/FilterEditForm.tsx
Normal file
86
src/components/common/FilterEditForm.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
64
src/components/common/FilterRecord.tsx
Normal file
64
src/components/common/FilterRecord.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,8 +1,46 @@
|
|||
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() {
|
||||
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 = {
|
||||
[OPERATORS.equals]: formatMessage(labels.is),
|
||||
|
|
@ -37,15 +75,38 @@ export function useFilters() {
|
|||
uuid: [OPERATORS.equals],
|
||||
};
|
||||
|
||||
const filters = Object.keys(typeFilters).flatMap(key => {
|
||||
return (
|
||||
typeFilters[key]?.map(value => ({ type: key, value, label: operatorLabels[value] })) ?? []
|
||||
);
|
||||
});
|
||||
const filters = Object.keys(query).reduce((arr, key) => {
|
||||
if (FILTER_COLUMNS[key]) {
|
||||
let operator = 'eq';
|
||||
let value = safeDecodeURIComponent(query[key]);
|
||||
const label = fields.find(({ name }) => name === key)?.label;
|
||||
|
||||
const getFilters = type => {
|
||||
return typeFilters[type]?.map(key => ({ type, value: key, label: operatorLabels[key] })) ?? [];
|
||||
const match = value.match(/^([a-z]+)~(.*)/);
|
||||
|
||||
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 };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,8 +18,8 @@ export function useNavigation() {
|
|||
return obj;
|
||||
}, [params]);
|
||||
|
||||
function renderUrl(params: any, reset?: boolean) {
|
||||
return reset ? pathname : buildUrl(pathname, { ...query, ...params });
|
||||
function renderUrl(params: any) {
|
||||
return !params ? pathname : buildUrl(pathname, { ...query, ...params });
|
||||
}
|
||||
|
||||
function renderTeamUrl(url: string) {
|
||||
|
|
|
|||
|
|
@ -299,6 +299,7 @@ export const labels = defineMessages({
|
|||
grouped: { id: 'label.grouped', defaultMessage: 'Grouped' },
|
||||
other: { id: 'label.other', defaultMessage: 'Other' },
|
||||
boards: { id: 'label.boards', defaultMessage: 'Boards' },
|
||||
apply: { id: 'label.apply', defaultMessage: 'Apply' },
|
||||
});
|
||||
|
||||
export const messages = defineMessages({
|
||||
|
|
|
|||
|
|
@ -1,52 +1,14 @@
|
|||
import { MouseEvent } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Icon,
|
||||
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 { Button, Icon, Icons, Text, Row, TooltipTrigger, Tooltip } from '@umami/react-zen';
|
||||
import { useNavigation, useMessages, useFormat, useFilters } from '@/components/hooks';
|
||||
import { isSearchOperator } from '@/lib/params';
|
||||
import { WebsiteFilterButton } from '@/app/(main)/websites/[websiteId]/WebsiteFilterButton';
|
||||
|
||||
export function FilterBar({ websiteId }: { websiteId: string }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { formatValue } = useFormat();
|
||||
const { dateRange } = useDateRange(websiteId);
|
||||
const {
|
||||
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 { router, renderUrl } = useNavigation();
|
||||
const { filters, operatorLabels } = useFilters();
|
||||
|
||||
const handleCloseFilter = (param: string, e: MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
|
|
@ -54,25 +16,17 @@ export function FilterBar({ websiteId }: { websiteId: string }) {
|
|||
};
|
||||
|
||||
const handleResetFilter = () => {
|
||||
router.push(renderUrl({ view }, true));
|
||||
router.push(renderUrl(false));
|
||||
};
|
||||
|
||||
const handleChangeFilter = (
|
||||
values: { name: string; operator: string; value: string },
|
||||
close: () => void,
|
||||
) => {
|
||||
const { name, operator, value } = values;
|
||||
const prefix = OPERATOR_PREFIXES[operator];
|
||||
|
||||
router.push(renderUrl({ [name]: prefix + value }));
|
||||
close();
|
||||
};
|
||||
if (!filters.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Row
|
||||
className="dark-theme"
|
||||
gap="3"
|
||||
backgroundColor="3"
|
||||
backgroundColor="2"
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
paddingY="3"
|
||||
|
|
@ -85,46 +39,26 @@ export function FilterBar({ websiteId }: { websiteId: string }) {
|
|||
<Text color="11" weight="bold">
|
||||
{formatMessage(labels.filters)}
|
||||
</Text>
|
||||
{Object.keys(params).map(key => {
|
||||
if (!params[key]) {
|
||||
return null;
|
||||
}
|
||||
const label = fields.find(f => f.name === key)?.label;
|
||||
const { operator, value } = parseParameterValue(params[key]);
|
||||
{Object.keys(filters).map(key => {
|
||||
const filter = filters[key];
|
||||
const { name, label, operator, value } = filter;
|
||||
const paramValue = isSearchOperator(operator) ? value : formatValue(value, key);
|
||||
|
||||
return (
|
||||
<MenuTrigger key={key}>
|
||||
<Button variant="outline">
|
||||
<Button key={name} variant="outline">
|
||||
<Row alignItems="center" gap="6">
|
||||
<Row alignItems="center" gap="2">
|
||||
<Text weight="bold">{label}</Text>
|
||||
<Text transform="uppercase" color="muted">
|
||||
<Text transform="uppercase" color="11" size="1">
|
||||
{operatorLabels[operator]}
|
||||
</Text>
|
||||
<Text weight="bold">{paramValue}</Text>
|
||||
<Icon onClick={e => handleCloseFilter(key, e)}>
|
||||
<Icons.Close />
|
||||
</Icon>
|
||||
</Row>
|
||||
</Button>
|
||||
<Popover placement="start">
|
||||
{({ close }: any) => {
|
||||
return (
|
||||
<FieldFilterEditForm
|
||||
label={label}
|
||||
type="string"
|
||||
websiteId={websiteId}
|
||||
name={key}
|
||||
operator={operator}
|
||||
defaultValue={value}
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
onChange={values => handleChangeFilter(values, close)}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</Popover>
|
||||
</MenuTrigger>
|
||||
<Icon onClick={e => handleCloseFilter(name, e)}>
|
||||
<Icons.Close />
|
||||
</Icon>
|
||||
</Row>
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
<WebsiteFilterButton websiteId={websiteId} alignment="center" showText={false} />
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ export function PagesTable({ allowFilter, ...props }: PagesTableProps) {
|
|||
const { domain } = useContext(WebsiteContext);
|
||||
|
||||
const handleSelect = (key: any) => {
|
||||
router.push(renderUrl({ view: key }), { scroll: false });
|
||||
router.push(renderUrl({ view: key }));
|
||||
};
|
||||
|
||||
const buttons = [
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ export function ReferrersTable({ allowFilter, ...props }: ReferrersTableProps) {
|
|||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
const handleSelect = (key: any) => {
|
||||
router.push(renderUrl({ view: key }), { scroll: false });
|
||||
router.push(renderUrl({ view: key }));
|
||||
};
|
||||
|
||||
const buttons = [
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue