mirror of
https://github.com/umami-software/umami.git
synced 2026-02-04 04:37:11 +01:00
Updated layout. Fixed properties rendering.
This commit is contained in:
parent
01bfd7f52e
commit
876f4c883e
18 changed files with 214 additions and 176 deletions
|
|
@ -82,7 +82,7 @@
|
|||
"@react-spring/web": "^9.7.3",
|
||||
"@svgr/cli": "^8.1.0",
|
||||
"@tanstack/react-query": "^5.80.10",
|
||||
"@umami/react-zen": "^0.148.0",
|
||||
"@umami/react-zen": "^0.150.0",
|
||||
"@umami/redis-client": "^0.27.0",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"chalk": "^4.1.1",
|
||||
|
|
|
|||
10
pnpm-lock.yaml
generated
10
pnpm-lock.yaml
generated
|
|
@ -45,8 +45,8 @@ importers:
|
|||
specifier: ^5.80.10
|
||||
version: 5.80.10(react@19.1.0)
|
||||
'@umami/react-zen':
|
||||
specifier: ^0.148.0
|
||||
version: 0.148.0(@babel/core@7.27.1)(@types/react@19.1.8)(babel-plugin-react-compiler@19.1.0-rc.2)(immer@9.0.21)(use-sync-external-store@1.5.0(react@19.1.0))
|
||||
specifier: ^0.150.0
|
||||
version: 0.150.0(@babel/core@7.27.1)(@types/react@19.1.8)(babel-plugin-react-compiler@19.1.0-rc.2)(immer@9.0.21)(use-sync-external-store@1.5.0(react@19.1.0))
|
||||
'@umami/redis-client':
|
||||
specifier: ^0.27.0
|
||||
version: 0.27.0
|
||||
|
|
@ -2553,8 +2553,8 @@ packages:
|
|||
resolution: {integrity: sha512-xoh5rJ+tgsRKoXnkBPFRLZ7rjKM0AfVbC68UZ/ECXoDbfggb9RbEySN359acY1vS3qZ0jVTVWzbtfapwm5ztxw==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
'@umami/react-zen@0.148.0':
|
||||
resolution: {integrity: sha512-4bI8wBgqep6bYNsnhQ87lSAYqiKLj/9o1emFfIot0NIxKd2jsmgEwGrymxDjmunXK4OwRs8ukOYvzuH3WbieGA==}
|
||||
'@umami/react-zen@0.150.0':
|
||||
resolution: {integrity: sha512-ogtdNm7jg7BnzWnQtchOAE1eE36EuxRLZaoalPLm8VrK4eQweqnbRFw+ddeAA12xI0hPP8gKOxOJDhH9pJpVVg==}
|
||||
|
||||
'@umami/redis-client@0.27.0':
|
||||
resolution: {integrity: sha512-SbHTpxhgeZyTBUSp2zdZM+XUtpsaSL4Tad8QXIEhEtjWhvvfoornyT5kLuyYCVtzSAT4daALeGmOO1z6EE1KcA==}
|
||||
|
|
@ -9739,7 +9739,7 @@ snapshots:
|
|||
'@typescript-eslint/types': 8.34.1
|
||||
eslint-visitor-keys: 4.2.1
|
||||
|
||||
'@umami/react-zen@0.148.0(@babel/core@7.27.1)(@types/react@19.1.8)(babel-plugin-react-compiler@19.1.0-rc.2)(immer@9.0.21)(use-sync-external-store@1.5.0(react@19.1.0))':
|
||||
'@umami/react-zen@0.150.0(@babel/core@7.27.1)(@types/react@19.1.8)(babel-plugin-react-compiler@19.1.0-rc.2)(immer@9.0.21)(use-sync-external-store@1.5.0(react@19.1.0))':
|
||||
dependencies:
|
||||
'@fontsource/jetbrains-mono': 5.2.6
|
||||
'@internationalized/date': 3.8.2
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
'use client';
|
||||
import { Grid, Loading, Column, Row } from '@umami/react-zen';
|
||||
import { Grid, Loading, Column } from '@umami/react-zen';
|
||||
import Script from 'next/script';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { UpdateNotice } from './UpdateNotice';
|
||||
|
|
@ -30,20 +30,12 @@ export function App({ children }) {
|
|||
}
|
||||
|
||||
return (
|
||||
<Grid height="100vh" width="100%" columns="auto 1fr" rows="auto 1fr" backgroundColor="2">
|
||||
<Column gridColumn="1 / 2" gridRow="1 / 3" backgroundColor>
|
||||
<Grid height="100vh" width="100%" columns="auto 1fr" backgroundColor="2">
|
||||
<Column>
|
||||
<SideNav />
|
||||
</Column>
|
||||
<Row gridColumn="2 / 3" gridRow="1 / 2">
|
||||
<Column alignItems="center" overflow="auto" position="relative">
|
||||
<TopNav />
|
||||
</Row>
|
||||
<Column
|
||||
gridColumn="2 / 3"
|
||||
gridRow="2 / 3"
|
||||
alignItems="center"
|
||||
overflow="auto"
|
||||
position="relative"
|
||||
>
|
||||
{children}
|
||||
</Column>
|
||||
<UpdateNotice user={user} config={config} />
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
import Link from 'next/link';
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarHeader,
|
||||
SidebarSection,
|
||||
SidebarItem,
|
||||
Button,
|
||||
Icon,
|
||||
SidebarHeader,
|
||||
Row,
|
||||
SidebarProps,
|
||||
} from '@umami/react-zen';
|
||||
import {
|
||||
Globe,
|
||||
|
|
@ -16,14 +15,14 @@ import {
|
|||
Grid2X2,
|
||||
Settings,
|
||||
LockKeyhole,
|
||||
PanelLeft,
|
||||
} from '@/components/icons';
|
||||
import { useMessages, useNavigation, useGlobalState } from '@/components/hooks';
|
||||
import { WebsiteNav } from '@/app/(main)/websites/[websiteId]/WebsiteNav';
|
||||
|
||||
export function SideNav(props: any) {
|
||||
export function SideNav(props: SidebarProps) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { pathname, renderUrl } = useNavigation();
|
||||
const [isCollapsed, setCollapsed] = useGlobalState('sidenav-collapsed');
|
||||
const { pathname, renderUrl, websiteId } = useNavigation();
|
||||
const [isCollapsed] = useGlobalState('sidenav-collapsed');
|
||||
|
||||
const links = [
|
||||
{
|
||||
|
|
@ -68,37 +67,37 @@ export function SideNav(props: any) {
|
|||
];
|
||||
|
||||
return (
|
||||
<Sidebar {...props} isCollapsed={isCollapsed} variant="quiet" showBorder={true}>
|
||||
<SidebarSection>
|
||||
<SidebarHeader label="umami" icon={<Logo />} />
|
||||
</SidebarSection>
|
||||
<SidebarSection flexGrow={1}>
|
||||
{links.map(({ id, path, label, icon }) => {
|
||||
return (
|
||||
<Link key={id} href={renderUrl(path, false)} role="button">
|
||||
<SidebarItem label={label} icon={icon} isSelected={pathname.endsWith(path)} />
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</SidebarSection>
|
||||
<SidebarSection>
|
||||
{bottomLinks.map(({ id, path, label, icon }) => {
|
||||
return (
|
||||
<Link key={id} href={path} role="button">
|
||||
<SidebarItem label={label} icon={icon} isSelected={pathname.startsWith(path)} />
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</SidebarSection>
|
||||
<SidebarSection>
|
||||
<Row>
|
||||
<Button onPress={() => setCollapsed(!isCollapsed)} variant="quiet">
|
||||
<Icon>
|
||||
<PanelLeft />
|
||||
</Icon>
|
||||
</Button>
|
||||
</Row>
|
||||
</SidebarSection>
|
||||
</Sidebar>
|
||||
<Row height="100%" margin="2" backgroundColor border borderRadius>
|
||||
<Sidebar
|
||||
{...props}
|
||||
isCollapsed={isCollapsed || websiteId}
|
||||
muteItems={false}
|
||||
variant="quiet"
|
||||
showBorder={false}
|
||||
>
|
||||
<SidebarSection>
|
||||
<SidebarHeader label="umami" icon={<Logo />} />
|
||||
</SidebarSection>
|
||||
<SidebarSection flexGrow={1}>
|
||||
{links.map(({ id, path, label, icon }) => {
|
||||
return (
|
||||
<Link key={id} href={renderUrl(path, false)} role="button">
|
||||
<SidebarItem label={label} icon={icon} isSelected={pathname.endsWith(path)} />
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</SidebarSection>
|
||||
<SidebarSection>
|
||||
{bottomLinks.map(({ id, path, label, icon }) => {
|
||||
return (
|
||||
<Link key={id} href={path} role="button">
|
||||
<SidebarItem label={label} icon={icon} isSelected={pathname.startsWith(path)} />
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</SidebarSection>
|
||||
</Sidebar>
|
||||
{websiteId && <WebsiteNav websiteId={websiteId} />}
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { TeamsButton } from '@/components/input/TeamsButton';
|
|||
import { WebsiteSelect } from '@/components/input/WebsiteSelect';
|
||||
import { Slash } from '@/components/icons';
|
||||
import { useNavigation } from '@/components/hooks';
|
||||
import { PanelButton } from '@/components/input/PanelButton';
|
||||
|
||||
export function TopNav() {
|
||||
const { teamId, websiteId, pathname } = useNavigation();
|
||||
|
|
@ -14,21 +15,27 @@ export function TopNav() {
|
|||
<Row
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
paddingY="3"
|
||||
paddingY="2"
|
||||
paddingX="3"
|
||||
paddingRight="5"
|
||||
border="bottom"
|
||||
width="100%"
|
||||
style={{ position: 'sticky', top: 0 }}
|
||||
backgroundColor="2"
|
||||
zIndex={1}
|
||||
>
|
||||
<Row alignItems="center">
|
||||
<PanelButton />
|
||||
<Seperator />
|
||||
<Row alignItems="center" gap="1">
|
||||
<TeamsButton />
|
||||
{websiteId && !isSettings && (
|
||||
<>
|
||||
<Icon strokeColor="7" rotate={-25}>
|
||||
<Slash />
|
||||
</Icon>
|
||||
<WebsiteSelect variant="quiet" websiteId={websiteId} teamId={teamId} />
|
||||
<Seperator />
|
||||
<WebsiteSelect
|
||||
buttonProps={{ variant: 'quiet' }}
|
||||
websiteId={websiteId}
|
||||
teamId={teamId}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Row>
|
||||
|
|
@ -41,3 +48,11 @@ export function TopNav() {
|
|||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
const Seperator = () => {
|
||||
return (
|
||||
<Icon strokeColor="7" rotate={-25}>
|
||||
<Slash />
|
||||
</Icon>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,14 +1,13 @@
|
|||
'use client';
|
||||
import { ReactNode } from 'react';
|
||||
import { Grid, Column } from '@umami/react-zen';
|
||||
import { useLoginQuery, useMessages, useNavigation } from '@/components/hooks';
|
||||
import { useMessages, useNavigation } from '@/components/hooks';
|
||||
import { SideMenu } from '@/components/common/SideMenu';
|
||||
import { PageHeader } from '@/components/common/PageHeader';
|
||||
import { Panel } from '@/components/common/Panel';
|
||||
import { PageBody } from '@/components/common/PageBody';
|
||||
|
||||
export function SettingsLayout({ children }: { children: ReactNode }) {
|
||||
const { user } = useLoginQuery();
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { pathname } = useNavigation();
|
||||
|
||||
|
|
@ -23,13 +22,13 @@ export function SettingsLayout({ children }: { children: ReactNode }) {
|
|||
label: formatMessage(labels.profile),
|
||||
url: '/settings/profile',
|
||||
},
|
||||
{ id: 'teams', label: formatMessage(labels.teams), url: '/settings/teams' },
|
||||
user.isAdmin && {
|
||||
{
|
||||
id: 'websites',
|
||||
label: formatMessage(labels.websites),
|
||||
url: '/settings/websites',
|
||||
},
|
||||
].filter(n => n);
|
||||
{ id: 'teams', label: formatMessage(labels.teams), url: '/settings/teams' },
|
||||
];
|
||||
|
||||
const value = items.find(({ url }) => pathname.includes(url))?.id;
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ export function WebsiteHeader() {
|
|||
const website = useWebsite();
|
||||
|
||||
return (
|
||||
<PageHeader title={website.name} icon={<Favicon domain={website.domain} />}>
|
||||
<PageHeader title={website.name} icon={<Favicon domain={website.domain} />} marginBottom="3">
|
||||
<Row alignItems="center" gap="6">
|
||||
<ActiveUsers websiteId={website.id} />
|
||||
<Row alignItems="center" gap>
|
||||
|
|
|
|||
|
|
@ -1,25 +1,17 @@
|
|||
'use client';
|
||||
import { ReactNode } from 'react';
|
||||
import { Column, Grid } from '@umami/react-zen';
|
||||
import { Column } from '@umami/react-zen';
|
||||
import { WebsiteProvider } from './WebsiteProvider';
|
||||
import { PageBody } from '@/components/common/PageBody';
|
||||
import { WebsiteHeader } from './WebsiteHeader';
|
||||
import { WebsiteNav } from '@/app/(main)/websites/[websiteId]/WebsiteNav';
|
||||
|
||||
export function WebsiteLayout({ websiteId, children }: { websiteId: string; children: ReactNode }) {
|
||||
return (
|
||||
<WebsiteProvider websiteId={websiteId}>
|
||||
<Grid columns="200px 1fr" gap width="100%">
|
||||
<Column padding>
|
||||
<WebsiteNav websiteId={websiteId} />
|
||||
</Column>
|
||||
<PageBody>
|
||||
<Column gap>
|
||||
<WebsiteHeader />
|
||||
<Column>{children}</Column>
|
||||
</Column>
|
||||
</PageBody>
|
||||
</Grid>
|
||||
<PageBody gap>
|
||||
<WebsiteHeader />
|
||||
<Column>{children}</Column>
|
||||
</PageBody>
|
||||
</WebsiteProvider>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Icon, Text, Row, NavMenu, NavMenuItem, NavMenuGroup } from '@umami/react-zen';
|
||||
import { Icon, Text, Row, NavMenu, NavMenuItem, NavMenuGroup, Column } from '@umami/react-zen';
|
||||
import {
|
||||
Eye,
|
||||
Lightning,
|
||||
|
|
@ -12,6 +12,8 @@ import {
|
|||
Tag,
|
||||
Money,
|
||||
Network,
|
||||
UserPlus,
|
||||
ChartPie,
|
||||
} from '@/components/icons';
|
||||
import { useMessages, useNavigation } from '@/components/hooks';
|
||||
import Link from 'next/link';
|
||||
|
|
@ -22,7 +24,7 @@ export function WebsiteNav({ websiteId }: { websiteId: string }) {
|
|||
|
||||
const links = [
|
||||
{
|
||||
label: formatMessage(labels.core),
|
||||
label: formatMessage(labels.traffic),
|
||||
items: [
|
||||
{
|
||||
id: 'overview',
|
||||
|
|
@ -88,6 +90,18 @@ export function WebsiteNav({ websiteId }: { websiteId: string }) {
|
|||
icon: <Sheet />,
|
||||
path: '/breakdown',
|
||||
},
|
||||
{
|
||||
id: 'segments',
|
||||
label: formatMessage(labels.segments),
|
||||
icon: <ChartPie />,
|
||||
path: '/segments',
|
||||
},
|
||||
{
|
||||
id: 'cohorts',
|
||||
label: formatMessage(labels.cohorts),
|
||||
icon: <UserPlus />,
|
||||
path: '/cohorts',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
@ -120,27 +134,29 @@ export function WebsiteNav({ websiteId }: { websiteId: string }) {
|
|||
'overview';
|
||||
|
||||
return (
|
||||
<NavMenu highlightColor="3" style={{ position: 'sticky', top: 'var(--spacing-2)' }}>
|
||||
{links.map(({ label, items }) => {
|
||||
return (
|
||||
<NavMenuGroup title={label} key={label} gap="2">
|
||||
{items.map(({ id, label, icon, path }) => {
|
||||
const isSelected = selected === id;
|
||||
<Column gap padding width="240px" border="left">
|
||||
<NavMenu highlightColor="2">
|
||||
{links.map(({ label, items }) => {
|
||||
return (
|
||||
<NavMenuGroup title={label} key={label} gap="1">
|
||||
{items.map(({ id, label, icon, path }) => {
|
||||
const isSelected = selected === id;
|
||||
|
||||
return (
|
||||
<Link key={id} href={renderUrl(`/websites/${websiteId}${path}`)}>
|
||||
<NavMenuItem isSelected={isSelected}>
|
||||
<Row alignItems="center" gap>
|
||||
<Icon>{icon}</Icon>
|
||||
<Text>{label}</Text>
|
||||
</Row>
|
||||
</NavMenuItem>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</NavMenuGroup>
|
||||
);
|
||||
})}
|
||||
</NavMenu>
|
||||
return (
|
||||
<Link key={id} href={renderUrl(`/websites/${websiteId}${path}`)}>
|
||||
<NavMenuItem isSelected={isSelected}>
|
||||
<Row alignItems="center" gap>
|
||||
<Icon>{icon}</Icon>
|
||||
<Text>{label}</Text>
|
||||
</Row>
|
||||
</NavMenuItem>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</NavMenuGroup>
|
||||
);
|
||||
})}
|
||||
</NavMenu>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,41 +28,43 @@ export function EventProperties({ websiteId }: { websiteId: string }) {
|
|||
|
||||
return (
|
||||
<LoadingPanel
|
||||
data={data}
|
||||
isLoading={isLoading}
|
||||
isFetching={isFetching}
|
||||
data={data}
|
||||
error={error}
|
||||
minHeight="300px"
|
||||
gap="6"
|
||||
>
|
||||
<Grid columns="repeat(auto-fill, minmax(300px, 1fr))" gap>
|
||||
<Select
|
||||
label={formatMessage(labels.event)}
|
||||
value={eventName}
|
||||
onChange={setEventName}
|
||||
placeholder=""
|
||||
>
|
||||
{events?.map(p => (
|
||||
<ListItem key={p} id={p}>
|
||||
{p}
|
||||
</ListItem>
|
||||
))}
|
||||
</Select>
|
||||
<Select
|
||||
label={formatMessage(labels.property)}
|
||||
value={propertyName}
|
||||
onChange={setPropertyName}
|
||||
isDisabled={!eventName}
|
||||
placeholder=""
|
||||
>
|
||||
{properties?.map(p => (
|
||||
<ListItem key={p} id={p}>
|
||||
{p}
|
||||
</ListItem>
|
||||
))}
|
||||
</Select>
|
||||
</Grid>
|
||||
{propertyName && (
|
||||
{data && (
|
||||
<Grid columns="repeat(auto-fill, minmax(300px, 1fr))" gap>
|
||||
<Select
|
||||
label={formatMessage(labels.event)}
|
||||
value={eventName}
|
||||
onChange={setEventName}
|
||||
placeholder=""
|
||||
>
|
||||
{events?.map(p => (
|
||||
<ListItem key={p} id={p}>
|
||||
{p}
|
||||
</ListItem>
|
||||
))}
|
||||
</Select>
|
||||
<Select
|
||||
label={formatMessage(labels.property)}
|
||||
value={propertyName}
|
||||
onChange={setPropertyName}
|
||||
isDisabled={!eventName}
|
||||
placeholder=""
|
||||
>
|
||||
{properties?.map(p => (
|
||||
<ListItem key={p} id={p}>
|
||||
{p}
|
||||
</ListItem>
|
||||
))}
|
||||
</Select>
|
||||
</Grid>
|
||||
)}
|
||||
{eventName && propertyName && (
|
||||
<EventValues websiteId={websiteId} eventName={eventName} propertyName={propertyName} />
|
||||
)}
|
||||
</LoadingPanel>
|
||||
|
|
@ -113,10 +115,12 @@ const EventValues = ({ websiteId, eventName, propertyName }) => {
|
|||
minHeight="300px"
|
||||
gap="6"
|
||||
>
|
||||
<Grid columns="1fr 1fr" gap>
|
||||
{values && <ListTable title={propertyName} data={tableData} />}
|
||||
<PieChart key={propertyName + eventName} type="doughnut" chartData={chartData} />
|
||||
</Grid>
|
||||
{values && (
|
||||
<Grid columns="1fr 1fr" gap>
|
||||
<ListTable title={propertyName} data={tableData} />
|
||||
<PieChart type="doughnut" chartData={chartData} />
|
||||
</Grid>
|
||||
)}
|
||||
</LoadingPanel>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -26,20 +26,22 @@ export function SessionProperties({ websiteId }: { websiteId: string }) {
|
|||
minHeight="300px"
|
||||
gap="6"
|
||||
>
|
||||
<Grid columns="repeat(auto-fill, minmax(300px, 1fr))" gap>
|
||||
<Select
|
||||
label={formatMessage(labels.event)}
|
||||
value={propertyName}
|
||||
onChange={setPropertyName}
|
||||
placeholder=""
|
||||
>
|
||||
{properties?.map(p => (
|
||||
<ListItem key={p} id={p}>
|
||||
{p}
|
||||
</ListItem>
|
||||
))}
|
||||
</Select>
|
||||
</Grid>
|
||||
{data && (
|
||||
<Grid columns="repeat(auto-fill, minmax(300px, 1fr))" gap>
|
||||
<Select
|
||||
label={formatMessage(labels.event)}
|
||||
value={propertyName}
|
||||
onChange={setPropertyName}
|
||||
placeholder=""
|
||||
>
|
||||
{properties?.map(p => (
|
||||
<ListItem key={p} id={p}>
|
||||
{p}
|
||||
</ListItem>
|
||||
))}
|
||||
</Select>
|
||||
</Grid>
|
||||
)}
|
||||
{propertyName && <SessionValues websiteId={websiteId} propertyName={propertyName} />}
|
||||
</LoadingPanel>
|
||||
);
|
||||
|
|
@ -84,10 +86,12 @@ const SessionValues = ({ websiteId, propertyName }) => {
|
|||
minHeight="300px"
|
||||
gap="6"
|
||||
>
|
||||
<Grid columns="1fr 1fr" gap>
|
||||
{data && <ListTable title={propertyName} data={tableData} />}
|
||||
<PieChart key={propertyName} type="doughnut" chartData={chartData} />
|
||||
</Grid>
|
||||
{data && (
|
||||
<Grid columns="1fr 1fr" gap>
|
||||
<ListTable title={propertyName} data={tableData} />
|
||||
<PieChart type="doughnut" chartData={chartData} />
|
||||
</Grid>
|
||||
)}
|
||||
</LoadingPanel>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ export function Empty({ message }: EmptyProps) {
|
|||
width="100%"
|
||||
height="100%"
|
||||
minHeight="70px"
|
||||
flexGrow={1}
|
||||
>
|
||||
{message || formatMessage(messages.noDataAvailable)}
|
||||
</Row>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { ReactNode } from 'react';
|
||||
import { Heading, Icon, Row, Text } from '@umami/react-zen';
|
||||
import { Heading, Icon, Row, RowProps, Text } from '@umami/react-zen';
|
||||
|
||||
export function PageHeader({
|
||||
title,
|
||||
|
|
@ -7,6 +7,7 @@ export function PageHeader({
|
|||
icon,
|
||||
showBorder = true,
|
||||
children,
|
||||
...props
|
||||
}: {
|
||||
title: string;
|
||||
description?: string;
|
||||
|
|
@ -15,7 +16,7 @@ export function PageHeader({
|
|||
allowEdit?: boolean;
|
||||
className?: string;
|
||||
children?: ReactNode;
|
||||
}) {
|
||||
} & RowProps) {
|
||||
return (
|
||||
<Row
|
||||
justifyContent="space-between"
|
||||
|
|
@ -23,6 +24,7 @@ export function PageHeader({
|
|||
paddingY="6"
|
||||
border={showBorder ? 'bottom' : undefined}
|
||||
width="100%"
|
||||
{...props}
|
||||
>
|
||||
<Row alignItems="center" gap="3">
|
||||
{icon && <Icon>{icon}</Icon>}
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ export {
|
|||
Upload,
|
||||
User,
|
||||
Users,
|
||||
UserPlus,
|
||||
X as Close,
|
||||
} from 'lucide-react';
|
||||
export * from '@/components/svg';
|
||||
|
|
|
|||
14
src/components/input/PanelButton.tsx
Normal file
14
src/components/input/PanelButton.tsx
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import { Button, Icon } from '@umami/react-zen';
|
||||
import { PanelLeft } from '@/components/icons';
|
||||
import { useGlobalState } from '@/components/hooks';
|
||||
|
||||
export function PanelButton() {
|
||||
const [isCollapsed, setIsCollapsed] = useGlobalState('sidenav-collapsed');
|
||||
return (
|
||||
<Button onPress={() => setIsCollapsed(!isCollapsed)} variant="quiet">
|
||||
<Icon>
|
||||
<PanelLeft />
|
||||
</Icon>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
|
@ -16,13 +16,7 @@ import {
|
|||
import { useLoginQuery, useMessages, useUserTeamsQuery, useNavigation } from '@/components/hooks';
|
||||
import { Chevron, User, Users } from '@/components/icons';
|
||||
|
||||
export function TeamsButton({
|
||||
className,
|
||||
showText = true,
|
||||
}: {
|
||||
className?: string;
|
||||
showText?: boolean;
|
||||
}) {
|
||||
export function TeamsButton({ showText = true }: { showText?: boolean }) {
|
||||
const { user } = useLoginQuery();
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { data } = useUserTeamsQuery(user.id);
|
||||
|
|
@ -41,13 +35,17 @@ export function TeamsButton({
|
|||
|
||||
return (
|
||||
<MenuTrigger>
|
||||
<Button className={className} variant="quiet">
|
||||
<Row alignItems="center" gap="3">
|
||||
<Icon>{teamId ? <Users /> : <User />}</Icon>
|
||||
{showText && <Text>{teamId ? team?.name : user.username}</Text>}
|
||||
<Icon rotate={90} size="sm">
|
||||
<Chevron />
|
||||
</Icon>
|
||||
<Button variant="quiet">
|
||||
<Row alignItems="center" justifyContent="space-between" width="100%" gap>
|
||||
<Row alignItems="center" gap>
|
||||
<Icon>{teamId ? <Users /> : <User />}</Icon>
|
||||
{showText && <Text truncate>{teamId ? team?.name : user.username}</Text>}
|
||||
</Row>
|
||||
{showText && (
|
||||
<Icon rotate={90} size="sm">
|
||||
<Chevron />
|
||||
</Icon>
|
||||
)}
|
||||
</Row>
|
||||
</Button>
|
||||
<Popover placement="bottom start">
|
||||
|
|
|
|||
|
|
@ -1,16 +1,17 @@
|
|||
import { useState } from 'react';
|
||||
import { Select, SelectProps, ListItem } from '@umami/react-zen';
|
||||
import { useUserWebsitesQuery, useWebsiteQuery, useNavigation } from '@/components/hooks';
|
||||
import { ButtonProps } from 'react-basics';
|
||||
|
||||
export function WebsiteSelect({
|
||||
websiteId,
|
||||
teamId,
|
||||
variant,
|
||||
buttonProps,
|
||||
...props
|
||||
}: {
|
||||
websiteId?: string;
|
||||
teamId?: string;
|
||||
variant?: 'primary' | 'outline' | 'quiet' | 'danger' | 'zero';
|
||||
buttonProps?: ButtonProps;
|
||||
} & SelectProps) {
|
||||
const { router, renderUrl } = useNavigation();
|
||||
const [search, setSearch] = useState('');
|
||||
|
|
@ -32,7 +33,7 @@ export function WebsiteSelect({
|
|||
items={data?.['data'] || []}
|
||||
value={websiteId}
|
||||
isLoading={isLoading}
|
||||
buttonProps={{ variant }}
|
||||
buttonProps={buttonProps}
|
||||
allowSearch={true}
|
||||
searchValue={search}
|
||||
onSearch={handleSearch}
|
||||
|
|
|
|||
|
|
@ -338,7 +338,7 @@ export const labels = defineMessages({
|
|||
location: { id: 'label.location', defaultMessage: 'Location' },
|
||||
chart: { id: 'label.chart', defaultMessage: 'Chart' },
|
||||
table: { id: 'label.table', defaultMessage: 'Table' },
|
||||
core: { id: 'label.core', defaultMessage: 'Core' },
|
||||
traffic: { id: 'label.traffic', defaultMessage: 'Traffic' },
|
||||
behavior: { id: 'label.behavior', defaultMessage: 'Behavior' },
|
||||
growth: { id: 'label.growth', defaultMessage: 'Growth' },
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue