Updates session details page.

This commit is contained in:
Mike Cao 2025-06-12 00:31:09 -07:00
parent 095d1f2070
commit 1649992654
9 changed files with 123 additions and 223 deletions

View file

@ -138,6 +138,7 @@ export function Revenue({ websiteId, startDate, endDate }: RevenueProps) {
<Panel> <Panel>
<ListTable <ListTable
title={formatMessage(labels.country)} title={formatMessage(labels.country)}
metric={formatMessage(labels.revenue)}
data={data?.country.map(({ name, value }: { name: string; value: number }) => ({ data={data?.country.map(({ name, value }: { name: string; value: number }) => ({
x: name, x: name,
y: value, y: value,

View file

@ -1,38 +0,0 @@
.data {
display: flex;
flex-direction: column;
gap: 20px;
position: relative;
}
.header {
font-weight: bold;
margin-bottom: 20px;
}
.empty {
color: var(--font-color300);
text-align: center;
}
.label {
display: flex;
align-items: center;
justify-content: space-between;
}
.type {
font-size: 11px;
padding: 0 6px;
border-radius: 4px;
border: 1px solid var(--base400);
}
.name {
color: var(--font-color200);
font-weight: bold;
}
.value {
margin: 5px 0;
}

View file

@ -1,33 +1,33 @@
import { Text } from '@umami/react-zen'; import { Text, Column, Row, Label, Box } from '@umami/react-zen';
import { useMessages, useSessionDataQuery } from '@/components/hooks'; import { useSessionDataQuery } from '@/components/hooks';
import { Empty } from '@/components/common/Empty'; import { Empty } from '@/components/common/Empty';
import { DATA_TYPES } from '@/lib/constants'; import { DATA_TYPES } from '@/lib/constants';
import styles from './SessionData.module.css';
import { LoadingPanel } from '@/components/common/LoadingPanel'; import { LoadingPanel } from '@/components/common/LoadingPanel';
export function SessionData({ websiteId, sessionId }: { websiteId: string; sessionId: string }) { export function SessionData({ websiteId, sessionId }: { websiteId: string; sessionId: string }) {
const { formatMessage, labels } = useMessages(); const { data, isLoading, error } = useSessionDataQuery(websiteId, sessionId);
const { data, ...query } = useSessionDataQuery(websiteId, sessionId); const isEmpty = !data?.length;
return ( return (
<> <LoadingPanel isEmpty={isEmpty} isLoading={isLoading} error={error}>
<div className={styles.header}>{formatMessage(labels.properties)}</div> {!data?.length && <Empty />}
<LoadingPanel className={styles.data} {...query} data={data}> <Column gap="6">
{!data?.length && <Empty className={styles.empty} />}
{data?.map(({ dataKey, dataType, stringValue }) => { {data?.map(({ dataKey, dataType, stringValue }) => {
return ( return (
<div key={dataKey}> <Column key={dataKey}>
<div className={styles.label}> <Label>{dataKey}</Label>
<div className={styles.name}> <Row alignItems="center" gap>
<Text>{dataKey}</Text> <Text>{stringValue}</Text>
</div> <Box paddingY="1" paddingX="2" border borderRadius borderColor="5">
<div className={styles.type}>{DATA_TYPES[dataType]}</div> <Text color="muted" size="1">
</div> {DATA_TYPES[dataType]}
<div className={styles.value}>{stringValue}</div> </Text>
</div> </Box>
</Row>
</Column>
); );
})} })}
</LoadingPanel> </Column>
</> </LoadingPanel>
); );
} }

View file

@ -1,47 +0,0 @@
.page {
display: grid;
grid-template-columns: max-content 1fr max-content;
margin-bottom: 40px;
position: relative;
}
.sidebar {
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
gap: 20px;
width: 300px;
padding-right: 20px;
border-right: 1px solid var(--base300);
position: relative;
}
.content {
display: flex;
flex-direction: column;
gap: 30px;
padding: 0 20px;
position: relative;
}
.data {
width: 300px;
border-left: 1px solid var(--base300);
padding-left: 20px;
position: relative;
transition: width 200ms ease-in-out;
}
@media screen and (max-width: 992px) {
.page {
grid-template-columns: 1fr;
gap: 30px;
}
.sidebar,
.data {
border: 0;
width: auto;
}
}

View file

@ -1,8 +1,8 @@
'use client'; 'use client';
import { Grid, Row, Column } from '@umami/react-zen'; import { Grid, Row, Column, Tabs, TabList, Tab, TabPanel } from '@umami/react-zen';
import { Avatar } from '@/components/common/Avatar'; import { Avatar } from '@/components/common/Avatar';
import { LoadingPanel } from '@/components/common/LoadingPanel'; import { LoadingPanel } from '@/components/common/LoadingPanel';
import { useWebsiteSessionQuery } from '@/components/hooks'; import { useMessages, useWebsiteSessionQuery } from '@/components/hooks';
import { SessionActivity } from './SessionActivity'; import { SessionActivity } from './SessionActivity';
import { SessionData } from './SessionData'; import { SessionData } from './SessionData';
import { SessionInfo } from './SessionInfo'; import { SessionInfo } from './SessionInfo';
@ -16,38 +16,41 @@ export function SessionDetailsPage({
websiteId: string; websiteId: string;
sessionId: string; sessionId: string;
}) { }) {
const { data, ...query } = useWebsiteSessionQuery(websiteId, sessionId); const { data, isLoading, error } = useWebsiteSessionQuery(websiteId, sessionId);
const { formatMessage, labels } = useMessages();
return ( return (
<LoadingPanel {...query} loadingIcon="spinner" data={data}> <LoadingPanel isLoading={isLoading} error={error}>
<Grid <Grid columns="260px 1fr" gap>
gap <Column gap="6">
columns={{ xs: '1fr', sm: '1fr', md: '1fr 1fr', lg: '1fr 2fr 1fr', xl: '1fr 2fr 1fr' }} <Row justifyContent="center">
> <Avatar seed={data?.id} size={128} />
<Panel> </Row>
<Column gap="6" maxWidth="200px"> <SessionInfo data={data} />
<Row justifyContent="center"> </Column>
<Avatar seed={data?.id} size={128} />
</Row> <Column gap>
<SessionInfo data={data} /> <SessionStats data={data} />
</Column> <Panel>
</Panel> <Tabs>
<Panel> <TabList>
<Column gap="6"> <Tab id="activity">{formatMessage(labels.activity)}</Tab>
<SessionStats data={data} /> <Tab id="properties">{formatMessage(labels.properties)}</Tab>
<SessionActivity </TabList>
websiteId={websiteId} <TabPanel id="activity">
sessionId={sessionId} <SessionActivity
startDate={data?.firstAt} websiteId={websiteId}
endDate={data?.lastAt} sessionId={sessionId}
/> startDate={data?.firstAt}
</Column> endDate={data?.lastAt}
</Panel> />
<Panel> </TabPanel>
<Column gap="6"> <TabPanel id="properties">
<SessionData websiteId={websiteId} sessionId={sessionId} /> <SessionData sessionId={sessionId} websiteId={websiteId} />
</Column> </TabPanel>
</Panel> </Tabs>
</Panel>
</Column>
</Grid> </Grid>
</LoadingPanel> </LoadingPanel>
); );

View file

@ -1,21 +0,0 @@
.info {
display: grid;
gap: 10px;
}
.info dl {
width: 100%;
}
.info dt {
color: var(--font-color200);
font-weight: bold;
}
.info dd {
display: flex;
gap: 10px;
align-items: center;
margin: 5px 0 28px;
text-align: left;
}

View file

@ -1,7 +1,8 @@
import { Icon, TextField, Column, Row, Label, Box, Text } from '@umami/react-zen'; import { ReactNode } from 'react';
import { Icon, TextField, Column, Row, Label, Text } from '@umami/react-zen';
import { useFormat, useLocale, useMessages, useRegionNames, useTimezone } from '@/components/hooks'; import { useFormat, useLocale, useMessages, useRegionNames, useTimezone } from '@/components/hooks';
import { TypeIcon } from '@/components/common/TypeIcon'; import { TypeIcon } from '@/components/common/TypeIcon';
import { Location } from '@/components/icons'; import { Location, KeyRound, Calendar } from '@/components/icons';
export function SessionInfo({ data }) { export function SessionInfo({ data }) {
const { locale } = useLocale(); const { locale } = useLocale();
@ -12,77 +13,77 @@ export function SessionInfo({ data }) {
return ( return (
<Column gap="6"> <Column gap="6">
<Box> <Info label="ID">
<Label>ID</Label>
<TextField value={data?.id} allowCopy /> <TextField value={data?.id} allowCopy />
</Box> </Info>
<Box> <Info label={formatMessage(labels.distinctId)} icon={<KeyRound />}>
<Label>{formatMessage(labels.distinctId)}</Label> {data?.distinctId}
<Row>{data?.distinctId}</Row> </Info>
</Box>
<Box> <Info label={formatMessage(labels.lastSeen)} icon={<Calendar />}>
<Label>{formatMessage(labels.lastSeen)}</Label> {formatTimezoneDate(data?.lastAt, 'PPPPpp')}
<Row>{formatTimezoneDate(data?.lastAt, 'PPPPpp')}</Row> </Info>
</Box>
<Box> <Info label={formatMessage(labels.firstSeen)} icon={<Calendar />}>
<Label>{formatMessage(labels.firstSeen)}</Label> {formatTimezoneDate(data?.firstAt, 'PPPPpp')}
<Row>{formatTimezoneDate(data?.firstAt, 'PPPPpp')}</Row> </Info>
</Box>
<Box> <Info
<Label>{formatMessage(labels.country)}</Label> label={formatMessage(labels.country)}
<Row gap="3"> icon={<TypeIcon type="country" value={data?.country} />}
<TypeIcon type="country" value={data?.country} /> >
<Text>{formatValue(data?.country, 'country')}</Text> {formatValue(data?.country, 'country')}
</Row> </Info>
</Box>
<Row> <Info label={formatMessage(labels.region)} icon={<Location />}>
<Label>{formatMessage(labels.region)}</Label> {getRegionName(data?.region)}
<Row gap="3"> </Info>
<Icon>
<Location />
</Icon>
{getRegionName(data?.region)}
</Row>
</Row>
<Box> <Info label={formatMessage(labels.city)} icon={<Location />}>
<Label>{formatMessage(labels.city)}</Label> {data?.city}
<Row gap="3"> </Info>
<Icon>
<Location />
</Icon>
<Text>{data?.city}</Text>
</Row>
</Box>
<Box> <Info
<Label>{formatMessage(labels.os)}</Label> label={formatMessage(labels.os)}
<Row gap="3"> icon={<TypeIcon type="os" value={data?.os?.toLowerCase()?.replaceAll(/\W/g, '-')} />}
<TypeIcon type="os" value={data?.os?.toLowerCase()?.replaceAll(/\W/g, '-')} /> >
<Text>{formatValue(data?.os, 'os')}</Text> {formatValue(data?.os, 'os')}
</Row> </Info>
</Box>
<Box> <Info
<Label>{formatMessage(labels.device)}</Label> label={formatMessage(labels.device)}
<Row gap="3"> icon={<TypeIcon type="device" value={data?.device} />}
<TypeIcon type="device" value={data?.device} /> >
<Text>{formatValue(data?.device, 'device')}</Text> {formatValue(data?.device, 'device')}
</Row> </Info>
</Box>
<Box> <Info
<Label>{formatMessage(labels.browser)}</Label> label={formatMessage(labels.browser)}
<Row gap="3"> icon={<TypeIcon type="browser" value={data?.browser} />}
<TypeIcon type="browser" value={data?.browser} /> >
<Text>{formatValue(data?.browser, 'browser')}</Text> {formatValue(data?.browser, 'browser')}
</Row> </Info>
</Box>
</Column> </Column>
); );
} }
const Info = ({
label,
icon,
children,
}: {
label: string;
icon?: ReactNode;
children: ReactNode;
}) => {
return (
<Column>
<Label>{label}</Label>
<Row alignItems="center" gap>
{icon && <Icon>{icon}</Icon>}
<Text>{children || '—'}</Text>
</Row>
</Column>
);
};

View file

@ -24,7 +24,7 @@ export function useFormat() {
}; };
const formatCountry = (value: string): string => { const formatCountry = (value: string): string => {
return countryNames[value] || value || labels.unknown; return countryNames[value] || value;
}; };
const formatRegion = (value?: string): string => { const formatRegion = (value?: string): string => {
@ -57,7 +57,7 @@ export function useFormat() {
case 'language': case 'language':
return formatLanguage(value); return formatLanguage(value);
default: default:
return value; return typeof value === 'string' ? value : undefined;
} }
}; };

View file

@ -14,6 +14,7 @@ export {
File, File,
Globe, Globe,
Grid2X2, Grid2X2,
KeyRound,
LayoutDashboard, LayoutDashboard,
Link, Link,
ListFilter, ListFilter,