mirror of
https://github.com/umami-software/umami.git
synced 2025-12-08 05:12:36 +01:00
Updated filter bar.
This commit is contained in:
parent
34a8fa100c
commit
2b99274895
11 changed files with 1101 additions and 1048 deletions
|
|
@ -1,11 +1,6 @@
|
||||||
{
|
{
|
||||||
"extends": [
|
"extends": ["stylelint-config-recommended", "stylelint-config-css-modules"],
|
||||||
"stylelint-config-recommended",
|
|
||||||
"stylelint-config-css-modules",
|
|
||||||
"stylelint-config-prettier"
|
|
||||||
],
|
|
||||||
"rules": {
|
"rules": {
|
||||||
"no-descending-specificity": null
|
"no-descending-specificity": null
|
||||||
},
|
}
|
||||||
"ignoreFiles": ["**/*.js", "**/*.md"]
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,7 @@
|
||||||
"@react-spring/web": "^9.7.5",
|
"@react-spring/web": "^9.7.5",
|
||||||
"@tanstack/react-query": "^5.68.0",
|
"@tanstack/react-query": "^5.68.0",
|
||||||
"@umami/prisma-client": "^0.16.0",
|
"@umami/prisma-client": "^0.16.0",
|
||||||
"@umami/react-zen": "^0.73.0",
|
"@umami/react-zen": "^0.76.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",
|
||||||
|
|
@ -178,7 +178,6 @@
|
||||||
"rollup-plugin-postcss": "^4.0.2",
|
"rollup-plugin-postcss": "^4.0.2",
|
||||||
"stylelint": "^15.11.0",
|
"stylelint": "^15.11.0",
|
||||||
"stylelint-config-css-modules": "^4.4.0",
|
"stylelint-config-css-modules": "^4.4.0",
|
||||||
"stylelint-config-prettier": "^9.0.5",
|
|
||||||
"stylelint-config-recommended": "^14.0.1",
|
"stylelint-config-recommended": "^14.0.1",
|
||||||
"tar": "^6.2.1",
|
"tar": "^6.2.1",
|
||||||
"ts-jest": "^29.2.6",
|
"ts-jest": "^29.2.6",
|
||||||
|
|
|
||||||
1794
pnpm-lock.yaml
generated
1794
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
|
@ -38,7 +38,7 @@ export function App({ children }) {
|
||||||
gridRow="2 / 3"
|
gridRow="2 / 3"
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
overflow="auto"
|
overflow="auto"
|
||||||
backgroundColor="1"
|
backgroundColor="0"
|
||||||
>
|
>
|
||||||
<Page>
|
<Page>
|
||||||
{children}
|
{children}
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ export function MenuBar(props: RowProps) {
|
||||||
paddingY="3"
|
paddingY="3"
|
||||||
paddingX="3"
|
paddingX="3"
|
||||||
paddingRight="5"
|
paddingRight="5"
|
||||||
backgroundColor="1"
|
backgroundColor="0"
|
||||||
style={{ borderBottom: '1px solid var(--border-color)' }}
|
style={{ borderBottom: '1px solid var(--border-color)' }}
|
||||||
>
|
>
|
||||||
<Row>
|
<Row>
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ import { WebsiteTabs } from '@/app/(main)/websites/[websiteId]/WebsiteTabs';
|
||||||
import { useWebsite } from '@/components/hooks/useWebsite';
|
import { useWebsite } from '@/components/hooks/useWebsite';
|
||||||
import { WebsiteFilterButton } from '@/app/(main)/websites/[websiteId]/WebsiteFilterButton';
|
import { WebsiteFilterButton } from '@/app/(main)/websites/[websiteId]/WebsiteFilterButton';
|
||||||
import { WebsiteDateFilter } from '@/components/input/WebsiteDateFilter';
|
import { WebsiteDateFilter } from '@/components/input/WebsiteDateFilter';
|
||||||
import { FilterTags } from '@/components/metrics/FilterTags';
|
import { FilterBar } from '@/components/metrics/FilterBar';
|
||||||
import { useMessages } from '@/components/hooks';
|
import { useMessages } from '@/components/hooks';
|
||||||
|
|
||||||
export function WebsiteHeader({
|
export function WebsiteHeader({
|
||||||
|
|
@ -73,7 +73,7 @@ export function WebsiteHeader({
|
||||||
</Row>
|
</Row>
|
||||||
</Row>
|
</Row>
|
||||||
{compareMode && items.map(item => <div key={item.value}>{item.label}</div>)}
|
{compareMode && items.map(item => <div key={item.value}>{item.label}</div>)}
|
||||||
<FilterTags websiteId={websiteId} />
|
<FilterBar websiteId={websiteId} />
|
||||||
<WebsiteTabs websiteId={websiteId} />
|
<WebsiteTabs websiteId={websiteId} />
|
||||||
</Column>
|
</Column>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { Grid } from '@umami/react-zen';
|
||||||
import { Panel } from '@/components/common/Panel';
|
import { Panel } from '@/components/common/Panel';
|
||||||
import { WebsiteHeader } from '../WebsiteHeader';
|
import { WebsiteHeader } from '../WebsiteHeader';
|
||||||
import { WebsiteMetricsBar } from '../WebsiteMetricsBar';
|
import { WebsiteMetricsBar } from '../WebsiteMetricsBar';
|
||||||
import { FilterTags } from '@/components/metrics/FilterTags';
|
import { FilterBar } from '@/components/metrics/FilterBar';
|
||||||
import { WebsiteChart } from '../WebsiteChart';
|
import { WebsiteChart } from '../WebsiteChart';
|
||||||
import { WebsiteCompareTables } from './WebsiteCompareTables';
|
import { WebsiteCompareTables } from './WebsiteCompareTables';
|
||||||
|
|
||||||
|
|
@ -11,7 +11,7 @@ export function WebsiteComparePage({ websiteId }) {
|
||||||
return (
|
return (
|
||||||
<Grid gap="3">
|
<Grid gap="3">
|
||||||
<WebsiteHeader websiteId={websiteId} />
|
<WebsiteHeader websiteId={websiteId} />
|
||||||
<FilterTags websiteId={websiteId} />
|
<FilterBar websiteId={websiteId} />
|
||||||
<WebsiteMetricsBar websiteId={websiteId} compareMode={true} showFilter={true} />
|
<WebsiteMetricsBar websiteId={websiteId} compareMode={true} showFilter={true} />
|
||||||
<Panel>
|
<Panel>
|
||||||
<WebsiteChart websiteId={websiteId} compareMode={true} />
|
<WebsiteChart websiteId={websiteId} compareMode={true} />
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { Icon, Icons } from '@umami/react-zen';
|
import { Icon, Icons, Text } from '@umami/react-zen';
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
import styles from './ChangeLabel.module.css';
|
import styles from './ChangeLabel.module.css';
|
||||||
|
|
||||||
|
|
@ -38,7 +38,7 @@ export function ChangeLabel({
|
||||||
<Icons.Arrow />
|
<Icons.Arrow />
|
||||||
</Icon>
|
</Icon>
|
||||||
)}
|
)}
|
||||||
{children || value}
|
<Text>{children || value}</Text>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
144
src/components/metrics/FilterBar.tsx
Normal file
144
src/components/metrics/FilterBar.tsx
Normal file
|
|
@ -0,0 +1,144 @@
|
||||||
|
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 { 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 handleCloseFilter = (param: string, e: MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
router.push(renderUrl({ [param]: undefined }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleResetFilter = () => {
|
||||||
|
router.push(renderUrl({ view }, true));
|
||||||
|
};
|
||||||
|
|
||||||
|
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();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Row
|
||||||
|
className="dark-theme"
|
||||||
|
gap="3"
|
||||||
|
backgroundColor="3"
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="space-between"
|
||||||
|
paddingY="3"
|
||||||
|
paddingLeft="5"
|
||||||
|
paddingRight="2"
|
||||||
|
border
|
||||||
|
borderRadius="2"
|
||||||
|
>
|
||||||
|
<Row alignItems="center" gap="3" wrap="wrap">
|
||||||
|
<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]);
|
||||||
|
const paramValue = isSearchOperator(operator) ? value : formatValue(value, key);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MenuTrigger key={key}>
|
||||||
|
<Button variant="outline">
|
||||||
|
<Row alignItems="center" gap="2">
|
||||||
|
<Text weight="bold">{label}</Text>
|
||||||
|
<Text transform="uppercase" color="muted">
|
||||||
|
{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>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<WebsiteFilterButton websiteId={websiteId} alignment="center" showText={false} />
|
||||||
|
</Row>
|
||||||
|
<TooltipTrigger delay={0}>
|
||||||
|
<Button variant="quiet" onPress={handleResetFilter}>
|
||||||
|
<Icon>
|
||||||
|
<Icons.Close />
|
||||||
|
</Icon>
|
||||||
|
</Button>
|
||||||
|
<Tooltip>
|
||||||
|
<Text>{formatMessage(labels.clearAll)}</Text>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipTrigger>
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,60 +0,0 @@
|
||||||
.filters {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
background: var(--base75);
|
|
||||||
padding: 10px 20px;
|
|
||||||
border: 1px solid var(--base400);
|
|
||||||
border-radius: 8px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.label {
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tag {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
font-size: 12px;
|
|
||||||
background: var(--base50);
|
|
||||||
border: 1px solid var(--base400);
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
box-shadow: 1px 1px 1px var(--base500);
|
|
||||||
padding: 6px 14px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tag:hover {
|
|
||||||
background: var(--base100);
|
|
||||||
}
|
|
||||||
|
|
||||||
.close {
|
|
||||||
font-weight: 700;
|
|
||||||
align-self: center;
|
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.name,
|
|
||||||
.value {
|
|
||||||
color: var(--base700);
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.operator {
|
|
||||||
text-transform: lowercase;
|
|
||||||
font-weight: 900;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
margin-left: 10px;
|
|
||||||
padding: 2px;
|
|
||||||
border-radius: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon:hover {
|
|
||||||
background: var(--base200);
|
|
||||||
}
|
|
||||||
|
|
@ -1,123 +0,0 @@
|
||||||
import { MouseEvent } from 'react';
|
|
||||||
import { Button, Icon, Icons, Popover, MenuTrigger, Text, Row } 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 styles from './FilterTags.module.css';
|
|
||||||
import { WebsiteFilterButton } from '@/app/(main)/websites/[websiteId]/WebsiteFilterButton';
|
|
||||||
|
|
||||||
export function FilterTags({ 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 handleCloseFilter = (param: string, e: MouseEvent) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
router.push(renderUrl({ [param]: undefined }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleResetFilter = () => {
|
|
||||||
router.push(renderUrl({ view }, true));
|
|
||||||
};
|
|
||||||
|
|
||||||
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();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Row
|
|
||||||
gap="3"
|
|
||||||
backgroundColor="2"
|
|
||||||
alignItems="center"
|
|
||||||
paddingX="5"
|
|
||||||
paddingY="3"
|
|
||||||
border
|
|
||||||
borderRadius="2"
|
|
||||||
wrap="wrap"
|
|
||||||
>
|
|
||||||
<Text 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]);
|
|
||||||
const paramValue = isSearchOperator(operator) ? value : formatValue(value, key);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<MenuTrigger key={key}>
|
|
||||||
<Button variant="outline">
|
|
||||||
<Row alignItems="center" gap="3">
|
|
||||||
<Text weight="bold">{label}</Text>
|
|
||||||
<Text>{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>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
<WebsiteFilterButton websiteId={websiteId} alignment="center" showText={false} />
|
|
||||||
<Button className={styles.close} variant="quiet" onPress={handleResetFilter}>
|
|
||||||
<Icon>
|
|
||||||
<Icons.Close />
|
|
||||||
</Icon>
|
|
||||||
<Text>{formatMessage(labels.clearAll)}</Text>
|
|
||||||
</Button>
|
|
||||||
</Row>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue