mirror of
https://github.com/umami-software/umami.git
synced 2025-12-06 01:18:00 +01:00
Export metrics components.
Some checks failed
Node.js CI / build (postgresql, 18.18) (push) Has been cancelled
Some checks failed
Node.js CI / build (postgresql, 18.18) (push) Has been cancelled
This commit is contained in:
parent
c4114f4349
commit
dc1736458b
16 changed files with 140 additions and 126 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
10
pnpm-lock.yaml
generated
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
.chart {
|
||||
display: flex;
|
||||
}
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
.container {
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
|
@ -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}`}>
|
||||
|
|
|
|||
20
src/index.ts
20
src/index.ts
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue