Export metrics components.
Some checks failed
Node.js CI / build (postgresql, 18.18) (push) Has been cancelled

This commit is contained in:
Mike Cao 2025-09-03 17:16:03 -07:00
parent c4114f4349
commit dc1736458b
16 changed files with 140 additions and 126 deletions

View file

@ -163,12 +163,6 @@ if (trackerScriptName) {
}
if (cloudMode && cloudUrl) {
redirects.push({
source: '/settings/:path*',
destination: `${cloudUrl}/settings/:path*`,
permanent: false,
});
redirects.push({
source: '/login',
destination: cloudUrl,

View file

@ -1,6 +1,6 @@
{
"name": "@umami/components",
"version": "0.111.0",
"version": "0.115.0",
"description": "Umami React components.",
"author": "Mike Cao <mike@mikecao.com>",
"license": "MIT",

View file

@ -82,7 +82,7 @@
"@react-spring/web": "^10.0.1",
"@svgr/cli": "^8.1.0",
"@tanstack/react-query": "^5.85.5",
"@umami/react-zen": "^0.175.0",
"@umami/react-zen": "^0.176.0",
"@umami/redis-client": "^0.29.0",
"bcryptjs": "^3.0.2",
"chalk": "^5.6.0",

10
pnpm-lock.yaml generated
View file

@ -45,8 +45,8 @@ importers:
specifier: ^5.85.5
version: 5.85.5(react@19.1.1)
'@umami/react-zen':
specifier: ^0.175.0
version: 0.175.0(@babel/core@7.28.3)(@types/react@19.1.12)(babel-plugin-react-compiler@19.1.0-rc.2)(immer@10.1.1)(use-sync-external-store@1.5.0(react@19.1.1))
specifier: ^0.176.0
version: 0.176.0(@babel/core@7.28.3)(@types/react@19.1.12)(babel-plugin-react-compiler@19.1.0-rc.2)(immer@10.1.1)(use-sync-external-store@1.5.0(react@19.1.1))
'@umami/redis-client':
specifier: ^0.29.0
version: 0.29.0
@ -2735,8 +2735,8 @@ packages:
'@prisma/client': ^6.1.0
'@prisma/extension-read-replicas': ^0.4.1
'@umami/react-zen@0.175.0':
resolution: {integrity: sha512-iOUCZwmr09RnqIm01wnjcSTTJ5iJdXRmFlmja2Qf42di/SOOTSBJmu1fKiYESp4dLrXXcmMeDfDqtJli0PPSRw==}
'@umami/react-zen@0.176.0':
resolution: {integrity: sha512-GP+Df68w0Kfo09ZC5WgK1YCg/IbqOz7HWPw8PLYG3shYm+feuF8ND8DHjoTle4qXY4oRJFufXCEsDbTb8FITkg==}
'@umami/redis-client@0.29.0':
resolution: {integrity: sha512-Jaqh++jskqDB7ny75pfC02OvKp1JTS4asGDsFrRL3qy8sxL3PAl9+/mybCJe4/6vWrXDJKqpgkSfUDJq2bFjyw==}
@ -10333,7 +10333,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@umami/react-zen@0.175.0(@babel/core@7.28.3)(@types/react@19.1.12)(babel-plugin-react-compiler@19.1.0-rc.2)(immer@10.1.1)(use-sync-external-store@1.5.0(react@19.1.1))':
'@umami/react-zen@0.176.0(@babel/core@7.28.3)(@types/react@19.1.12)(babel-plugin-react-compiler@19.1.0-rc.2)(immer@10.1.1)(use-sync-external-store@1.5.0(react@19.1.1))':
dependencies:
'@fontsource/jetbrains-mono': 5.2.6
'@internationalized/date': 3.9.0

View file

@ -22,10 +22,11 @@ import { TeamsButton } from '@/components/input/TeamsButton';
import { PanelButton } from '@/components/input/PanelButton';
import { ProfileButton } from '@/components/input/ProfileButton';
import { LanguageButton } from '@/components/input/LanguageButton';
import { Key } from 'react';
export function SideNav(props: SidebarProps) {
const { formatMessage, labels } = useMessages();
const { pathname, renderUrl, websiteId } = useNavigation();
const { pathname, renderUrl, websiteId, router } = useNavigation();
const [isCollapsed, setIsCollapsed] = useGlobalState('sidenav-collapsed');
const hasNav = !!(websiteId || pathname.startsWith('/admin') || pathname.includes('/settings'));
@ -66,21 +67,29 @@ export function SideNav(props: SidebarProps) {
},
];
const handleSelect = (id: Key) => {
console.log({ id });
router.push(id === 'user' ? '/websites' : `/teams/${id}/websites`);
};
return (
<Row height="100%" backgroundColor border="right">
<Sidebar {...props} isCollapsed={isCollapsed || hasNav} muteItems={false} showBorder={false}>
<SidebarSection onClick={() => setIsCollapsed(false)}>
<SidebarHeader
label="umami"
icon={isCollapsed && !hasNav ? <PanelLeft /> : <Logo />}
icon={
isCollapsed && !hasNav ? (
<PanelLeft />
) : (
<Logo onClick={() => (window.location.href = process.env.cloudUrl)} />
)
}
style={{ maxHeight: 40 }}
>
{!isCollapsed && !hasNav && <PanelButton />}
</SidebarHeader>
</SidebarSection>
<SidebarSection style={{ paddingTop: 0, paddingBottom: 0 }}>
<TeamsButton showText={!hasNav && !isCollapsed} />
</SidebarSection>
<SidebarSection flexGrow={1}>
{links.map(({ id, path, label, icon }) => {
return (
@ -95,6 +104,9 @@ export function SideNav(props: SidebarProps) {
);
})}
</SidebarSection>
<SidebarSection style={{ paddingTop: 0, paddingBottom: 0 }}>
<TeamsButton showText={!hasNav && !isCollapsed} onAction={handleSelect} />
</SidebarSection>
<SidebarSection>
{bottomLinks.map(({ id, path, label, icon }) => {
return (
@ -108,7 +120,7 @@ export function SideNav(props: SidebarProps) {
</Link>
);
})}
<Row alignItems="center" justifyContent="space-between" height="40px">
<Row alignItems="center" height="40px">
<ProfileButton />
{!isCollapsed && !hasNav && (
<Row>

View file

@ -53,6 +53,7 @@ export function SettingsLayout({ children }: { children: ReactNode }) {
title={formatMessage(labels.settings)}
selectedKey={selectedKey}
allowMinimize={false}
muteItems={false}
/>
</Column>
<Column gap="6" margin="2">

View file

@ -12,8 +12,20 @@ import {
} from '@umami/react-zen';
import Link from 'next/link';
interface SideMenuData {
id: string;
label: string;
icon?: any;
path: string;
}
interface SideMenuItems {
label?: string;
items: SideMenuData[];
}
export interface SideMenuProps extends NavMenuProps {
items: { label: string; items: { id: string; label: string; icon?: any; path: string }[] }[];
items: SideMenuItems[];
title?: string;
selectedKey?: string;
allowMinimize?: boolean;
@ -28,6 +40,23 @@ export function SideMenu({
children,
...props
}: SideMenuProps) {
const renderItems = (items: SideMenuData[]) => {
return items?.map(({ id, label, icon, path }) => {
const isSelected = selectedKey === id;
return (
<Link key={id} href={path}>
<NavMenuItem isSelected={isSelected}>
<Row alignItems="center" gap>
<Icon>{icon}</Icon>
<Text>{label}</Text>
</Row>
</NavMenuItem>
</Link>
);
});
};
return (
<Column
gap
@ -47,30 +76,19 @@ export function SideMenu({
)}
<NavMenu gap="6" {...props}>
{items?.map(({ label, items }, index) => {
return (
<NavMenuGroup
title={label}
key={`${label}${index}`}
gap="1"
allowMinimize={allowMinimize}
marginBottom="3"
>
{items?.map(({ id, label, icon, path }) => {
const isSelected = selectedKey === id;
return (
<Link key={id} href={path}>
<NavMenuItem isSelected={isSelected}>
<Row alignItems="center" gap>
<Icon>{icon}</Icon>
<Text>{label}</Text>
</Row>
</NavMenuItem>
</Link>
);
})}
</NavMenuGroup>
);
if (label) {
return (
<NavMenuGroup
title={label}
key={`${label}${index}`}
gap="1"
allowMinimize={allowMinimize}
marginBottom="3"
>
{renderItems(items)}
</NavMenuGroup>
);
}
})}
</NavMenu>
</Column>

View file

@ -11,25 +11,29 @@ import {
Popover,
Row,
Box,
SidebarItem,
Pressable,
Button,
Loading,
} from '@umami/react-zen';
import { useLoginQuery, useMessages, useUserTeamsQuery, useNavigation } from '@/components/hooks';
import { Chevron, User, Users } from '@/components/icons';
export function TeamsButton({ showText = true }: { showText?: boolean }) {
export interface TeamsButtonProps {
showText?: boolean;
onAction?: (id: any) => void;
}
export function TeamsButton({ showText = true, onAction }: TeamsButtonProps) {
const { user } = useLoginQuery();
const { formatMessage, labels } = useMessages();
const { data } = useUserTeamsQuery(user.id);
const { data, isLoading } = useUserTeamsQuery(user.id);
const { teamId } = useNavigation();
const router = useRouter();
const team = data?.data?.find(({ id }) => id === teamId);
const selectedKeys = new Set([teamId || user.id]);
const selectedKeys = new Set([teamId || 'user']);
const label = teamId ? team?.name : user.username;
const handleSelect = (id: Key) => {
router.push(id === user.id ? '/websites' : `/teams/${id}/websites`);
};
if (isLoading) {
return <Loading icon="dots" position="center" />;
}
if (!data?.count) {
return null;
@ -37,27 +41,29 @@ export function TeamsButton({ showText = true }: { showText?: boolean }) {
return (
<MenuTrigger>
<Pressable>
<Row role="button" width="100%" backgroundColor="2" border borderRadius>
<SidebarItem role="button" label={label} icon={teamId ? <Users /> : <User />}>
{showText && (
<Icon rotate={90} size="sm">
<Chevron />
</Icon>
)}
</SidebarItem>
<Button variant="outline">
<Row alignItems="center" justifyContent="space-between" flexGrow={1}>
<Row alignItems="center" gap>
<Icon>{teamId ? <Users /> : <User />}</Icon>
{showText && <Text>{label}</Text>}
</Row>
{showText && (
<Icon rotate={90} size="sm">
<Chevron />
</Icon>
)}
</Row>
</Pressable>
</Button>
<Popover placement="bottom start">
<Box minWidth="300px">
<Menu
selectionMode="single"
selectedKeys={selectedKeys}
autoFocus="last"
onAction={handleSelect}
onAction={onAction}
>
<MenuSection title={formatMessage(labels.myAccount)}>
<MenuItem id={user.id}>
<MenuItem id={'user'}>
<Row alignItems="center" gap>
<Icon>
<User />

View file

@ -359,6 +359,7 @@ export const labels = defineMessages({
invalidUrl: { id: 'label.invalid-url', defaultMessage: 'Invalid URL' },
environment: { id: 'label.environment', defaultMessage: 'Environment' },
criteria: { id: 'label.criteria', defaultMessage: 'Criteria' },
share: { defaultMessage: 'label.share', id: 'Share' },
});
export const messages = defineMessages({

View file

@ -1,25 +0,0 @@
.label {
display: flex;
align-items: center;
gap: 5px;
font-size: var(--font-size);
padding: 0.1em 0.5em;
border-radius: 5px;
color: var(--base500);
align-self: flex-start;
}
.positive {
color: var(--success-color);
background: color-mix(in srgb, var(--success-color), var(--background-color) 95%);
}
.negative {
color: var(--danger-color);
background: color-mix(in srgb, var(--danger-color), var(--background-color) 95%);
}
.neutral {
color: var(--font-color-muted);
background: var(--base-color-2);
}

View file

@ -1,14 +1,26 @@
import classNames from 'classnames';
import { Icon, Text } from '@umami/react-zen';
import { HTMLAttributes, ReactNode } from 'react';
import { Icon, Text, Row, RowProps } from '@umami/react-zen';
import { ReactNode } from 'react';
import { Arrow } from '@/components/icons';
import styles from './ChangeLabel.module.css';
const STYLES = {
positive: {
color: `var(--success-color)`,
background: `color-mix(in srgb, var(--success-color), var(--background-color) 95%)`,
},
negative: {
color: `var(--danger-color)`,
background: `color-mix(in srgb, var(--danger-color), var(--background-color) 95%)`,
},
neutral: {
color: `var(--font-color-muted)`,
background: `var(--base-color-2)`,
},
};
export function ChangeLabel({
value,
size,
reverseColors,
className,
children,
...props
}: {
@ -17,29 +29,24 @@ export function ChangeLabel({
title?: string;
reverseColors?: boolean;
showPercentage?: boolean;
className?: string;
children?: ReactNode;
} & HTMLAttributes<HTMLDivElement>) {
} & RowProps) {
const positive = value >= 0;
const negative = value < 0;
const neutral = value === 0 || isNaN(value);
const good = reverseColors ? negative : positive;
const style =
STYLES[good && 'positive'] || STYLES[!good && 'negative'] || STYLES[neutral && 'neutral'];
return (
<div
{...props}
className={classNames(styles.label, className, {
[styles.positive]: good,
[styles.negative]: !good,
[styles.neutral]: neutral,
})}
>
<Row {...props} style={style} alignSelf="flex-start" paddingX="2" paddingY="1">
{!neutral && (
<Icon rotate={positive ? -90 : 90} size={size}>
<Arrow />
</Icon>
)}
<Text>{children || value}</Text>
</div>
</Row>
);
}

View file

@ -1,3 +0,0 @@
.chart {
display: flex;
}

View file

@ -1,4 +0,0 @@
.container {
overflow: hidden;
position: relative;
}

View file

@ -1,7 +1,6 @@
import { FloatingTooltip, Column, useTheme, ColumnProps } from '@umami/react-zen';
import { useState, useMemo } from 'react';
import { ComposableMap, Geographies, Geography, ZoomableGroup } from 'react-simple-maps';
import classNames from 'classnames';
import { colord } from 'colord';
import { ISO_COUNTRIES, MAP_FILE } from '@/lib/constants';
import {
@ -12,7 +11,6 @@ import {
} from '@/components/hooks';
import { formatLongNumber } from '@/lib/format';
import { percentFilter } from '@/lib/filters';
import styles from './WorldMap.module.css';
import { getThemeColors } from '@/lib/colors';
export interface WorldMapProps extends ColumnProps {
@ -20,7 +18,7 @@ export interface WorldMapProps extends ColumnProps {
data?: any[];
}
export function WorldMap({ websiteId, data, className, ...props }: WorldMapProps) {
export function WorldMap({ websiteId, data, ...props }: WorldMapProps) {
const [tooltip, setTooltipPopup] = useState();
const { theme } = useTheme();
const { colors } = getThemeColors(theme);
@ -67,12 +65,7 @@ export function WorldMap({ websiteId, data, className, ...props }: WorldMapProps
};
return (
<Column
{...props}
className={classNames(styles.container, className)}
data-tip=""
data-for="world-map-tooltip"
>
<Column {...props} data-tip="" data-for="world-map-tooltip" style={{ margin: 'auto 0' }}>
<ComposableMap projection="geoMercator">
<ZoomableGroup zoom={0.8} minZoom={0.7} center={[0, 40]}>
<Geographies geography={`${process.env.basePath || ''}${MAP_FILE}`}>

View file

@ -1,5 +1,3 @@
export * from '@/components/hooks';
export * from '@/app/(main)/teams/[teamId]/TeamMemberEditButton';
export * from '@/app/(main)/teams/[teamId]/TeamMemberEditForm';
export * from '@/app/(main)/teams/[teamId]/TeamMemberRemoveButton';
@ -37,9 +35,14 @@ export * from '@/app/(main)/websites/WebsiteAddForm';
export * from '@/app/(main)/websites/WebsitesDataTable';
export * from '@/app/(main)/websites/WebsitesHeader';
export * from '@/app/(main)/websites/WebsitesTable';
export * from '@/app/(main)/websites/WebsiteProvider';
export * from '@/components/charts/BarChart';
export * from '@/components/charts/BubbleChart';
export * from '@/components/charts/Chart';
export * from '@/components/charts/ChartTooltip';
export * from '@/components/charts/PieChart';
export * from '@/components/common/ActionForm';
export * from '@/components/common/ConfirmationForm';
export * from '@/components/common/DataGrid';
@ -52,6 +55,7 @@ export * from '@/components/common/ErrorMessage';
export * from '@/components/common/ExternalLink';
export * from '@/components/common/Favicon';
export * from '@/components/common/LinkButton';
export * from '@/components/common/LoadingPanel';
export * from '@/components/common/PageBody';
export * from '@/components/common/PageHeader';
export * from '@/components/common/Pager';
@ -62,3 +66,13 @@ export * from '@/components/common/TypeConfirmationForm';
export * from '@/components/input/FilterButtons';
export * from '@/components/input/TeamsButton';
export * from '@/components/input/ProfileButton';
export * from '@/components/input/WebsiteSelect';
export * from '@/components/metrics/ChangeLabel';
export * from '@/components/metrics/ListTable';
export * from '@/components/metrics/MetricCard';
export * from '@/components/metrics/MetricLabel';
export * from '@/components/metrics/MetricsBar';
export * from '@/components/hooks';

View file

@ -7,7 +7,7 @@ export default defineConfig({
dts: true,
splitting: false,
sourcemap: false,
clean: true,
clean: false,
external: ['react', 'react-dom', 'react/jsx-runtime', '@swc/helpers'],
esbuildOptions(options) {
options.jsx = 'automatic';