mirror of
https://github.com/kremalicious/umami.git
synced 2024-12-18 15:23:38 +01:00
Events table.
This commit is contained in:
parent
a19248d713
commit
5e9299be1e
@ -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 && (
|
||||||
|
@ -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;
|
||||||
|
32
components/metrics/EventsTable.js
Normal file
32
components/metrics/EventsTable.js
Normal 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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
7
components/metrics/EventsTable.module.css
Normal file
7
components/metrics/EventsTable.module.css
Normal 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;
|
||||||
|
}
|
@ -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>
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -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 }));
|
||||||
};
|
};
|
||||||
|
@ -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([]);
|
||||||
|
}
|
||||||
|
@ -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);
|
||||||
};
|
};
|
||||||
|
@ -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);
|
||||||
|
@ -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);
|
||||||
|
Loading…
Reference in New Issue
Block a user