mirror of
https://github.com/umami-software/umami.git
synced 2026-02-05 13:17:19 +01:00
New settings layouts. Segment management screen.
This commit is contained in:
parent
2dbcf63eeb
commit
eb7b6978d3
70 changed files with 762 additions and 499 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ export {
|
|||
Plus,
|
||||
RefreshCw as Refresh,
|
||||
Settings,
|
||||
Settings2 as Knobs,
|
||||
Share,
|
||||
Sheet,
|
||||
Slash,
|
||||
|
|
|
|||
26
src/components/input/ActionButton.tsx
Normal file
26
src/components/input/ActionButton.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
26
src/components/input/SegmentSaveButton.tsx
Normal file
26
src/components/input/SegmentSaveButton.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue