mirror of
https://github.com/umami-software/umami.git
synced 2026-02-05 13:17:19 +01:00
Added segment filtering to filter form.
This commit is contained in:
parent
2e69e57445
commit
2ad624ccc8
15 changed files with 301 additions and 193 deletions
|
|
@ -1,99 +1,68 @@
|
|||
import { useState, Key } from 'react';
|
||||
import { Grid, Row, Column, Label, List, ListItem, Button, Heading } from '@umami/react-zen';
|
||||
import { useDateRange, useFilters, useMessages } from '@/components/hooks';
|
||||
import { FilterRecord } from '@/components/common/FilterRecord';
|
||||
import { Empty } from '@/components/common/Empty';
|
||||
import { useState } from 'react';
|
||||
import { Column, Tabs, TabList, Tab, TabPanel, Row, Button } from '@umami/react-zen';
|
||||
import { useMessages } from '@/components/hooks';
|
||||
import { FieldFilters } from '@/components/input/FieldFilters';
|
||||
import { SegmentFilters } from '@/components/input/SegmentFilters';
|
||||
|
||||
export interface FilterEditFormProps {
|
||||
websiteId?: string;
|
||||
data: any[];
|
||||
onChange?: (filters: { name: string; type: string; operator: string; value: string }[]) => void;
|
||||
filters: any[];
|
||||
segmentId?: string;
|
||||
onChange?: (params: { filters: any[]; segment: any }) => void;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
export function FilterEditForm({ websiteId, data = [], onChange, onClose }: FilterEditFormProps) {
|
||||
const { formatMessage, labels, messages } = useMessages();
|
||||
const [filters, setFilters] = useState(data);
|
||||
const { fields } = useFilters();
|
||||
const {
|
||||
dateRange: { startDate, endDate },
|
||||
} = useDateRange(websiteId);
|
||||
export function FilterEditForm({
|
||||
websiteId,
|
||||
filters = [],
|
||||
segmentId,
|
||||
onChange,
|
||||
onClose,
|
||||
}: FilterEditFormProps) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const [currentFilters, setCurrentFilters] = useState(filters);
|
||||
const [currentSegment, setCurrentSegment] = useState(null);
|
||||
|
||||
const updateFilter = (name: string, props: Record<string, any>) => {
|
||||
setFilters(filters =>
|
||||
filters.map(filter => (filter.name === name ? { ...filter, ...props } : filter)),
|
||||
);
|
||||
const handleReset = () => {
|
||||
setCurrentFilters([]);
|
||||
};
|
||||
|
||||
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));
|
||||
const handleSave = () => {
|
||||
onChange?.({ filters: currentFilters.filter(f => f.value), segment: currentSegment });
|
||||
onClose?.();
|
||||
};
|
||||
|
||||
const handleSegmentChange = (segment?: { id: string }) => {
|
||||
setCurrentSegment(segment);
|
||||
};
|
||||
|
||||
return (
|
||||
<Grid columns="160px 1fr" width="760px" overflow="hidden" gapY="6">
|
||||
<Row gridColumn="span 2">
|
||||
<Heading>{formatMessage(labels.filters)}</Heading>
|
||||
<Column>
|
||||
<Tabs>
|
||||
<TabList>
|
||||
<Tab id="fields">{formatMessage(labels.fields)}</Tab>
|
||||
<Tab id="segments">{formatMessage(labels.segments)}</Tab>
|
||||
</TabList>
|
||||
<TabPanel id="fields">
|
||||
<FieldFilters websiteId={websiteId} filters={currentFilters} onSave={setCurrentFilters} />
|
||||
</TabPanel>
|
||||
<TabPanel id="segments">
|
||||
<SegmentFilters
|
||||
websiteId={websiteId}
|
||||
segmentId={segmentId}
|
||||
onSave={handleSegmentChange}
|
||||
/>
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
<Row alignItems="center" justifyContent="space-between" gridColumn="span 2" gap>
|
||||
<Button onPress={handleReset}>{formatMessage(labels.reset)}</Button>
|
||||
<Row alignItems="center" justifyContent="flex-end" gridColumn="span 2" gap>
|
||||
<Button onPress={onClose}>{formatMessage(labels.cancel)}</Button>
|
||||
<Button variant="primary" onPress={handleSave}>
|
||||
{formatMessage(labels.apply)}
|
||||
</Button>
|
||||
</Row>
|
||||
</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"
|
||||
maxHeight="600px"
|
||||
style={{ contain: 'layout' }}
|
||||
>
|
||||
{filters.map(filter => {
|
||||
return (
|
||||
<FilterRecord
|
||||
key={filter.name}
|
||||
websiteId={websiteId}
|
||||
type={filter.name}
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
{...filter}
|
||||
onSelect={handleSelect}
|
||||
onRemove={handleRemove}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{!filters.length && <Empty message={formatMessage(messages.nothingSelected)} />}
|
||||
</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>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ export * from './queries/useTeamMembersQuery';
|
|||
export * from './queries/useUserQuery';
|
||||
export * from './queries/useUsersQuery';
|
||||
export * from './queries/useWebsiteQuery';
|
||||
export * from './queries/useWebsiteSegementsQuery';
|
||||
export * from './queries/useWebsitesQuery';
|
||||
export * from './queries/useWebsiteEventsQuery';
|
||||
export * from './queries/useWebsiteEventsSeriesQuery';
|
||||
|
|
|
|||
21
src/components/hooks/queries/useWebsiteSegementsQuery.ts
Normal file
21
src/components/hooks/queries/useWebsiteSegementsQuery.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { useApi } from '../useApi';
|
||||
import { useModified } from '@/components/hooks';
|
||||
import { keepPreviousData } from '@tanstack/react-query';
|
||||
import { ReactQueryOptions } from '@/lib/types';
|
||||
|
||||
export function useWebsiteSegmentsQuery(
|
||||
websiteId: string,
|
||||
params?: Record<string, string>,
|
||||
options?: ReactQueryOptions<any>,
|
||||
) {
|
||||
const { get, useQuery } = useApi();
|
||||
const { modified } = useModified(`website:${websiteId}`);
|
||||
|
||||
return useQuery({
|
||||
queryKey: ['website:segments', { websiteId, modified, ...params }],
|
||||
queryFn: () => get(`/websites/${websiteId}/segments`, { ...params }),
|
||||
enabled: !!websiteId,
|
||||
placeholderData: keepPreviousData,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
|
@ -6,7 +6,6 @@ export function useFields() {
|
|||
const fields = [
|
||||
{ name: 'path', type: 'string', label: formatMessage(labels.path) },
|
||||
// { name: 'cohort', type: 'string', label: formatMessage(labels.cohort) },
|
||||
// { name: 'segment', type: 'string', label: formatMessage(labels.segment) },
|
||||
{ name: 'title', type: 'string', label: formatMessage(labels.pageTitle) },
|
||||
{ name: 'referrer', type: 'string', label: formatMessage(labels.referrer) },
|
||||
//{ name: 'query', type: 'string', label: formatMessage(labels.query) },
|
||||
|
|
|
|||
|
|
@ -11,7 +11,11 @@ export function useNavigation() {
|
|||
const [queryParams, setQueryParams] = useState(Object.fromEntries(searchParams));
|
||||
|
||||
const updateParams = (params?: Record<string, string | number>) => {
|
||||
return !params ? pathname : buildUrl(pathname, { ...queryParams, ...params });
|
||||
return buildUrl(pathname, { ...queryParams, ...params });
|
||||
};
|
||||
|
||||
const replaceParams = (params?: Record<string, string | number>) => {
|
||||
return buildUrl(pathname, params);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -33,6 +37,7 @@ export function useNavigation() {
|
|||
teamId,
|
||||
websiteId,
|
||||
updateParams,
|
||||
replaceParams,
|
||||
renderUrl,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
74
src/components/input/FieldFilters.tsx
Normal file
74
src/components/input/FieldFilters.tsx
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
import { Key } from 'react';
|
||||
import { Grid, Column, List, ListItem } from '@umami/react-zen';
|
||||
import { useDateRange, useFields, useMessages } from '@/components/hooks';
|
||||
import { FilterRecord } from '@/components/common/FilterRecord';
|
||||
import { Empty } from '@/components/common/Empty';
|
||||
|
||||
export interface FieldFiltersProps {
|
||||
websiteId: string;
|
||||
filters: { name: string; operator: string; value: string }[];
|
||||
onSave?: (data: any) => void;
|
||||
}
|
||||
|
||||
export function FieldFilters({ websiteId, filters, onSave }: FieldFiltersProps) {
|
||||
const { formatMessage, messages } = useMessages();
|
||||
const { fields } = useFields();
|
||||
const {
|
||||
dateRange: { startDate, endDate },
|
||||
} = useDateRange(websiteId);
|
||||
|
||||
const updateFilter = (name: string, props: Record<string, any>) => {
|
||||
onSave(filters.map(filter => (filter.name === name ? { ...filter, ...props } : filter)));
|
||||
};
|
||||
|
||||
const handleAdd = (name: Key) => {
|
||||
onSave(filters.concat({ name: name.toString(), 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) => {
|
||||
onSave(filters.filter(filter => filter.name !== name));
|
||||
};
|
||||
|
||||
return (
|
||||
<Grid columns="160px 1fr" overflow="hidden" gapY="6">
|
||||
<Column border="right" paddingRight="3">
|
||||
<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" height="500px" style={{ contain: 'layout' }}>
|
||||
{filters.map(filter => {
|
||||
return (
|
||||
<FilterRecord
|
||||
key={filter.name}
|
||||
websiteId={websiteId}
|
||||
type={filter.name}
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
{...filter}
|
||||
onSelect={handleSelect}
|
||||
onRemove={handleRemove}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{!filters.length && <Empty message={formatMessage(messages.nothingSelected)} />}
|
||||
</Column>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
import { MouseEvent } from 'react';
|
||||
import { Button, Icon, Text, Row, TooltipTrigger, Tooltip } from '@umami/react-zen';
|
||||
import { useNavigation, useMessages, useFormat, useFilters } from '@/components/hooks';
|
||||
import { Close } from '@/components/icons';
|
||||
|
|
@ -7,57 +6,55 @@ import { isSearchOperator } from '@/lib/params';
|
|||
export function FilterBar() {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { formatValue } = useFormat();
|
||||
const { router, updateParams } = useNavigation();
|
||||
const {
|
||||
router,
|
||||
updateParams,
|
||||
replaceParams,
|
||||
query: { segment },
|
||||
} = useNavigation();
|
||||
const { filters, operatorLabels } = useFilters();
|
||||
|
||||
const handleCloseFilter = (param: string, e: MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
const handleCloseFilter = (param: string) => {
|
||||
router.push(updateParams({ [param]: undefined }));
|
||||
};
|
||||
|
||||
const handleResetFilter = () => {
|
||||
router.push(updateParams());
|
||||
router.push(replaceParams());
|
||||
};
|
||||
|
||||
if (!filters.length) {
|
||||
const handleSegmentRemove = () => {
|
||||
router.push(updateParams({ segment: undefined }));
|
||||
};
|
||||
|
||||
if (!filters.length && !segment) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Row gap alignItems="center" justifyContent="space-between" padding="2" backgroundColor="3">
|
||||
<Row alignItems="center" gap="2" wrap="wrap">
|
||||
{Object.keys(filters).map(key => {
|
||||
const filter = filters[key];
|
||||
{segment && (
|
||||
<FilterItem
|
||||
name="segment"
|
||||
label={formatMessage(labels.segment)}
|
||||
value={segment}
|
||||
operator={operatorLabels.eq}
|
||||
onRemove={handleSegmentRemove}
|
||||
/>
|
||||
)}
|
||||
{filters.map(filter => {
|
||||
const { name, label, operator, value } = filter;
|
||||
const paramValue = isSearchOperator(operator) ? value : formatValue(value, name);
|
||||
|
||||
return (
|
||||
<Row
|
||||
<FilterItem
|
||||
key={name}
|
||||
border
|
||||
padding="2"
|
||||
color
|
||||
backgroundColor
|
||||
borderRadius
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
theme="dark"
|
||||
>
|
||||
<Row alignItems="center" gap="4">
|
||||
<Row alignItems="center" gap="2">
|
||||
<Text color="12" weight="bold">
|
||||
{label}
|
||||
</Text>
|
||||
<Text color="11">{operatorLabels[operator]}</Text>
|
||||
<Text color="12" weight="bold">
|
||||
{paramValue}
|
||||
</Text>
|
||||
</Row>
|
||||
<Icon onClick={e => handleCloseFilter(name, e)} size="xs">
|
||||
<Close />
|
||||
</Icon>
|
||||
</Row>
|
||||
</Row>
|
||||
name={name}
|
||||
label={label}
|
||||
operator={operatorLabels[operator]}
|
||||
value={paramValue}
|
||||
onRemove={name => handleCloseFilter(name)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Row>
|
||||
|
|
@ -74,3 +71,33 @@ export function FilterBar() {
|
|||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
const FilterItem = ({ name, label, operator, value, onRemove }) => {
|
||||
return (
|
||||
<Row
|
||||
border
|
||||
padding="2"
|
||||
color
|
||||
backgroundColor
|
||||
borderRadius
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
theme="dark"
|
||||
>
|
||||
<Row alignItems="center" gap="4">
|
||||
<Row alignItems="center" gap="2">
|
||||
<Text color="12" weight="bold">
|
||||
{label}
|
||||
</Text>
|
||||
<Text color="11">{operator}</Text>
|
||||
<Text color="12" weight="bold">
|
||||
{value}
|
||||
</Text>
|
||||
</Row>
|
||||
<Icon onClick={() => onRemove(name)} size="xs">
|
||||
<Close />
|
||||
</Icon>
|
||||
</Row>
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
36
src/components/input/SegmentFilters.tsx
Normal file
36
src/components/input/SegmentFilters.tsx
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import { useState } from 'react';
|
||||
import { List, Column, ListItem } from '@umami/react-zen';
|
||||
import { useWebsiteSegmentsQuery } from '@/components/hooks';
|
||||
import { LoadingPanel } from '@/components/common/LoadingPanel';
|
||||
|
||||
export interface SegmentFiltersProps {
|
||||
websiteId: string;
|
||||
segmentId: string;
|
||||
onSave?: (data: any) => void;
|
||||
}
|
||||
|
||||
export function SegmentFilters({ websiteId, segmentId, onSave }: SegmentFiltersProps) {
|
||||
const { data, isLoading } = useWebsiteSegmentsQuery(websiteId, { type: 'segment' });
|
||||
const [currentSegment, setCurrentSegment] = useState(segmentId);
|
||||
|
||||
const handleSave = (id: string) => {
|
||||
setCurrentSegment(id);
|
||||
onSave?.(data.find(item => item.id === id));
|
||||
};
|
||||
|
||||
return (
|
||||
<Column height="400px" gap>
|
||||
<LoadingPanel data={data} isLoading={isLoading} overflowY="auto">
|
||||
<List selectionMode="single" value={[currentSegment]} onChange={id => handleSave(id[0])}>
|
||||
{data?.map(item => {
|
||||
return (
|
||||
<ListItem key={item.id} id={item.id}>
|
||||
{item.name}
|
||||
</ListItem>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
</LoadingPanel>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
|
@ -100,9 +100,9 @@ export function WebsiteDateFilter({
|
|||
<Icon fillColor>{compare ? <Close /> : <Compare />}</Icon>
|
||||
</Button>
|
||||
<Tooltip>{formatMessage(compare ? labels.cancel : labels.compareDates)}</Tooltip>
|
||||
<ExportButton websiteId={websiteId} />
|
||||
</TooltipTrigger>
|
||||
)}
|
||||
<ExportButton websiteId={websiteId} />
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue