mirror of
https://github.com/umami-software/umami.git
synced 2025-12-06 01:18:00 +01:00
Merged nav menus.
This commit is contained in:
parent
2f5e69229c
commit
554054d3a1
12 changed files with 164 additions and 47 deletions
|
|
@ -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
48
pnpm-lock.yaml
generated
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
1
src/assets/switch.svg
Normal 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 |
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
9
src/components/svg/Download.tsx
Normal file
9
src/components/svg/Download.tsx
Normal 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;
|
||||
12
src/components/svg/Export.tsx
Normal file
12
src/components/svg/Export.tsx
Normal 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;
|
||||
19
src/components/svg/Switch.tsx
Normal file
19
src/components/svg/Switch.tsx
Normal 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;
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue