Merge pull request #3507 from basbroek/feature/table-view-events

Feature: Table view for event properties
This commit is contained in:
Mike Cao 2025-07-13 22:03:59 -07:00 committed by GitHub
commit 195619aeed
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 124 additions and 79 deletions

View file

@ -103,7 +103,7 @@
"kafkajs": "^2.1.0",
"maxmind": "^4.3.24",
"md5": "^2.3.0",
"next": "15.3.1",
"next": "15.3.3",
"node-fetch": "^3.2.8",
"npm-run-all": "^4.1.5",
"prisma": "6.7.0",

119
pnpm-lock.yaml generated
View file

@ -120,8 +120,8 @@ importers:
specifier: ^2.3.0
version: 2.3.0
next:
specifier: 15.3.1
version: 15.3.1(@babel/core@7.27.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
specifier: 15.3.3
version: 15.3.3(@babel/core@7.26.10)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
node-fetch:
specifier: ^3.2.8
version: 3.3.2
@ -1298,8 +1298,8 @@ packages:
peerDependencies:
'@dicebear/core': ^9.0.0
'@emnapi/core@1.4.3':
resolution: {integrity: sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g==}
'@emnapi/core@1.4.1':
resolution: {integrity: sha512-4JFstCTaToCFrPqrGzgkF8N2NHjtsaY4uRh6brZQ5L9e4wbMieX8oDT8N7qfVFTQecHFEtkj4ve49VIZ3mKVqw==}
'@emnapi/runtime@1.4.3':
resolution: {integrity: sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==}
@ -1795,56 +1795,56 @@ packages:
resolution: {integrity: sha512-9Hgd/J5nP2U/Vv0teytq9uUAGppiKV9t5tzpsuMLqeqUGD9STxXwKmyZd2v8Z4THSW9rw4+8w7dH7LVlFoym2A==}
engines: {node: '>=18.0.0'}
'@next/env@15.3.1':
resolution: {integrity: sha512-cwK27QdzrMblHSn9DZRV+DQscHXRuJv6MydlJRpFSqJWZrTYMLzKDeyueJNN9MGd8NNiUKzDQADAf+dMLXX7YQ==}
'@next/env@15.3.3':
resolution: {integrity: sha512-OdiMrzCl2Xi0VTjiQQUK0Xh7bJHnOuET2s+3V+Y40WJBAXrJeGA3f+I8MZJ/YQ3mVGi5XGR1L66oFlgqXhQ4Vw==}
'@next/eslint-plugin-next@14.2.29':
resolution: {integrity: sha512-qpxSYiPNJTr9RzqjGi5yom8AIC8Kgdtw4oNIXAB/gDYMDctmfMEv452FRUhT06cWPgcmSsbZiEPYhbFiQtCWTg==}
'@next/swc-darwin-arm64@15.3.1':
resolution: {integrity: sha512-hjDw4f4/nla+6wysBL07z52Gs55Gttp5Bsk5/8AncQLJoisvTBP0pRIBK/B16/KqQyH+uN4Ww8KkcAqJODYH3w==}
'@next/swc-darwin-arm64@15.3.3':
resolution: {integrity: sha512-WRJERLuH+O3oYB4yZNVahSVFmtxRNjNF1I1c34tYMoJb0Pve+7/RaLAJJizyYiFhjYNGHRAE1Ri2Fd23zgDqhg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
'@next/swc-darwin-x64@15.3.1':
resolution: {integrity: sha512-q+aw+cJ2ooVYdCEqZVk+T4Ni10jF6Fo5DfpEV51OupMaV5XL6pf3GCzrk6kSSZBsMKZtVC1Zm/xaNBFpA6bJ2g==}
'@next/swc-darwin-x64@15.3.3':
resolution: {integrity: sha512-XHdzH/yBc55lu78k/XwtuFR/ZXUTcflpRXcsu0nKmF45U96jt1tsOZhVrn5YH+paw66zOANpOnFQ9i6/j+UYvw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
'@next/swc-linux-arm64-gnu@15.3.1':
resolution: {integrity: sha512-wBQ+jGUI3N0QZyWmmvRHjXjTWFy8o+zPFLSOyAyGFI94oJi+kK/LIZFJXeykvgXUk1NLDAEFDZw/NVINhdk9FQ==}
'@next/swc-linux-arm64-gnu@15.3.3':
resolution: {integrity: sha512-VZ3sYL2LXB8znNGcjhocikEkag/8xiLgnvQts41tq6i+wql63SMS1Q6N8RVXHw5pEUjiof+II3HkDd7GFcgkzw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
'@next/swc-linux-arm64-musl@15.3.1':
resolution: {integrity: sha512-IIxXEXRti/AulO9lWRHiCpUUR8AR/ZYLPALgiIg/9ENzMzLn3l0NSxVdva7R/VDcuSEBo0eGVCe3evSIHNz0Hg==}
'@next/swc-linux-arm64-musl@15.3.3':
resolution: {integrity: sha512-h6Y1fLU4RWAp1HPNJWDYBQ+e3G7sLckyBXhmH9ajn8l/RSMnhbuPBV/fXmy3muMcVwoJdHL+UtzRzs0nXOf9SA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
'@next/swc-linux-x64-gnu@15.3.1':
resolution: {integrity: sha512-bfI4AMhySJbyXQIKH5rmLJ5/BP7bPwuxauTvVEiJ/ADoddaA9fgyNNCcsbu9SlqfHDoZmfI6g2EjzLwbsVTr5A==}
'@next/swc-linux-x64-gnu@15.3.3':
resolution: {integrity: sha512-jJ8HRiF3N8Zw6hGlytCj5BiHyG/K+fnTKVDEKvUCyiQ/0r5tgwO7OgaRiOjjRoIx2vwLR+Rz8hQoPrnmFbJdfw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
'@next/swc-linux-x64-musl@15.3.1':
resolution: {integrity: sha512-FeAbR7FYMWR+Z+M5iSGytVryKHiAsc0x3Nc3J+FD5NVbD5Mqz7fTSy8CYliXinn7T26nDMbpExRUI/4ekTvoiA==}
'@next/swc-linux-x64-musl@15.3.3':
resolution: {integrity: sha512-HrUcTr4N+RgiiGn3jjeT6Oo208UT/7BuTr7K0mdKRBtTbT4v9zJqCDKO97DUqqoBK1qyzP1RwvrWTvU6EPh/Cw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
'@next/swc-win32-arm64-msvc@15.3.1':
resolution: {integrity: sha512-yP7FueWjphQEPpJQ2oKmshk/ppOt+0/bB8JC8svPUZNy0Pi3KbPx2Llkzv1p8CoQa+D2wknINlJpHf3vtChVBw==}
'@next/swc-win32-arm64-msvc@15.3.3':
resolution: {integrity: sha512-SxorONgi6K7ZUysMtRF3mIeHC5aA3IQLmKFQzU0OuhuUYwpOBc1ypaLJLP5Bf3M9k53KUUUj4vTPwzGvl/NwlQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
'@next/swc-win32-x64-msvc@15.3.1':
resolution: {integrity: sha512-3PMvF2zRJAifcRNni9uMk/gulWfWS+qVI/pagd+4yLF5bcXPZPPH2xlYRYOsUjmCJOXSTAC2PjRzbhsRzR2fDQ==}
'@next/swc-win32-x64-msvc@15.3.3':
resolution: {integrity: sha512-4QZG6F8enl9/S2+yIiOiju0iCTFd93d8VC1q9LZS4p/Xuk81W2QDjCFeoogmrWWkAD59z8ZxepBQap2dKS5ruw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
@ -2803,8 +2803,8 @@ packages:
caniuse-api@3.0.0:
resolution: {integrity: sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==}
caniuse-lite@1.0.30001718:
resolution: {integrity: sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==}
caniuse-lite@1.0.30001726:
resolution: {integrity: sha512-VQAUIUzBiZ/UnlM28fSp2CRF3ivUn1BWEvxMcVTNwpw91Py1pGbPIyIKtd+tzct9C3ouceCVdGAXxZOpZAsgdw==}
caseless@0.12.0:
resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==}
@ -4882,8 +4882,8 @@ packages:
natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
next@15.3.1:
resolution: {integrity: sha512-8+dDV0xNLOgHlyBxP1GwHGVaNXsmp+2NhZEYrXr24GWLHtt27YrBPbPuHvzlhi7kZNYjeJNR93IF5zfFu5UL0g==}
next@15.3.3:
resolution: {integrity: sha512-JqNj29hHNmCLtNvd090SyRbXJiivQ+58XjCcrC50Crb5g5u2zi7Y2YivbsEfzk6AtVI80akdOQbaMZwWB1Hthw==}
engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0}
hasBin: true
peerDependencies:
@ -5938,6 +5938,11 @@ packages:
engines: {node: '>=10'}
hasBin: true
semver@7.7.2:
resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==}
engines: {node: '>=10'}
hasBin: true
serialize-error@12.0.0:
resolution: {integrity: sha512-ZYkZLAvKTKQXWuh5XpBw7CdbSzagarX39WyZ2H07CDLC5/KfsRGlIXV8d4+tfqX1M7916mRqR1QfNHSij+c9Pw==}
engines: {node: '>=18'}
@ -7776,9 +7781,9 @@ snapshots:
dependencies:
'@dicebear/core': 9.2.2
'@emnapi/core@1.4.3':
'@emnapi/core@1.4.1':
dependencies:
'@emnapi/wasi-threads': 1.0.2
'@emnapi/wasi-threads': 1.0.1
tslib: 2.8.1
optional: true
@ -8332,41 +8337,41 @@ snapshots:
'@napi-rs/wasm-runtime@0.2.10':
dependencies:
'@emnapi/core': 1.4.3
'@emnapi/core': 1.4.1
'@emnapi/runtime': 1.4.3
'@tybys/wasm-util': 0.9.0
optional: true
'@netlify/plugin-nextjs@5.11.2': {}
'@next/env@15.3.1': {}
'@next/env@15.3.3': {}
'@next/eslint-plugin-next@14.2.29':
dependencies:
glob: 10.3.10
'@next/swc-darwin-arm64@15.3.1':
'@next/swc-darwin-arm64@15.3.3':
optional: true
'@next/swc-darwin-x64@15.3.1':
'@next/swc-darwin-x64@15.3.3':
optional: true
'@next/swc-linux-arm64-gnu@15.3.1':
'@next/swc-linux-arm64-gnu@15.3.3':
optional: true
'@next/swc-linux-arm64-musl@15.3.1':
'@next/swc-linux-arm64-musl@15.3.3':
optional: true
'@next/swc-linux-x64-gnu@15.3.1':
'@next/swc-linux-x64-gnu@15.3.3':
optional: true
'@next/swc-linux-x64-musl@15.3.1':
'@next/swc-linux-x64-musl@15.3.3':
optional: true
'@next/swc-win32-arm64-msvc@15.3.1':
'@next/swc-win32-arm64-msvc@15.3.3':
optional: true
'@next/swc-win32-x64-msvc@15.3.1':
'@next/swc-win32-x64-msvc@15.3.3':
optional: true
'@nodelib/fs.scandir@2.1.5':
@ -9224,8 +9229,8 @@ snapshots:
autoprefixer@10.4.21(postcss@8.5.3):
dependencies:
browserslist: 4.24.5
caniuse-lite: 1.0.30001718
browserslist: 4.24.4
caniuse-lite: 1.0.30001726
fraction.js: 4.3.7
normalize-range: 0.1.2
picocolors: 1.1.1
@ -9372,8 +9377,8 @@ snapshots:
browserslist@4.24.5:
dependencies:
caniuse-lite: 1.0.30001718
electron-to-chromium: 1.5.158
caniuse-lite: 1.0.30001726
electron-to-chromium: 1.5.137
node-releases: 2.0.19
update-browserslist-db: 1.1.3(browserslist@4.24.5)
@ -9440,12 +9445,12 @@ snapshots:
caniuse-api@3.0.0:
dependencies:
browserslist: 4.24.5
caniuse-lite: 1.0.30001718
browserslist: 4.24.4
caniuse-lite: 1.0.30001726
lodash.memoize: 4.1.2
lodash.uniq: 4.5.0
caniuse-lite@1.0.30001718: {}
caniuse-lite@1.0.30001726: {}
caseless@0.12.0: {}
@ -11939,26 +11944,26 @@ snapshots:
natural-compare@1.4.0: {}
next@15.3.1(@babel/core@7.27.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
next@15.3.3(@babel/core@7.26.10)(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
dependencies:
'@next/env': 15.3.1
'@next/env': 15.3.3
'@swc/counter': 0.1.3
'@swc/helpers': 0.5.15
busboy: 1.6.0
caniuse-lite: 1.0.30001718
caniuse-lite: 1.0.30001726
postcss: 8.4.31
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
styled-jsx: 5.1.6(@babel/core@7.27.3)(react@19.1.0)
optionalDependencies:
'@next/swc-darwin-arm64': 15.3.1
'@next/swc-darwin-x64': 15.3.1
'@next/swc-linux-arm64-gnu': 15.3.1
'@next/swc-linux-arm64-musl': 15.3.1
'@next/swc-linux-x64-gnu': 15.3.1
'@next/swc-linux-x64-musl': 15.3.1
'@next/swc-win32-arm64-msvc': 15.3.1
'@next/swc-win32-x64-msvc': 15.3.1
'@next/swc-darwin-arm64': 15.3.3
'@next/swc-darwin-x64': 15.3.3
'@next/swc-linux-arm64-gnu': 15.3.3
'@next/swc-linux-arm64-musl': 15.3.3
'@next/swc-linux-x64-gnu': 15.3.3
'@next/swc-linux-x64-musl': 15.3.3
'@next/swc-win32-arm64-msvc': 15.3.3
'@next/swc-win32-x64-msvc': 15.3.3
sharp: 0.34.2
transitivePeerDependencies:
- '@babel/core'
@ -13031,6 +13036,8 @@ snapshots:
semver@7.7.2: {}
semver@7.7.2: {}
serialize-error@12.0.0:
dependencies:
type-fest: 4.41.0

View file

@ -14,12 +14,14 @@
color: var(--primary400);
}
.title {
text-align: center;
font-weight: bold;
margin: 20px 0;
.header {
margin-bottom: 40px;
}
.chart {
.title {
font-weight: bold;
}
.data {
min-height: 620px;
}

View file

@ -1,7 +1,9 @@
import { GridColumn, GridTable } from 'react-basics';
import { useMemo } from 'react';
import { GridColumn, GridTable, Flexbox, Button, ButtonGroup, Loading } from 'react-basics';
import { useEventDataProperties, useEventDataValues, useMessages } from '@/components/hooks';
import { LoadingPanel } from '@/components/common/LoadingPanel';
import PieChart from '@/components/charts/PieChart';
import ListTable from '@/components/metrics/ListTable';
import { useState } from 'react';
import { CHART_COLORS } from '@/lib/constants';
import styles from './EventProperties.module.css';
@ -9,22 +11,38 @@ import styles from './EventProperties.module.css';
export function EventProperties({ websiteId }: { websiteId: string }) {
const [propertyName, setPropertyName] = useState('');
const [eventName, setEventName] = useState('');
const [propertyView, setPropertyView] = useState('table');
const { formatMessage, labels } = useMessages();
const { data, isLoading, isFetched, error } = useEventDataProperties(websiteId);
const { data: values } = useEventDataValues(websiteId, eventName, propertyName);
const chartData =
propertyName && values
? {
labels: values.map(({ value }) => value),
datasets: [
{
data: values.map(({ total }) => total),
backgroundColor: CHART_COLORS,
borderWidth: 0,
},
],
}
: null;
const propertySum = useMemo(() => {
return values?.reduce((sum, { total }) => sum + total, 0) ?? 0;
}, [values]);
const chartData = useMemo(() => {
if (!propertyName || !values) return null;
return {
labels: values.map(({ value }) => value),
datasets: [
{
data: values.map(({ total }) => total),
backgroundColor: CHART_COLORS,
borderWidth: 0,
},
],
};
}, [propertyName, values]);
const tableData = useMemo(() => {
if (!propertyName || !values || propertySum === 0) return [];
return values.map(({ value, total }) => ({
x: value,
y: total,
z: 100 * (total / propertySum),
}));
}, [propertyName, values, propertySum]);
const handleRowClick = row => {
setEventName(row.eventName);
@ -52,9 +70,25 @@ export function EventProperties({ websiteId }: { websiteId: string }) {
<GridColumn name="total" label={formatMessage(labels.count)} alignment="end" />
</GridTable>
{propertyName && (
<div className={styles.chart}>
<div className={styles.title}>{propertyName}</div>
<PieChart key={propertyName + eventName} type="doughnut" data={chartData} />
<div className={styles.data}>
<Flexbox className={styles.header} gap={12} justifyContent="space-between">
<div className={styles.title}>{`${eventName}: ${propertyName}`}</div>
<ButtonGroup
selectedKey={propertyView}
onSelect={key => setPropertyView(key as string)}
>
<Button key="table">{formatMessage(labels.table)}</Button>
<Button key="chart">{formatMessage(labels.chart)}</Button>
</ButtonGroup>
</Flexbox>
{!values ? (
<Loading icon="dots" />
) : propertyView === 'table' ? (
<ListTable data={tableData} />
) : (
<PieChart key={propertyName + eventName} type="doughnut" data={chartData} />
)}
</div>
)}
</div>

View file

@ -314,6 +314,8 @@ export const labels = defineMessages({
paidVideo: { id: 'label.paid-video', defaultMessage: 'Paid video' },
grouped: { id: 'label.grouped', defaultMessage: 'Grouped' },
other: { id: 'label.other', defaultMessage: 'Other' },
chart: { id: 'label.chart', defaultMessage: 'Chart' },
table: { id: 'label.table', defaultMessage: 'Table' },
});
export const messages = defineMessages({