diff --git a/next.config.ts b/next.config.ts
index eac6f327a..17705dc20 100644
--- a/next.config.ts
+++ b/next.config.ts
@@ -3,15 +3,16 @@ import pkg from './package.json' assert { type: 'json' };
const TRACKER_SCRIPT = '/script.js';
-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 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 contentSecurityPolicy = [
`default-src 'self'`,
@@ -163,6 +164,7 @@ export default {
env: {
basePath,
cloudMode,
+ cloudUrl,
currentVersion: pkg.version,
defaultLocale,
},
diff --git a/package.json b/package.json
index 44ec9f088..3328c5c71 100644
--- a/package.json
+++ b/package.json
@@ -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.186.0",
+ "@umami/react-zen": "^0.187.0",
"@umami/redis-client": "^0.29.0",
"bcryptjs": "^3.0.2",
"chalk": "^5.6.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 1f3fdc90c..949928622 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -45,8 +45,8 @@ importers:
specifier: ^5.85.5
version: 5.85.5(react@19.1.1)
'@umami/react-zen':
- 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))
+ 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))
'@umami/redis-client':
specifier: ^0.29.0
version: 0.29.0
@@ -367,7 +367,44 @@ importers:
specifier: ^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:
@@ -2738,8 +2775,8 @@ packages:
'@prisma/client': ^6.1.0
'@prisma/extension-read-replicas': ^0.4.1
- '@umami/react-zen@0.186.0':
- resolution: {integrity: sha512-s+x4cJK5UTHQ0l2TTUb3zX8P2U6bMw35NRjIqG+OJvljJf5NNdRo6WChZOvnh/08XxGI30jntFhUYdup255rFg==}
+ '@umami/react-zen@0.187.0':
+ resolution: {integrity: sha512-CiTGBqEvN/dcZ1Tq4R+mj9ynN1opZF81iukUzElChJ5XF/Ec9HhPR+KM2r8PXt+uWeVVe1aZtjyVOdwUR/ndXg==}
'@umami/redis-client@0.29.0':
resolution: {integrity: sha512-Jaqh++jskqDB7ny75pfC02OvKp1JTS4asGDsFrRL3qy8sxL3PAl9+/mybCJe4/6vWrXDJKqpgkSfUDJq2bFjyw==}
@@ -5246,6 +5283,11 @@ 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:
@@ -10343,7 +10385,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@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))':
+ '@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))':
dependencies:
'@fontsource/jetbrains-mono': 5.2.8
'@internationalized/date': 3.9.0
@@ -13376,6 +13418,10 @@ 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
diff --git a/src/app/(main)/App.tsx b/src/app/(main)/App.tsx
index 6c14a484a..c3f88c5b0 100644
--- a/src/app/(main)/App.tsx
+++ b/src/app/(main)/App.tsx
@@ -1,22 +1,25 @@
'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 } from '@/components/hooks';
+import { useLoginQuery, useConfig, useNavigation } from '@/components/hooks';
export function App({ children }) {
const { user, isLoading, error } = useLoginQuery();
const config = useConfig();
- const pathname = usePathname();
+ const { pathname, router } = useNavigation();
if (isLoading || !config) {
return ;
}
if (error) {
- window.location.href = `${process.env.basePath || ''}/login`;
+ if (process.env.cloudMode) {
+ window.location.href = '/login';
+ } else {
+ router.push('/login');
+ }
return null;
}
diff --git a/src/app/(main)/SideNav.tsx b/src/app/(main)/SideNav.tsx
index a50f0c1f1..4398009e6 100644
--- a/src/app/(main)/SideNav.tsx
+++ b/src/app/(main)/SideNav.tsx
@@ -1,3 +1,4 @@
+import { Key } from 'react';
import Link from 'next/link';
import {
Sidebar,
@@ -12,8 +13,7 @@ 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 { Key } from 'react';
-import { SettingsButton } from '@/components/input/SettingsButton';
+import { LanguageButton } from '@/components/input/LanguageButton';
export function SideNav(props: SidebarProps) {
const { formatMessage, labels } = useMessages();
@@ -77,9 +77,9 @@ export function SideNav(props: SidebarProps) {
})}
-
-
- {!isCollapsed && !hasNav && }
+
+
+
diff --git a/src/app/Providers.tsx b/src/app/Providers.tsx
index 77be52016..b8898e884 100644
--- a/src/app/Providers.tsx
+++ b/src/app/Providers.tsx
@@ -36,9 +36,21 @@ 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 (
-
+
{children}
diff --git a/src/assets/switch.svg b/src/assets/switch.svg
new file mode 100644
index 000000000..86166cc5c
--- /dev/null
+++ b/src/assets/switch.svg
@@ -0,0 +1 @@
+
diff --git a/src/components/icons.ts b/src/components/icons.ts
index ddcda3b66..e0e7c6787 100644
--- a/src/components/icons.ts
+++ b/src/components/icons.ts
@@ -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';
diff --git a/src/components/input/NavButton.tsx b/src/components/input/NavButton.tsx
index ebe1a19a6..240c67351 100644
--- a/src/components/input/NavButton.tsx
+++ b/src/components/input/NavButton.tsx
@@ -6,27 +6,49 @@ import {
MenuTrigger,
MenuSection,
MenuSeparator,
+ SubmenuTrigger,
Popover,
Row,
Column,
Pressable,
+ IconLabel,
} from '@umami/react-zen';
-import { useLoginQuery, useMessages, useNavigation } from '@/components/hooks';
-import { ChevronRight, User, Users } from '@/components/icons';
+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';
export interface TeamsButtonProps {
showText?: boolean;
onAction?: (id: any) => void;
}
-export function NavButton({ showText = true, onAction }: TeamsButtonProps) {
+export function NavButton({ showText = true }: 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 (
@@ -54,35 +76,91 @@ export function NavButton({ showText = true, onAction }: TeamsButtonProps) {
-
diff --git a/src/components/messages.ts b/src/components/messages.ts
index c40a3b05f..0438c06e6 100644
--- a/src/components/messages.ts
+++ b/src/components/messages.ts
@@ -362,6 +362,7 @@ 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({
diff --git a/src/components/svg/Download.tsx b/src/components/svg/Download.tsx
new file mode 100644
index 000000000..56d4d683c
--- /dev/null
+++ b/src/components/svg/Download.tsx
@@ -0,0 +1,9 @@
+import * as React from 'react';
+import type { SVGProps } from 'react';
+const SvgDownload = (props: SVGProps) => (
+
+);
+export default SvgDownload;
diff --git a/src/components/svg/Export.tsx b/src/components/svg/Export.tsx
new file mode 100644
index 000000000..355321cf1
--- /dev/null
+++ b/src/components/svg/Export.tsx
@@ -0,0 +1,12 @@
+import * as React from 'react';
+import type { SVGProps } from 'react';
+const SvgExport = (props: SVGProps) => (
+
+);
+export default SvgExport;
diff --git a/src/components/svg/Switch.tsx b/src/components/svg/Switch.tsx
new file mode 100644
index 000000000..2a12f3936
--- /dev/null
+++ b/src/components/svg/Switch.tsx
@@ -0,0 +1,19 @@
+import * as React from 'react';
+import type { SVGProps } from 'react';
+const SvgSwitch = (props: SVGProps) => (
+
+);
+export default SvgSwitch;
diff --git a/src/components/svg/index.ts b/src/components/svg/index.ts
index 86c3ea947..1bfc728a8 100644
--- a/src/components/svg/index.ts
+++ b/src/components/svg/index.ts
@@ -6,7 +6,9 @@ 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';
@@ -28,6 +30,7 @@ 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';