mirror of
https://github.com/umami-software/umami.git
synced 2025-12-06 01:18:00 +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": [
|
||||
"stylelint-config-recommended",
|
||||
"stylelint-config-css-modules",
|
||||
"stylelint-config-prettier"
|
||||
],
|
||||
"extends": ["stylelint-config-recommended", "stylelint-config-css-modules"],
|
||||
"rules": {
|
||||
"no-descending-specificity": null
|
||||
},
|
||||
"ignoreFiles": ["**/*.js", "**/*.md"]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@
|
|||
"@react-spring/web": "^9.7.5",
|
||||
"@tanstack/react-query": "^5.68.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",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"chalk": "^4.1.2",
|
||||
|
|
@ -178,7 +178,6 @@
|
|||
"rollup-plugin-postcss": "^4.0.2",
|
||||
"stylelint": "^15.11.0",
|
||||
"stylelint-config-css-modules": "^4.4.0",
|
||||
"stylelint-config-prettier": "^9.0.5",
|
||||
"stylelint-config-recommended": "^14.0.1",
|
||||
"tar": "^6.2.1",
|
||||
"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"
|
||||
alignItems="center"
|
||||
overflow="auto"
|
||||
backgroundColor="1"
|
||||
backgroundColor="0"
|
||||
>
|
||||
<Page>
|
||||
{children}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ export function MenuBar(props: RowProps) {
|
|||
paddingY="3"
|
||||
paddingX="3"
|
||||
paddingRight="5"
|
||||
backgroundColor="1"
|
||||
backgroundColor="0"
|
||||
style={{ borderBottom: '1px solid var(--border-color)' }}
|
||||
>
|
||||
<Row>
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import { WebsiteTabs } from '@/app/(main)/websites/[websiteId]/WebsiteTabs';
|
|||
import { useWebsite } from '@/components/hooks/useWebsite';
|
||||
import { WebsiteFilterButton } from '@/app/(main)/websites/[websiteId]/WebsiteFilterButton';
|
||||
import { WebsiteDateFilter } from '@/components/input/WebsiteDateFilter';
|
||||
import { FilterTags } from '@/components/metrics/FilterTags';
|
||||
import { FilterBar } from '@/components/metrics/FilterBar';
|
||||
import { useMessages } from '@/components/hooks';
|
||||
|
||||
export function WebsiteHeader({
|
||||
|
|
@ -73,7 +73,7 @@ export function WebsiteHeader({
|
|||
</Row>
|
||||
</Row>
|
||||
{compareMode && items.map(item => <div key={item.value}>{item.label}</div>)}
|
||||
<FilterTags websiteId={websiteId} />
|
||||
<FilterBar websiteId={websiteId} />
|
||||
<WebsiteTabs websiteId={websiteId} />
|
||||
</Column>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { Grid } from '@umami/react-zen';
|
|||
import { Panel } from '@/components/common/Panel';
|
||||
import { WebsiteHeader } from '../WebsiteHeader';
|
||||
import { WebsiteMetricsBar } from '../WebsiteMetricsBar';
|
||||
import { FilterTags } from '@/components/metrics/FilterTags';
|
||||
import { FilterBar } from '@/components/metrics/FilterBar';
|
||||
import { WebsiteChart } from '../WebsiteChart';
|
||||
import { WebsiteCompareTables } from './WebsiteCompareTables';
|
||||
|
||||
|
|
@ -11,7 +11,7 @@ export function WebsiteComparePage({ websiteId }) {
|
|||
return (
|
||||
<Grid gap="3">
|
||||
<WebsiteHeader websiteId={websiteId} />
|
||||
<FilterTags websiteId={websiteId} />
|
||||
<FilterBar websiteId={websiteId} />
|
||||
<WebsiteMetricsBar websiteId={websiteId} compareMode={true} showFilter={true} />
|
||||
<Panel>
|
||||
<WebsiteChart websiteId={websiteId} compareMode={true} />
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import classNames from 'classnames';
|
||||
import { Icon, Icons } from '@umami/react-zen';
|
||||
import { Icon, Icons, Text } from '@umami/react-zen';
|
||||
import { ReactNode } from 'react';
|
||||
import styles from './ChangeLabel.module.css';
|
||||
|
||||
|
|
@ -38,7 +38,7 @@ export function ChangeLabel({
|
|||
<Icons.Arrow />
|
||||
</Icon>
|
||||
)}
|
||||
{children || value}
|
||||
<Text>{children || value}</Text>
|
||||
</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