Responsive everything.

This commit is contained in:
Mike Cao 2025-10-16 02:42:13 -07:00
parent 9df012084d
commit 4d2a7ea947
23 changed files with 286 additions and 287 deletions

View file

@ -1,6 +1,6 @@
{
"name": "@umami/components",
"version": "0.129.0",
"version": "0.130.0",
"description": "Umami React components.",
"author": "Mike Cao <mike@mikecao.com>",
"license": "MIT",

View file

@ -78,7 +78,7 @@
"@react-spring/web": "^10.0.3",
"@svgr/cli": "^8.1.0",
"@tanstack/react-query": "^5.90.2",
"@umami/react-zen": "^0.196.0",
"@umami/react-zen": "^0.198.0",
"@umami/redis-client": "^0.29.0",
"bcryptjs": "^3.0.2",
"chalk": "^5.6.2",

12
pnpm-lock.yaml generated
View file

@ -45,8 +45,8 @@ importers:
specifier: ^5.90.2
version: 5.90.2(react@19.1.1)
'@umami/react-zen':
specifier: ^0.196.0
version: 0.196.0(@babel/core@7.28.3)(@types/react@19.1.16)(babel-plugin-react-compiler@19.1.0-rc.2)(immer@10.1.3)(use-sync-external-store@1.6.0(react@19.1.1))
specifier: ^0.198.0
version: 0.198.0(@babel/core@7.28.3)(@types/react@19.1.16)(babel-plugin-react-compiler@19.1.0-rc.2)(immer@10.1.3)(use-sync-external-store@1.6.0(react@19.1.1))
'@umami/redis-client':
specifier: ^0.29.0
version: 0.29.0
@ -364,6 +364,8 @@ importers:
specifier: ^5.9.3
version: 5.9.3
dist: {}
packages:
'@ampproject/remapping@2.3.0':
@ -2754,8 +2756,8 @@ packages:
resolution: {integrity: sha512-qsaFBA3e09MIDAGFUrTk+dzqtfv1XPVz8t8d1f0ybTzrCY7BKiMC5cjrl1O/P7UmHsNyW90EYSkU/ZWpmXelag==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@umami/react-zen@0.196.0':
resolution: {integrity: sha512-CLxrDAJOdo+0aJAclOq7naIDg+2I5wP9wXxAFhxhQVPXHV8yUHqH9Ula632cLMo51JYp0l+eEtOtuimpuKX3jg==}
'@umami/react-zen@0.198.0':
resolution: {integrity: sha512-usxXJPz5XnHzJ3Qo2RD0UZw+De/1QOx1f0SJDZNx8TZGLBNFqUHTAZgwOZANP7JgkS9cd4Q+WSobqP4JOp2saQ==}
'@umami/redis-client@0.29.0':
resolution: {integrity: sha512-Jaqh++jskqDB7ny75pfC02OvKp1JTS4asGDsFrRL3qy8sxL3PAl9+/mybCJe4/6vWrXDJKqpgkSfUDJq2bFjyw==}
@ -10385,7 +10387,7 @@ snapshots:
'@typescript-eslint/types': 8.45.0
eslint-visitor-keys: 4.2.1
'@umami/react-zen@0.196.0(@babel/core@7.28.3)(@types/react@19.1.16)(babel-plugin-react-compiler@19.1.0-rc.2)(immer@10.1.3)(use-sync-external-store@1.6.0(react@19.1.1))':
'@umami/react-zen@0.198.0(@babel/core@7.28.3)(@types/react@19.1.16)(babel-plugin-react-compiler@19.1.0-rc.2)(immer@10.1.3)(use-sync-external-store@1.6.0(react@19.1.1))':
dependencies:
'@fontsource/jetbrains-mono': 5.2.8
'@internationalized/date': 3.10.0

View file

@ -1,22 +1,11 @@
import {
Row,
Dialog,
DialogTrigger,
Button,
Icon,
Modal,
NavMenu,
NavMenuItem,
IconLabel,
Text,
Grid,
} from '@umami/react-zen';
import { Globe, Grid2x2, LinkIcon, Menu } from '@/components/icons';
import { Row, NavMenu, NavMenuItem, IconLabel, Text, Grid } from '@umami/react-zen';
import { Globe, Grid2x2, LinkIcon } from '@/components/icons';
import { useMessages, useNavigation } from '@/components/hooks';
import Link from 'next/link';
import { WebsiteNav } from '@/app/(main)/websites/[websiteId]/WebsiteNav';
import { Logo } from '@/components/svg';
import { NavButton } from '@/components/input/NavButton';
import { MobileMenu } from '@/components/common/MobileMenu';
export function MobileNav() {
const { formatMessage, labels } = useMessages();
@ -45,30 +34,27 @@ export function MobileNav() {
return (
<Grid columns="auto 1fr" flexGrow={1}>
<DialogTrigger>
<Button>
<Icon>
<Menu />
</Icon>
</Button>
<Modal position="left" offset="80px">
<Dialog variant="sheet">
<NavMenu padding="3">
<NavButton />
{links.map(link => {
return (
<Link key={link.id} href={link.path}>
<NavMenuItem>
<IconLabel icon={link.icon} label={link.label} />
</NavMenuItem>
</Link>
);
})}
</NavMenu>
{websiteId && <WebsiteNav websiteId={websiteId} />}
</Dialog>
</Modal>
</DialogTrigger>
<MobileMenu>
{({ close }) => {
return (
<>
<NavMenu padding="3" onItemClick={close} border="bottom">
<NavButton />
{links.map(link => {
return (
<Link key={link.id} href={link.path}>
<NavMenuItem>
<IconLabel icon={link.icon} label={link.label} />
</NavMenuItem>
</Link>
);
})}
</NavMenu>
{websiteId && <WebsiteNav websiteId={websiteId} onItemClick={close} />}
</>
);
}}
</MobileMenu>
<Row alignItems="center" justifyContent="center" flexGrow={1}>
<IconLabel icon={<Logo />} style={{ width: 'auto' }}>
<Text weight="bold">umami</Text>

View file

@ -1,4 +1,4 @@
import { Dialog, Modal } from '@umami/react-zen';
import { Dialog, Modal, useBreakpoint } from '@umami/react-zen';
import { WebsiteExpandedView } from '@/app/(main)/websites/[websiteId]/WebsiteExpandedView';
import { useNavigation } from '@/components/hooks';
@ -14,6 +14,8 @@ export function ExpandedViewModal({
query: { view },
updateParams,
} = useNavigation();
const breakpoint = useBreakpoint();
const isMobile = ['xs', 'sm', 'md'].includes(breakpoint);
const handleClose = (close: () => void) => {
router.push(updateParams({ view: undefined }));
@ -28,7 +30,13 @@ export function ExpandedViewModal({
return (
<Modal isOpen={!!view} onOpenChange={handleOpenChange} isDismissable>
<Dialog style={{ maxWidth: 1320, width: '100vw', height: 'calc(100vh - 40px)' }}>
<Dialog
style={{
maxWidth: 1320,
width: '100vw',
height: isMobile ? '100dvh' : 'calc(100dvh - 40px)',
}}
>
{({ close }) => {
return (
<WebsiteExpandedView

View file

@ -22,7 +22,13 @@ import {
} from '@/components/icons';
import { Lightning } from '@/components/svg';
export function WebsiteExpandedMenu({ excludedIds = [] }: { excludedIds?: string[] }) {
export function WebsiteExpandedMenu({
excludedIds = [],
onItemClick,
}: {
excludedIds?: string[];
onItemClick?: () => void;
}) {
const { formatMessage, labels } = useMessages();
const {
updateParams,
@ -173,5 +179,5 @@ export function WebsiteExpandedMenu({ excludedIds = [] }: { excludedIds?: string
},
];
return <SideMenu items={items} selectedKey={view} />;
return <SideMenu items={items} selectedKey={view} onItemClick={onItemClick} />;
}

View file

@ -1,7 +1,8 @@
import { Grid, Column } from '@umami/react-zen';
import { Grid, Column, Row } from '@umami/react-zen';
import { useMessages, useNavigation } from '@/components/hooks';
import { MetricsExpandedTable } from '@/components/metrics/MetricsExpandedTable';
import { WebsiteExpandedMenu } from '@/app/(main)/websites/[websiteId]/WebsiteExpandedMenu';
import { MobileMenu } from '@/components/common/MobileMenu';
export function WebsiteExpandedView({
websiteId,
@ -18,24 +19,33 @@ export function WebsiteExpandedView({
} = useNavigation();
return (
<Grid columns={{ xs: '1fr', md: 'auto 1fr' }} gap="6" height="100%" overflow="hidden">
<Column
display={{ xs: 'none', md: 'flex' }}
gap="6"
border="right"
paddingRight="3"
overflowY="auto"
>
<WebsiteExpandedMenu excludedIds={excludedIds} />
</Column>
<Column overflow="hidden">
<MetricsExpandedTable
title={formatMessage(labels[view])}
type={view}
websiteId={websiteId}
onClose={onClose}
/>
</Column>
</Grid>
<Column gap>
<Row display={{ xs: 'flex', md: 'none' }}>
<MobileMenu>
{({ close }) => {
return <WebsiteExpandedMenu excludedIds={excludedIds} onItemClick={close} />;
}}
</MobileMenu>
</Row>
<Grid columns={{ xs: '1fr', md: 'auto 1fr' }} gap="6" height="100%" overflow="hidden">
<Column
display={{ xs: 'none', md: 'flex' }}
gap="6"
border="right"
paddingRight="3"
overflow="auto"
>
<WebsiteExpandedMenu excludedIds={excludedIds} />
</Column>
<Column overflow="hidden">
<MetricsExpandedTable
title={formatMessage(labels[view])}
type={view}
websiteId={websiteId}
onClose={onClose}
/>
</Column>
</Grid>
</Column>
);
}

View file

@ -1,4 +1,4 @@
import { Text } from '@umami/react-zen';
import { Text, Column } from '@umami/react-zen';
import {
Eye,
User,
@ -14,7 +14,13 @@ import { useMessages, useNavigation } from '@/components/hooks';
import { SideMenu } from '@/components/common/SideMenu';
import { WebsiteSelect } from '@/components/input/WebsiteSelect';
export function WebsiteNav({ websiteId }: { websiteId: string }) {
export function WebsiteNav({
websiteId,
onItemClick,
}: {
websiteId: string;
onItemClick?: () => void;
}) {
const { formatMessage, labels } = useMessages();
const { pathname, renderUrl, teamId, router } = useNavigation();
@ -155,7 +161,7 @@ export function WebsiteNav({ websiteId }: { websiteId: string }) {
.find(({ path }) => path && pathname.endsWith(path.split('?')[0]))?.id;
return (
<SideMenu items={items} selectedKey={selectedKey} allowMinimize={false}>
<Column padding="3" gap>
<WebsiteSelect
websiteId={websiteId}
teamId={teamId}
@ -163,6 +169,12 @@ export function WebsiteNav({ websiteId }: { websiteId: string }) {
renderValue={renderValue}
buttonProps={{ style: { outline: 'none' } }}
/>
</SideMenu>
<SideMenu
items={items}
selectedKey={selectedKey}
allowMinimize={false}
onItemClick={onItemClick}
/>
</Column>
);
}

View file

@ -1,9 +1,8 @@
import { ReactNode } from 'react';
import { Icon, TextField, Column, Row, Label } from '@umami/react-zen';
import { Icon, Grid, Column, Row, Label } from '@umami/react-zen';
import { useFormat, useLocale, useMessages, useRegionNames } from '@/components/hooks';
import { TypeIcon } from '@/components/common/TypeIcon';
import { KeyRound, Calendar } from '@/components/icons';
import { Location } from '@/components/svg';
import { KeyRound, Calendar, MapPin, Landmark } from '@/components/icons';
import { DateDistance } from '@/components/common/DateDistance';
export function SessionInfo({ data }) {
@ -13,11 +12,7 @@ export function SessionInfo({ data }) {
const { getRegionName } = useRegionNames(locale);
return (
<Column gap="6">
<Info label="ID">
<TextField value={data?.id} style={{ width: '100%' }} allowCopy />
</Info>
<Grid columns="repeat(auto-fit, minmax(200px, 1fr)" gap>
<Info label={formatMessage(labels.distinctId)} icon={<KeyRound />}>
{data?.distinctId}
</Info>
@ -37,11 +32,11 @@ export function SessionInfo({ data }) {
{formatValue(data?.country, 'country')}
</Info>
<Info label={formatMessage(labels.region)} icon={<Location />}>
<Info label={formatMessage(labels.region)} icon={<MapPin />}>
{getRegionName(data?.region)}
</Info>
<Info label={formatMessage(labels.city)} icon={<Location />}>
<Info label={formatMessage(labels.city)} icon={<Landmark />}>
{data?.city}
</Info>
@ -65,7 +60,7 @@ export function SessionInfo({ data }) {
>
{formatValue(data?.device, 'device')}
</Info>
</Column>
</Grid>
);
}

View file

@ -1,4 +1,14 @@
import { Grid, Row, Column, Tabs, TabList, Tab, TabPanel, Icon, Button } from '@umami/react-zen';
import {
TextField,
Row,
Column,
Tabs,
TabList,
Tab,
TabPanel,
Icon,
Button,
} from '@umami/react-zen';
import { Avatar } from '@/components/common/Avatar';
import { LoadingPanel } from '@/components/common/LoadingPanel';
import { X } from '@/components/icons';
@ -37,35 +47,34 @@ export function SessionProfile({
</Icon>
</Button>
</Row>
<Grid columns="340px 1fr" gap="6">
<Column gap="6">
<Row justifyContent="center">
<Avatar seed={data?.id} size={128} />
</Row>
<SessionInfo data={data} />
</Column>
<Column gap>
<SessionStats data={data} />
<Column gap="6">
<Row justifyContent="center" alignItems="center" gap="6">
<Avatar seed={data?.id} size={128} />
<Column width="360px">
<TextField label="ID" value={data?.id} allowCopy />
</Column>
</Row>
<SessionStats data={data} />
<SessionInfo data={data} />
<Tabs>
<TabList>
<Tab id="activity">{formatMessage(labels.activity)}</Tab>
<Tab id="properties">{formatMessage(labels.properties)}</Tab>
</TabList>
<TabPanel id="activity">
<SessionActivity
websiteId={websiteId}
sessionId={sessionId}
startDate={data?.firstAt}
endDate={data?.lastAt}
/>
</TabPanel>
<TabPanel id="properties">
<SessionData sessionId={sessionId} websiteId={websiteId} />
</TabPanel>
</Tabs>
</Column>
</Grid>
<Tabs>
<TabList>
<Tab id="activity">{formatMessage(labels.activity)}</Tab>
<Tab id="properties">{formatMessage(labels.properties)}</Tab>
</TabList>
<TabPanel id="activity">
<SessionActivity
websiteId={websiteId}
sessionId={sessionId}
startDate={data?.firstAt}
endDate={data?.lastAt}
/>
</TabPanel>
<TabPanel id="properties">
<SessionData sessionId={sessionId} websiteId={websiteId} />
</TabPanel>
</Tabs>
</Column>
</Column>
)}
</LoadingPanel>

View file

@ -65,11 +65,11 @@ export function FilterRecord({
<Column>
<Label>{fields.find(f => f.name === name)?.label}</Label>
<Grid columns="1fr auto" gap>
<Grid columns="200px 1fr" gap>
<Grid columns={{ xs: '1fr', md: '200px 1fr' }} gap>
<Select
items={operators.filter(({ type }) => type === 'string')}
value={operator}
onSelectionChange={handleSelectOperator}
onChange={handleSelectOperator}
>
{({ name, label }: any) => {
return (

View file

@ -1,20 +0,0 @@
import { Button, Icon } from '@umami/react-zen';
import { useState } from 'react';
import { Close, Menu } from '@/components/icons';
import { MobileMenu } from './MobileMenu';
export function HamburgerButton({ menuItems }: { menuItems: any[] }) {
const [active, setActive] = useState(false);
const handleClick = () => setActive(state => !state);
const handleClose = () => setActive(false);
return (
<>
<Button variant="quiet" onClick={handleClick}>
<Icon>{active ? <Close /> : <Menu />}</Icon>
</Button>
{active && <MobileMenu items={menuItems} onClose={handleClose} />}
</>
);
}

View file

@ -26,7 +26,7 @@ export function LoadingPanel({
renderEmpty = () => <Empty />,
children,
...props
}: LoadingPanelProps) {
}: LoadingPanelProps): ReactNode {
const empty = isEmpty ?? checkEmpty(data);
// Show loading spinner only if no data exists

View file

@ -1,39 +0,0 @@
.menu {
position: fixed;
top: 60px;
left: 0;
right: 0;
bottom: 0;
margin: auto;
display: flex;
flex-direction: column;
background-color: var(--base50);
z-index: var(--z-index-popup);
overflow: auto;
}
.items {
display: flex;
flex-direction: column;
}
.item {
font-size: var(--font-size-lg);
font-weight: 700;
line-height: 80px;
padding: 0 40px;
}
a.item {
color: var(--base600);
}
a.item.selected,
.submenu a.item.selected {
color: var(--base900);
}
.submenu a.item {
color: var(--base600);
margin-inline-start: 40px;
}

View file

@ -1,45 +1,17 @@
import { createPortal } from 'react-dom';
import classNames from 'classnames';
import { usePathname } from 'next/navigation';
import Link from 'next/link';
import styles from './MobileMenu.module.css';
import { Dialog, DialogTrigger, Button, Icon, Modal, DialogProps } from '@umami/react-zen';
import { Menu } from '@/components/icons';
export function MobileMenu({
items = [],
onClose,
}: {
items: any[];
className?: string;
onClose: () => void;
}): any {
const pathname = usePathname();
const Items = ({ items, className }: { items: any[]; className?: string }): any => (
<div className={classNames(styles.items, className)}>
{items.map(({ label, url, children }: { label: string; url: string; children: any[] }) => {
const selected = pathname.startsWith(url);
return (
<>
<Link
key={url}
href={url}
className={classNames(styles.item, { [styles.selected]: selected })}
onClick={onClose}
>
{label}
</Link>
{children && <Items items={children} className={styles.submenu} />}
</>
);
})}
</div>
);
return createPortal(
<div className={classNames(styles.menu)}>
<Items items={items} />
</div>,
document.body,
export function MobileMenu(props: DialogProps) {
return (
<DialogTrigger>
<Button>
<Icon>
<Menu />
</Icon>
</Button>
<Modal position="left" offset="80px">
<Dialog variant="sheet" {...props} />
</Modal>
</DialogTrigger>
);
}

View file

@ -1,4 +1,3 @@
import { ReactNode } from 'react';
import {
Heading,
NavMenu,
@ -28,7 +27,6 @@ export interface SideMenuProps extends NavMenuProps {
title?: string;
selectedKey?: string;
allowMinimize?: boolean;
children?: ReactNode;
}
export function SideMenu({
@ -36,7 +34,6 @@ export function SideMenu({
title,
selectedKey,
allowMinimize,
children,
...props
}: SideMenuProps) {
const renderItems = (items: SideMenuData[]) => {
@ -54,16 +51,7 @@ export function SideMenu({
};
return (
<Column
gap
padding
overflowY="auto"
justifyContent="space-between"
position="sticky"
top="0"
backgroundColor
>
{children}
<Column gap overflowY="auto" justifyContent="space-between">
{title && (
<Row padding>
<Heading size="1">{title}</Heading>

View file

@ -5,6 +5,7 @@ export function useFields() {
const fields = [
{ name: 'path', type: 'string', label: formatMessage(labels.path) },
{ name: 'query', type: 'string', label: formatMessage(labels.query) },
{ name: 'title', type: 'string', label: formatMessage(labels.pageTitle) },
{ name: 'referrer', type: 'string', label: formatMessage(labels.referrer) },
{ name: 'browser', type: 'string', label: formatMessage(labels.browser) },

View file

@ -1,5 +1,13 @@
import { useState, Key, Fragment } from 'react';
import { Modal, Select, ListItem, ListSeparator, Dialog, SelectProps } from '@umami/react-zen';
import {
Modal,
Select,
ListItem,
ListSeparator,
Dialog,
SelectProps,
useBreakpoint,
} from '@umami/react-zen';
import { endOfYear } from 'date-fns';
import { DatePickerForm } from '@/components/metrics/DatePickerForm';
import { useMessages } from '@/components/hooks';
@ -11,7 +19,7 @@ export interface DateFilterProps extends SelectProps {
onChange?: (value: string) => void;
showAllTime?: boolean;
renderDate?: boolean;
placement?: string;
placement?: any;
}
export function DateFilter({
@ -25,6 +33,8 @@ export function DateFilter({
const { formatMessage, labels } = useMessages();
const [showPicker, setShowPicker] = useState(false);
const { startDate, endDate } = parseDateRange(value) || {};
const breakpoint = useBreakpoint();
const isMobile = ['xs', 'sm', 'md'].includes(breakpoint);
const options = [
{ label: formatMessage(labels.today), value: '0day' },
@ -109,7 +119,7 @@ export function DateFilter({
placeholder={formatMessage(labels.selectDate)}
onChange={handleChange}
renderValue={renderValue}
popoverProps={{ placement: placement as any }}
popoverProps={{ placement, isNonModal: isMobile }}
>
{options.map(({ label, value, divider }: any) => {
return (

View file

@ -1,7 +1,20 @@
import { Key } from 'react';
import { subMonths, endOfDay } from 'date-fns';
import { Grid, Column, List, ListItem } from '@umami/react-zen';
import {
Grid,
Column,
List,
ListItem,
Row,
Button,
Popover,
MenuTrigger,
Menu,
MenuItem,
Icon,
} from '@umami/react-zen';
import { useFields, useMessages } from '@/components/hooks';
import { Plus } from '@/components/icons';
import { FilterRecord } from '@/components/common/FilterRecord';
import { Empty } from '@/components/common/Empty';
@ -39,8 +52,31 @@ export function FieldFilters({ websiteId, value, exclude = [], onChange }: Field
};
return (
<Grid columns="160px 1fr" overflow="hidden" gapY="6">
<Column border="right" paddingRight="3">
<Grid columns={{ xs: '1fr', md: '180px 1fr' }} overflow="hidden" gapY="6">
<Row display={{ xs: 'flex', md: 'none' }}>
<MenuTrigger>
<Button>
<Icon>
<Plus />
</Icon>
</Button>
<Popover placement="bottom start">
<Menu onAction={handleAdd}>
{fields
.filter(({ name }) => !exclude.includes(name))
.map(field => {
const isDisabled = !!value.find(({ name }) => name === field.name);
return (
<MenuItem key={field.name} id={field.name} isDisabled={isDisabled}>
{field.label}
</MenuItem>
);
})}
</Menu>
</Popover>
</MenuTrigger>
</Row>
<Column display={{ xs: 'none', md: 'flex' }} border="right" paddingRight="3" marginRight="6">
<List onAction={handleAdd}>
{fields
.filter(({ name }) => !exclude.includes(name))
@ -54,7 +90,7 @@ export function FieldFilters({ websiteId, value, exclude = [], onChange }: Field
})}
</List>
</Column>
<Column paddingLeft="6" overflow="auto" gapY="4" height="500px" style={{ contain: 'layout' }}>
<Column overflow="auto" gapY="4" style={{ contain: 'layout' }}>
{value.map(filter => {
return (
<FilterRecord

View file

@ -1,7 +1,7 @@
import { useFilters, useMessages, useNavigation } from '@/components/hooks';
import { FieldFilters } from '@/components/input/FieldFilters';
import { SegmentFilters } from '@/components/input/SegmentFilters';
import { Button, Column, Row, Tab, TabList, TabPanel, Tabs } from '@umami/react-zen';
import { Button, Column, Row, Tab, TabList, TabPanel, Tabs, useBreakpoint } from '@umami/react-zen';
import { useState } from 'react';
export interface FilterEditFormProps {
@ -20,9 +20,8 @@ export function FilterEditForm({ websiteId, onChange, onClose }: FilterEditFormP
const [currentFilters, setCurrentFilters] = useState(filters);
const [currentSegment, setCurrentSegment] = useState(segment);
const [currentCohort, setCurrentCohort] = useState(cohort);
const panelProps = {
style: { height: 500 },
};
const breakpoint = useBreakpoint();
const isMobile = ['xs', 'sm', 'md'].includes(breakpoint);
const excludeFilters = pathname.includes('/pixels') || pathname.includes('/links');
const handleReset = () => {
@ -46,42 +45,44 @@ export function FilterEditForm({ websiteId, onChange, onClose }: FilterEditFormP
};
return (
<Column>
<Tabs>
<TabList>
<Tab id="fields">{formatMessage(labels.fields)}</Tab>
{!excludeFilters && (
<>
<Tab id="segments">{formatMessage(labels.segments)}</Tab>
<Tab id="cohorts">{formatMessage(labels.cohorts)}</Tab>
</>
)}
</TabList>
<TabPanel id="fields" {...panelProps}>
<FieldFilters
websiteId={websiteId}
value={currentFilters}
onChange={setCurrentFilters}
exclude={excludeFilters ? ['path', 'title', 'hostname', 'tag', 'event'] : []}
/>
</TabPanel>
<TabPanel id="segments" {...panelProps}>
<SegmentFilters
websiteId={websiteId}
segmentId={currentSegment}
onChange={handleSegmentChange}
/>
</TabPanel>
<TabPanel id="cohorts" {...panelProps}>
<SegmentFilters
type="cohort"
websiteId={websiteId}
segmentId={currentCohort}
onChange={handleSegmentChange}
/>
</TabPanel>
</Tabs>
<Row alignItems="center" justifyContent="space-between" gridColumn="span 2" marginTop="6" gap>
<Column width={isMobile ? 'auto' : '800px'} gap="6">
<Column minHeight="500px">
<Tabs>
<TabList>
<Tab id="fields">{formatMessage(labels.fields)}</Tab>
{!excludeFilters && (
<>
<Tab id="segments">{formatMessage(labels.segments)}</Tab>
<Tab id="cohorts">{formatMessage(labels.cohorts)}</Tab>
</>
)}
</TabList>
<TabPanel id="fields">
<FieldFilters
websiteId={websiteId}
value={currentFilters}
onChange={setCurrentFilters}
exclude={excludeFilters ? ['path', 'title', 'hostname', 'tag', 'event'] : []}
/>
</TabPanel>
<TabPanel id="segments">
<SegmentFilters
websiteId={websiteId}
segmentId={currentSegment}
onChange={handleSegmentChange}
/>
</TabPanel>
<TabPanel id="cohorts">
<SegmentFilters
type="cohort"
websiteId={websiteId}
segmentId={currentCohort}
onChange={handleSegmentChange}
/>
</TabPanel>
</Tabs>
</Column>
<Row alignItems="center" justifyContent="space-between" gap>
<Button onPress={handleReset}>{formatMessage(labels.reset)}</Button>
<Row alignItems="center" justifyContent="flex-end" gridColumn="span 2" gap>
<Button onPress={onClose}>{formatMessage(labels.cancel)}</Button>

View file

@ -12,6 +12,7 @@ import {
Column,
Pressable,
IconLabel,
useBreakpoint,
} from '@umami/react-zen';
import { useConfig, useLoginQuery, useMessages, useNavigation } from '@/components/hooks';
import {
@ -39,9 +40,11 @@ export function NavButton({ showText = true }: TeamsButtonProps) {
const { cloudMode } = useConfig();
const { formatMessage, labels } = useMessages();
const { teamId } = useNavigation();
const breakpoint = useBreakpoint();
const team = user?.teams?.find(({ id }) => id === teamId);
const selectedKeys = new Set([teamId || 'user']);
const label = teamId ? team?.name : user.username;
const isMobile = ['xs', 'sm', 'md'].includes(breakpoint);
const getUrl = (url: string) => {
return cloudMode ? `${process.env.cloudUrl}${url}` : url;
@ -82,7 +85,7 @@ export function NavButton({ showText = true }: TeamsButtonProps) {
<MenuItem id="teams" showChecked={false} showSubMenuIcon>
<IconLabel icon={<Switch />} label={formatMessage(labels.switchAccount)} />
</MenuItem>
<Popover placement="right top">
<Popover placement={isMobile ? 'bottom start' : 'right top'}>
<Column minWidth="300px">
<Menu selectionMode="single" selectedKeys={selectedKeys}>
<MenuSection title={formatMessage(labels.myAccount)}>

View file

@ -1,4 +1,4 @@
import { Button, Icon, DialogTrigger, Dialog, Popover, Text } from '@umami/react-zen';
import { Button, Icon, DialogTrigger, Dialog, Text, Modal, useBreakpoint } from '@umami/react-zen';
import { ListFilter } from '@/components/icons';
import { FilterEditForm } from '@/components/input/FilterEditForm';
import { useMessages, useNavigation } from '@/components/hooks';
@ -15,6 +15,8 @@ export function WebsiteFilterButton({
}) {
const { formatMessage, labels } = useMessages();
const { updateParams, router } = useNavigation();
const breakpoint = useBreakpoint();
const isMobile = ['xs', 'sm', 'md'].includes(breakpoint);
const handleChange = ({ filters, segment, cohort }: any) => {
const params = filtersArrayToObject(filters);
@ -32,13 +34,13 @@ export function WebsiteFilterButton({
</Icon>
{showText && <Text>{formatMessage(labels.filter)}</Text>}
</Button>
<Popover placement="bottom start">
<Dialog title={formatMessage(labels.filters)} style={{ width: 800, minHeight: 600 }}>
<Modal position={isMobile ? 'fullscreen' : 'center'}>
<Dialog title={formatMessage(labels.filters)}>
{({ close }) => {
return <FilterEditForm websiteId={websiteId} onChange={handleChange} onClose={close} />;
}}
</Dialog>
</Popover>
</Modal>
</DialogTrigger>
);
}

View file

@ -69,23 +69,38 @@ export function MetricsExpandedTable({
height="100%"
loadingIcon="spinner"
>
<Column overflowY="auto" minHeight="0" height="100%" paddingRight="3" overflow="hidden">
<Column overflow="auto" minHeight="0" height="100%" paddingRight="3">
{items && (
<DataTable data={items}>
<DataColumn id="label" label={title} width="2fr" align="start">
<DataColumn id="label" label={title} width="minmax(200px, 2fr)" align="start">
{row => (
<Row overflow="hidden">
<MetricLabel type={type} data={row} />
</Row>
)}
</DataColumn>
<DataColumn id="visitors" label={formatMessage(labels.visitors)} align="end">
<DataColumn
id="visitors"
label={formatMessage(labels.visitors)}
align="end"
width="120px"
>
{row => row?.['visitors']?.toLocaleString()}
</DataColumn>
<DataColumn id="visits" label={formatMessage(labels.visits)} align="end">
<DataColumn
id="visits"
label={formatMessage(labels.visits)}
align="end"
width="120px"
>
{row => row?.['visits']?.toLocaleString()}
</DataColumn>
<DataColumn id="pageviews" label={formatMessage(labels.views)} align="end">
<DataColumn
id="pageviews"
label={formatMessage(labels.views)}
align="end"
width="120px"
>
{row => row?.['pageviews']?.toLocaleString()}
</DataColumn>
{showBounceDuration && [
@ -94,6 +109,7 @@ export function MetricsExpandedTable({
id="bounceRate"
label={formatMessage(labels.bounceRate)}
align="end"
width="120px"
>
{row => {
const n = (Math.min(row?.['visits'], row?.['bounces']) / row?.['visits']) * 100;
@ -106,6 +122,7 @@ export function MetricsExpandedTable({
id="visitDuration"
label={formatMessage(labels.visitDuration)}
align="end"
width="120px"
>
{row => {
const n = row?.['totaltime'] / row?.['visits'];