Merged nav menus.
Some checks are pending
Create docker images / Build, push, and deploy (push) Waiting to run
Node.js CI / build (postgresql, 18.18, 10) (push) Waiting to run

This commit is contained in:
Mike Cao 2025-09-25 20:46:00 -07:00
parent 2f5e69229c
commit 554054d3a1
12 changed files with 164 additions and 47 deletions

View file

@ -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,
},

48
pnpm-lock.yaml generated
View file

@ -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:
@ -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:
@ -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

View file

@ -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 <Loading placement="absolute" />;
}
if (error) {
window.location.href = `${process.env.basePath || ''}/login`;
if (process.env.cloudMode) {
window.location.href = '/login';
} else {
router.push('/login');
}
return null;
}

View file

@ -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) {
})}
</SidebarSection>
<SidebarSection justifyContent="flex-start">
<Row>
<SettingsButton />
{!isCollapsed && !hasNav && <ThemeButton />}
<Row wrap="wrap">
<LanguageButton />
<ThemeButton />
</Row>
</SidebarSection>
</Sidebar>

1
src/assets/switch.svg Normal file
View file

@ -0,0 +1 @@
<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>

After

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

@ -24,9 +24,10 @@ import {
Settings,
User,
Users,
SwitchSvg,
} from '@/components/icons';
import { DOCS_URL } from '@/lib/constants';
import * as url from 'node:url';
import { ArrowRight } from 'lucide-react';
export interface TeamsButtonProps {
showText?: boolean;
@ -43,7 +44,7 @@ export function NavButton({ showText = true }: TeamsButtonProps) {
const label = teamId ? team?.name : user.username;
const getUrl = (url: string) => {
return cloudMode ? `${process.env.cloudUrl}/${url}` : url;
return cloudMode ? `${process.env.cloudUrl}${url}` : url;
};
const handleAction = async () => {};
@ -75,42 +76,52 @@ export function NavButton({ showText = true }: TeamsButtonProps) {
</Pressable>
<Popover placement="bottom start">
<Column minWidth="300px">
<Menu
selectionMode="single"
selectedKeys={selectedKeys}
autoFocus="last"
onAction={handleAction}
>
<MenuSection title={formatMessage(labels.myAccount)}>
<MenuItem id="user">
<IconLabel icon={<User />} label={user.username} />
</MenuItem>
</MenuSection>
<MenuSeparator />
<Menu autoFocus="last" onAction={handleAction}>
<SubmenuTrigger>
<MenuItem id="teams" showChecked={false} showSubMenuIcon>
<IconLabel icon={<Users />} label={formatMessage(labels.teams)} />
<IconLabel icon={<SwitchSvg />} label={formatMessage(labels.switchAccount)} />
</MenuItem>
<Popover placement="right top">
<Column minWidth="300px">
<Menu>
<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}>
<Row alignItems="center" gap>
<Icon size="sm">
<Users />
</Icon>
<MenuItem key={id} id={id} href={getUrl(`/teams/${id}`)}>
<IconLabel icon={<Users />}>
<Text wrap="nowrap">{name}</Text>
</Row>
</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>
<MenuItem id="settings" icon={<Settings />} label={formatMessage(labels.settings)} />
<MenuSeparator />
<MenuItem
id="settings"
href={getUrl('/settings')}
icon={<Settings />}
label={formatMessage(labels.settings)}
/>
{cloudMode && (
<>
<MenuItem
@ -132,14 +143,24 @@ export function NavButton({ showText = true }: TeamsButtonProps) {
/>
</>
)}
{user.isAdmin && (
{!cloudMode && user.isAdmin && (
<>
<MenuSeparator />
<MenuItem id="/admin" icon={<LockKeyhole />} label={formatMessage(labels.admin)} />
<MenuItem
id="/admin"
href="/admin"
icon={<LockKeyhole />}
label={formatMessage(labels.admin)}
/>
</>
)}
<MenuSeparator />
<MenuItem id="/logout" icon={<LogOut />} label={formatMessage(labels.logout)} />
<MenuItem
id="logout"
href={getUrl('/logout')}
icon={<LogOut />}
label={formatMessage(labels.logout)}
/>
</Menu>
</Column>
</Popover>

View file

@ -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({

View file

@ -0,0 +1,9 @@
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

@ -0,0 +1,12 @@
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

@ -0,0 +1,19 @@
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,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';