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

@ -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';