From f59d34f64dd10bc7e49d5a59cff73bc5371a9d8b Mon Sep 17 00:00:00 2001 From: Fabian Hiller Date: Sat, 24 Feb 2024 23:47:06 -0500 Subject: [PATCH 01/91] Upgrade Next.js plugin of Netlify in package.json --- package.json | 2 +- yarn.lock | 43 +++++++++++++++++++++---------------------- 2 files changed, 22 insertions(+), 23 deletions(-) diff --git a/package.json b/package.json index 0f437c35..4eec520f 100644 --- a/package.json +++ b/package.json @@ -117,7 +117,7 @@ }, "devDependencies": { "@formatjs/cli": "^4.2.29", - "@netlify/plugin-nextjs": "^4.27.3", + "@netlify/plugin-nextjs": "^4.41.3", "@rollup/plugin-alias": "^5.0.0", "@rollup/plugin-buble": "^1.0.2", "@rollup/plugin-commonjs": "^25.0.4", diff --git a/yarn.lock b/yarn.lock index b24830a3..62fa6ff2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1704,20 +1704,19 @@ dependencies: is-promise "^4.0.0" -"@netlify/functions@^2.1.0": - version "2.3.0" - resolved "https://registry.yarnpkg.com/@netlify/functions/-/functions-2.3.0.tgz#37e2ca41c0034a10de4addbdff7fbb8ec669e8c7" - integrity sha512-E3kzXPWMP/r1rAWhjTaXcaOT47dhEvg/eQUJjRLhD9Zzp0WqkdynHr+bqff4rFNv6tuXrtFZrpbPJFKHH0c0zw== +"@netlify/functions@^2.4.0": + version "2.6.0" + resolved "https://registry.yarnpkg.com/@netlify/functions/-/functions-2.6.0.tgz#801a6fe8ceef2ce1512c637a28e53e6a3aae289b" + integrity sha512-vU20tij0fb4nRGACqb+5SQvKd50JYyTyEhQetCMHdakcJFzjLDivvRR16u1G2Oy4A7xNAtGJF1uz8reeOtTVcQ== dependencies: - "@netlify/serverless-functions-api" "1.9.0" - is-promise "^4.0.0" + "@netlify/serverless-functions-api" "1.14.0" -"@netlify/ipx@^1.4.5": - version "1.4.5" - resolved "https://registry.yarnpkg.com/@netlify/ipx/-/ipx-1.4.5.tgz#c0b38628457786ca3edf365a9a0cf97cdd9d0883" - integrity sha512-QuPxUj8Bn8hXwjdcA1BF+HPLqFJ2e9OCNrKX/s3hoUFjjqQrNSK8lLARAtzGfOM3BRsTXyi/zGdwBE+oJKd0dw== +"@netlify/ipx@^1.4.6": + version "1.4.6" + resolved "https://registry.yarnpkg.com/@netlify/ipx/-/ipx-1.4.6.tgz#0bd308d70a1d2e1928e66cb49e36294f66f7b8b2" + integrity sha512-rnKR2LXhtnflitPX9CQIv+XSrNlYIqGsV54xrXifhbtHHjCjCw/lixsi8qwAXqEIgZBC9b4Y7prhHqRtC4oIjw== dependencies: - "@netlify/functions" "^2.1.0" + "@netlify/functions" "^2.4.0" etag "^1.8.1" fs-extra "^11.0.0" ipx "^1.3.1" @@ -1726,22 +1725,22 @@ murmurhash "^2.0.0" node-fetch "^2.0.0" ufo "^1.0.0" - unstorage "^1.0.0" + unstorage "1.9.0" "@netlify/node-cookies@^0.1.0": version "0.1.0" resolved "https://registry.yarnpkg.com/@netlify/node-cookies/-/node-cookies-0.1.0.tgz#dda912ba618527695cf519fafa221c5e6777c612" integrity sha512-OAs1xG+FfLX0LoRASpqzVntVV/RpYkgpI0VrUnw2u0Q1qiZUzcPffxRK8HF3gc4GjuhG5ahOEMJ9bswBiZPq0g== -"@netlify/plugin-nextjs@^4.27.3": - version "4.41.0" - resolved "https://registry.yarnpkg.com/@netlify/plugin-nextjs/-/plugin-nextjs-4.41.0.tgz#34c4af80b8f1575d6053d38f59e9b1e0aa1c3321" - integrity sha512-Yq1hw/Ip3OGhQhG9xVNIf+lyY6XIbkDzxpXV3wIFbfZDOzgXxBfHixB+AWRoztiYPDknIRC3UOQKwdlRkHyqhw== +"@netlify/plugin-nextjs@^4.41.3": + version "4.41.3" + resolved "https://registry.yarnpkg.com/@netlify/plugin-nextjs/-/plugin-nextjs-4.41.3.tgz#f8274526147f652438cc6790b6819ca15d441dd3" + integrity sha512-l8TB61u7A1ZF22QpoyZtresSUsHOJGP9DatECnqlNab3lG8id1kz9Pso+nZVOznWOm98o7w51k2+TIf52x+DBQ== dependencies: "@netlify/blobs" "^2.2.0" "@netlify/esbuild" "0.14.39" "@netlify/functions" "^1.6.0" - "@netlify/ipx" "^1.4.5" + "@netlify/ipx" "^1.4.6" "@vercel/node-bridge" "^2.1.0" chalk "^4.1.2" chokidar "^3.5.3" @@ -1763,10 +1762,10 @@ slash "^3.0.0" tiny-glob "^0.2.9" -"@netlify/serverless-functions-api@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@netlify/serverless-functions-api/-/serverless-functions-api-1.9.0.tgz#3e58249e57350aee2c5143c282fddb4abbae4a9d" - integrity sha512-Jq4uk1Mwa5vyxImupJYXPP+I5yYcp3PtguvXtJRutKdm9DPALXfZVtCQzBWMNdZiqVWCM3La9hvaBsPjSMfeug== +"@netlify/serverless-functions-api@1.14.0": + version "1.14.0" + resolved "https://registry.yarnpkg.com/@netlify/serverless-functions-api/-/serverless-functions-api-1.14.0.tgz#2bedff76cf898e24e48161aa2508776c4d261ed1" + integrity sha512-HUNETLNvNiC2J+SB/YuRwJA9+agPrc0azSoWVk8H85GC+YE114hcS5JW+dstpKwVerp2xILE3vNWN7IMXP5Q5Q== dependencies: "@netlify/node-cookies" "^0.1.0" urlpattern-polyfill "8.0.2" @@ -8993,7 +8992,7 @@ universalify@^2.0.0: resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== -unstorage@^1.0.0: +unstorage@1.9.0: version "1.9.0" resolved "https://registry.yarnpkg.com/unstorage/-/unstorage-1.9.0.tgz#0c1977f4e769a48344339ac97ec3f2feea94d43d" integrity sha512-VpD8ZEYc/le8DZCrny3bnqKE4ZjioQxBRnWE+j5sGNvziPjeDlaS1NaFFHzl/kkXaO3r7UaF8MGQrs14+1B4pQ== From 5153f320f6c5dba1df5c1dab3ceaaf81cfe582bd Mon Sep 17 00:00:00 2001 From: Fabian Hiller Date: Tue, 27 Feb 2024 01:39:57 -0500 Subject: [PATCH 02/91] Add data-strip-search config to tracker --- src/tracker/index.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/tracker/index.js b/src/tracker/index.js index d5278b21..fea66e09 100644 --- a/src/tracker/index.js +++ b/src/tracker/index.js @@ -14,10 +14,12 @@ const _data = 'data-'; const _false = 'false'; + const _true = 'true'; const attr = currentScript.getAttribute.bind(currentScript); const website = attr(_data + 'website-id'); const hostUrl = attr(_data + 'host-url'); const autoTrack = attr(_data + 'auto-track') !== _false; + const stripSearch = attr(_data + 'strip-search') === _true; const dnt = attr(_data + 'do-not-track'); const domain = attr(_data + 'domains') || ''; const domains = domain.split(',').map(n => n.trim()); @@ -218,7 +220,7 @@ }; } - let currentUrl = `${pathname}${search}`; + let currentUrl = `${pathname}${stripSearch ? '' : search}`; let currentRef = document.referrer; let title = document.title; let cache; From 2a8402218acb547c3dcd9b8c965a54b117e33d2a Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Wed, 6 Mar 2024 11:36:40 -0800 Subject: [PATCH 03/91] clean-up cypress docker config --- cypress.config.ts | 5 +++++ cypress/docker-compose.yml | 3 ++- cypress/e2e/login.cy.ts | 8 ++++++-- cypress/e2e/website.cy.ts | 30 ++++++++++++++---------------- 4 files changed, 27 insertions(+), 19 deletions(-) diff --git a/cypress.config.ts b/cypress.config.ts index 5bed49b8..4b01931b 100644 --- a/cypress.config.ts +++ b/cypress.config.ts @@ -4,4 +4,9 @@ export default defineConfig({ e2e: { baseUrl: 'http://localhost:3000', }, + // default username / password on init + env: { + umami_user: 'admin', + umami_password: 'umami', + }, }); diff --git a/cypress/docker-compose.yml b/cypress/docker-compose.yml index 3cd8f546..01a47bd8 100644 --- a/cypress/docker-compose.yml +++ b/cypress/docker-compose.yml @@ -43,9 +43,10 @@ services: - CYPRESS_umami_user=admin - CYPRESS_umami_password=umami volumes: - - ../tsconfig.json:/tsconfig.json + - ./tsconfig.json:/tsconfig.json - ../cypress.config.ts:/cypress.config.ts - ./:/cypress - ../node_modules/:/node_modules + - ../src/lib/crypto.ts:/src/lib/crypto.ts volumes: umami-db-data: diff --git a/cypress/e2e/login.cy.ts b/cypress/e2e/login.cy.ts index 288d5c51..5831c81d 100644 --- a/cypress/e2e/login.cy.ts +++ b/cypress/e2e/login.cy.ts @@ -6,8 +6,12 @@ describe('Login tests', () => { }, () => { cy.visit('/login'); - cy.getDataTest('input-username').find('input').type(Cypress.env('umami_user')); - cy.getDataTest('input-password').find('input').type(Cypress.env('umami_password')); + cy.getDataTest('input-username').find('input').click(); + cy.getDataTest('input-username').find('input').type(Cypress.env('umami_user'), { delay: 50 }); + cy.getDataTest('input-password').find('input').click(); + cy.getDataTest('input-password') + .find('input') + .type(Cypress.env('umami_password'), { delay: 50 }); cy.getDataTest('button-submit').click(); cy.url().should('eq', Cypress.config().baseUrl + '/dashboard'); cy.getDataTest('button-profile').click(); diff --git a/cypress/e2e/website.cy.ts b/cypress/e2e/website.cy.ts index feec39de..b60d8e7a 100644 --- a/cypress/e2e/website.cy.ts +++ b/cypress/e2e/website.cy.ts @@ -10,8 +10,10 @@ describe('Website tests', () => { cy.visit('/settings/websites'); cy.getDataTest('button-website-add').click(); cy.contains(/Add website/i).should('be.visible'); - cy.getDataTest('input-name').find('input').wait(500).type('Add test', { delay: 50 }); - cy.getDataTest('input-domain').find('input').wait(500).type('addtest.com', { delay: 50 }); + cy.getDataTest('input-name').find('input').click(); + cy.getDataTest('input-name').find('input').type('Add test', { delay: 50 }); + cy.getDataTest('input-domain').find('input').click(); + cy.getDataTest('input-domain').find('input').type('addtest.com', { delay: 50 }); cy.getDataTest('button-submit').click(); cy.get('td[label="Name"]').should('contain.text', 'Add test'); cy.get('td[label="Domain"]').should('contain.text', 'addtest.com'); @@ -26,10 +28,10 @@ describe('Website tests', () => { cy.deleteWebsite(websiteId); }); cy.visit('/settings/websites'); - cy.contains('Add test').should('not.exist'); + cy.contains(/Add test/i).should('not.exist'); }); - it.only('Edit a website', () => { + it('Edit a website', () => { // prep data cy.addWebsite('Update test', 'updatetest.com'); cy.visit('/settings/websites'); @@ -37,16 +39,12 @@ describe('Website tests', () => { // edit website cy.getDataTest('link-button-edit').first().click(); cy.contains(/Details/i).should('be.visible'); - cy.getDataTest('input-name') - .find('input') - .wait(500) - .clear() - .type('Updated website', { delay: 50 }); - cy.getDataTest('input-domain') - .find('input') - .wait(500) - .clear() - .type('updatedwebsite.com', { delay: 50 }); + cy.getDataTest('input-name').find('input').click(); + cy.getDataTest('input-name').find('input').clear(); + cy.getDataTest('input-name').find('input').type('Updated website', { delay: 50 }); + cy.getDataTest('input-domain').find('input').click(); + cy.getDataTest('input-domain').find('input').clear(); + cy.getDataTest('input-domain').find('input').type('updatedwebsite.com', { delay: 50 }); cy.getDataTest('button-submit').click({ force: true }); cy.getDataTest('input-name').find('input').should('have.value', 'Updated website'); cy.getDataTest('input-domain').find('input').should('have.value', 'updatedwebsite.com'); @@ -69,7 +67,7 @@ describe('Website tests', () => { cy.deleteWebsite(websiteId); }); cy.visit('/settings/websites'); - cy.contains('Add test').should('not.exist'); + cy.contains(/Add test/i).should('not.exist'); }); it('Delete a website', () => { @@ -86,6 +84,6 @@ describe('Website tests', () => { cy.contains(/Type DELETE in the box below to confirm./i).should('be.visible'); cy.get('input[name="confirm"').type('DELETE'); cy.get('button[type="submit"]').click(); - cy.contains('Delete test').should('not.exist'); + cy.contains(/Delete test/i).should('not.exist'); }); }); From 16036579953ef313d89a1c2d59559e4d894b5293 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Wed, 6 Mar 2024 17:08:24 -0800 Subject: [PATCH 04/91] Update bar chart only if dataset is different. --- src/components/metrics/BarChart.module.css | 8 ------ src/components/metrics/BarChart.tsx | 30 +++++++++++++++------- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/src/components/metrics/BarChart.module.css b/src/components/metrics/BarChart.module.css index 6af22abe..ee61f29b 100644 --- a/src/components/metrics/BarChart.module.css +++ b/src/components/metrics/BarChart.module.css @@ -1,7 +1,3 @@ -.container { - display: grid; -} - .chart { position: relative; height: 400px; @@ -13,7 +9,3 @@ flex-direction: column; gap: 10px; } - -.tooltip .value { - text-transform: lowercase; -} diff --git a/src/components/metrics/BarChart.tsx b/src/components/metrics/BarChart.tsx index 1b1c7588..8c26afa0 100644 --- a/src/components/metrics/BarChart.tsx +++ b/src/components/metrics/BarChart.tsx @@ -119,7 +119,7 @@ export function BarChart({ locale, ]); - const createChart = () => { + const createChart = (datasets: any[]) => { Chart.defaults.font.family = 'Inter'; chart.current = new Chart(canvas.current, { @@ -133,24 +133,36 @@ export function BarChart({ onCreate?.(chart.current); }; - const updateChart = () => { + const updateChart = (datasets: any[]) => { setTooltipPopup(null); - chart.current.data.datasets = datasets; + 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.options = getOptions(); + if (diff) { + chart.current.data.datasets = datasets; + chart.current.options = getOptions(); + chart.current.update(); - onUpdate?.(chart.current); - - chart.current.update(); + onUpdate?.(chart.current); + } }; useEffect(() => { if (datasets) { if (!chart.current) { - createChart(); + createChart(datasets); } else { - updateChart(); + updateChart(datasets); } } }, [datasets, unit, theme, animationDuration, locale]); From 35fde36b61a4e5cd63cdd323c21f83dcf5f44abb Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Fri, 8 Mar 2024 01:11:03 -0800 Subject: [PATCH 05/91] Refactored realtime page. Fixed render issue. --- package.json | 2 +- .../[websiteId]/realtime/Realtime.module.css | 16 --- .../[websiteId]/realtime/Realtime.tsx | 113 ------------------ .../realtime/RealtimeLog.module.css | 5 - .../[websiteId]/realtime/RealtimeLog.tsx | 11 +- .../[websiteId]/realtime/RealtimeUrls.tsx | 14 +-- .../realtime/WebsiteRealtimePage.tsx | 40 ++++++- src/components/hooks/index.ts | 1 + src/components/hooks/queries/useRealtime.ts | 87 ++++++++++++++ src/components/metrics/BarChart.tsx | 27 ++--- yarn.lock | 8 +- 11 files changed, 152 insertions(+), 172 deletions(-) delete mode 100644 src/app/(main)/websites/[websiteId]/realtime/Realtime.module.css delete mode 100644 src/app/(main)/websites/[websiteId]/realtime/Realtime.tsx create mode 100644 src/components/hooks/queries/useRealtime.ts diff --git a/package.json b/package.json index ea1ad8c3..0ab7b075 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/app/(main)/websites/[websiteId]/realtime/Realtime.module.css b/src/app/(main)/websites/[websiteId]/realtime/Realtime.module.css deleted file mode 100644 index 465be551..00000000 --- a/src/app/(main)/websites/[websiteId]/realtime/Realtime.module.css +++ /dev/null @@ -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; -} diff --git a/src/app/(main)/websites/[websiteId]/realtime/Realtime.tsx b/src/app/(main)/websites/[websiteId]/realtime/Realtime.tsx deleted file mode 100644 index 6314fbb8..00000000 --- a/src/app/(main)/websites/[websiteId]/realtime/Realtime.tsx +++ /dev/null @@ -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(); - 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 ; - } - - return ( - <> - - - - - - - - - - - - - - - ); -} - -export default Realtime; diff --git a/src/app/(main)/websites/[websiteId]/realtime/RealtimeLog.module.css b/src/app/(main)/websites/[websiteId]/realtime/RealtimeLog.module.css index fb5fdecf..19d02384 100644 --- a/src/app/(main)/websites/[websiteId]/realtime/RealtimeLog.module.css +++ b/src/app/(main)/websites/[websiteId]/realtime/RealtimeLog.module.css @@ -35,11 +35,6 @@ overflow: hidden; } -.website { - text-align: right; - padding: 0 20px; -} - .detail { display: flex; align-items: center; diff --git a/src/app/(main)/websites/[websiteId]/realtime/RealtimeLog.tsx b/src/app/(main)/websites/[websiteId]/realtime/RealtimeLog.tsx index d9aad35b..c26d0629 100644 --- a/src/app/(main)/websites/[websiteId]/realtime/RealtimeLog.tsx +++ b/src/app/(main)/websites/[websiteId]/realtime/RealtimeLog.tsx @@ -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]: , }; -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: {eventName || formatMessage(labels.unknown)}, url: ( (FILTER_REFERRERS); @@ -31,7 +27,7 @@ export function RealtimeUrls({ ]; const renderLink = ({ x }) => { - const domain = x.startsWith('/') ? websiteDomain : ''; + const domain = x.startsWith('/') ? website?.domain : ''; return ( {x} diff --git a/src/app/(main)/websites/[websiteId]/realtime/WebsiteRealtimePage.tsx b/src/app/(main)/websites/[websiteId]/realtime/WebsiteRealtimePage.tsx index 7e538812..8c1e3800 100644 --- a/src/app/(main)/websites/[websiteId]/realtime/WebsiteRealtimePage.tsx +++ b/src/app/(main)/websites/[websiteId]/realtime/WebsiteRealtimePage.tsx @@ -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 ; +export function WebsiteRealtimePage({ websiteId }) { + const { data, isLoading, error } = useRealtime(websiteId); + + if (isLoading || error) { + return ; + } + + return ( + + + + + + + + + + + + + + + + ); } + +export default WebsiteRealtimePage; diff --git a/src/components/hooks/index.ts b/src/components/hooks/index.ts index 560d48a0..a737ba20 100644 --- a/src/components/hooks/index.ts +++ b/src/components/hooks/index.ts @@ -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'; diff --git a/src/components/hooks/queries/useRealtime.ts b/src/components/hooks/queries/useRealtime.ts new file mode 100644 index 00000000..ccf6a62d --- /dev/null +++ b/src/components/hooks/queries/useRealtime.ts @@ -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({ + 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; diff --git a/src/components/metrics/BarChart.tsx b/src/components/metrics/BarChart.tsx index 8c26afa0..46e10399 100644 --- a/src/components/metrics/BarChart.tsx +++ b/src/components/metrics/BarChart.tsx @@ -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(() => { diff --git a/yarn.lock b/yarn.lock index a376eeea..b4fd51c4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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" From dfe7a573fa5d641849cfac6d785359bfc158cd49 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Fri, 8 Mar 2024 19:32:00 -0800 Subject: [PATCH 06/91] Fixed chart legend not rendering. --- src/components/metrics/BarChart.tsx | 17 +++++++++++++++-- src/components/metrics/Legend.tsx | 24 ++++++++++-------------- 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/src/components/metrics/BarChart.tsx b/src/components/metrics/BarChart.tsx index 46e10399..257b9334 100644 --- a/src/components/metrics/BarChart.tsx +++ b/src/components/metrics/BarChart.tsx @@ -48,6 +48,7 @@ export function BarChart({ const [tooltip, setTooltipPopup] = useState(null); const { locale } = useLocale(); const { theme, colors } = useTheme(); + const [legendItems, setLegendItems] = useState([]); const getOptions = useCallback(() => { return { @@ -133,13 +134,15 @@ export function BarChart({ }); onCreate?.(chart.current); + + setLegendItems(chart.current.legend.legendItems); }; const updateChart = (datasets: any[]) => { setTooltipPopup(null); chart.current.data.datasets.forEach((dataset: { data: any }, index: string | number) => { - dataset.data = datasets[index].data; + dataset.data = datasets[index]?.data; }); chart.current.options = getOptions(); @@ -148,6 +151,8 @@ export function BarChart({ onUpdate?.(chart.current); chart.current.update(updateMode); + + setLegendItems(chart.current.legend.legendItems); }; useEffect(() => { @@ -160,13 +165,21 @@ export function BarChart({ } }, [datasets, unit, theme, animationDuration, locale]); + const handleLegendClick = (index: number) => { + const meta = chart.current.getDatasetMeta(index); + + meta.hidden = meta.hidden === null ? !chart.current.data.datasets[index].hidden : null; + + chart.current.update(); + }; + return ( <>
{isLoading && }
- + {tooltip && (
{tooltip}
diff --git a/src/components/metrics/Legend.tsx b/src/components/metrics/Legend.tsx index 6349b3d4..c2bed639 100644 --- a/src/components/metrics/Legend.tsx +++ b/src/components/metrics/Legend.tsx @@ -6,38 +6,34 @@ import { useLocale } from 'components/hooks'; import { useForceUpdate } from 'components/hooks'; import styles from './Legend.module.css'; -export function Legend({ chart }) { +export function Legend({ + items = [], + onClick, +}: { + items: any[]; + onClick: (index: number) => void; +}) { const { locale } = useLocale(); const forceUpdate = useForceUpdate(); - const handleClick = (index: string | number) => { - const meta = chart.getDatasetMeta(index); - - meta.hidden = meta.hidden === null ? !chart.data.datasets[index].hidden : null; - - chart.update(); - - forceUpdate(); - }; - useEffect(() => { forceUpdate(); }, [locale, forceUpdate]); - if (!chart?.legend?.legendItems.find(({ text }) => text)) { + if (!items.find(({ text }) => text)) { return null; } return (
- {chart.legend.legendItems.map(({ text, fillStyle, datasetIndex, hidden }) => { + {items.map(({ text, fillStyle, datasetIndex, hidden }) => { const color = colord(fillStyle); return (