Compare commits

..

No commits in common. "554054d3a1ca5de57ff58ac5b42daaf69edd7115" and "5bbc1a94b3c2f0e2763e66dbaf17da62ee6e0dd1" have entirely different histories.

14 changed files with 58 additions and 244 deletions

View file

@ -3,16 +3,15 @@ import pkg from './package.json' assert { type: 'json' };
const TRACKER_SCRIPT = '/script.js';
const basePath = process.env.BASE_PATH || '';
const cloudMode = process.env.CLOUD_MODE || '';
const cloudUrl = process.env.CLOUD_URL || '';
const collectApiEndpoint = process.env.COLLECT_API_ENDPOINT || '';
const corsMaxAge = process.env.CORS_MAX_AGE || '';
const defaultLocale = process.env.DEFAULT_LOCALE || '';
const forceSSL = process.env.FORCE_SSL || '';
const frameAncestors = process.env.ALLOWED_FRAME_URLS || '';
const trackerScriptName = process.env.TRACKER_SCRIPT_NAME || '';
const trackerScriptURL = process.env.TRACKER_SCRIPT_URL || '';
const basePath = process.env.BASE_PATH;
const collectApiEndpoint = process.env.COLLECT_API_ENDPOINT;
const cloudMode = !!process.env.CLOUD_MODE;
const corsMaxAge = process.env.CORS_MAX_AGE;
const defaultLocale = process.env.DEFAULT_LOCALE;
const forceSSL = process.env.FORCE_SSL;
const frameAncestors = process.env.ALLOWED_FRAME_URLS ?? '';
const trackerScriptName = process.env.TRACKER_SCRIPT_NAME;
const trackerScriptURL = process.env.TRACKER_SCRIPT_URL;
const contentSecurityPolicy = [
`default-src 'self'`,
@ -164,7 +163,6 @@ export default {
env: {
basePath,
cloudMode,
cloudUrl,
currentVersion: pkg.version,
defaultLocale,
},

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.187.0",
"@umami/react-zen": "^0.186.0",
"@umami/redis-client": "^0.29.0",
"bcryptjs": "^3.0.2",
"chalk": "^5.6.0",

58
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.187.0
version: 0.187.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.186.0
version: 0.186.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
@ -367,44 +367,7 @@ importers:
specifier: ^5.9.2
version: 5.9.2
dist:
dependencies:
chart.js:
specifier: ^4.5.0
version: 4.5.0
chartjs-adapter-date-fns:
specifier: ^3.0.0
version: 3.0.0(chart.js@4.5.0)(date-fns@2.30.0)
colord:
specifier: ^2.9.2
version: 2.9.3
jsonwebtoken:
specifier: ^9.0.2
version: 9.0.2
lucide-react:
specifier: ^0.542.0
version: 0.542.0(react@19.1.1)
pure-rand:
specifier: ^7.0.1
version: 7.0.1
react-simple-maps:
specifier: ^2.3.0
version: 2.3.0(prop-types@15.8.1)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
react-use-measure:
specifier: ^2.0.4
version: 2.1.7(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
react-window:
specifier: ^1.8.6
version: 1.8.11(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
serialize-error:
specifier: ^12.0.0
version: 12.0.0
thenby:
specifier: ^1.3.4
version: 1.3.4
uuid:
specifier: ^11.1.0
version: 11.1.0
dist: {}
packages:
@ -2775,8 +2738,8 @@ packages:
'@prisma/client': ^6.1.0
'@prisma/extension-read-replicas': ^0.4.1
'@umami/react-zen@0.187.0':
resolution: {integrity: sha512-CiTGBqEvN/dcZ1Tq4R+mj9ynN1opZF81iukUzElChJ5XF/Ec9HhPR+KM2r8PXt+uWeVVe1aZtjyVOdwUR/ndXg==}
'@umami/react-zen@0.186.0':
resolution: {integrity: sha512-s+x4cJK5UTHQ0l2TTUb3zX8P2U6bMw35NRjIqG+OJvljJf5NNdRo6WChZOvnh/08XxGI30jntFhUYdup255rFg==}
'@umami/redis-client@0.29.0':
resolution: {integrity: sha512-Jaqh++jskqDB7ny75pfC02OvKp1JTS4asGDsFrRL3qy8sxL3PAl9+/mybCJe4/6vWrXDJKqpgkSfUDJq2bFjyw==}
@ -5283,11 +5246,6 @@ packages:
peerDependencies:
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
lucide-react@0.542.0:
resolution: {integrity: sha512-w3hD8/SQB7+lzU2r4VdFyzzOzKnUjTZIF/MQJGSSvni7Llewni4vuViRppfRAa2guOsY5k4jZyxw/i9DQHv+dw==}
peerDependencies:
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
lucide-react@0.543.0:
resolution: {integrity: sha512-fpVfuOQO0V3HBaOA1stIiP/A2fPCXHIleRZL16Mx3HmjTYwNSbimhnFBygs2CAfU1geexMX5ItUcWBGUaqw5CA==}
peerDependencies:
@ -10385,7 +10343,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@umami/react-zen@0.187.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.186.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.8
'@internationalized/date': 3.9.0
@ -13418,10 +13376,6 @@ snapshots:
dependencies:
react: 19.1.1
lucide-react@0.542.0(react@19.1.1):
dependencies:
react: 19.1.1
lucide-react@0.543.0(react@19.1.1):
dependencies:
react: 19.1.1

View file

@ -1,25 +1,22 @@
'use client';
import { Grid, Loading, Column } from '@umami/react-zen';
import Script from 'next/script';
import { usePathname } from 'next/navigation';
import { UpdateNotice } from './UpdateNotice';
import { SideNav } from '@/app/(main)/SideNav';
import { useLoginQuery, useConfig, useNavigation } from '@/components/hooks';
import { useLoginQuery, useConfig } from '@/components/hooks';
export function App({ children }) {
const { user, isLoading, error } = useLoginQuery();
const config = useConfig();
const { pathname, router } = useNavigation();
const pathname = usePathname();
if (isLoading || !config) {
return <Loading placement="absolute" />;
}
if (error) {
if (process.env.cloudMode) {
window.location.href = '/login';
} else {
router.push('/login');
}
window.location.href = `${process.env.basePath || ''}/login`;
return null;
}

View file

@ -1,4 +1,3 @@
import { Key } from 'react';
import Link from 'next/link';
import {
Sidebar,
@ -13,7 +12,8 @@ import { Globe, LinkIcon, LogoSvg, Grid2x2, PanelLeft } from '@/components/icons
import { useMessages, useNavigation, useGlobalState } from '@/components/hooks';
import { NavButton } from '@/components/input/NavButton';
import { PanelButton } from '@/components/input/PanelButton';
import { LanguageButton } from '@/components/input/LanguageButton';
import { Key } from 'react';
import { SettingsButton } from '@/components/input/SettingsButton';
export function SideNav(props: SidebarProps) {
const { formatMessage, labels } = useMessages();
@ -77,9 +77,9 @@ export function SideNav(props: SidebarProps) {
})}
</SidebarSection>
<SidebarSection justifyContent="flex-start">
<Row wrap="wrap">
<LanguageButton />
<ThemeButton />
<Row>
<SettingsButton />
{!isCollapsed && !hasNav && <ThemeButton />}
</Row>
</SidebarSection>
</Sidebar>

View file

@ -36,21 +36,9 @@ function MessagesProvider({ children }) {
export function Providers({ children }) {
const router = useRouter();
function navigate(url: string) {
if (shouldUseNativeLink(url)) {
window.location.href = url;
} else {
router.push(url);
}
}
function shouldUseNativeLink(url: string) {
return url.startsWith('http');
}
return (
<ZenProvider>
<RouterProvider navigate={navigate}>
<RouterProvider navigate={router.push}>
<MessagesProvider>
<QueryClientProvider client={client}>
<ErrorBoundary>{children}</ErrorBoundary>

View file

@ -1 +0,0 @@
<svg stroke="currentColor" fill="none" stroke-width="2" viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" height="200px" width="200px" xmlns="http://www.w3.org/2000/svg"><path d="M16 3l4 4l-4 4"></path><path d="M10 7l10 0"></path><path d="M8 13l-4 4l4 4"></path><path d="M4 17l9 0"></path></svg>

Before

Width:  |  Height:  |  Size: 313 B

View file

@ -1,14 +1,14 @@
export * from 'lucide-react';
export {
Logo as LogoSvg,
Compare as CompareSvg,
Funnel as FunnelSvg,
Lightning as LightningSvg,
Location as LocationSvg,
Logo as LogoSvg,
Magnet as MagnetSvg,
Money as MoneySvg,
Network as NetworkSvg,
Path as PathSvg,
Switch as SwitchSvg,
Target as TargetSvg,
AddUser as AddUserSvg,
} from '@/components/svg';

View file

@ -6,49 +6,27 @@ import {
MenuTrigger,
MenuSection,
MenuSeparator,
SubmenuTrigger,
Popover,
Row,
Column,
Pressable,
IconLabel,
} from '@umami/react-zen';
import { useConfig, useLoginQuery, useMessages, useNavigation } from '@/components/hooks';
import {
BookText,
ChevronRight,
ExternalLink,
LifeBuoy,
LockKeyhole,
LogOut,
Settings,
User,
Users,
SwitchSvg,
} from '@/components/icons';
import { DOCS_URL } from '@/lib/constants';
import { ArrowRight } from 'lucide-react';
import { useLoginQuery, useMessages, useNavigation } from '@/components/hooks';
import { ChevronRight, User, Users } from '@/components/icons';
export interface TeamsButtonProps {
showText?: boolean;
onAction?: (id: any) => void;
}
export function NavButton({ showText = true }: TeamsButtonProps) {
export function NavButton({ showText = true, onAction }: TeamsButtonProps) {
const { user } = useLoginQuery();
const { cloudMode } = useConfig();
const { formatMessage, labels } = useMessages();
const { teamId } = useNavigation();
const team = user?.teams?.find(({ id }) => id === teamId);
const selectedKeys = new Set([teamId || 'user']);
const label = teamId ? team?.name : user.username;
const getUrl = (url: string) => {
return cloudMode ? `${process.env.cloudUrl}${url}` : url;
};
const handleAction = async () => {};
return (
<MenuTrigger>
<Pressable>
@ -76,91 +54,35 @@ export function NavButton({ showText = true }: TeamsButtonProps) {
</Pressable>
<Popover placement="bottom start">
<Column minWidth="300px">
<Menu autoFocus="last" onAction={handleAction}>
<SubmenuTrigger>
<MenuItem id="teams" showChecked={false} showSubMenuIcon>
<IconLabel icon={<SwitchSvg />} label={formatMessage(labels.switchAccount)} />
</MenuItem>
<Popover placement="right top">
<Column minWidth="300px">
<Menu selectionMode="single" selectedKeys={selectedKeys}>
<MenuSection title={formatMessage(labels.myAccount)}>
<MenuItem id="user" href={getUrl('/')}>
<IconLabel icon={<User />} label={user.username} />
</MenuItem>
</MenuSection>
<MenuSeparator />
<MenuSection title={formatMessage(labels.teams)}>
{user?.teams?.map(({ id, name }) => (
<MenuItem key={id} id={id} href={getUrl(`/teams/${id}`)}>
<IconLabel icon={<Users />}>
<Text wrap="nowrap">{name}</Text>
</IconLabel>
</MenuItem>
))}
{user?.teams?.length === 0 && (
<MenuItem id="manage-teams">
<a href="/settings/teams" style={{ width: '100%' }}>
<Row alignItems="center" justifyContent="space-between" gap>
<Text align="center">Manage teams</Text>
<Icon>
<ArrowRight />
</Icon>
</Row>
</a>
</MenuItem>
)}
</MenuSection>
</Menu>
</Column>
</Popover>
</SubmenuTrigger>
<MenuSeparator />
<MenuItem
id="settings"
href={getUrl('/settings')}
icon={<Settings />}
label={formatMessage(labels.settings)}
/>
{cloudMode && (
<>
<MenuItem
id="docs"
href={DOCS_URL}
target="_blank"
icon={<BookText />}
label={formatMessage(labels.documentation)}
>
<Icon color="muted">
<ExternalLink />
<Menu
selectionMode="single"
selectedKeys={selectedKeys}
autoFocus="last"
onAction={onAction}
>
<MenuSection title={formatMessage(labels.myAccount)}>
<MenuItem id={'user'}>
<Row alignItems="center" gap>
<Icon>
<User />
</Icon>
</MenuItem>
<MenuItem
id="support"
href={getUrl('/settings/support')}
icon={<LifeBuoy />}
label={formatMessage(labels.support)}
/>
</>
)}
{!cloudMode && user.isAdmin && (
<>
<MenuSeparator />
<MenuItem
id="/admin"
href="/admin"
icon={<LockKeyhole />}
label={formatMessage(labels.admin)}
/>
</>
)}
<Text wrap="nowrap">{user.username}</Text>
</Row>
</MenuItem>
</MenuSection>
<MenuSeparator />
<MenuItem
id="logout"
href={getUrl('/logout')}
icon={<LogOut />}
label={formatMessage(labels.logout)}
/>
<MenuSection title={formatMessage(labels.teams)}>
{user?.teams?.map(({ id, name }) => (
<MenuItem key={id} id={id}>
<Row alignItems="center" gap>
<Icon size="sm">
<Users />
</Icon>
<Text wrap="nowrap">{name}</Text>
</Row>
</MenuItem>
))}
</MenuSection>
</Menu>
</Column>
</Popover>

View file

@ -362,7 +362,6 @@ export const labels = defineMessages({
share: { id: 'label.share', defaultMessage: 'Share' },
support: { id: 'label.support', defaultMessage: 'Support' },
documentation: { id: 'label.documentation', defaultMessage: 'Documentation' },
switchAccount: { id: 'label.switch-account', defaultMessage: 'Switch account' },
});
export const messages = defineMessages({

View file

@ -1,9 +0,0 @@
import * as React from 'react';
import type { SVGProps } from 'react';
const SvgDownload = (props: SVGProps<SVGSVGElement>) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" {...props}>
<path d="M97.5 82.656V71.357a3.545 3.545 0 0 0-3.545-3.544H89.17a3.545 3.545 0 0 0-3.545 3.544v11.3c0 1.639-1.33 2.968-2.969 2.968H17.344a2.97 2.97 0 0 1-2.969-2.969V71.357a3.545 3.545 0 0 0-3.545-3.545H6.045A3.545 3.545 0 0 0 2.5 71.357v11.3C2.5 90.853 9.146 97.5 17.344 97.5h65.312c8.198 0 14.844-6.646 14.844-14.844" />
<path d="m29.68 44.105-3.387 3.388a3.545 3.545 0 0 0 0 5.014l19.506 19.506a5.94 5.94 0 0 0 8.397.005l.005-.005 19.506-19.506a3.545 3.545 0 0 0 0-5.014l-3.388-3.388a3.545 3.545 0 0 0-5.013 0l-9.368 9.368V6.045A3.545 3.545 0 0 0 52.393 2.5h-4.786a3.545 3.545 0 0 0-3.544 3.545v47.428l-9.369-9.368a3.545 3.545 0 0 0-5.013 0" />
</svg>
);
export default SvgDownload;

View file

@ -1,12 +0,0 @@
import * as React from 'react';
import type { SVGProps } from 'react';
const SvgExport = (props: SVGProps<SVGSVGElement>) => (
<svg xmlns="http://www.w3.org/2000/svg" width={512} height={512} viewBox="0 0 24 24" {...props}>
<switch>
<g>
<path d="M8.7 7.7 11 5.4V15c0 .6.4 1 1 1s1-.4 1-1V5.4l2.3 2.3c.4.4 1 .4 1.4 0s.4-1 0-1.4l-4-4c-.1-.1-.2-.2-.3-.2-.2-.1-.5-.1-.8 0-.1 0-.2.1-.3.2l-4 4c-.4.4-.4 1 0 1.4s1 .4 1.4 0M21 14c-.6 0-1 .4-1 1v4c0 .6-.4 1-1 1H5c-.6 0-1-.4-1-1v-4c0-.6-.4-1-1-1s-1 .4-1 1v4c0 1.7 1.3 3 3 3h14c1.7 0 3-1.3 3-3v-4c0-.6-.4-1-1-1" />
</g>
</switch>
</svg>
);
export default SvgExport;

View file

@ -1,19 +0,0 @@
import * as React from 'react';
import type { SVGProps } from 'react';
const SvgSwitch = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={200}
height={200}
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
viewBox="0 0 24 24"
{...props}
>
<path d="m16 3 4 4-4 4M10 7h10M8 13l-4 4 4 4M4 17h9" />
</svg>
);
export default SvgSwitch;

View file

@ -6,9 +6,7 @@ export { default as Bookmark } from './Bookmark';
export { default as Change } from './Change';
export { default as Compare } from './Compare';
export { default as Dashboard } from './Dashboard';
export { default as Download } from './Download';
export { default as Expand } from './Expand';
export { default as Export } from './Export';
export { default as Flag } from './Flag';
export { default as Funnel } from './Funnel';
export { default as Gear } from './Gear';
@ -30,7 +28,6 @@ export { default as Redo } from './Redo';
export { default as Reports } from './Reports';
export { default as Security } from './Security';
export { default as Speaker } from './Speaker';
export { default as Switch } from './Switch';
export { default as Tag } from './Tag';
export { default as Target } from './Target';
export { default as Visitor } from './Visitor';