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 TRACKER_SCRIPT = '/script.js';
const basePath = process.env.BASE_PATH || ''; const basePath = process.env.BASE_PATH;
const cloudMode = process.env.CLOUD_MODE || ''; const collectApiEndpoint = process.env.COLLECT_API_ENDPOINT;
const cloudUrl = process.env.CLOUD_URL || ''; const cloudMode = !!process.env.CLOUD_MODE;
const collectApiEndpoint = process.env.COLLECT_API_ENDPOINT || ''; const corsMaxAge = process.env.CORS_MAX_AGE;
const corsMaxAge = process.env.CORS_MAX_AGE || ''; const defaultLocale = process.env.DEFAULT_LOCALE;
const defaultLocale = process.env.DEFAULT_LOCALE || ''; const forceSSL = process.env.FORCE_SSL;
const forceSSL = process.env.FORCE_SSL || ''; const frameAncestors = process.env.ALLOWED_FRAME_URLS ?? '';
const frameAncestors = process.env.ALLOWED_FRAME_URLS || ''; const trackerScriptName = process.env.TRACKER_SCRIPT_NAME;
const trackerScriptName = process.env.TRACKER_SCRIPT_NAME || ''; const trackerScriptURL = process.env.TRACKER_SCRIPT_URL;
const trackerScriptURL = process.env.TRACKER_SCRIPT_URL || '';
const contentSecurityPolicy = [ const contentSecurityPolicy = [
`default-src 'self'`, `default-src 'self'`,
@ -164,7 +163,6 @@ export default {
env: { env: {
basePath, basePath,
cloudMode, cloudMode,
cloudUrl,
currentVersion: pkg.version, currentVersion: pkg.version,
defaultLocale, defaultLocale,
}, },

View file

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

58
pnpm-lock.yaml generated
View file

@ -45,8 +45,8 @@ importers:
specifier: ^5.85.5 specifier: ^5.85.5
version: 5.85.5(react@19.1.1) version: 5.85.5(react@19.1.1)
'@umami/react-zen': '@umami/react-zen':
specifier: ^0.187.0 specifier: ^0.186.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)) 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': '@umami/redis-client':
specifier: ^0.29.0 specifier: ^0.29.0
version: 0.29.0 version: 0.29.0
@ -367,44 +367,7 @@ importers:
specifier: ^5.9.2 specifier: ^5.9.2
version: 5.9.2 version: 5.9.2
dist: 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
packages: packages:
@ -2775,8 +2738,8 @@ packages:
'@prisma/client': ^6.1.0 '@prisma/client': ^6.1.0
'@prisma/extension-read-replicas': ^0.4.1 '@prisma/extension-read-replicas': ^0.4.1
'@umami/react-zen@0.187.0': '@umami/react-zen@0.186.0':
resolution: {integrity: sha512-CiTGBqEvN/dcZ1Tq4R+mj9ynN1opZF81iukUzElChJ5XF/Ec9HhPR+KM2r8PXt+uWeVVe1aZtjyVOdwUR/ndXg==} resolution: {integrity: sha512-s+x4cJK5UTHQ0l2TTUb3zX8P2U6bMw35NRjIqG+OJvljJf5NNdRo6WChZOvnh/08XxGI30jntFhUYdup255rFg==}
'@umami/redis-client@0.29.0': '@umami/redis-client@0.29.0':
resolution: {integrity: sha512-Jaqh++jskqDB7ny75pfC02OvKp1JTS4asGDsFrRL3qy8sxL3PAl9+/mybCJe4/6vWrXDJKqpgkSfUDJq2bFjyw==} resolution: {integrity: sha512-Jaqh++jskqDB7ny75pfC02OvKp1JTS4asGDsFrRL3qy8sxL3PAl9+/mybCJe4/6vWrXDJKqpgkSfUDJq2bFjyw==}
@ -5283,11 +5246,6 @@ packages:
peerDependencies: peerDependencies:
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 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: lucide-react@0.543.0:
resolution: {integrity: sha512-fpVfuOQO0V3HBaOA1stIiP/A2fPCXHIleRZL16Mx3HmjTYwNSbimhnFBygs2CAfU1geexMX5ItUcWBGUaqw5CA==} resolution: {integrity: sha512-fpVfuOQO0V3HBaOA1stIiP/A2fPCXHIleRZL16Mx3HmjTYwNSbimhnFBygs2CAfU1geexMX5ItUcWBGUaqw5CA==}
peerDependencies: peerDependencies:
@ -10385,7 +10343,7 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - 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: dependencies:
'@fontsource/jetbrains-mono': 5.2.8 '@fontsource/jetbrains-mono': 5.2.8
'@internationalized/date': 3.9.0 '@internationalized/date': 3.9.0
@ -13418,10 +13376,6 @@ snapshots:
dependencies: dependencies:
react: 19.1.1 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): lucide-react@0.543.0(react@19.1.1):
dependencies: dependencies:
react: 19.1.1 react: 19.1.1

View file

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

View file

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

View file

@ -36,21 +36,9 @@ function MessagesProvider({ children }) {
export function Providers({ children }) { export function Providers({ children }) {
const router = useRouter(); 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 ( return (
<ZenProvider> <ZenProvider>
<RouterProvider navigate={navigate}> <RouterProvider navigate={router.push}>
<MessagesProvider> <MessagesProvider>
<QueryClientProvider client={client}> <QueryClientProvider client={client}>
<ErrorBoundary>{children}</ErrorBoundary> <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 * from 'lucide-react';
export { export {
Logo as LogoSvg,
Compare as CompareSvg, Compare as CompareSvg,
Funnel as FunnelSvg, Funnel as FunnelSvg,
Lightning as LightningSvg, Lightning as LightningSvg,
Location as LocationSvg, Location as LocationSvg,
Logo as LogoSvg,
Magnet as MagnetSvg, Magnet as MagnetSvg,
Money as MoneySvg, Money as MoneySvg,
Network as NetworkSvg, Network as NetworkSvg,
Path as PathSvg, Path as PathSvg,
Switch as SwitchSvg,
Target as TargetSvg, Target as TargetSvg,
AddUser as AddUserSvg,
} from '@/components/svg'; } from '@/components/svg';

View file

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

View file

@ -362,7 +362,6 @@ export const labels = defineMessages({
share: { id: 'label.share', defaultMessage: 'Share' }, share: { id: 'label.share', defaultMessage: 'Share' },
support: { id: 'label.support', defaultMessage: 'Support' }, support: { id: 'label.support', defaultMessage: 'Support' },
documentation: { id: 'label.documentation', defaultMessage: 'Documentation' }, documentation: { id: 'label.documentation', defaultMessage: 'Documentation' },
switchAccount: { id: 'label.switch-account', defaultMessage: 'Switch account' },
}); });
export const messages = defineMessages({ 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 Change } from './Change';
export { default as Compare } from './Compare'; export { default as Compare } from './Compare';
export { default as Dashboard } from './Dashboard'; export { default as Dashboard } from './Dashboard';
export { default as Download } from './Download';
export { default as Expand } from './Expand'; export { default as Expand } from './Expand';
export { default as Export } from './Export';
export { default as Flag } from './Flag'; export { default as Flag } from './Flag';
export { default as Funnel } from './Funnel'; export { default as Funnel } from './Funnel';
export { default as Gear } from './Gear'; 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 Reports } from './Reports';
export { default as Security } from './Security'; export { default as Security } from './Security';
export { default as Speaker } from './Speaker'; export { default as Speaker } from './Speaker';
export { default as Switch } from './Switch';
export { default as Tag } from './Tag'; export { default as Tag } from './Tag';
export { default as Target } from './Target'; export { default as Target } from './Target';
export { default as Visitor } from './Visitor'; export { default as Visitor } from './Visitor';