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>
<ListTable
title={formatMessage(labels.country)}
metric={formatMessage(labels.revenue)}
data={data?.country.map(({ name, value }: { name: string; value: number }) => ({
x: name,
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 { useMessages, useSessionDataQuery } from '@/components/hooks';
import { Text, Column, Row, Label, Box } from '@umami/react-zen';
import { useSessionDataQuery } from '@/components/hooks';
import { Empty } from '@/components/common/Empty';
import { DATA_TYPES } from '@/lib/constants';
import styles from './SessionData.module.css';
import { LoadingPanel } from '@/components/common/LoadingPanel';
export function SessionData({ websiteId, sessionId }: { websiteId: string; sessionId: string }) {
const { formatMessage, labels } = useMessages();
const { data, ...query } = useSessionDataQuery(websiteId, sessionId);
const { data, isLoading, error } = useSessionDataQuery(websiteId, sessionId);
const isEmpty = !data?.length;
return (
<>
<div className={styles.header}>{formatMessage(labels.properties)}</div>
<LoadingPanel className={styles.data} {...query} data={data}>
{!data?.length && <Empty className={styles.empty} />}
<LoadingPanel isEmpty={isEmpty} isLoading={isLoading} error={error}>
{!data?.length && <Empty />}
<Column gap="6">
{data?.map(({ dataKey, dataType, stringValue }) => {
return (
<div key={dataKey}>
<div className={styles.label}>
<div className={styles.name}>
<Text>{dataKey}</Text>
</div>
<div className={styles.type}>{DATA_TYPES[dataType]}</div>
</div>
<div className={styles.value}>{stringValue}</div>
</div>
<Column key={dataKey}>
<Label>{dataKey}</Label>
<Row alignItems="center" gap>
<Text>{stringValue}</Text>
<Box paddingY="1" paddingX="2" border borderRadius borderColor="5">
<Text color="muted" size="1">
{DATA_TYPES[dataType]}
</Text>
</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';
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 { LoadingPanel } from '@/components/common/LoadingPanel';
import { useWebsiteSessionQuery } from '@/components/hooks';
import { useMessages, useWebsiteSessionQuery } from '@/components/hooks';
import { SessionActivity } from './SessionActivity';
import { SessionData } from './SessionData';
import { SessionInfo } from './SessionInfo';
@ -16,38 +16,41 @@ export function SessionDetailsPage({
websiteId: string;
sessionId: string;
}) {
const { data, ...query } = useWebsiteSessionQuery(websiteId, sessionId);
const { data, isLoading, error } = useWebsiteSessionQuery(websiteId, sessionId);
const { formatMessage, labels } = useMessages();
return (
<LoadingPanel {...query} loadingIcon="spinner" data={data}>
<Grid
gap
columns={{ xs: '1fr', sm: '1fr', md: '1fr 1fr', lg: '1fr 2fr 1fr', xl: '1fr 2fr 1fr' }}
>
<Panel>
<Column gap="6" maxWidth="200px">
<Row justifyContent="center">
<Avatar seed={data?.id} size={128} />
</Row>
<SessionInfo data={data} />
</Column>
</Panel>
<Panel>
<Column gap="6">
<SessionStats data={data} />
<SessionActivity
websiteId={websiteId}
sessionId={sessionId}
startDate={data?.firstAt}
endDate={data?.lastAt}
/>
</Column>
</Panel>
<Panel>
<Column gap="6">
<SessionData websiteId={websiteId} sessionId={sessionId} />
</Column>
</Panel>
<LoadingPanel isLoading={isLoading} error={error}>
<Grid columns="260px 1fr" gap>
<Column gap="6">
<Row justifyContent="center">
<Avatar seed={data?.id} size={128} />
</Row>
<SessionInfo data={data} />
</Column>
<Column gap>
<SessionStats data={data} />
<Panel>
<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>
</Panel>
</Column>
</Grid>
</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 { TypeIcon } from '@/components/common/TypeIcon';
import { Location } from '@/components/icons';
import { Location, KeyRound, Calendar } from '@/components/icons';
export function SessionInfo({ data }) {
const { locale } = useLocale();
@ -12,77 +13,77 @@ export function SessionInfo({ data }) {
return (
<Column gap="6">
<Box>
<Label>ID</Label>
<Info label="ID">
<TextField value={data?.id} allowCopy />
</Box>
</Info>
<Box>
<Label>{formatMessage(labels.distinctId)}</Label>
<Row>{data?.distinctId}</Row>
</Box>
<Info label={formatMessage(labels.distinctId)} icon={<KeyRound />}>
{data?.distinctId}
</Info>
<Box>
<Label>{formatMessage(labels.lastSeen)}</Label>
<Row>{formatTimezoneDate(data?.lastAt, 'PPPPpp')}</Row>
</Box>
<Info label={formatMessage(labels.lastSeen)} icon={<Calendar />}>
{formatTimezoneDate(data?.lastAt, 'PPPPpp')}
</Info>
<Box>
<Label>{formatMessage(labels.firstSeen)}</Label>
<Row>{formatTimezoneDate(data?.firstAt, 'PPPPpp')}</Row>
</Box>
<Info label={formatMessage(labels.firstSeen)} icon={<Calendar />}>
{formatTimezoneDate(data?.firstAt, 'PPPPpp')}
</Info>
<Box>
<Label>{formatMessage(labels.country)}</Label>
<Row gap="3">
<TypeIcon type="country" value={data?.country} />
<Text>{formatValue(data?.country, 'country')}</Text>
</Row>
</Box>
<Info
label={formatMessage(labels.country)}
icon={<TypeIcon type="country" value={data?.country} />}
>
{formatValue(data?.country, 'country')}
</Info>
<Row>
<Label>{formatMessage(labels.region)}</Label>
<Row gap="3">
<Icon>
<Location />
</Icon>
{getRegionName(data?.region)}
</Row>
</Row>
<Info label={formatMessage(labels.region)} icon={<Location />}>
{getRegionName(data?.region)}
</Info>
<Box>
<Label>{formatMessage(labels.city)}</Label>
<Row gap="3">
<Icon>
<Location />
</Icon>
<Text>{data?.city}</Text>
</Row>
</Box>
<Info label={formatMessage(labels.city)} icon={<Location />}>
{data?.city}
</Info>
<Box>
<Label>{formatMessage(labels.os)}</Label>
<Row gap="3">
<TypeIcon type="os" value={data?.os?.toLowerCase()?.replaceAll(/\W/g, '-')} />
<Text>{formatValue(data?.os, 'os')}</Text>
</Row>
</Box>
<Info
label={formatMessage(labels.os)}
icon={<TypeIcon type="os" value={data?.os?.toLowerCase()?.replaceAll(/\W/g, '-')} />}
>
{formatValue(data?.os, 'os')}
</Info>
<Box>
<Label>{formatMessage(labels.device)}</Label>
<Row gap="3">
<TypeIcon type="device" value={data?.device} />
<Text>{formatValue(data?.device, 'device')}</Text>
</Row>
</Box>
<Info
label={formatMessage(labels.device)}
icon={<TypeIcon type="device" value={data?.device} />}
>
{formatValue(data?.device, 'device')}
</Info>
<Box>
<Label>{formatMessage(labels.browser)}</Label>
<Row gap="3">
<TypeIcon type="browser" value={data?.browser} />
<Text>{formatValue(data?.browser, 'browser')}</Text>
</Row>
</Box>
<Info
label={formatMessage(labels.browser)}
icon={<TypeIcon type="browser" value={data?.browser} />}
>
{formatValue(data?.browser, 'browser')}
</Info>
</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 => {
return countryNames[value] || value || labels.unknown;
return countryNames[value] || value;
};
const formatRegion = (value?: string): string => {
@ -57,7 +57,7 @@ export function useFormat() {
case 'language':
return formatLanguage(value);
default:
return value;
return typeof value === 'string' ? value : undefined;
}
};

View file

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