New settings layouts. Segment management screen.

This commit is contained in:
Mike Cao 2025-08-07 05:14:35 -07:00
parent 2dbcf63eeb
commit eb7b6978d3
70 changed files with 762 additions and 499 deletions

View file

@ -27,7 +27,7 @@ export function PageHeader({
{...props}
>
<Row alignItems="center" gap="3">
{icon && <Icon>{icon}</Icon>}
{icon && <Icon size="md">{icon}</Icon>}
{title && <Heading size="4">{title}</Heading>}
{description && <Text color="muted">{description}</Text>}
</Row>

View file

@ -1,27 +1,68 @@
import { ReactNode } from 'react';
import { Text, NavMenu, NavMenuItem, Icon, Row } from '@umami/react-zen';
import {
Text,
Heading,
NavMenu,
NavMenuItem,
Icon,
Row,
Column,
NavMenuGroup,
} from '@umami/react-zen';
import Link from 'next/link';
export interface SideMenuProps {
items: { id: string; label: string; url: string; icon?: ReactNode }[];
items: { label: string; items: { id: string; label: string; icon?: any; path: string }[] }[];
title?: string;
selectedKey?: string;
allowMinimize?: boolean;
}
export function SideMenu({ items, selectedKey }: SideMenuProps) {
export function SideMenu({ items, title, selectedKey, allowMinimize, children }: SideMenuProps) {
return (
<NavMenu highlightColor="3">
{items.map(({ id, label, url, icon }) => {
return (
<Link key={id} href={url}>
<NavMenuItem isSelected={id === selectedKey}>
<Row alignItems="center" gap>
{icon && <Icon>{icon}</Icon>}
<Text>{label}</Text>
</Row>
</NavMenuItem>
</Link>
);
})}
</NavMenu>
<Column
gap
padding
width="240px"
overflowY="auto"
justifyContent="space-between"
position="sticky"
top="0"
backgroundColor
>
{children}
{title && (
<Row padding>
<Heading size="1">{title}</Heading>
</Row>
)}
<NavMenu muteItems={false} gap="6">
{items.map(({ label, items }) => {
return (
<NavMenuGroup
title={label}
key={label}
gap="1"
allowMinimize={allowMinimize}
marginBottom="3"
>
{items.map(({ id, label, icon, path }) => {
const isSelected = selectedKey === id;
return (
<Link key={id} href={path}>
<NavMenuItem isSelected={isSelected}>
<Row alignItems="center" gap>
<Icon>{icon}</Icon>
<Text>{label}</Text>
</Row>
</NavMenuItem>
</Link>
);
})}
</NavMenuGroup>
);
})}
</NavMenu>
</Column>
);
}

View file

@ -35,6 +35,7 @@ export {
Plus,
RefreshCw as Refresh,
Settings,
Settings2 as Knobs,
Share,
Sheet,
Slash,

View file

@ -0,0 +1,26 @@
import { ReactNode } from 'react';
import { Button, Icon, Modal, DialogTrigger, TooltipTrigger, Tooltip } from '@umami/react-zen';
export function ActionButton({
onClick,
icon,
tooltip,
children,
}: {
onSave?: () => void;
icon?: ReactNode;
tooltip?: string;
children?: React.ReactNode;
}) {
return (
<DialogTrigger>
<TooltipTrigger delay={0}>
<Button variant="quiet" onPress={onClick}>
<Icon>{icon}</Icon>
</Button>
<Tooltip>{tooltip}</Tooltip>
</TooltipTrigger>
<Modal>{children}</Modal>
</DialogTrigger>
);
}

View file

@ -1,4 +1,14 @@
import { Button, Icon, Text, Row, TooltipTrigger, Tooltip } from '@umami/react-zen';
import {
Button,
Icon,
Text,
Row,
TooltipTrigger,
Tooltip,
Modal,
Dialog,
DialogTrigger,
} from '@umami/react-zen';
import {
useNavigation,
useMessages,
@ -6,8 +16,9 @@ import {
useFilters,
useWebsiteSegmentQuery,
} from '@/components/hooks';
import { Close } from '@/components/icons';
import { Close, Bookmark } from '@/components/icons';
import { isSearchOperator } from '@/lib/params';
import { SegmentEditForm } from '@/app/(main)/websites/[websiteId]/segments/SegmentEditForm';
export function FilterBar({ websiteId }: { websiteId: string }) {
const { formatMessage, labels } = useMessages();
@ -65,16 +76,37 @@ export function FilterBar({ websiteId }: { websiteId: string }) {
);
})}
</Row>
<TooltipTrigger delay={0}>
<Button variant="zero" onPress={handleResetFilter} style={{ alignSelf: 'flex-start' }}>
<Icon>
<Close />
</Icon>
</Button>
<Tooltip>
<Text>{formatMessage(labels.clearAll)}</Text>
</Tooltip>
</TooltipTrigger>
<Row alignItems="center">
<DialogTrigger>
<TooltipTrigger delay={0}>
<Button variant="zero">
<Icon>
<Bookmark />
</Icon>
</Button>
<Tooltip>
<Text>{formatMessage(labels.saveSegment)}</Text>
</Tooltip>
</TooltipTrigger>
<Modal>
<Dialog title={formatMessage(labels.segment)} style={{ width: 800 }}>
{({ close }) => {
return <SegmentEditForm websiteId={websiteId} onClose={close} />;
}}
</Dialog>
</Modal>
</DialogTrigger>
<TooltipTrigger delay={0}>
<Button variant="zero" onPress={handleResetFilter}>
<Icon>
<Close />
</Icon>
</Button>
<Tooltip>
<Text>{formatMessage(labels.clearAll)}</Text>
</Tooltip>
</TooltipTrigger>
</Row>
</Row>
);
}

View file

@ -57,7 +57,7 @@ export function FilterEditForm({
onSave={setCurrentFilters}
/>
</TabPanel>
<TabPanel id="segments">
<TabPanel id="segments" style={{ height: 400 }}>
<SegmentFilters
websiteId={websiteId}
segmentId={currentSegment}
@ -65,7 +65,7 @@ export function FilterEditForm({
/>
</TabPanel>
</Tabs>
<Row alignItems="center" justifyContent="space-between" gridColumn="span 2" gap>
<Row alignItems="center" justifyContent="space-between" gridColumn="span 2" marginTop="6" 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

@ -9,6 +9,7 @@ import {
MenuSeparator,
MenuSection,
Text,
Row,
} from '@umami/react-zen';
import { useRouter } from 'next/navigation';
import { useMessages, useLoginQuery } from '@/components/hooks';
@ -36,25 +37,31 @@ export function ProfileButton() {
<MenuSection title={user.username}>
<MenuSeparator />
<MenuItem id="settings">
<Icon>
<Settings />
</Icon>
<Text>{formatMessage(labels.settings)}</Text>
<Row alignItems="center" gap>
<Icon>
<Settings />
</Icon>
<Text>{formatMessage(labels.settings)}</Text>
</Row>
</MenuItem>
{user.isAdmin && (
<MenuItem id="admin">
<Icon>
<LockKeyhole />
</Icon>
<Text>{formatMessage(labels.admin)}</Text>
<Row alignItems="center" gap>
<Icon>
<LockKeyhole />
</Icon>
<Text>{formatMessage(labels.admin)}</Text>
</Row>
</MenuItem>
)}
{!cloudMode && (
<MenuItem data-test="item-logout" id="logout">
<Icon>
<LogOut />
</Icon>
<Text>{formatMessage(labels.logout)}</Text>
<Row alignItems="center" gap>
<Icon>
<LogOut />
</Icon>
<Text>{formatMessage(labels.logout)}</Text>
</Row>
</MenuItem>
)}
</MenuSection>

View file

@ -1,4 +1,4 @@
import { List, Column, ListItem } from '@umami/react-zen';
import { List, ListItem } from '@umami/react-zen';
import { useWebsiteSegmentsQuery } from '@/components/hooks';
import { LoadingPanel } from '@/components/common/LoadingPanel';
@ -9,25 +9,23 @@ export interface SegmentFiltersProps {
}
export function SegmentFilters({ websiteId, segmentId, onSave }: SegmentFiltersProps) {
const { data, isLoading } = useWebsiteSegmentsQuery(websiteId, { type: 'segment' });
const { data, isLoading, isFetching } = useWebsiteSegmentsQuery(websiteId, { type: 'segment' });
const handleChange = (id: string) => {
onSave?.(id);
};
return (
<Column height="400px" gap>
<LoadingPanel data={data} isLoading={isLoading} overflowY="auto">
<List selectionMode="single" value={[segmentId]} onChange={id => handleChange(id[0])}>
{data?.data?.map(item => {
return (
<ListItem key={item.id} id={item.id}>
{item.name}
</ListItem>
);
})}
</List>
</LoadingPanel>
</Column>
<LoadingPanel data={data} isLoading={isLoading} isFetching={isFetching} overflowY="auto">
<List selectionMode="single" value={[segmentId]} onChange={id => handleChange(id[0])}>
{data?.data?.map(item => {
return (
<ListItem key={item.id} id={item.id}>
{item.name}
</ListItem>
);
})}
</List>
</LoadingPanel>
);
}

View file

@ -0,0 +1,26 @@
import { Button, DialogTrigger, Modal, Text, Icon, Dialog } from '@umami/react-zen';
import { useMessages } from '@/components/hooks';
import { Plus } from '@/components/icons';
import { SegmentEditForm } from '@/app/(main)/websites/[websiteId]/segments/SegmentEditForm';
export function SegmentSaveButton({ websiteId }: { websiteId: string }) {
const { formatMessage, labels } = useMessages();
return (
<DialogTrigger>
<Button variant="primary">
<Icon>
<Plus />
</Icon>
<Text>{formatMessage(labels.segment)}</Text>
</Button>
<Modal>
<Dialog title={formatMessage(labels.segment)} style={{ width: 800 }}>
{({ close }) => {
return <SegmentEditForm websiteId={websiteId} onClose={close} />;
}}
</Dialog>
</Modal>
</DialogTrigger>
);
}

View file

@ -15,7 +15,7 @@ import {
Pressable,
} from '@umami/react-zen';
import { useLoginQuery, useMessages, useUserTeamsQuery, useNavigation } from '@/components/hooks';
import { Chevron, User, Users } from '@/components/icons';
import { Chevron, User, Users, LogOut } from '@/components/icons';
export function TeamsButton({ showText = true }: { showText?: boolean }) {
const { user } = useLoginQuery();
@ -81,6 +81,17 @@ export function TeamsButton({ showText = true }: { showText?: boolean }) {
</MenuItem>
))}
</MenuSection>
<MenuSeparator />
<MenuSection>
<MenuItem id="logout">
<Row alignItems="center" gap>
<Icon size="sm">
<LogOut />
</Icon>
<Text wrap="nowrap">{formatMessage(labels.logout)}</Text>
</Row>
</MenuItem>
</MenuSection>
</Menu>
</Box>
</Popover>

View file

@ -33,13 +33,13 @@ export function WebsiteSelect({
items={data?.['data'] || []}
value={websiteId}
isLoading={isLoading}
buttonProps={buttonProps}
buttonProps={{ ...buttonProps }}
allowSearch={true}
searchValue={search}
onSearch={handleSearch}
onChange={handleSelect}
renderValue={() => (
<Text truncate style={{ maxWidth: 160 }}>
<Text truncate weight="bold" style={{ maxWidth: 160, lineHeight: 1 }}>
{website?.name}
</Text>
)}

View file

@ -342,6 +342,10 @@ export const labels = defineMessages({
traffic: { id: 'label.traffic', defaultMessage: 'Traffic' },
behavior: { id: 'label.behavior', defaultMessage: 'Behavior' },
growth: { id: 'label.growth', defaultMessage: 'Growth' },
account: { id: 'label.account', defaultMessage: 'Account' },
application: { id: 'label.application', defaultMessage: 'Application' },
saveSegment: { id: 'label.save-segment', defaultMessage: 'Save segment' },
saveCohort: { id: 'label.save-cohort', defaultMessage: 'Save cohort' },
});
export const messages = defineMessages({

View file

@ -1,5 +1,5 @@
import { WebsiteContext } from '@/app/(main)/websites/[websiteId]/WebsiteProvider';
import { FilterButtons } from '@/components/common/FilterButtons';
import { FilterButtons } from '@/components/input/FilterButtons';
import { FilterLink } from '@/components/common/FilterLink';
import { useMessages, useNavigation } from '@/components/hooks';
import { emptyFilter } from '@/lib/filters';

View file

@ -1,6 +1,6 @@
import { useState } from 'react';
import { Row, Text } from '@umami/react-zen';
import { FilterButtons } from '@/components/common/FilterButtons';
import { FilterButtons } from '@/components/input/FilterButtons';
import { emptyFilter, paramFilter } from '@/lib/filters';
import { MetricsTable, MetricsTableProps } from './MetricsTable';
import { useMessages } from '@/components/hooks';

View file

@ -1,7 +1,7 @@
import { Row } from '@umami/react-zen';
import { FilterLink } from '@/components/common/FilterLink';
import { Favicon } from '@/components/common/Favicon';
import { FilterButtons } from '@/components/common/FilterButtons';
import { FilterButtons } from '@/components/input/FilterButtons';
import { useMessages, useNavigation } from '@/components/hooks';
import { MetricsTable, MetricsTableProps } from './MetricsTable';
import thenby from 'thenby';