mirror of
https://github.com/umami-software/umami.git
synced 2026-02-04 12:47:13 +01:00
Updated menus, chart tooltips, styles.
This commit is contained in:
parent
0a16ab38e4
commit
92b283486e
23 changed files with 179 additions and 208 deletions
|
|
@ -79,7 +79,7 @@
|
|||
"@prisma/extension-read-replicas": "^0.4.1",
|
||||
"@react-spring/web": "^9.7.3",
|
||||
"@tanstack/react-query": "^5.74.11",
|
||||
"@umami/react-zen": "^0.89.0",
|
||||
"@umami/react-zen": "^0.90.0",
|
||||
"@umami/redis-client": "^0.27.0",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"chalk": "^4.1.1",
|
||||
|
|
|
|||
10
pnpm-lock.yaml
generated
10
pnpm-lock.yaml
generated
|
|
@ -39,8 +39,8 @@ importers:
|
|||
specifier: ^5.74.11
|
||||
version: 5.74.11(react@19.1.0)
|
||||
'@umami/react-zen':
|
||||
specifier: ^0.89.0
|
||||
version: 0.89.0(@babel/core@7.26.10)(@types/react@19.1.2)(immer@9.0.21)(use-sync-external-store@1.5.0(react@19.1.0))
|
||||
specifier: ^0.90.0
|
||||
version: 0.90.0(@babel/core@7.26.10)(@types/react@19.1.2)(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
|
||||
|
|
@ -2992,8 +2992,8 @@ packages:
|
|||
resolution: {integrity: sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==}
|
||||
engines: {node: ^16.0.0 || >=18.0.0}
|
||||
|
||||
'@umami/react-zen@0.89.0':
|
||||
resolution: {integrity: sha512-Lcvgh6Y4DKlUUDE84WowvxvJkgI4INW6lVM32L8+XUJVxBrEBa41RF7jF6KTgD6IizAwHtSouh4gVLzzBDmlCw==}
|
||||
'@umami/react-zen@0.90.0':
|
||||
resolution: {integrity: sha512-Hj0/GSQPUtiRwq1ri3nX+anWp5udNQmrKZcHOH/j1B3z4KL/AW+llYyXqP9loG1N+NgEzW66P791Pac8Wo7qpw==}
|
||||
|
||||
'@umami/redis-client@0.27.0':
|
||||
resolution: {integrity: sha512-SbHTpxhgeZyTBUSp2zdZM+XUtpsaSL4Tad8QXIEhEtjWhvvfoornyT5kLuyYCVtzSAT4daALeGmOO1z6EE1KcA==}
|
||||
|
|
@ -10784,7 +10784,7 @@ snapshots:
|
|||
'@typescript-eslint/types': 6.21.0
|
||||
eslint-visitor-keys: 3.4.3
|
||||
|
||||
'@umami/react-zen@0.89.0(@babel/core@7.26.10)(@types/react@19.1.2)(immer@9.0.21)(use-sync-external-store@1.5.0(react@19.1.0))':
|
||||
'@umami/react-zen@0.90.0(@babel/core@7.26.10)(@types/react@19.1.2)(immer@9.0.21)(use-sync-external-store@1.5.0(react@19.1.0))':
|
||||
dependencies:
|
||||
'@fontsource/jetbrains-mono': 5.2.5
|
||||
'@internationalized/date': 3.8.0
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { TeamsButton } from '@/components/input/TeamsButton';
|
|||
import type { RowProps } from '@umami/react-zen/Row';
|
||||
import useGlobalState from '@/components/hooks/useGlobalState';
|
||||
import { Lucide } from '@/components/icons';
|
||||
import { WebsiteSelect } from '@/components/input/WebsiteSelect';
|
||||
|
||||
export function MenuBar(props: RowProps) {
|
||||
const [isCollapsed, setCollapsed] = useGlobalState('sidenav-collapsed');
|
||||
|
|
@ -20,15 +21,19 @@ export function MenuBar(props: RowProps) {
|
|||
backgroundColor="2"
|
||||
border="bottom"
|
||||
>
|
||||
<Row>
|
||||
<Row alignItems="center">
|
||||
<Button onPress={() => setCollapsed(!isCollapsed)} variant="quiet">
|
||||
<Icon>
|
||||
<Lucide.PanelLeft />
|
||||
</Icon>
|
||||
</Button>
|
||||
<TeamsButton />
|
||||
<Icon>
|
||||
<Lucide.Slash />
|
||||
</Icon>
|
||||
<WebsiteSelect />
|
||||
</Row>
|
||||
<Row justifyContent="flex-end">
|
||||
<Row alignItems="center" justifyContent="flex-end">
|
||||
<ThemeButton />
|
||||
<LanguageButton />
|
||||
<ProfileButton />
|
||||
|
|
|
|||
|
|
@ -2,15 +2,15 @@
|
|||
import { ProfileSettings } from './ProfileSettings';
|
||||
import { useMessages } from '@/components/hooks';
|
||||
import { SectionHeader } from '@/components/common/SectionHeader';
|
||||
import { Column } from '@umami/react-zen';
|
||||
|
||||
export function ProfilePage() {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Column gap>
|
||||
<SectionHeader title={formatMessage(labels.profile)} />
|
||||
|
||||
<ProfileSettings />
|
||||
</>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,12 +28,11 @@ export function TeamSettingsLayout({ children }: { children: ReactNode }) {
|
|||
},
|
||||
].filter(n => n);
|
||||
|
||||
const value = items.find(({ url }) => pathname.endsWith(url))?.id;
|
||||
const value = items.find(({ url }) => pathname.includes(url))?.id;
|
||||
|
||||
return (
|
||||
<Column gap="6">
|
||||
<PageHeader title={formatMessage(labels.teamSettings)} />
|
||||
|
||||
<Column gap="6">
|
||||
<Grid columns="200px 1fr">
|
||||
<Column marginTop="6">
|
||||
|
|
|
|||
|
|
@ -1,12 +0,0 @@
|
|||
.footer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
font-size: var(--font-size-sm);
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.footer a {
|
||||
color: var(--font-color100);
|
||||
}
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
import { Row, Text } from '@umami/react-zen';
|
||||
import { CURRENT_VERSION, HOMEPAGE_URL } from '@/lib/constants';
|
||||
import styles from './Footer.module.css';
|
||||
|
||||
export function Footer() {
|
||||
return (
|
||||
<footer className={styles.footer}>
|
||||
<a href={HOMEPAGE_URL}>
|
||||
<b>umami</b> {`v${CURRENT_VERSION}`}
|
||||
<Row as="footer">
|
||||
<a href={HOMEPAGE_URL} target="_blank">
|
||||
<Text weight="bold">umami</Text> {`v${CURRENT_VERSION}`}
|
||||
</a>
|
||||
</footer>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,31 +0,0 @@
|
|||
.header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: 700;
|
||||
color: var(--font-color100) !important;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 992px) {
|
||||
.header .buttons {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,27 +1,25 @@
|
|||
import { ThemeButton } from '@umami/react-zen';
|
||||
import { Icon, Text } from '@umami/react-zen';
|
||||
import { Row, Icon, Text, ThemeButton } from '@umami/react-zen';
|
||||
import Link from 'next/link';
|
||||
import { LanguageButton } from '@/components/input/LanguageButton';
|
||||
import { SettingsButton } from '@/components/input/SettingsButton';
|
||||
import { Icons } from '@/components/icons';
|
||||
import styles from './Header.module.css';
|
||||
|
||||
export function Header() {
|
||||
return (
|
||||
<header className={styles.header}>
|
||||
<div>
|
||||
<Link href="https://umami.is" target="_blank" className={styles.title}>
|
||||
<Row as="header">
|
||||
<Row gap>
|
||||
<Link href="https://umami.is" target="_blank">
|
||||
<Icon size="lg">
|
||||
<Icons.Logo />
|
||||
</Icon>
|
||||
<Text>umami</Text>
|
||||
</Link>
|
||||
</div>
|
||||
<div className={styles.buttons}>
|
||||
</Row>
|
||||
<Row alignItems="center" gap>
|
||||
<ThemeButton />
|
||||
<LanguageButton />
|
||||
<SettingsButton />
|
||||
</div>
|
||||
</header>
|
||||
</Row>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +0,0 @@
|
|||
.container {
|
||||
flex: 1;
|
||||
min-height: calc(100vh - 200px);
|
||||
min-height: calc(100dvh - 200px);
|
||||
}
|
||||
|
|
@ -1,11 +1,10 @@
|
|||
'use client';
|
||||
import { WebsiteDetailsPage } from '../../(main)/websites/[websiteId]/WebsiteDetailsPage';
|
||||
import { WebsiteProvider } from '@/app/(main)/websites/[websiteId]/WebsiteProvider';
|
||||
import { WebsiteDetailsPage } from '@/app/(main)/websites/[websiteId]/WebsiteDetailsPage';
|
||||
import { useShareTokenQuery } from '@/components/hooks';
|
||||
import { Page } from '@/components/common/Page';
|
||||
import { Header } from './Header';
|
||||
import { Footer } from './Footer';
|
||||
import styles from './SharePage.module.css';
|
||||
import { WebsiteProvider } from '@/app/(main)/websites/[websiteId]/WebsiteProvider';
|
||||
|
||||
export function SharePage({ shareId }) {
|
||||
const { shareToken, isLoading } = useShareTokenQuery(shareId);
|
||||
|
|
@ -15,14 +14,12 @@ export function SharePage({ shareId }) {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<Page>
|
||||
<Header />
|
||||
<WebsiteProvider websiteId={shareToken.websiteId}>
|
||||
<WebsiteDetailsPage websiteId={shareToken.websiteId} />
|
||||
</WebsiteProvider>
|
||||
<Footer />
|
||||
</Page>
|
||||
</div>
|
||||
<Page>
|
||||
<Header />
|
||||
<WebsiteProvider websiteId={shareToken.websiteId}>
|
||||
<WebsiteDetailsPage websiteId={shareToken.websiteId} />
|
||||
</WebsiteProvider>
|
||||
<Footer />
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
1
src/assets/security.svg
Normal file
1
src/assets/security.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg id="Layer_1" height="512" viewBox="0 0 36 36" width="512" xmlns="http://www.w3.org/2000/svg" data-name="Layer 1"><path d="m18 34a1.07 1.07 0 0 1 -.48-.11l-4.87-2.43a13.79 13.79 0 0 1 -7.65-12.41v-12.14a1.07 1.07 0 0 1 1.05-1.07h3.47a7.45 7.45 0 0 0 4-1.19l3.87-2.48a1.07 1.07 0 0 1 1.15 0l3.87 2.48a7.45 7.45 0 0 0 4 1.19h3.47a1.07 1.07 0 0 1 1.12 1.07v12.14a13.79 13.79 0 0 1 -7.67 12.4l-4.87 2.43a1.07 1.07 0 0 1 -.46.12zm-10.88-26v11.05a11.67 11.67 0 0 0 6.49 10.49l4.39 2.2 4.39-2.2a11.67 11.67 0 0 0 6.49-10.49v-11.05h-2.4a9.57 9.57 0 0 1 -5.19-1.53l-3.29-2.14-3.29 2.12a9.57 9.57 0 0 1 -5.19 1.55z"/><path d="m18 18.8a4.8 4.8 0 1 1 4.8-4.8 4.81 4.81 0 0 1 -4.8 4.8zm0-7.47a2.67 2.67 0 1 0 2.67 2.67 2.67 2.67 0 0 0 -2.67-2.66z"/><path d="m24.4 24.67h-2.13a2.14 2.14 0 0 0 -2.13-2.13h-4.28a2.13 2.13 0 0 0 -2.13 2.13h-2.13a4.26 4.26 0 0 1 4.26-4.26h4.27a4.27 4.27 0 0 1 4.27 4.26z"/></svg>
|
||||
|
After Width: | Height: | Size: 899 B |
|
|
@ -1,9 +1,24 @@
|
|||
import { useMemo, useState } from 'react';
|
||||
import { useTheme } from '@umami/react-zen';
|
||||
import { BarChartTooltip } from '@/components/charts/BarChartTooltip';
|
||||
import { ChartTooltip } from '@/components/charts/ChartTooltip';
|
||||
import { Chart, ChartProps } from '@/components/charts/Chart';
|
||||
import { useLocale } from '@/components/hooks';
|
||||
import { renderNumberLabels } from '@/lib/charts';
|
||||
import { getThemeColors } from '@/lib/colors';
|
||||
import { formatDate } from '@/lib/date';
|
||||
import { formatLongCurrency, formatLongNumber } from '@/lib/format';
|
||||
|
||||
const dateFormats = {
|
||||
millisecond: 'T',
|
||||
second: 'pp',
|
||||
minute: 'p',
|
||||
hour: 'p - PP',
|
||||
day: 'PPPP',
|
||||
week: 'PPPP',
|
||||
month: 'LLLL yyyy',
|
||||
quarter: 'qqq',
|
||||
year: 'yyyy',
|
||||
};
|
||||
|
||||
export interface BarChartProps extends ChartProps {
|
||||
unit: string;
|
||||
|
|
@ -18,22 +33,23 @@ export interface BarChartProps extends ChartProps {
|
|||
isAllTime?: boolean;
|
||||
}
|
||||
|
||||
export function BarChart(props: BarChartProps) {
|
||||
export function BarChart({
|
||||
renderXLabel,
|
||||
renderYLabel,
|
||||
unit,
|
||||
XAxisType = 'time',
|
||||
YAxisType = 'linear',
|
||||
stacked = false,
|
||||
minDate,
|
||||
maxDate,
|
||||
currency,
|
||||
isAllTime,
|
||||
...props
|
||||
}: BarChartProps) {
|
||||
const [tooltip, setTooltip] = useState(null);
|
||||
const { theme } = useTheme();
|
||||
const { locale } = useLocale();
|
||||
const { colors } = getThemeColors(theme);
|
||||
const {
|
||||
renderXLabel,
|
||||
renderYLabel,
|
||||
unit,
|
||||
XAxisType = 'time',
|
||||
YAxisType = 'linear',
|
||||
stacked = false,
|
||||
minDate,
|
||||
maxDate,
|
||||
currency,
|
||||
isAllTime,
|
||||
} = props;
|
||||
|
||||
const options: any = useMemo(() => {
|
||||
return {
|
||||
|
|
@ -80,9 +96,23 @@ export function BarChart(props: BarChartProps) {
|
|||
}, [colors, unit, stacked, renderXLabel, renderYLabel]);
|
||||
|
||||
const handleTooltip = ({ tooltip }: { tooltip: any }) => {
|
||||
const { opacity } = tooltip;
|
||||
const { opacity, labelColors, dataPoints } = tooltip;
|
||||
|
||||
setTooltip(opacity ? tooltip : null);
|
||||
if (opacity) {
|
||||
setTooltip({
|
||||
title: formatDate(
|
||||
new Date(dataPoints[0].raw?.d || dataPoints[0].raw?.x || dataPoints[0].raw),
|
||||
dateFormats[unit],
|
||||
locale,
|
||||
),
|
||||
color: labelColors?.[0]?.backgroundColor,
|
||||
value: currency
|
||||
? formatLongCurrency(dataPoints[0].raw.y, currency)
|
||||
: `${formatLongNumber(dataPoints[0].raw.y)} ${dataPoints[0].dataset.label}`,
|
||||
});
|
||||
} else {
|
||||
setTooltip(null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -92,9 +122,9 @@ export function BarChart(props: BarChartProps) {
|
|||
type="bar"
|
||||
chartOptions={options}
|
||||
onTooltip={handleTooltip}
|
||||
style={{ height: 400 }}
|
||||
height="400px"
|
||||
/>
|
||||
{tooltip && <BarChartTooltip tooltip={tooltip} unit={unit} currency={currency} />}
|
||||
{tooltip && <ChartTooltip {...tooltip} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,38 +0,0 @@
|
|||
import { useLocale } from '@/components/hooks';
|
||||
import { formatDate } from '@/lib/date';
|
||||
import { formatLongCurrency, formatLongNumber } from '@/lib/format';
|
||||
import { Column, Row, StatusLight, FloatingTooltip } from '@umami/react-zen';
|
||||
|
||||
const formats = {
|
||||
millisecond: 'T',
|
||||
second: 'pp',
|
||||
minute: 'p',
|
||||
hour: 'p - PP',
|
||||
day: 'PPPP',
|
||||
week: 'PPPP',
|
||||
month: 'LLLL yyyy',
|
||||
quarter: 'qqq',
|
||||
year: 'yyyy',
|
||||
};
|
||||
|
||||
export function BarChartTooltip({ tooltip, unit, currency }) {
|
||||
const { locale } = useLocale();
|
||||
const { labelColors, dataPoints } = tooltip;
|
||||
|
||||
return (
|
||||
<FloatingTooltip>
|
||||
<Column gap="3" fontSize="1">
|
||||
<Row alignItems="center">
|
||||
{formatDate(new Date(dataPoints[0].raw.d || dataPoints[0].raw.x), formats[unit], locale)}
|
||||
</Row>
|
||||
<Row alignItems="center">
|
||||
<StatusLight color={labelColors?.[0]?.backgroundColor}>
|
||||
{currency
|
||||
? formatLongCurrency(dataPoints[0].raw.y, currency)
|
||||
: `${formatLongNumber(dataPoints[0].raw.y)} ${dataPoints[0].dataset.label}`}
|
||||
</StatusLight>
|
||||
</Row>
|
||||
</Column>
|
||||
</FloatingTooltip>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,27 +1,31 @@
|
|||
import { Chart, ChartProps } from '@/components/charts/Chart';
|
||||
import { useState } from 'react';
|
||||
import { StatusLight } from '@umami/react-zen';
|
||||
import { formatLongNumber } from '@/lib/format';
|
||||
import { ChartTooltip } from '@/components/charts/ChartTooltip';
|
||||
|
||||
export interface BubbleChartProps extends ChartProps {
|
||||
type?: 'bubble';
|
||||
}
|
||||
|
||||
export function BubbleChart(props: BubbleChartProps) {
|
||||
export function BubbleChart({ type = 'bubble', ...props }: BubbleChartProps) {
|
||||
const [tooltip, setTooltip] = useState(null);
|
||||
const { type = 'bubble' } = props;
|
||||
|
||||
const handleTooltip = ({ tooltip }) => {
|
||||
const { labelColors, dataPoints } = tooltip;
|
||||
const { opacity, labelColors, title, dataPoints } = tooltip;
|
||||
|
||||
setTooltip(
|
||||
tooltip.opacity ? (
|
||||
<StatusLight color={labelColors?.[0]?.backgroundColor}>
|
||||
{formatLongNumber(dataPoints?.[0]?.raw)} {dataPoints?.[0]?.label}
|
||||
</StatusLight>
|
||||
) : null,
|
||||
opacity
|
||||
? {
|
||||
color: labelColors?.[0]?.backgroundColor,
|
||||
value: `${title}: ${dataPoints[0].raw}`,
|
||||
}
|
||||
: null,
|
||||
);
|
||||
};
|
||||
|
||||
return <Chart {...props} type={type} tooltip={tooltip} onTooltip={handleTooltip} />;
|
||||
return (
|
||||
<>
|
||||
<Chart {...props} type={type} onTooltip={handleTooltip} />
|
||||
{tooltip && <ChartTooltip {...tooltip} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import { useState, useRef, useEffect, useMemo, HTMLAttributes } from 'react';
|
||||
import { Loading } from '@umami/react-zen';
|
||||
import { useState, useRef, useEffect, useMemo } from 'react';
|
||||
import { Loading, Box, Column } from '@umami/react-zen';
|
||||
import ChartJS, { LegendItem, ChartOptions } from 'chart.js/auto';
|
||||
import { Legend } from '@/components/metrics/Legend';
|
||||
import { DEFAULT_ANIMATION_DURATION } from '@/lib/constants';
|
||||
import type { BoxProps } from '@umami/react-zen/Box';
|
||||
|
||||
export interface ChartProps extends HTMLAttributes<HTMLDivElement> {
|
||||
export interface ChartProps extends BoxProps {
|
||||
type?: 'bar' | 'bubble' | 'doughnut' | 'pie' | 'line' | 'polarArea' | 'radar' | 'scatter';
|
||||
data?: object;
|
||||
isLoading?: boolean;
|
||||
|
|
@ -138,12 +139,12 @@ export function Chart({
|
|||
}, [data, options]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div {...props}>
|
||||
<Column gap="6">
|
||||
<Box {...props}>
|
||||
{isLoading && <Loading position="page" icon="dots" />}
|
||||
<canvas ref={canvas} />
|
||||
</div>
|
||||
</Box>
|
||||
<Legend items={legendItems} onClick={handleLegendClick} />
|
||||
</>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
23
src/components/charts/ChartTooltip.tsx
Normal file
23
src/components/charts/ChartTooltip.tsx
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import { ReactNode } from 'react';
|
||||
import { Column, Row, StatusLight, FloatingTooltip } from '@umami/react-zen';
|
||||
|
||||
export function ChartTooltip({
|
||||
title,
|
||||
color,
|
||||
value,
|
||||
}: {
|
||||
title?: string;
|
||||
color?: string;
|
||||
value?: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<FloatingTooltip>
|
||||
<Column gap="3" fontSize="1">
|
||||
{title && <Row alignItems="center">{title}</Row>}
|
||||
<Row alignItems="center">
|
||||
<StatusLight color={color}>{value}</StatusLight>
|
||||
</Row>
|
||||
</Column>
|
||||
</FloatingTooltip>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,27 +1,31 @@
|
|||
import { Chart, ChartProps } from '@/components/charts/Chart';
|
||||
import { useState } from 'react';
|
||||
import { StatusLight } from '@umami/react-zen';
|
||||
import { formatLongNumber } from '@/lib/format';
|
||||
import { ChartTooltip } from '@/components/charts/ChartTooltip';
|
||||
|
||||
export interface PieChartProps extends ChartProps {
|
||||
type?: 'doughnut' | 'pie';
|
||||
}
|
||||
|
||||
export function PieChart(props: PieChartProps) {
|
||||
export function PieChart({ type = 'pie', ...props }: PieChartProps) {
|
||||
const [tooltip, setTooltip] = useState(null);
|
||||
const { type = 'pie' } = props;
|
||||
|
||||
const handleTooltip = ({ tooltip }) => {
|
||||
const { labelColors, dataPoints } = tooltip;
|
||||
const { opacity, labelColors, title, dataPoints } = tooltip;
|
||||
|
||||
setTooltip(
|
||||
tooltip.opacity ? (
|
||||
<StatusLight color={labelColors?.[0]?.backgroundColor}>
|
||||
{formatLongNumber(dataPoints?.[0]?.raw)} {dataPoints?.[0]?.label}
|
||||
</StatusLight>
|
||||
) : null,
|
||||
opacity
|
||||
? {
|
||||
color: labelColors?.[0]?.backgroundColor,
|
||||
value: `${title}: ${dataPoints[0].raw}`,
|
||||
}
|
||||
: null,
|
||||
);
|
||||
};
|
||||
|
||||
return <Chart {...props} type={type} tooltip={tooltip} onTooltip={handleTooltip} />;
|
||||
return (
|
||||
<>
|
||||
<Chart {...props} type={type} onTooltip={handleTooltip} />
|
||||
{tooltip && <ChartTooltip {...tooltip} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import { Text, List, ListItem } from '@umami/react-zen';
|
||||
|
||||
export interface MenuNavProps {
|
||||
export interface SideMenuProps {
|
||||
items: { id: string; label: string; url: string }[];
|
||||
selectedKey?: string;
|
||||
}
|
||||
|
||||
export function SideMenu({ items, selectedKey }: MenuNavProps) {
|
||||
export function SideMenu({ items, selectedKey }: SideMenuProps) {
|
||||
return (
|
||||
<List>
|
||||
{items.map(({ id, label, url }) => {
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ export function DateFilter({
|
|||
value,
|
||||
onChange,
|
||||
showAllTime = false,
|
||||
...props
|
||||
}: DateFilterProps) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const [showPicker, setShowPicker] = useState(false);
|
||||
|
|
@ -102,6 +103,7 @@ export function DateFilter({
|
|||
return (
|
||||
<>
|
||||
<Select
|
||||
{...props}
|
||||
selectedKey={value}
|
||||
placeholder={formatMessage(labels.selectDate)}
|
||||
onSelectionChange={handleChange}
|
||||
|
|
|
|||
|
|
@ -1,21 +0,0 @@
|
|||
.legend {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: var(--font-size-sm);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.label + .label {
|
||||
margin-inline-start: 20px;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
color: var(--base400);
|
||||
}
|
||||
|
|
@ -1,8 +1,6 @@
|
|||
import { StatusLight } from '@umami/react-zen';
|
||||
import { Row, StatusLight, Text } from '@umami/react-zen';
|
||||
import { colord } from 'colord';
|
||||
import classNames from 'classnames';
|
||||
import { LegendItem } from 'chart.js/auto';
|
||||
import styles from './Legend.module.css';
|
||||
|
||||
export function Legend({
|
||||
items = [],
|
||||
|
|
@ -16,21 +14,21 @@ export function Legend({
|
|||
}
|
||||
|
||||
return (
|
||||
<div className={styles.legend}>
|
||||
<Row gap wrap="wrap" justifyContent="center">
|
||||
{items.map(item => {
|
||||
const { text, fillStyle, hidden } = item;
|
||||
const color = colord(fillStyle);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={text}
|
||||
className={classNames(styles.label, { [styles.hidden]: hidden })}
|
||||
onClick={() => onClick(item)}
|
||||
>
|
||||
<StatusLight color={color.alpha(color.alpha() + 0.2).toHex()}>{text}</StatusLight>
|
||||
</div>
|
||||
<Row key={text} onClick={() => onClick(item)}>
|
||||
<StatusLight color={color.alpha(color.alpha() + 0.2).toHex()}>
|
||||
<Text size="1" color={hidden ? 'disabled' : undefined} wrap="nowrap">
|
||||
{text}
|
||||
</Text>
|
||||
</StatusLight>
|
||||
</Row>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
16
src/components/svg/Security.tsx
Normal file
16
src/components/svg/Security.tsx
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import * as React from 'react';
|
||||
import type { SVGProps } from 'react';
|
||||
const SvgSecurity = (props: SVGProps<SVGSVGElement>) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={512}
|
||||
height={512}
|
||||
data-name="Layer 1"
|
||||
viewBox="0 0 36 36"
|
||||
{...props}
|
||||
>
|
||||
<path d="M18 34a1.1 1.1 0 0 1-.48-.11l-4.87-2.43A13.79 13.79 0 0 1 5 19.05V6.91a1.07 1.07 0 0 1 1.05-1.07h3.47a7.45 7.45 0 0 0 4-1.19l3.87-2.48a1.07 1.07 0 0 1 1.15 0l3.87 2.48a7.45 7.45 0 0 0 4 1.19h3.47A1.07 1.07 0 0 1 31 6.91v12.14a13.79 13.79 0 0 1-7.67 12.4l-4.87 2.43A1.1 1.1 0 0 1 18 34M7.12 8v11.05a11.67 11.67 0 0 0 6.49 10.49l4.39 2.2 4.39-2.2a11.67 11.67 0 0 0 6.49-10.49V8h-2.4a9.57 9.57 0 0 1-5.19-1.53L18 4.33l-3.29 2.12A9.57 9.57 0 0 1 9.52 8z" />
|
||||
<path d="M18 18.8a4.8 4.8 0 1 1 4.8-4.8 4.81 4.81 0 0 1-4.8 4.8m0-7.47A2.67 2.67 0 1 0 20.67 14 2.67 2.67 0 0 0 18 11.34zM24.4 24.67h-2.13a2.14 2.14 0 0 0-2.13-2.13h-4.28a2.13 2.13 0 0 0-2.13 2.13H11.6a4.26 4.26 0 0 1 4.26-4.26h4.27a4.27 4.27 0 0 1 4.27 4.26" />
|
||||
</svg>
|
||||
);
|
||||
export default SvgSecurity;
|
||||
Loading…
Add table
Add a link
Reference in a new issue