mirror of
https://github.com/umami-software/umami.git
synced 2026-02-06 21:57:16 +01:00
Updated reports components.
This commit is contained in:
parent
f5bc3dc6c2
commit
0f6cdf8b80
95 changed files with 580 additions and 698 deletions
|
|
@ -1,7 +1,9 @@
|
|||
import { Column } from '@umami/react-zen';
|
||||
|
||||
export interface BoardProps {}
|
||||
|
||||
export function Board(props: BoardProps) {
|
||||
return <Column>{}</Column>;
|
||||
export interface BoardProps {
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function Board({ children }: BoardProps) {
|
||||
return <Column>{children}</Column>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ export function Chart({
|
|||
onCreate,
|
||||
onUpdate,
|
||||
onTooltip,
|
||||
className,
|
||||
chartOptions,
|
||||
tooltip,
|
||||
...props
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { Row, Column, Text } from '@umami/react-zen';
|
|||
|
||||
export function ActionForm({ label, description, children }) {
|
||||
return (
|
||||
<Row padding="6" borderSize="1" borderRadius="3" justifyContent="space-between" shadow="2">
|
||||
<Row padding="6" border borderRadius="3" justifyContent="space-between" shadow="2">
|
||||
<Column>
|
||||
<Text weight="bold">{label}</Text>
|
||||
<Text>{description}</Text>
|
||||
|
|
@ -1,21 +1,25 @@
|
|||
import { ReactNode } from 'react';
|
||||
import { Heading, Icon, Row } from '@umami/react-zen';
|
||||
import { Heading, Icon, Row, Text } from '@umami/react-zen';
|
||||
|
||||
export function PageHeader({
|
||||
title,
|
||||
description,
|
||||
icon,
|
||||
children,
|
||||
}: {
|
||||
title?: ReactNode;
|
||||
title: string;
|
||||
description?: string;
|
||||
icon?: ReactNode;
|
||||
allowEdit?: boolean;
|
||||
className?: string;
|
||||
children?: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<Row justifyContent="space-between" alignItems="center">
|
||||
<Row justifyContent="space-between" alignItems="center" marginY="6">
|
||||
<Row gap="3">
|
||||
{icon && <Icon size="lg">{icon}</Icon>}
|
||||
{title && <Heading>{title}</Heading>}
|
||||
{title && <Heading size="2">{title}</Heading>}
|
||||
{description && <Text color="muted">{description}</Text>}
|
||||
</Row>
|
||||
<Row justifyContent="flex-end">{children}</Row>
|
||||
</Row>
|
||||
6
src/components/common/Panel.tsx
Normal file
6
src/components/common/Panel.tsx
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { Box } from '@umami/react-zen';
|
||||
import type { BoxProps } from '@umami/react-zen/Box';
|
||||
|
||||
export function Panel(props: BoxProps) {
|
||||
return <Box padding="6" border borderRadius="3" backgroundColor="solid" shadow="4" {...props} />;
|
||||
}
|
||||
|
|
@ -41,6 +41,7 @@ export * from './useMessages';
|
|||
export * from './useModified';
|
||||
export * from './usePagedQuery';
|
||||
export * from './useRegionNames';
|
||||
export * from './useReport';
|
||||
export * from './useSticky';
|
||||
export * from './useNavigation';
|
||||
export * from './useTheme';
|
||||
|
|
|
|||
6
src/components/hooks/useReport.ts
Normal file
6
src/components/hooks/useReport.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { useContext } from 'react';
|
||||
import { ReportContext } from '@/app/(main)/reports/[reportId]/Report';
|
||||
|
||||
export function useReport() {
|
||||
return useContext(ReportContext);
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ const selector = (state: { theme: string }) => state.theme;
|
|||
|
||||
export function useTheme() {
|
||||
const theme = useApp(selector) || getItem(THEME_CONFIG) || DEFAULT_THEME;
|
||||
const { primary, text, line, fill } = THEME_COLORS[theme];
|
||||
const primaryColor = colord(THEME_COLORS[theme].primary);
|
||||
|
||||
const colors = useMemo(() => {
|
||||
|
|
@ -16,8 +17,8 @@ export function useTheme() {
|
|||
...THEME_COLORS[theme],
|
||||
},
|
||||
chart: {
|
||||
text: THEME_COLORS[theme].gray700,
|
||||
line: THEME_COLORS[theme].gray200,
|
||||
text,
|
||||
line,
|
||||
views: {
|
||||
hoverBackgroundColor: primaryColor.alpha(0.7).toRgbString(),
|
||||
backgroundColor: primaryColor.alpha(0.4).toRgbString(),
|
||||
|
|
@ -32,10 +33,10 @@ export function useTheme() {
|
|||
},
|
||||
},
|
||||
map: {
|
||||
baseColor: THEME_COLORS[theme].primary,
|
||||
fillColor: THEME_COLORS[theme].gray100,
|
||||
strokeColor: THEME_COLORS[theme].primary,
|
||||
hoverColor: THEME_COLORS[theme].primary,
|
||||
baseColor: primary,
|
||||
fillColor: fill,
|
||||
strokeColor: primary,
|
||||
hoverColor: primary,
|
||||
},
|
||||
};
|
||||
}, [theme]);
|
||||
|
|
|
|||
|
|
@ -103,7 +103,9 @@ export function DateFilter({
|
|||
return (
|
||||
<>
|
||||
{divider && <ListSeparator />}
|
||||
<ListItem id={value}>{label}</ListItem>
|
||||
<ListItem key={value} id={value}>
|
||||
{label}
|
||||
</ListItem>
|
||||
</>
|
||||
);
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
import { useDateRange } from '@/components/hooks';
|
||||
import { isAfter } from 'date-fns';
|
||||
import { getOffsetDateRange } from '@/lib/date';
|
||||
import { Button, Icon, Icons } from '@umami/react-zen';
|
||||
import { Button, Icon, Icons, Row } from '@umami/react-zen';
|
||||
import { DateFilter } from './DateFilter';
|
||||
import styles from './WebsiteDateFilter.module.css';
|
||||
import { DateRange } from '@/lib/types';
|
||||
|
||||
export function WebsiteDateFilter({
|
||||
|
|
@ -27,9 +26,22 @@ export function WebsiteDateFilter({
|
|||
};
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<Row gap="3">
|
||||
{value !== 'all' && !value.startsWith('range') && (
|
||||
<Row gap="1">
|
||||
<Button onPress={() => handleIncrement(-1)} variant="quiet">
|
||||
<Icon size="xs" rotate={180}>
|
||||
<Icons.Chevron />
|
||||
</Icon>
|
||||
</Button>
|
||||
<Button onPress={() => handleIncrement(1)} variant="quiet" isDisabled={disableForward}>
|
||||
<Icon size="xs">
|
||||
<Icons.Chevron />
|
||||
</Icon>
|
||||
</Button>
|
||||
</Row>
|
||||
)}
|
||||
<DateFilter
|
||||
className={styles.dropdown}
|
||||
value={value}
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
|
|
@ -37,20 +49,6 @@ export function WebsiteDateFilter({
|
|||
onChange={handleChange}
|
||||
showAllTime={showAllTime}
|
||||
/>
|
||||
{value !== 'all' && !value.startsWith('range') && (
|
||||
<div className={styles.buttons}>
|
||||
<Button onPress={() => handleIncrement(-1)}>
|
||||
<Icon size="sm" rotate={180}>
|
||||
<Icons.Chevron />
|
||||
</Icon>
|
||||
</Button>
|
||||
<Button onPress={() => handleIncrement(1)} isDisabled={disableForward}>
|
||||
<Icon size="sm">
|
||||
<Icons.Chevron />
|
||||
</Icon>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +0,0 @@
|
|||
import { Box } from '@umami/react-zen';
|
||||
import type { BoxProps } from '@umami/react-zen/Box';
|
||||
|
||||
export function Panel(props: BoxProps) {
|
||||
return (
|
||||
<Box
|
||||
padding="6"
|
||||
borderSize="1"
|
||||
borderRadius="3"
|
||||
backgroundColor="solid"
|
||||
shadow="4"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -9,18 +9,12 @@ import {
|
|||
useFilters,
|
||||
} from '@/components/hooks';
|
||||
import { FieldFilterEditForm } from '@/app/(main)/reports/[reportId]/FieldFilterEditForm';
|
||||
import { OPERATOR_PREFIXES } from '@/lib/constants';
|
||||
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,
|
||||
params,
|
||||
}: {
|
||||
websiteId: string;
|
||||
params: { [key: string]: string };
|
||||
}) {
|
||||
export function FilterTags({ websiteId }: { websiteId: string }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { formatValue } = useFormat();
|
||||
const { dateRange } = useDateRange(websiteId);
|
||||
|
|
@ -32,6 +26,14 @@ export function FilterTags({
|
|||
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;
|
||||
|
|
@ -60,13 +62,13 @@ export function FilterTags({
|
|||
return (
|
||||
<Row
|
||||
gap="3"
|
||||
backgroundColor="1"
|
||||
backgroundColor="2"
|
||||
alignItems="center"
|
||||
paddingX="3"
|
||||
paddingY="2"
|
||||
paddingX="5"
|
||||
paddingY="3"
|
||||
border
|
||||
borderRadius="2"
|
||||
borderSize="1"
|
||||
marginBottom="6"
|
||||
wrap="wrap"
|
||||
>
|
||||
<Text weight="bold">{formatMessage(labels.filters)}</Text>
|
||||
{Object.keys(params).map(key => {
|
||||
|
|
|
|||
|
|
@ -1,37 +1,8 @@
|
|||
.card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.card.compare .change {
|
||||
font-size: 16px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.card:first-child {
|
||||
padding-left: 0;
|
||||
border-right: 1px solid var(--border-color);
|
||||
padding: 0 50px;
|
||||
}
|
||||
|
||||
.card:last-child {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: 36px;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
color: var(--base900);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.value.prev {
|
||||
color: var(--base800);
|
||||
}
|
||||
|
||||
.label {
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
color: var(--base800);
|
||||
border-right: 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import classNames from 'classnames';
|
||||
import { Text, Column } from '@umami/react-zen';
|
||||
import { useSpring } from '@react-spring/web';
|
||||
import { formatNumber } from '@/lib/format';
|
||||
import { AnimatedDiv } from '@/components/common/AnimatedDiv';
|
||||
|
|
@ -15,7 +15,6 @@ export interface MetricCardProps {
|
|||
showLabel?: boolean;
|
||||
showChange?: boolean;
|
||||
showPrevious?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const MetricCard = ({
|
||||
|
|
@ -27,7 +26,6 @@ export const MetricCard = ({
|
|||
showLabel = true,
|
||||
showChange = false,
|
||||
showPrevious = false,
|
||||
className,
|
||||
}: MetricCardProps) => {
|
||||
const diff = value - change;
|
||||
const pct = ((value - diff) / diff) * 100;
|
||||
|
|
@ -36,26 +34,19 @@ export const MetricCard = ({
|
|||
const prevProps = useSpring({ x: Number(diff) || 0, from: { x: 0 } });
|
||||
|
||||
return (
|
||||
<div className={classNames(styles.card, className, showPrevious && styles.compare)}>
|
||||
{showLabel && <div className={styles.label}>{label}</div>}
|
||||
<AnimatedDiv className={styles.value} title={value?.toString()}>
|
||||
{props?.x?.to(x => formatValue(x))}
|
||||
</AnimatedDiv>
|
||||
<Column className={styles.card} justifyContent="center">
|
||||
{showLabel && <Text weight="bold">{label}</Text>}
|
||||
<Text size="8" weight="bold" wrap="nowrap">
|
||||
<AnimatedDiv title={value?.toString()}>{props?.x?.to(x => formatValue(x))}</AnimatedDiv>
|
||||
</Text>
|
||||
{showChange && (
|
||||
<ChangeLabel
|
||||
className={styles.change}
|
||||
value={change}
|
||||
title={formatValue(change)}
|
||||
reverseColors={reverseColors}
|
||||
>
|
||||
<ChangeLabel value={change} title={formatValue(change)} reverseColors={reverseColors}>
|
||||
<AnimatedDiv>{changeProps?.x?.to(x => `${Math.abs(~~x)}%`)}</AnimatedDiv>
|
||||
</ChangeLabel>
|
||||
)}
|
||||
{showPrevious && (
|
||||
<AnimatedDiv className={classNames(styles.value, styles.prev)} title={diff.toString()}>
|
||||
{prevProps?.x?.to(x => formatValue(x))}
|
||||
</AnimatedDiv>
|
||||
<AnimatedDiv title={diff.toString()}>{prevProps?.x?.to(x => formatValue(x))}</AnimatedDiv>
|
||||
)}
|
||||
</div>
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,13 +0,0 @@
|
|||
.bar {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(140px, max-content));
|
||||
gap: 20px;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.bar {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,6 @@
|
|||
import { ReactNode } from 'react';
|
||||
import { Loading, Row } from '@umami/react-zen';
|
||||
import { cloneChildren } from '@/lib/react';
|
||||
import { Grid, Loading } from '@umami/react-zen';
|
||||
import { ErrorMessage } from '@/components/common/ErrorMessage';
|
||||
import { formatLongNumber } from '@/lib/format';
|
||||
|
||||
export interface MetricsBarProps {
|
||||
isLoading?: boolean;
|
||||
|
|
@ -12,18 +10,15 @@ export interface MetricsBarProps {
|
|||
}
|
||||
|
||||
export function MetricsBar({ children, isLoading, isFetched, error }: MetricsBarProps) {
|
||||
const formatFunc = n => (n >= 0 ? formatLongNumber(n) : `-${formatLongNumber(Math.abs(n))}`);
|
||||
|
||||
return (
|
||||
<Row>
|
||||
<>
|
||||
{isLoading && !isFetched && <Loading icon="dots" />}
|
||||
{error && <ErrorMessage />}
|
||||
{!isLoading &&
|
||||
!error &&
|
||||
isFetched &&
|
||||
cloneChildren(children, child => {
|
||||
return { format: child?.props['format'] || formatFunc };
|
||||
})}
|
||||
</Row>
|
||||
{!isLoading && !error && isFetched && (
|
||||
<Grid columns="repeat(auto-fill, minmax(200px, 1fr))" width="100%" gapY="3">
|
||||
{children}
|
||||
</Grid>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue