mirror of
https://github.com/kremalicious/umami.git
synced 2025-02-14 21:10:34 +01:00
Refactored realtime page. Fixed render issue.
This commit is contained in:
parent
42f184584f
commit
35fde36b61
@ -73,7 +73,7 @@
|
||||
"@umami/prisma-client": "^0.14.0",
|
||||
"@umami/redis-client": "^0.18.0",
|
||||
"chalk": "^4.1.1",
|
||||
"chart.js": "^4.2.1",
|
||||
"chart.js": "^4.4.2",
|
||||
"chartjs-adapter-date-fns": "^3.0.0",
|
||||
"classnames": "^2.3.1",
|
||||
"colord": "^2.9.2",
|
||||
|
@ -1,16 +0,0 @@
|
||||
.container {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.chart {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.sticky {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
background: var(--base50);
|
||||
border-bottom: 1px solid var(--base300);
|
||||
z-index: 1;
|
||||
padding: 10px 0;
|
||||
}
|
@ -1,113 +0,0 @@
|
||||
import { useMemo, useState, useEffect } from 'react';
|
||||
import { subMinutes, startOfMinute } from 'date-fns';
|
||||
import thenby from 'thenby';
|
||||
import { Grid, GridRow } from 'components/layout/Grid';
|
||||
import Page from 'components/layout/Page';
|
||||
import RealtimeChart from 'components/metrics/RealtimeChart';
|
||||
import WorldMap from 'components/metrics/WorldMap';
|
||||
import { useApi, useWebsite } from 'components/hooks';
|
||||
import { percentFilter } from 'lib/filters';
|
||||
import { REALTIME_RANGE, REALTIME_INTERVAL } from 'lib/constants';
|
||||
import { RealtimeData } from 'lib/types';
|
||||
import RealtimeLog from './RealtimeLog';
|
||||
import RealtimeHeader from './RealtimeHeader';
|
||||
import RealtimeUrls from './RealtimeUrls';
|
||||
import RealtimeCountries from './RealtimeCountries';
|
||||
import WebsiteHeader from '../WebsiteHeader';
|
||||
import styles from './Realtime.module.css';
|
||||
|
||||
function mergeData(state = [], data = [], time: number) {
|
||||
const ids = state.map(({ id }) => id);
|
||||
return state
|
||||
.concat(data.filter(({ id }) => !ids.includes(id)))
|
||||
.filter(({ timestamp }) => timestamp >= time);
|
||||
}
|
||||
|
||||
export function Realtime({ websiteId }) {
|
||||
const [currentData, setCurrentData] = useState<RealtimeData>();
|
||||
const { get, useQuery } = useApi();
|
||||
const { data: website } = useWebsite(websiteId);
|
||||
const { data, isLoading, error } = useQuery({
|
||||
queryKey: ['realtime', websiteId],
|
||||
queryFn: () => get(`/realtime/${websiteId}`, { startAt: currentData?.timestamp || 0 }),
|
||||
enabled: !!(websiteId && website),
|
||||
refetchInterval: REALTIME_INTERVAL,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
const date = subMinutes(startOfMinute(new Date()), REALTIME_RANGE);
|
||||
const time = date.getTime();
|
||||
const { pageviews, sessions, events, timestamp } = data;
|
||||
|
||||
setCurrentData(state => ({
|
||||
pageviews: mergeData(state?.pageviews, pageviews, time),
|
||||
sessions: mergeData(state?.sessions, sessions, time),
|
||||
events: mergeData(state?.events, events, time),
|
||||
timestamp,
|
||||
}));
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
const realtimeData: RealtimeData = useMemo(() => {
|
||||
if (!currentData) {
|
||||
return { pageviews: [], sessions: [], events: [], countries: [], visitors: [], timestamp: 0 };
|
||||
}
|
||||
|
||||
currentData.countries = percentFilter(
|
||||
currentData.sessions
|
||||
.reduce((arr, data) => {
|
||||
if (!arr.find(({ id }) => id === data.id)) {
|
||||
return arr.concat(data);
|
||||
}
|
||||
return arr;
|
||||
}, [])
|
||||
.reduce((arr: { x: any; y: number }[], { country }: any) => {
|
||||
if (country) {
|
||||
const row = arr.find(({ x }) => x === country);
|
||||
|
||||
if (!row) {
|
||||
arr.push({ x: country, y: 1 });
|
||||
} else {
|
||||
row.y += 1;
|
||||
}
|
||||
}
|
||||
return arr;
|
||||
}, [])
|
||||
.sort(thenby.firstBy('y', -1)),
|
||||
);
|
||||
|
||||
currentData.visitors = currentData.sessions.reduce((arr, val) => {
|
||||
if (!arr.find(({ id }) => id === val.id)) {
|
||||
return arr.concat(val);
|
||||
}
|
||||
return arr;
|
||||
}, []);
|
||||
|
||||
return currentData;
|
||||
}, [currentData]);
|
||||
|
||||
if (isLoading || error) {
|
||||
return <Page isLoading={isLoading} error={error} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<WebsiteHeader websiteId={websiteId} />
|
||||
<RealtimeHeader data={realtimeData} />
|
||||
<RealtimeChart className={styles.chart} data={realtimeData} unit="minute" />
|
||||
<Grid>
|
||||
<GridRow columns="one-two">
|
||||
<RealtimeUrls websiteDomain={website?.domain} data={realtimeData} />
|
||||
<RealtimeLog websiteDomain={website?.domain} data={realtimeData} />
|
||||
</GridRow>
|
||||
<GridRow columns="one-two">
|
||||
<RealtimeCountries data={realtimeData?.countries} />
|
||||
<WorldMap data={realtimeData?.countries} />
|
||||
</GridRow>
|
||||
</Grid>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Realtime;
|
@ -35,11 +35,6 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.website {
|
||||
text-align: right;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.detail {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useContext, useMemo, useState } from 'react';
|
||||
import { StatusLight, Icon, Text, SearchField } from 'react-basics';
|
||||
import { FixedSizeList } from 'react-window';
|
||||
import { format } from 'date-fns';
|
||||
@ -11,6 +11,8 @@ import Icons from 'components/icons';
|
||||
import useFormat from 'components//hooks/useFormat';
|
||||
import { BROWSERS } from 'lib/constants';
|
||||
import { stringToColor } from 'lib/format';
|
||||
import { RealtimeData } from 'lib/types';
|
||||
import { WebsiteContext } from '../WebsiteProvider';
|
||||
import styles from './RealtimeLog.module.css';
|
||||
|
||||
const TYPE_ALL = 'all';
|
||||
@ -24,7 +26,8 @@ const icons = {
|
||||
[TYPE_EVENT]: <Icons.Bolt />,
|
||||
};
|
||||
|
||||
export function RealtimeLog({ data, websiteDomain }) {
|
||||
export function RealtimeLog({ data }: { data: RealtimeData }) {
|
||||
const website = useContext(WebsiteContext);
|
||||
const [search, setSearch] = useState('');
|
||||
const { formatMessage, labels, messages, FormattedMessage } = useMessages();
|
||||
const { formatValue } = useFormat();
|
||||
@ -76,7 +79,7 @@ export function RealtimeLog({ data, websiteDomain }) {
|
||||
event: <b>{eventName || formatMessage(labels.unknown)}</b>,
|
||||
url: (
|
||||
<a
|
||||
href={`//${websiteDomain}${url}`}
|
||||
href={`//${website?.domain}${url}`}
|
||||
className={styles.link}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
@ -92,7 +95,7 @@ export function RealtimeLog({ data, websiteDomain }) {
|
||||
if (__type === TYPE_PAGEVIEW) {
|
||||
return (
|
||||
<a
|
||||
href={`//${websiteDomain}${url}`}
|
||||
href={`//${website?.domain}${url}`}
|
||||
className={styles.link}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Key, useMemo, useState } from 'react';
|
||||
import { Key, useContext, useMemo, useState } from 'react';
|
||||
import { ButtonGroup, Button, Flexbox } from 'react-basics';
|
||||
import thenby from 'thenby';
|
||||
import { percentFilter } from 'lib/filters';
|
||||
@ -6,14 +6,10 @@ import ListTable from 'components/metrics/ListTable';
|
||||
import { FILTER_PAGES, FILTER_REFERRERS } from 'lib/constants';
|
||||
import { useMessages } from 'components/hooks';
|
||||
import { RealtimeData } from 'lib/types';
|
||||
import { WebsiteContext } from '../WebsiteProvider';
|
||||
|
||||
export function RealtimeUrls({
|
||||
websiteDomain,
|
||||
data,
|
||||
}: {
|
||||
websiteDomain: string;
|
||||
data: RealtimeData;
|
||||
}) {
|
||||
export function RealtimeUrls({ data }: { data: RealtimeData }) {
|
||||
const website = useContext(WebsiteContext);
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { pageviews } = data || {};
|
||||
const [filter, setFilter] = useState<Key>(FILTER_REFERRERS);
|
||||
@ -31,7 +27,7 @@ export function RealtimeUrls({
|
||||
];
|
||||
|
||||
const renderLink = ({ x }) => {
|
||||
const domain = x.startsWith('/') ? websiteDomain : '';
|
||||
const domain = x.startsWith('/') ? website?.domain : '';
|
||||
return (
|
||||
<a href={`//${domain}${x}`} target="_blank" rel="noreferrer noopener">
|
||||
{x}
|
||||
|
@ -1,6 +1,40 @@
|
||||
'use client';
|
||||
import Realtime from './Realtime';
|
||||
import { Grid, GridRow } from 'components/layout/Grid';
|
||||
import Page from 'components/layout/Page';
|
||||
import RealtimeChart from 'components/metrics/RealtimeChart';
|
||||
import WorldMap from 'components/metrics/WorldMap';
|
||||
import { useRealtime } from 'components/hooks';
|
||||
import RealtimeLog from './RealtimeLog';
|
||||
import RealtimeHeader from './RealtimeHeader';
|
||||
import RealtimeUrls from './RealtimeUrls';
|
||||
import RealtimeCountries from './RealtimeCountries';
|
||||
import WebsiteHeader from '../WebsiteHeader';
|
||||
import WebsiteProvider from '../WebsiteProvider';
|
||||
|
||||
export default function WebsiteRealtimePage({ websiteId }) {
|
||||
return <Realtime websiteId={websiteId} />;
|
||||
export function WebsiteRealtimePage({ websiteId }) {
|
||||
const { data, isLoading, error } = useRealtime(websiteId);
|
||||
|
||||
if (isLoading || error) {
|
||||
return <Page isLoading={isLoading} error={error} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<WebsiteProvider websiteId={websiteId}>
|
||||
<WebsiteHeader websiteId={websiteId} />
|
||||
<RealtimeHeader data={data} />
|
||||
<RealtimeChart data={data} unit="minute" />
|
||||
<Grid>
|
||||
<GridRow columns="one-two">
|
||||
<RealtimeUrls data={data} />
|
||||
<RealtimeLog data={data} />
|
||||
</GridRow>
|
||||
<GridRow columns="one-two">
|
||||
<RealtimeCountries data={data?.countries} />
|
||||
<WorldMap data={data?.countries} />
|
||||
</GridRow>
|
||||
</Grid>
|
||||
</WebsiteProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default WebsiteRealtimePage;
|
||||
|
@ -2,6 +2,7 @@ export * from './queries/useApi';
|
||||
export * from './queries/useConfig';
|
||||
export * from './queries/useFilterQuery';
|
||||
export * from './queries/useLogin';
|
||||
export * from './queries/useRealtime';
|
||||
export * from './queries/useReport';
|
||||
export * from './queries/useReports';
|
||||
export * from './queries/useShareToken';
|
||||
|
87
src/components/hooks/queries/useRealtime.ts
Normal file
87
src/components/hooks/queries/useRealtime.ts
Normal file
@ -0,0 +1,87 @@
|
||||
import { useMemo, useRef } from 'react';
|
||||
import { RealtimeData } from 'lib/types';
|
||||
import { useApi } from 'components/hooks';
|
||||
import { REALTIME_INTERVAL, REALTIME_RANGE } from 'lib/constants';
|
||||
import { startOfMinute, subMinutes } from 'date-fns';
|
||||
import { percentFilter } from 'lib/filters';
|
||||
import thenby from 'thenby';
|
||||
|
||||
function mergeData(state = [], data = [], time: number) {
|
||||
const ids = state.map(({ id }) => id);
|
||||
return state
|
||||
.concat(data.filter(({ id }) => !ids.includes(id)))
|
||||
.filter(({ timestamp }) => timestamp >= time);
|
||||
}
|
||||
|
||||
export function useRealtime(websiteId: string) {
|
||||
const currentData = useRef({
|
||||
pageviews: [],
|
||||
sessions: [],
|
||||
events: [],
|
||||
countries: [],
|
||||
visitors: [],
|
||||
timestamp: 0,
|
||||
});
|
||||
const { get, useQuery } = useApi();
|
||||
const { data, isLoading, error } = useQuery<RealtimeData>({
|
||||
queryKey: ['realtime', websiteId],
|
||||
queryFn: async () => {
|
||||
const state = currentData.current;
|
||||
const data = await get(`/realtime/${websiteId}`, { startAt: state?.timestamp || 0 });
|
||||
const date = subMinutes(startOfMinute(new Date()), REALTIME_RANGE);
|
||||
const time = date.getTime();
|
||||
const { pageviews, sessions, events, timestamp } = data;
|
||||
|
||||
return {
|
||||
pageviews: mergeData(state?.pageviews, pageviews, time),
|
||||
sessions: mergeData(state?.sessions, sessions, time),
|
||||
events: mergeData(state?.events, events, time),
|
||||
timestamp,
|
||||
};
|
||||
},
|
||||
enabled: !!websiteId,
|
||||
refetchInterval: REALTIME_INTERVAL,
|
||||
});
|
||||
|
||||
const realtimeData: RealtimeData = useMemo(() => {
|
||||
if (!data) {
|
||||
return { pageviews: [], sessions: [], events: [], countries: [], visitors: [], timestamp: 0 };
|
||||
}
|
||||
|
||||
data.countries = percentFilter(
|
||||
data.sessions
|
||||
.reduce((arr, data) => {
|
||||
if (!arr.find(({ id }) => id === data.id)) {
|
||||
return arr.concat(data);
|
||||
}
|
||||
return arr;
|
||||
}, [])
|
||||
.reduce((arr: { x: any; y: number }[], { country }: any) => {
|
||||
if (country) {
|
||||
const row = arr.find(({ x }) => x === country);
|
||||
|
||||
if (!row) {
|
||||
arr.push({ x: country, y: 1 });
|
||||
} else {
|
||||
row.y += 1;
|
||||
}
|
||||
}
|
||||
return arr;
|
||||
}, [])
|
||||
.sort(thenby.firstBy('y', -1)),
|
||||
);
|
||||
|
||||
data.visitors = data.sessions.reduce((arr, val) => {
|
||||
if (!arr.find(({ id }) => id === val.id)) {
|
||||
return arr.concat(val);
|
||||
}
|
||||
return arr;
|
||||
}, []);
|
||||
|
||||
return data;
|
||||
}, [data]);
|
||||
|
||||
return { data: realtimeData, isLoading, error };
|
||||
}
|
||||
|
||||
export default useRealtime;
|
@ -21,6 +21,7 @@ export interface BarChartProps {
|
||||
XAxisType?: string;
|
||||
YAxisType?: string;
|
||||
renderTooltipPopup?: (setTooltipPopup: (data: any) => void, model: any) => void;
|
||||
updateMode?: string;
|
||||
onCreate?: (chart: any) => void;
|
||||
onUpdate?: (chart: any) => void;
|
||||
className?: string;
|
||||
@ -37,6 +38,7 @@ export function BarChart({
|
||||
XAxisType = 'time',
|
||||
YAxisType = 'linear',
|
||||
renderTooltipPopup,
|
||||
updateMode,
|
||||
onCreate,
|
||||
onUpdate,
|
||||
className,
|
||||
@ -136,25 +138,16 @@ export function BarChart({
|
||||
const updateChart = (datasets: any[]) => {
|
||||
setTooltipPopup(null);
|
||||
|
||||
const diff = chart.current.data.datasets.reduce(
|
||||
(found: boolean, dataset: { data: any[] }, set: number) => {
|
||||
if (!found) {
|
||||
return dataset.data.find((value, index) => {
|
||||
return datasets[set].data[index].y !== value.y;
|
||||
});
|
||||
}
|
||||
return found;
|
||||
},
|
||||
false,
|
||||
);
|
||||
chart.current.data.datasets.forEach((dataset: { data: any }, index: string | number) => {
|
||||
dataset.data = datasets[index].data;
|
||||
});
|
||||
|
||||
if (diff) {
|
||||
chart.current.data.datasets = datasets;
|
||||
chart.current.options = getOptions();
|
||||
chart.current.update();
|
||||
chart.current.options = getOptions();
|
||||
|
||||
onUpdate?.(chart.current);
|
||||
}
|
||||
// Allow config changes before update
|
||||
onUpdate?.(chart.current);
|
||||
|
||||
chart.current.update(updateMode);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -3987,10 +3987,10 @@ charenc@0.0.2:
|
||||
resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667"
|
||||
integrity sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==
|
||||
|
||||
chart.js@^4.2.1:
|
||||
version "4.4.1"
|
||||
resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-4.4.1.tgz#ac5dc0e69a7758909158a96fe80ce43b3bb96a9f"
|
||||
integrity sha512-C74QN1bxwV1v2PEujhmKjOZ7iUM4w6BWs23Md/6aOZZSlwMzeCIDGuZay++rBgChYru7/+QFeoQW0fQoP534Dg==
|
||||
chart.js@^4.4.2:
|
||||
version "4.4.2"
|
||||
resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-4.4.2.tgz#95962fa6430828ed325a480cc2d5f2b4e385ac31"
|
||||
integrity sha512-6GD7iKwFpP5kbSD4MeRRRlTnQvxfQREy36uEtm1hzHzcOqwWx0YEHuspuoNlslu+nciLIB7fjjsHkUv/FzFcOg==
|
||||
dependencies:
|
||||
"@kurkle/color" "^0.3.0"
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user