Events table.

This commit is contained in:
Mike Cao 2020-08-24 23:49:14 -07:00
parent 2addb96e4b
commit 5206622d5a
13 changed files with 131 additions and 31 deletions

View File

@ -16,6 +16,7 @@ import BrowsersTable from './metrics/BrowsersTable';
import OSTable from './metrics/OSTable'; import OSTable from './metrics/OSTable';
import DevicesTable from './metrics/DevicesTable'; import DevicesTable from './metrics/DevicesTable';
import CountriesTable from './metrics/CountriesTable'; import CountriesTable from './metrics/CountriesTable';
import EventsTable from './metrics/EventsTable';
export default function WebsiteDetails({ websiteId, defaultDateRange = '7day' }) { export default function WebsiteDetails({ websiteId, defaultDateRange = '7day' }) {
const [data, setData] = useState(); const [data, setData] = useState();
@ -23,6 +24,7 @@ export default function WebsiteDetails({ websiteId, defaultDateRange = '7day' })
const [countryData, setCountryData] = useState(); const [countryData, setCountryData] = useState();
const [dateRange, setDateRange] = useState(getDateRange(defaultDateRange)); const [dateRange, setDateRange] = useState(getDateRange(defaultDateRange));
const [expand, setExpand] = useState(); const [expand, setExpand] = useState();
const [showEvents, setShowEvents] = useState(false);
const { startDate, endDate } = dateRange; const { startDate, endDate } = dateRange;
const BackButton = () => ( const BackButton = () => (
@ -50,6 +52,7 @@ export default function WebsiteDetails({ websiteId, defaultDateRange = '7day' })
value: 'country', value: 'country',
component: props => <CountriesTable {...props} onDataLoad={data => setCountryData(data)} />, component: props => <CountriesTable {...props} onDataLoad={data => setCountryData(data)} />,
}, },
{ label: 'Events', value: 'event', component: EventsTable },
]; ];
const tableProps = { const tableProps = {
@ -135,6 +138,12 @@ export default function WebsiteDetails({ websiteId, defaultDateRange = '7day' })
<CountriesTable {...tableProps} onDataLoad={data => setCountryData(data)} /> <CountriesTable {...tableProps} onDataLoad={data => setCountryData(data)} />
</div> </div>
</div> </div>
<div className={classNames(styles.row, 'row', { [styles.hidden]: !showEvents })}>
<div className="col-12 col-md-12 col-lg-4">
<EventsTable {...tableProps} onDataLoad={data => setShowEvents(data.length > 0)} />
</div>
<div className="col-12 col-md-12 col-lg-8">events</div>
</div>
</> </>
)} )}
{expand && ( {expand && (

View File

@ -42,6 +42,10 @@
padding-right: 0; padding-right: 0;
} }
.hidden {
display: none;
}
@media only screen and (max-width: 992px) { @media only screen and (max-width: 992px) {
.row { .row {
border: 0; border: 0;

View File

@ -0,0 +1,32 @@
import React from 'react';
import MetricsTable from './MetricsTable';
import styles from './EventsTable.module.css';
export default function DevicesTable({
websiteId,
startDate,
endDate,
limit,
onExpand,
onDataLoad,
}) {
return (
<MetricsTable
title="Events"
type="event"
metric="Actions"
websiteId={websiteId}
startDate={startDate}
endDate={endDate}
limit={limit}
renderLabel={({ w, x }) => (
<>
<span className={styles.type}>{w}</span>
{x}
</>
)}
onExpand={onExpand}
onDataLoad={onDataLoad}
/>
);
}

View File

@ -0,0 +1,7 @@
.type {
font-size: var(--font-size-small);
padding: 2px 4px;
border: 1px solid var(--gray300);
border-radius: 4px;
margin-right: 10px;
}

View File

@ -21,9 +21,9 @@ export default function MetricsTable({
filterOptions, filterOptions,
limit, limit,
headerComponent, headerComponent,
renderLabel,
onDataLoad = () => {}, onDataLoad = () => {},
onExpand = () => {}, onExpand = () => {},
labelRenderer = e => e,
}) { }) {
const [data, setData] = useState(); const [data, setData] = useState();
const [format, setFormat] = useState(true); const [format, setFormat] = useState(true);
@ -43,37 +43,34 @@ export default function MetricsTable({
async function loadData() { async function loadData() {
const data = await get(`/api/website/${websiteId}/rankings`, { const data = await get(`/api/website/${websiteId}/rankings`, {
type,
start_at: +startDate, start_at: +startDate,
end_at: +endDate, end_at: +endDate,
type,
}); });
setData(data); setData(data);
onDataLoad(data); onDataLoad(data);
} }
function handleSetFormat() { const handleSetFormat = () => setFormat(state => !state);
setFormat(state => !state);
}
function getRow(x, y, z) { const getRow = row => {
const { x: label, y: value, z: percent } = row;
return ( return (
<AnimatedRow <AnimatedRow
key={x} key={label}
label={x} label={renderLabel ? renderLabel(row) : label}
value={y} value={value}
percent={z} percent={percent}
animate={shouldAnimate} animate={shouldAnimate}
format={formatFunc} format={formatFunc}
onClick={handleSetFormat} onClick={handleSetFormat}
labelRenderer={labelRenderer}
/> />
); );
} };
const Row = ({ index, style }) => { const Row = ({ index, style }) => {
const { x, y, z } = rankings[index]; return <div style={style}>{getRow(rankings[index])}</div>;
return <div style={style}>{getRow(x, y, z)}</div>;
}; };
useEffect(() => { useEffect(() => {
@ -96,13 +93,13 @@ export default function MetricsTable({
</div> </div>
</div> </div>
<div className={styles.body}> <div className={styles.body}>
{limit ? ( {limit
rankings.map(({ x, y, z }) => getRow(x, y, z)) ? rankings.map(row => getRow(row))
) : ( : data?.length > 0 && (
<FixedSizeList height={600} itemCount={rankings.length} itemSize={30}> <FixedSizeList height={500} itemCount={rankings.length} itemSize={30}>
{Row} {Row}
</FixedSizeList> </FixedSizeList>
)} )}
</div> </div>
<div className={styles.footer}> <div className={styles.footer}>
{limit && data.length > limit && ( {limit && data.length > limit && (
@ -115,7 +112,7 @@ export default function MetricsTable({
); );
} }
const AnimatedRow = ({ label, value = 0, percent, animate, format, onClick, labelRenderer }) => { const AnimatedRow = ({ label, value = 0, percent, animate, format, onClick }) => {
const props = useSpring({ const props = useSpring({
width: percent, width: percent,
y: value, y: value,
@ -125,7 +122,7 @@ const AnimatedRow = ({ label, value = 0, percent, animate, format, onClick, labe
return ( return (
<div className={styles.row}> <div className={styles.row}>
<div className={styles.label}>{labelRenderer(decodeURI(label))}</div> <div className={styles.label}>{label}</div>
<div className={styles.value} onClick={onClick}> <div className={styles.value} onClick={onClick}>
<animated.div className={styles.value}>{props.y?.interpolate(format)}</animated.div> <animated.div className={styles.value}>{props.y?.interpolate(format)}</animated.div>
</div> </div>

View File

@ -25,6 +25,7 @@ export default function PagesTable({
limit={limit} limit={limit}
dataFilter={urlFilter} dataFilter={urlFilter}
filterOptions={{ domain: websiteDomain, raw: filter === 'Raw' }} filterOptions={{ domain: websiteDomain, raw: filter === 'Raw' }}
renderLabel={({ x }) => decodeURI(x)}
onExpand={onExpand} onExpand={onExpand}
/> />
); );

View File

@ -13,13 +13,13 @@ export default function Referrers({
}) { }) {
const [filter, setFilter] = useState('Combined'); const [filter, setFilter] = useState('Combined');
const renderLink = url => { const renderLink = ({ x: url }) => {
return url.startsWith('http') ? ( return url.startsWith('http') ? (
<a href={url} target="_blank" rel="noreferrer"> <a href={url} target="_blank" rel="noreferrer">
{url} {decodeURI(url)}
</a> </a>
) : ( ) : (
url decodeURI(url)
); );
}; };
@ -40,7 +40,7 @@ export default function Referrers({
raw: filter === 'Raw', raw: filter === 'Raw',
}} }}
onExpand={onExpand} onExpand={onExpand}
labelRenderer={renderLink} renderLabel={renderLink}
/> />
); );
} }

View File

@ -35,5 +35,6 @@ export default prisma;
export async function runQuery(query) { export async function runQuery(query) {
return query.catch(e => { return query.catch(e => {
console.error(e); console.error(e);
throw e;
}); });
} }

View File

@ -124,5 +124,5 @@ export const countryFilter = data =>
export const percentFilter = data => { export const percentFilter = data => {
const total = data.reduce((n, { y }) => n + y, 0); const total = data.reduce((n, { y }) => n + y, 0);
return data.map(({ x, y }) => ({ x, y, z: total ? (y / total) * 100 : 0 })); return data.map(({ x, y, ...props }) => ({ x, y, z: total ? (y / total) * 100 : 0, ...props }));
}; };

View File

@ -400,3 +400,41 @@ export function getActiveVisitors(website_id) {
return Promise.resolve([]); return Promise.resolve([]);
} }
export function getEvents(website_id, start_at, end_at) {
const db = getDatabase();
if (db === POSTGRESQL) {
return prisma.$queryRaw(
`
select distinct event_type w, event_value x, count(*) y
from event
where website_id=$1
and created_at between $2 and $3
group by 1, 2
order by 3 desc
`,
website_id,
start_at,
end_at,
);
}
if (db === MYSQL) {
return prisma.$queryRaw(
`
select distinct event_type w, event_value x, count(*) y
from event
where website_id=?
and created_at between ? and ?
group by 1, 2
order by 3 desc
`,
website_id,
start_at,
end_at,
);
}
return Promise.resolve([]);
}

View File

@ -1,4 +1,4 @@
import { getRankings } from 'lib/queries'; import { getRankings, getEvents } from 'lib/queries';
import { ok, badRequest } from 'lib/response'; import { ok, badRequest } from 'lib/response';
const sessionColumns = ['browser', 'os', 'device', 'country']; const sessionColumns = ['browser', 'os', 'device', 'country'];
@ -6,14 +6,23 @@ const pageviewColumns = ['url', 'referrer'];
export default async (req, res) => { export default async (req, res) => {
const { id, type, start_at, end_at } = req.query; const { id, type, start_at, end_at } = req.query;
const websiteId = +id;
const startDate = new Date(+start_at);
const endDate = new Date(+end_at);
if (!sessionColumns.includes(type) && !pageviewColumns.includes(type)) { if (type !== 'event' && !sessionColumns.includes(type) && !pageviewColumns.includes(type)) {
return badRequest(res); return badRequest(res);
} }
if (type === 'event') {
const events = await getEvents(websiteId, startDate, endDate);
return ok(res, events);
}
const table = sessionColumns.includes(type) ? 'session' : 'pageview'; const table = sessionColumns.includes(type) ? 'session' : 'pageview';
const rankings = await getRankings(+id, new Date(+start_at), new Date(+end_at), type, table); const rankings = await getRankings(websiteId, startDate, endDate, type, table);
return ok(res, rankings); return ok(res, rankings);
}; };

View File

@ -71,6 +71,7 @@ create index session_website_id_idx on session(website_id);
create index pageview_created_at_idx on pageview(created_at); create index pageview_created_at_idx on pageview(created_at);
create index pageview_website_id_idx on pageview(website_id); create index pageview_website_id_idx on pageview(website_id);
create index pageview_session_id_idx on pageview(session_id); create index pageview_session_id_idx on pageview(session_id);
create index pageview_website_id_created_at_idx on pageview(website_id, created_at);
create index event_created_at_idx on event(created_at); create index event_created_at_idx on event(created_at);
create index event_website_id_idx on event(website_id); create index event_website_id_idx on event(website_id);

View File

@ -64,6 +64,7 @@ create index session_website_id_idx on session(website_id);
create index pageview_created_at_idx on pageview(created_at); create index pageview_created_at_idx on pageview(created_at);
create index pageview_website_id_idx on pageview(website_id); create index pageview_website_id_idx on pageview(website_id);
create index pageview_session_id_idx on pageview(session_id); create index pageview_session_id_idx on pageview(session_id);
create index pageview_website_id_created_at_idx on pageview(website_id, created_at);
create index event_created_at_idx on event(created_at); create index event_created_at_idx on event(created_at);
create index event_website_id_idx on event(website_id); create index event_website_id_idx on event(website_id);