mirror of
https://github.com/umami-software/umami.git
synced 2026-02-05 21:27:20 +01:00
Updates session details page.
This commit is contained in:
parent
095d1f2070
commit
1649992654
9 changed files with 123 additions and 223 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ export {
|
|||
File,
|
||||
Globe,
|
||||
Grid2X2,
|
||||
KeyRound,
|
||||
LayoutDashboard,
|
||||
Link,
|
||||
ListFilter,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue