From c8fe93dd9d73accbf1cd2ab5aa96656389bc49bb Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Sat, 23 Aug 2025 01:12:37 -0700 Subject: [PATCH] Refactored tables. --- package.json | 2 +- pnpm-lock.yaml | 10 +- src/app/(main)/SideNav.tsx | 6 +- src/app/(main)/links/[linkId]/LinkHeader.tsx | 4 +- src/app/(main)/links/[linkId]/LinkPage.tsx | 83 +--------- src/app/(main)/links/[linkId]/LinkPanels.tsx | 83 ++++++++++ .../(main)/pixels/[pixelId]/PixelHeader.tsx | 4 +- src/app/(main)/pixels/[pixelId]/PixelPage.tsx | 83 +--------- .../(main)/pixels/[pixelId]/PixelPanels.tsx | 83 ++++++++++ .../websites/[websiteId]/WebsiteChart.tsx | 8 +- .../[websiteId]/WebsiteCompareTables.tsx | 46 ++---- .../[websiteId]/WebsiteExpandedView.tsx | 54 +------ .../websites/[websiteId]/WebsitePage.tsx | 4 +- ...WebsiteTableView.tsx => WebsitePanels.tsx} | 54 ++++--- .../[websiteId]/cohorts/CohortsTable.tsx | 9 +- .../[websiteId]/events/EventProperties.tsx | 6 +- .../[websiteId]/events/EventsPage.tsx | 12 +- .../[websiteId]/segments/SegmentsTable.tsx | 9 +- .../sessions/SessionProperties.tsx | 6 +- .../settings/WebsiteTransferForm.tsx | 13 +- .../[websiteId]/sessions/weekly/route.ts | 4 +- src/components/common/FilterLink.tsx | 51 +++--- src/components/common/LoadingPanel.tsx | 2 +- src/components/common/PageHeader.tsx | 6 +- src/components/hooks/index.ts | 2 +- ...eeklyQuery.ts => useWeeklyTrafficQuery.ts} | 5 +- src/components/hooks/useRegionNames.ts | 7 +- src/components/icons.ts | 2 +- src/components/input/FilterBar.tsx | 22 +-- src/components/messages.ts | 6 +- src/components/metrics/BrowsersTable.tsx | 28 ---- src/components/metrics/ChannelsTable.tsx | 20 --- src/components/metrics/CitiesTable.tsx | 37 ----- src/components/metrics/CountriesTable.tsx | 17 +- src/components/metrics/DevicesTable.tsx | 29 ---- src/components/metrics/EventsTable.tsx | 33 ---- src/components/metrics/HostnamesTable.tsx | 33 ---- src/components/metrics/LanguagesTable.tsx | 25 --- .../metrics/ListExpandedTable.module.css | 7 - src/components/metrics/ListExpandedTable.tsx | 3 +- src/components/metrics/ListTable.tsx | 20 ++- src/components/metrics/MetricLabel.tsx | 149 ++++++++++++++++++ .../metrics/MetricsExpandedTable.tsx | 106 +++++++++++++ src/components/metrics/MetricsTable.tsx | 125 +++------------ src/components/metrics/OSTable.tsx | 27 ---- src/components/metrics/PagesTable.tsx | 67 -------- .../metrics/QueryParametersTable.module.css | 16 -- .../metrics/QueryParametersTable.tsx | 66 -------- src/components/metrics/ReferrersTable.tsx | 75 --------- src/components/metrics/RegionsTable.tsx | 31 ---- src/components/metrics/ScreenTable.tsx | 15 -- src/components/metrics/TagsTable.tsx | 28 ---- .../metrics/WeeklyTraffic.tsx} | 28 ++-- src/lang/en-US.json | 4 +- src/queries/index.ts | 2 +- ...eSessionsWeekly.ts => getWeeklyTraffic.ts} | 4 +- 56 files changed, 643 insertions(+), 1038 deletions(-) create mode 100644 src/app/(main)/links/[linkId]/LinkPanels.tsx create mode 100644 src/app/(main)/pixels/[pixelId]/PixelPanels.tsx rename src/app/(main)/websites/[websiteId]/{WebsiteTableView.tsx => WebsitePanels.tsx} (57%) rename src/components/hooks/queries/{useWebsiteSessionsWeeklyQuery.ts => useWeeklyTrafficQuery.ts} (85%) delete mode 100644 src/components/metrics/BrowsersTable.tsx delete mode 100644 src/components/metrics/ChannelsTable.tsx delete mode 100644 src/components/metrics/CitiesTable.tsx delete mode 100644 src/components/metrics/DevicesTable.tsx delete mode 100644 src/components/metrics/EventsTable.tsx delete mode 100644 src/components/metrics/HostnamesTable.tsx delete mode 100644 src/components/metrics/LanguagesTable.tsx delete mode 100644 src/components/metrics/ListExpandedTable.module.css create mode 100644 src/components/metrics/MetricLabel.tsx create mode 100644 src/components/metrics/MetricsExpandedTable.tsx delete mode 100644 src/components/metrics/OSTable.tsx delete mode 100644 src/components/metrics/PagesTable.tsx delete mode 100644 src/components/metrics/QueryParametersTable.module.css delete mode 100644 src/components/metrics/QueryParametersTable.tsx delete mode 100644 src/components/metrics/ReferrersTable.tsx delete mode 100644 src/components/metrics/RegionsTable.tsx delete mode 100644 src/components/metrics/ScreenTable.tsx delete mode 100644 src/components/metrics/TagsTable.tsx rename src/{app/(main)/websites/[websiteId]/sessions/SessionsWeekly.tsx => components/metrics/WeeklyTraffic.tsx} (82%) rename src/queries/sql/{sessions/getWebsiteSessionsWeekly.ts => getWeeklyTraffic.ts} (96%) diff --git a/package.json b/package.json index 7549b097..007839a8 100644 --- a/package.json +++ b/package.json @@ -82,7 +82,7 @@ "@react-spring/web": "^9.7.3", "@svgr/cli": "^8.1.0", "@tanstack/react-query": "^5.83.0", - "@umami/react-zen": "^0.163.0", + "@umami/react-zen": "^0.164.0", "@umami/redis-client": "^0.27.0", "bcryptjs": "^3.0.2", "chalk": "^5.4.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0cfda274..2db2f044 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -45,8 +45,8 @@ importers: specifier: ^5.83.0 version: 5.85.3(react@19.1.1) '@umami/react-zen': - specifier: ^0.163.0 - version: 0.163.0(@babel/core@7.28.3)(@types/react@19.1.10)(babel-plugin-react-compiler@19.1.0-rc.2)(immer@9.0.21)(use-sync-external-store@1.5.0(react@19.1.1)) + specifier: ^0.164.0 + version: 0.164.0(@babel/core@7.28.3)(@types/react@19.1.10)(babel-plugin-react-compiler@19.1.0-rc.2)(immer@9.0.21)(use-sync-external-store@1.5.0(react@19.1.1)) '@umami/redis-client': specifier: ^0.27.0 version: 0.27.0 @@ -2549,8 +2549,8 @@ packages: resolution: {integrity: sha512-W8FQi6kEh2e8zVhQ0eeRnxdvIoOkAp/CPAahcNio6nO9dsIwb9b34z90KOlheoyuVf6LSOEdjlkxSkapNEc+4A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@umami/react-zen@0.163.0': - resolution: {integrity: sha512-H+Z7sADljnBdzRQdOUIHXKphiPkzHKTLTNtBf/VbylzXg5A61e+OYoDG37eOkR+JFU9+KmJnF+zOiXyA33LW0A==} + '@umami/react-zen@0.164.0': + resolution: {integrity: sha512-z27uy0W3ZL0MH2cdVuu0c4guInHJQC2rYcAXxwxOAdEMtkzWym9ODfK3v5ihqS6oct+6er/bS1yVJ8gNnRvXDw==} '@umami/redis-client@0.27.0': resolution: {integrity: sha512-SbHTpxhgeZyTBUSp2zdZM+XUtpsaSL4Tad8QXIEhEtjWhvvfoornyT5kLuyYCVtzSAT4daALeGmOO1z6EE1KcA==} @@ -9849,7 +9849,7 @@ snapshots: '@typescript-eslint/types': 8.39.1 eslint-visitor-keys: 4.2.1 - '@umami/react-zen@0.163.0(@babel/core@7.28.3)(@types/react@19.1.10)(babel-plugin-react-compiler@19.1.0-rc.2)(immer@9.0.21)(use-sync-external-store@1.5.0(react@19.1.1))': + '@umami/react-zen@0.164.0(@babel/core@7.28.3)(@types/react@19.1.10)(babel-plugin-react-compiler@19.1.0-rc.2)(immer@9.0.21)(use-sync-external-store@1.5.0(react@19.1.1))': dependencies: '@fontsource/jetbrains-mono': 5.2.6 '@internationalized/date': 3.8.2 diff --git a/src/app/(main)/SideNav.tsx b/src/app/(main)/SideNav.tsx index e360dae1..fe07a1b6 100644 --- a/src/app/(main)/SideNav.tsx +++ b/src/app/(main)/SideNav.tsx @@ -13,7 +13,7 @@ import { LayoutDashboard, Link as LinkIcon, Logo, - Grid2X2, + Pixel, Settings, PanelLeft, } from '@/components/icons'; @@ -21,6 +21,7 @@ import { useMessages, useNavigation, useGlobalState } from '@/components/hooks'; import { TeamsButton } from '@/components/input/TeamsButton'; import { PanelButton } from '@/components/input/PanelButton'; import { ProfileButton } from '@/components/input/ProfileButton'; +import { LanguageButton } from '@/components/input/LanguageButton'; export function SideNav(props: SidebarProps) { const { formatMessage, labels } = useMessages(); @@ -52,7 +53,7 @@ export function SideNav(props: SidebarProps) { id: 'pixels', label: formatMessage(labels.pixels), path: '/pixels', - icon: , + icon: , }, ]; @@ -97,6 +98,7 @@ export function SideNav(props: SidebarProps) { {!isCollapsed && !hasNav && ( + )} diff --git a/src/app/(main)/links/[linkId]/LinkHeader.tsx b/src/app/(main)/links/[linkId]/LinkHeader.tsx index 37e64214..0d325ebb 100644 --- a/src/app/(main)/links/[linkId]/LinkHeader.tsx +++ b/src/app/(main)/links/[linkId]/LinkHeader.tsx @@ -1,7 +1,7 @@ import { useLink, useMessages, useSlug } from '@/components/hooks'; import { PageHeader } from '@/components/common/PageHeader'; import { Icon, Text } from '@umami/react-zen'; -import { ExternalLink } from '@/components/icons'; +import { ExternalLink, Link } from '@/components/icons'; import { LinkButton } from '@/components/common/LinkButton'; export function LinkHeader() { @@ -10,7 +10,7 @@ export function LinkHeader() { const link = useLink(); return ( - + }> diff --git a/src/app/(main)/links/[linkId]/LinkPage.tsx b/src/app/(main)/links/[linkId]/LinkPage.tsx index d349dc8c..a7751625 100644 --- a/src/app/(main)/links/[linkId]/LinkPage.tsx +++ b/src/app/(main)/links/[linkId]/LinkPage.tsx @@ -6,25 +6,9 @@ import { Panel } from '@/components/common/Panel'; import { WebsiteChart } from '@/app/(main)/websites/[websiteId]/WebsiteChart'; import { LinkMetricsBar } from '@/app/(main)/links/[linkId]/LinkMetricsBar'; import { LinkControls } from '@/app/(main)/links/[linkId]/LinkControls'; -import { Grid, Tab, TabList, TabPanel, Tabs } from '@umami/react-zen'; -import { GridRow } from '@/components/common/GridRow'; -import { ReferrersTable } from '@/components/metrics/ReferrersTable'; -import { BrowsersTable } from '@/components/metrics/BrowsersTable'; -import { OSTable } from '@/components/metrics/OSTable'; -import { DevicesTable } from '@/components/metrics/DevicesTable'; -import { WorldMap } from '@/components/metrics/WorldMap'; -import { CountriesTable } from '@/components/metrics/CountriesTable'; -import { ChannelsTable } from '@/components/metrics/ChannelsTable'; -import { RegionsTable } from '@/components/metrics/RegionsTable'; -import { CitiesTable } from '@/components/metrics/CitiesTable'; -import { SessionsWeekly } from '@/app/(main)/websites/[websiteId]/sessions/SessionsWeekly'; -import { useMessages } from '@/components/hooks'; +import { LinkPanels } from '@/app/(main)/links/[linkId]/LinkPanels'; export function LinkPage({ linkId }: { linkId: string }) { - const { formatMessage, labels } = useMessages(); - const tableProps = { websiteId: linkId, limit: 10, allowDownload: false }; - const rowProps = { minHeight: 570 }; - return ( @@ -34,70 +18,7 @@ export function LinkPage({ linkId }: { linkId: string }) { - - - - - - {formatMessage(labels.referrers)} - {formatMessage(labels.channels)} - - - - - - - - - - - - - - - - - - {formatMessage(labels.countries)} - {formatMessage(labels.regions)} - {formatMessage(labels.cities)} - - - - - - - - - - - - - - - - - - {formatMessage(labels.browsers)} - {formatMessage(labels.os)} - {formatMessage(labels.devices)} - - - - - - - - - - - - - - - - - + ); diff --git a/src/app/(main)/links/[linkId]/LinkPanels.tsx b/src/app/(main)/links/[linkId]/LinkPanels.tsx new file mode 100644 index 00000000..d6f515f5 --- /dev/null +++ b/src/app/(main)/links/[linkId]/LinkPanels.tsx @@ -0,0 +1,83 @@ +import { Grid, Tabs, Tab, TabList, TabPanel, Heading } from '@umami/react-zen'; +import { GridRow } from '@/components/common/GridRow'; +import { Panel } from '@/components/common/Panel'; +import { WorldMap } from '@/components/metrics/WorldMap'; +import { MetricsTable } from '@/components/metrics/MetricsTable'; +import { useMessages } from '@/components/hooks'; + +export function LinkPanels({ linkId }: { linkId: string }) { + const { formatMessage, labels } = useMessages(); + const tableProps = { + websiteId: linkId, + limit: 10, + allowDownload: false, + showMore: true, + metric: formatMessage(labels.visitors), + }; + const rowProps = { minHeight: 570 }; + + return ( + + + + {formatMessage(labels.sources)} + + + {formatMessage(labels.referrers)} + {formatMessage(labels.channels)} + + + + + + + + + + + {formatMessage(labels.environment)} + + + {formatMessage(labels.browsers)} + {formatMessage(labels.os)} + {formatMessage(labels.devices)} + + + + + + + + + + + + + + + + + + + {formatMessage(labels.location)} + + + {formatMessage(labels.countries)} + {formatMessage(labels.regions)} + {formatMessage(labels.cities)} + + + + + + + + + + + + + + + ); +} diff --git a/src/app/(main)/pixels/[pixelId]/PixelHeader.tsx b/src/app/(main)/pixels/[pixelId]/PixelHeader.tsx index 4b4357de..aba7c71c 100644 --- a/src/app/(main)/pixels/[pixelId]/PixelHeader.tsx +++ b/src/app/(main)/pixels/[pixelId]/PixelHeader.tsx @@ -1,7 +1,7 @@ import { usePixel, useMessages, useSlug } from '@/components/hooks'; import { PageHeader } from '@/components/common/PageHeader'; import { Icon, Text } from '@umami/react-zen'; -import { ExternalLink } from '@/components/icons'; +import { ExternalLink, Pixel } from '@/components/icons'; import { LinkButton } from '@/components/common/LinkButton'; export function PixelHeader() { @@ -10,7 +10,7 @@ export function PixelHeader() { const pixel = usePixel(); return ( - + }> diff --git a/src/app/(main)/pixels/[pixelId]/PixelPage.tsx b/src/app/(main)/pixels/[pixelId]/PixelPage.tsx index 45192801..6a55a6eb 100644 --- a/src/app/(main)/pixels/[pixelId]/PixelPage.tsx +++ b/src/app/(main)/pixels/[pixelId]/PixelPage.tsx @@ -6,25 +6,9 @@ import { Panel } from '@/components/common/Panel'; import { WebsiteChart } from '@/app/(main)/websites/[websiteId]/WebsiteChart'; import { PixelMetricsBar } from '@/app/(main)/pixels/[pixelId]/PixelMetricsBar'; import { PixelControls } from '@/app/(main)/pixels/[pixelId]/PixelControls'; -import { Grid, Tab, TabList, TabPanel, Tabs } from '@umami/react-zen'; -import { GridRow } from '@/components/common/GridRow'; -import { ReferrersTable } from '@/components/metrics/ReferrersTable'; -import { BrowsersTable } from '@/components/metrics/BrowsersTable'; -import { OSTable } from '@/components/metrics/OSTable'; -import { DevicesTable } from '@/components/metrics/DevicesTable'; -import { WorldMap } from '@/components/metrics/WorldMap'; -import { CountriesTable } from '@/components/metrics/CountriesTable'; -import { useMessages } from '@/components/hooks'; -import { ChannelsTable } from '@/components/metrics/ChannelsTable'; -import { RegionsTable } from '@/components/metrics/RegionsTable'; -import { CitiesTable } from '@/components/metrics/CitiesTable'; -import { SessionsWeekly } from '@/app/(main)/websites/[websiteId]/sessions/SessionsWeekly'; +import { PixelPanels } from '@/app/(main)/pixels/[pixelId]/PixelPanels'; export function PixelPage({ pixelId }: { pixelId: string }) { - const { formatMessage, labels } = useMessages(); - const tableProps = { websiteId: pixelId, limit: 10, allowDownload: false }; - const rowProps = { minHeight: 570 }; - return ( @@ -34,70 +18,7 @@ export function PixelPage({ pixelId }: { pixelId: string }) { - - - - - - {formatMessage(labels.referrers)} - {formatMessage(labels.channels)} - - - - - - - - - - - - - - - - - - {formatMessage(labels.countries)} - {formatMessage(labels.regions)} - {formatMessage(labels.cities)} - - - - - - - - - - - - - - - - - - {formatMessage(labels.browsers)} - {formatMessage(labels.os)} - {formatMessage(labels.devices)} - - - - - - - - - - - - - - - - - + ); diff --git a/src/app/(main)/pixels/[pixelId]/PixelPanels.tsx b/src/app/(main)/pixels/[pixelId]/PixelPanels.tsx new file mode 100644 index 00000000..a5ea248e --- /dev/null +++ b/src/app/(main)/pixels/[pixelId]/PixelPanels.tsx @@ -0,0 +1,83 @@ +import { Grid, Tabs, Tab, TabList, TabPanel, Heading } from '@umami/react-zen'; +import { GridRow } from '@/components/common/GridRow'; +import { Panel } from '@/components/common/Panel'; +import { WorldMap } from '@/components/metrics/WorldMap'; +import { MetricsTable } from '@/components/metrics/MetricsTable'; +import { useMessages } from '@/components/hooks'; + +export function PixelPanels({ pixelId }: { pixelId: string }) { + const { formatMessage, labels } = useMessages(); + const tableProps = { + websiteId: pixelId, + limit: 10, + allowDownload: false, + showMore: true, + metric: formatMessage(labels.visitors), + }; + const rowProps = { minHeight: 570 }; + + return ( + + + + {formatMessage(labels.sources)} + + + {formatMessage(labels.referrers)} + {formatMessage(labels.channels)} + + + + + + + + + + + {formatMessage(labels.environment)} + + + {formatMessage(labels.browsers)} + {formatMessage(labels.os)} + {formatMessage(labels.devices)} + + + + + + + + + + + + + + + + + + + {formatMessage(labels.location)} + + + {formatMessage(labels.countries)} + {formatMessage(labels.regions)} + {formatMessage(labels.cities)} + + + + + + + + + + + + + + + ); +} diff --git a/src/app/(main)/websites/[websiteId]/WebsiteChart.tsx b/src/app/(main)/websites/[websiteId]/WebsiteChart.tsx index 743b4f62..620f9f20 100644 --- a/src/app/(main)/websites/[websiteId]/WebsiteChart.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsiteChart.tsx @@ -47,7 +47,13 @@ export function WebsiteChart({ }, [data, startDate, endDate, unit]); return ( - + null); const items = [ { @@ -121,7 +90,7 @@ export function WebsiteCompareTables({ websiteId }: { websiteId: string }) { }, ]; - const renderChange = ({ x, y }) => { + const renderChange = ({ label: x, count: y }) => { const prev = data.find(d => d.x === x)?.y; const value = y - prev; const change = Math.abs(((y - prev) / prev) * 100); @@ -163,15 +132,22 @@ export function WebsiteCompareTables({ websiteId }: { websiteId: string }) { {formatMessage(labels.previous)} - + {formatMessage(labels.current)} - null); - return ( @@ -146,15 +108,13 @@ export function WebsiteExpandedView({ - diff --git a/src/app/(main)/websites/[websiteId]/WebsitePage.tsx b/src/app/(main)/websites/[websiteId]/WebsitePage.tsx index 8a0edbfc..0b8b1c59 100644 --- a/src/app/(main)/websites/[websiteId]/WebsitePage.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsitePage.tsx @@ -5,7 +5,7 @@ import { Panel } from '@/components/common/Panel'; import { WebsiteChart } from './WebsiteChart'; import { WebsiteExpandedView } from './WebsiteExpandedView'; import { WebsiteMetricsBar } from './WebsiteMetricsBar'; -import { WebsiteTableView } from './WebsiteTableView'; +import { WebsitePanels } from './WebsitePanels'; import { WebsiteControls } from './WebsiteControls'; export function WebsitePage({ websiteId }: { websiteId: string }) { @@ -32,7 +32,7 @@ export function WebsitePage({ websiteId }: { websiteId: string }) { - + {({ close }) => { diff --git a/src/app/(main)/websites/[websiteId]/WebsiteTableView.tsx b/src/app/(main)/websites/[websiteId]/WebsitePanels.tsx similarity index 57% rename from src/app/(main)/websites/[websiteId]/WebsiteTableView.tsx rename to src/app/(main)/websites/[websiteId]/WebsitePanels.tsx index f3a14418..1d8f89d3 100644 --- a/src/app/(main)/websites/[websiteId]/WebsiteTableView.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsitePanels.tsx @@ -1,28 +1,27 @@ -import { Grid, Tabs, Tab, TabList, TabPanel } from '@umami/react-zen'; +import { Grid, Tabs, Tab, TabList, TabPanel, Heading, Row } from '@umami/react-zen'; import { GridRow } from '@/components/common/GridRow'; import { Panel } from '@/components/common/Panel'; -import { PagesTable } from '@/components/metrics/PagesTable'; -import { ReferrersTable } from '@/components/metrics/ReferrersTable'; -import { BrowsersTable } from '@/components/metrics/BrowsersTable'; -import { OSTable } from '@/components/metrics/OSTable'; -import { DevicesTable } from '@/components/metrics/DevicesTable'; import { WorldMap } from '@/components/metrics/WorldMap'; -import { CountriesTable } from '@/components/metrics/CountriesTable'; +import { MetricsTable } from '@/components/metrics/MetricsTable'; +import { WeeklyTraffic } from '@/components/metrics/WeeklyTraffic'; import { useMessages } from '@/components/hooks'; -import { ChannelsTable } from '@/components/metrics/ChannelsTable'; -import { RegionsTable } from '@/components/metrics/RegionsTable'; -import { CitiesTable } from '@/components/metrics/CitiesTable'; -import { SessionsWeekly } from '@/app/(main)/websites/[websiteId]/sessions/SessionsWeekly'; -export function WebsiteTableView({ websiteId }: { websiteId: string }) { +export function WebsitePanels({ websiteId }: { websiteId: string }) { const { formatMessage, labels } = useMessages(); - const tableProps = { websiteId, limit: 10, allowDownload: false }; + const tableProps = { + websiteId, + limit: 10, + allowDownload: false, + showMore: true, + metric: formatMessage(labels.visitors), + }; const rowProps = { minHeight: 570 }; return ( + {formatMessage(labels.pages)} {formatMessage(labels.pages)} @@ -30,27 +29,28 @@ export function WebsiteTableView({ websiteId }: { websiteId: string }) { {formatMessage(labels.exit)} - + - + - + + {formatMessage(labels.sources)} {formatMessage(labels.referrers)} {formatMessage(labels.channels)} - + - + @@ -60,6 +60,7 @@ export function WebsiteTableView({ websiteId }: { websiteId: string }) { + {formatMessage(labels.location)} {formatMessage(labels.countries)} @@ -67,19 +68,20 @@ export function WebsiteTableView({ websiteId }: { websiteId: string }) { {formatMessage(labels.cities)} - + - + - + + {formatMessage(labels.environment)} {formatMessage(labels.browsers)} @@ -87,18 +89,20 @@ export function WebsiteTableView({ websiteId }: { websiteId: string }) { {formatMessage(labels.devices)} - + - + - + - + {formatMessage(labels.traffic)} + + diff --git a/src/app/(main)/websites/[websiteId]/cohorts/CohortsTable.tsx b/src/app/(main)/websites/[websiteId]/cohorts/CohortsTable.tsx index 578c4fc6..200449fc 100644 --- a/src/app/(main)/websites/[websiteId]/cohorts/CohortsTable.tsx +++ b/src/app/(main)/websites/[websiteId]/cohorts/CohortsTable.tsx @@ -5,10 +5,11 @@ import { DateDistance } from '@/components/common/DateDistance'; import { filtersObjectToArray } from '@/lib/params'; import { CohortEditButton } from '@/app/(main)/websites/[websiteId]/cohorts/CohortEditButton'; import { CohortDeleteButton } from '@/app/(main)/websites/[websiteId]/cohorts/CohortDeleteButton'; +import Link from 'next/link'; export function CohortsTable({ data = [] }) { const { formatMessage, labels } = useMessages(); - const { websiteId } = useNavigation(); + const { websiteId, renderUrl } = useNavigation(); if (data.length === 0) { return ; @@ -16,7 +17,11 @@ export function CohortsTable({ data = [] }) { return ( - + + {(row: any) => ( + {row.name} + )} + {(row: any) => } diff --git a/src/app/(main)/websites/[websiteId]/events/EventProperties.tsx b/src/app/(main)/websites/[websiteId]/events/EventProperties.tsx index 26d2f265..60985a4c 100644 --- a/src/app/(main)/websites/[websiteId]/events/EventProperties.tsx +++ b/src/app/(main)/websites/[websiteId]/events/EventProperties.tsx @@ -100,9 +100,9 @@ const EventValues = ({ websiteId, eventName, propertyName }) => { const tableData = useMemo(() => { if (!propertyName || !values || propertySum === 0) return []; return values.map(({ value, total }) => ({ - x: value, - y: total, - z: 100 * (total / propertySum), + label: value, + count: total, + percent: 100 * (total / propertySum), })); }, [propertyName, values, propertySum]); diff --git a/src/app/(main)/websites/[websiteId]/events/EventsPage.tsx b/src/app/(main)/websites/[websiteId]/events/EventsPage.tsx index 14a90c9f..14b89300 100644 --- a/src/app/(main)/websites/[websiteId]/events/EventsPage.tsx +++ b/src/app/(main)/websites/[websiteId]/events/EventsPage.tsx @@ -1,6 +1,6 @@ 'use client'; import { TabList, Tab, Tabs, TabPanel, Column } from '@umami/react-zen'; -import { EventsTable } from '@/components/metrics/EventsTable'; +import { MetricsTable } from '@/components/metrics/MetricsTable'; import { useState, Key } from 'react'; import { EventsDataTable } from './EventsDataTable'; import { Panel } from '@/components/common/Panel'; @@ -13,14 +13,9 @@ import { getItem, setItem } from '@/lib/storage'; const KEY_NAME = 'umami.events.tab'; export function EventsPage({ websiteId }) { - const [label, setLabel] = useState(null); const [tab, setTab] = useState(getItem(KEY_NAME) || 'chart'); const { formatMessage, labels } = useMessages(); - const handleLabelClick = (value: string) => { - setLabel(value !== label ? value : ''); - }; - const handleSelect = (value: Key) => { setItem(KEY_NAME, value); setTab(value); @@ -42,14 +37,13 @@ export function EventsPage({ websiteId }) { - + - diff --git a/src/app/(main)/websites/[websiteId]/segments/SegmentsTable.tsx b/src/app/(main)/websites/[websiteId]/segments/SegmentsTable.tsx index fe8317ce..6f75aa9e 100644 --- a/src/app/(main)/websites/[websiteId]/segments/SegmentsTable.tsx +++ b/src/app/(main)/websites/[websiteId]/segments/SegmentsTable.tsx @@ -5,10 +5,11 @@ import { DateDistance } from '@/components/common/DateDistance'; import { filtersObjectToArray } from '@/lib/params'; import { SegmentEditButton } from '@/app/(main)/websites/[websiteId]/segments/SegmentEditButton'; import { SegmentDeleteButton } from '@/app/(main)/websites/[websiteId]/segments/SegmentDeleteButton'; +import Link from 'next/link'; export function SegmentsTable({ data = [] }) { const { formatMessage, labels } = useMessages(); - const { websiteId } = useNavigation(); + const { websiteId, renderUrl } = useNavigation(); if (data.length === 0) { return ; @@ -16,7 +17,11 @@ export function SegmentsTable({ data = [] }) { return ( - + + {(row: any) => ( + {row.name} + )} + {(row: any) => } diff --git a/src/app/(main)/websites/[websiteId]/sessions/SessionProperties.tsx b/src/app/(main)/websites/[websiteId]/sessions/SessionProperties.tsx index d60713d9..ea4b5f03 100644 --- a/src/app/(main)/websites/[websiteId]/sessions/SessionProperties.tsx +++ b/src/app/(main)/websites/[websiteId]/sessions/SessionProperties.tsx @@ -71,9 +71,9 @@ const SessionValues = ({ websiteId, propertyName }) => { const tableData = useMemo(() => { if (!propertyName || !data || propertySum === 0) return []; return data.map(({ value, total }) => ({ - x: value, - y: total, - z: 100 * (total / propertySum), + label: value, + count: total, + percent: 100 * (total / propertySum), })); }, [propertyName, data, propertySum]); diff --git a/src/app/(main)/websites/[websiteId]/settings/WebsiteTransferForm.tsx b/src/app/(main)/websites/[websiteId]/settings/WebsiteTransferForm.tsx index f6d06783..d0daefa9 100644 --- a/src/app/(main)/websites/[websiteId]/settings/WebsiteTransferForm.tsx +++ b/src/app/(main)/websites/[websiteId]/settings/WebsiteTransferForm.tsx @@ -11,9 +11,9 @@ import { Text, } from '@umami/react-zen'; import { - useApi, useLoginQuery, useMessages, + useUpdateQuery, useUserTeamsQuery, useWebsite, } from '@/components/hooks'; @@ -32,10 +32,7 @@ export function WebsiteTransferForm({ const website = useWebsite(); const [teamId, setTeamId] = useState(null); const { formatMessage, labels, messages } = useMessages(); - const { post, useMutation } = useApi(); - const { mutate, error } = useMutation({ - mutationFn: (data: any) => post(`/websites/${websiteId}/transfer`, data), - }); + const { mutate, error, isPending } = useUpdateQuery(`/websites/${websiteId}/transfer`); const { data: teams, isLoading } = useUserTeamsQuery(user.id); const isTeamWebsite = !!website?.teamId; @@ -92,7 +89,11 @@ export function WebsiteTransferForm({ - + {formatMessage(labels.transfer)} diff --git a/src/app/api/websites/[websiteId]/sessions/weekly/route.ts b/src/app/api/websites/[websiteId]/sessions/weekly/route.ts index 5494b5a9..fa52e19c 100644 --- a/src/app/api/websites/[websiteId]/sessions/weekly/route.ts +++ b/src/app/api/websites/[websiteId]/sessions/weekly/route.ts @@ -3,7 +3,7 @@ import { getQueryFilters, parseRequest } from '@/lib/request'; import { unauthorized, json } from '@/lib/response'; import { canViewWebsite } from '@/validations'; import { pagingParams, timezoneParam } from '@/lib/schema'; -import { getWebsiteSessionsWeekly } from '@/queries'; +import { getWeeklyTraffic } from '@/queries'; export async function GET( request: Request, @@ -30,7 +30,7 @@ export async function GET( const filters = await getQueryFilters(query, websiteId); - const data = await getWebsiteSessionsWeekly(websiteId, filters); + const data = await getWeeklyTraffic(websiteId, filters); return json(data); } diff --git a/src/components/common/FilterLink.tsx b/src/components/common/FilterLink.tsx index 1767341d..982c0c3b 100644 --- a/src/components/common/FilterLink.tsx +++ b/src/components/common/FilterLink.tsx @@ -1,54 +1,47 @@ -import { ReactNode } from 'react'; -import classNames from 'classnames'; +import { HTMLAttributes, ReactNode, useState } from 'react'; import Link from 'next/link'; -import { Icon } from '@umami/react-zen'; +import { Icon, Row } from '@umami/react-zen'; import { useMessages, useNavigation } from '@/components/hooks'; import { ExternalLink } from '@/components/icons'; -import styles from './FilterLink.module.css'; -export interface FilterLinkProps { - id: string; +export interface FilterLinkProps extends HTMLAttributes { + type: string; value: string; label?: string; + icon?: ReactNode; externalUrl?: string; - className?: string; - children?: ReactNode; } -export function FilterLink({ - id, - value, - label, - externalUrl, - children, - className, -}: FilterLinkProps) { +export function FilterLink({ type, value, label, externalUrl, icon }: FilterLinkProps) { + const [showLink, setShowLink] = useState(false); const { formatMessage, labels } = useMessages(); const { updateParams, query } = useNavigation(); - const active = query[id] !== undefined; - const selected = query[id] === value; + const active = query[type] !== undefined; + const selected = query[type] === value; return ( -
setShowLink(true)} + onMouseOut={() => setShowLink(false)} > - {children} + {icon} {!value && `(${label || formatMessage(labels.unknown)})`} {value && ( - + {label || value} )} - {externalUrl && ( - - + {externalUrl && showLink && ( + + )} -
+ ); } diff --git a/src/components/common/LoadingPanel.tsx b/src/components/common/LoadingPanel.tsx index b276da6e..786dd275 100644 --- a/src/components/common/LoadingPanel.tsx +++ b/src/components/common/LoadingPanel.tsx @@ -30,7 +30,7 @@ export function LoadingPanel({ return ( <> {/* Show loading spinner only if no data exists */} - {(isLoading || isFetching) && !data && ( + {(isLoading || isFetching) && ( diff --git a/src/components/common/PageHeader.tsx b/src/components/common/PageHeader.tsx index 69361115..4d0dac5c 100644 --- a/src/components/common/PageHeader.tsx +++ b/src/components/common/PageHeader.tsx @@ -28,7 +28,11 @@ export function PageHeader({ > - {icon && {icon}} + {icon && ( + + {icon} + + )} {title && {title}} {description && {description}} diff --git a/src/components/hooks/index.ts b/src/components/hooks/index.ts index 98afd446..6af16999 100644 --- a/src/components/hooks/index.ts +++ b/src/components/hooks/index.ts @@ -50,7 +50,7 @@ export * from './queries/useWebsiteSegmentsQuery'; export * from './queries/useWebsiteSessionQuery'; export * from './queries/useWebsiteSessionStatsQuery'; export * from './queries/useWebsiteSessionsQuery'; -export * from './queries/useWebsiteSessionsWeeklyQuery'; +export * from './queries/useWeeklyTrafficQuery'; export * from './queries/useWebsiteStatsQuery'; export * from './queries/useWebsiteValuesQuery'; export * from './queries/useWebsitesQuery'; diff --git a/src/components/hooks/queries/useWebsiteSessionsWeeklyQuery.ts b/src/components/hooks/queries/useWeeklyTrafficQuery.ts similarity index 85% rename from src/components/hooks/queries/useWebsiteSessionsWeeklyQuery.ts rename to src/components/hooks/queries/useWeeklyTrafficQuery.ts index 231ef393..fe0bbae1 100644 --- a/src/components/hooks/queries/useWebsiteSessionsWeeklyQuery.ts +++ b/src/components/hooks/queries/useWeeklyTrafficQuery.ts @@ -3,10 +3,7 @@ import { useModified } from '../useModified'; import { useDateParameters } from '../useDateParameters'; import { useFilterParameters } from '@/components/hooks/useFilterParameters'; -export function useWebsiteSessionsWeeklyQuery( - websiteId: string, - params?: Record, -) { +export function useWeeklyTrafficQuery(websiteId: string, params?: Record) { const { get, useQuery } = useApi(); const { modified } = useModified(`sessions`); const date = useDateParameters(websiteId); diff --git a/src/components/hooks/useRegionNames.ts b/src/components/hooks/useRegionNames.ts index 85b2537c..746bd97e 100644 --- a/src/components/hooks/useRegionNames.ts +++ b/src/components/hooks/useRegionNames.ts @@ -9,7 +9,12 @@ export function useRegionNames(locale: string) { return regions[regionCode]; } - const region = regionCode.includes('-') ? regionCode : `${countryCode}-${regionCode}`; + if (!regionCode) { + return null; + } + + const region = regionCode?.includes('-') ? regionCode : `${countryCode}-${regionCode}`; + return regions[region] ? `${regions[region]}, ${countryNames[countryCode]}` : region; }; diff --git a/src/components/icons.ts b/src/components/icons.ts index c25aa003..d25b5b72 100644 --- a/src/components/icons.ts +++ b/src/components/icons.ts @@ -17,7 +17,7 @@ export { FileJson, FileText, Globe, - Grid2X2, + Grid2X2 as Pixel, KeyRound, LayoutDashboard, Link, diff --git a/src/components/input/FilterBar.tsx b/src/components/input/FilterBar.tsx index b9f3e768..6f33e687 100644 --- a/src/components/input/FilterBar.tsx +++ b/src/components/input/FilterBar.tsx @@ -78,16 +78,18 @@ export function FilterBar({ websiteId }: { websiteId: string }) { - - - - {formatMessage(labels.saveSegment)} - - + {!!filters.length && ( + + + + {formatMessage(labels.saveSegment)} + + + )} {({ close }) => { diff --git a/src/components/messages.ts b/src/components/messages.ts index c2e4232c..1abb9e3e 100644 --- a/src/components/messages.ts +++ b/src/components/messages.ts @@ -91,8 +91,8 @@ export const labels = defineMessages({ refresh: { id: 'label.refresh', defaultMessage: 'Refresh' }, page: { id: 'label.page', defaultMessage: 'Page' }, pages: { id: 'label.pages', defaultMessage: 'Pages' }, - entry: { id: 'label.entry', defaultMessage: 'Entry path' }, - exit: { id: 'label.exit', defaultMessage: 'Exit path' }, + entry: { id: 'label.entry', defaultMessage: 'Entry' }, + exit: { id: 'label.exit', defaultMessage: 'Exit' }, referrers: { id: 'label.referrers', defaultMessage: 'Referrers' }, screens: { id: 'label.screens', defaultMessage: 'Screens' }, browsers: { id: 'label.browsers', defaultMessage: 'Browsers' }, @@ -354,6 +354,8 @@ export const labels = defineMessages({ destinationUrl: { id: 'label.destination-url', defaultMessage: 'Destination URL' }, audience: { id: 'label.audience', defaultMessage: 'Audience' }, invalidUrl: { id: 'label.invalid-url', defaultMessage: 'Invalid URL' }, + environment: { id: 'label.environment', defaultMessage: 'Environment' }, + weekly: { id: 'label.weekly', defaultMessage: 'Weekly' }, }); export const messages = defineMessages({ diff --git a/src/components/metrics/BrowsersTable.tsx b/src/components/metrics/BrowsersTable.tsx deleted file mode 100644 index f280afa6..00000000 --- a/src/components/metrics/BrowsersTable.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { FilterLink } from '@/components/common/FilterLink'; -import { MetricsTable, MetricsTableProps } from '@/components/metrics/MetricsTable'; -import { useMessages } from '@/components/hooks'; -import { useFormat } from '@/components/hooks'; -import { TypeIcon } from '@/components/common/TypeIcon'; - -export function BrowsersTable(props: MetricsTableProps) { - const { formatMessage, labels } = useMessages(); - const { formatBrowser } = useFormat(); - - function renderLink({ x: browser }) { - return ( - - - - ); - } - - return ( - - ); -} diff --git a/src/components/metrics/ChannelsTable.tsx b/src/components/metrics/ChannelsTable.tsx deleted file mode 100644 index faff9ec7..00000000 --- a/src/components/metrics/ChannelsTable.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { MetricsTable, MetricsTableProps } from '@/components/metrics/MetricsTable'; -import { useMessages } from '@/components/hooks'; - -export function ChannelsTable(props: MetricsTableProps) { - const { formatMessage, labels } = useMessages(); - - const renderLabel = ({ x }) => { - return formatMessage(labels[x]); - }; - - return ( - - ); -} diff --git a/src/components/metrics/CitiesTable.tsx b/src/components/metrics/CitiesTable.tsx deleted file mode 100644 index fc08d3a2..00000000 --- a/src/components/metrics/CitiesTable.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { MetricsTable, MetricsTableProps } from './MetricsTable'; -import { emptyFilter } from '@/lib/filters'; -import { FilterLink } from '@/components/common/FilterLink'; -import { useMessages } from '@/components/hooks'; -import { useFormat } from '@/components/hooks'; - -export function CitiesTable(props: MetricsTableProps) { - const { formatMessage, labels } = useMessages(); - const { formatCity } = useFormat(); - - const renderLink = ({ x: city, country }) => { - return ( - - {country && ( - {country} - )} - - ); - }; - - return ( - - ); -} diff --git a/src/components/metrics/CountriesTable.tsx b/src/components/metrics/CountriesTable.tsx index 88520dfa..52c82803 100644 --- a/src/components/metrics/CountriesTable.tsx +++ b/src/components/metrics/CountriesTable.tsx @@ -3,17 +3,22 @@ import { useCountryNames } from '@/components/hooks'; import { useLocale, useMessages, useFormat } from '@/components/hooks'; import { MetricsTable, MetricsTableProps } from './MetricsTable'; import { TypeIcon } from '@/components/common/TypeIcon'; +import { MetricsExpandedTable } from '@/components/metrics/MetricsExpandedTable'; -export function CountriesTable({ ...props }: MetricsTableProps) { +export interface CountriesTableProps extends MetricsTableProps { + isExpanded?: boolean; +} + +export function CountriesTable({ isExpanded, ...props }: CountriesTableProps) { const { locale } = useLocale(); const { countryNames } = useCountryNames(locale); const { formatMessage, labels } = useMessages(); const { formatCountry } = useFormat(); - const renderLink = ({ x: code }) => { + const renderLabel = ({ label: code }) => { return ( @@ -22,13 +27,15 @@ export function CountriesTable({ ...props }: MetricsTableProps) { ); }; + const Component = isExpanded ? MetricsExpandedTable : MetricsTable; + return ( - ); diff --git a/src/components/metrics/DevicesTable.tsx b/src/components/metrics/DevicesTable.tsx deleted file mode 100644 index ee66bb4d..00000000 --- a/src/components/metrics/DevicesTable.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { MetricsTable, MetricsTableProps } from './MetricsTable'; -import { FilterLink } from '@/components/common/FilterLink'; -import { useMessages } from '@/components/hooks'; -import { useFormat } from '@/components/hooks'; -import { TypeIcon } from '@/components/common/TypeIcon'; - -export function DevicesTable(props: MetricsTableProps) { - const { formatMessage, labels } = useMessages(); - const { formatDevice } = useFormat(); - - function renderLink({ x: device }) { - return ( - - - - ); - } - - return ( - - ); -} diff --git a/src/components/metrics/EventsTable.tsx b/src/components/metrics/EventsTable.tsx deleted file mode 100644 index f0f5d495..00000000 --- a/src/components/metrics/EventsTable.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { MetricsTable, MetricsTableProps } from './MetricsTable'; -import { useMessages } from '@/components/hooks'; - -export interface EventsTableProps extends MetricsTableProps { - onLabelClick?: (value: string) => void; -} - -export function EventsTable({ onLabelClick, ...props }: EventsTableProps) { - const { formatMessage, labels } = useMessages(); - - const renderLabel = ({ x: label }) => { - if (onLabelClick) { - return ( -
onLabelClick(label)} style={{ cursor: 'pointer' }}> - {label} -
- ); - } - - return label; - }; - - return ( - - ); -} diff --git a/src/components/metrics/HostnamesTable.tsx b/src/components/metrics/HostnamesTable.tsx deleted file mode 100644 index a133fa39..00000000 --- a/src/components/metrics/HostnamesTable.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { MetricsTable, MetricsTableProps } from './MetricsTable'; -import { FilterLink } from '@/components/common/FilterLink'; -import { useMessages } from '@/components/hooks'; -import { Flexbox } from '@umami/react-zen'; - -export function HostnamesTable(props: MetricsTableProps) { - const { formatMessage, labels } = useMessages(); - - const renderLink = ({ x: hostname }) => { - return ( - - - - ); - }; - - return ( - <> - - - ); -} diff --git a/src/components/metrics/LanguagesTable.tsx b/src/components/metrics/LanguagesTable.tsx deleted file mode 100644 index df1ca6d5..00000000 --- a/src/components/metrics/LanguagesTable.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { MetricsTable, MetricsTableProps } from './MetricsTable'; -import { useLocale } from '@/components/hooks'; -import { useMessages } from '@/components/hooks'; -import { useFormat } from '@/components/hooks'; - -export function LanguagesTable(props: MetricsTableProps) { - const { formatMessage, labels } = useMessages(); - const { locale } = useLocale(); - const { formatLanguage } = useFormat(); - - const renderLabel = ({ x }) => { - return
{formatLanguage(x)}
; - }; - - return ( - - ); -} diff --git a/src/components/metrics/ListExpandedTable.module.css b/src/components/metrics/ListExpandedTable.module.css deleted file mode 100644 index 4bd287c4..00000000 --- a/src/components/metrics/ListExpandedTable.module.css +++ /dev/null @@ -1,7 +0,0 @@ -.truncate { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - width: 300px; - display: block; -} diff --git a/src/components/metrics/ListExpandedTable.tsx b/src/components/metrics/ListExpandedTable.tsx index 82285237..a413f0e4 100644 --- a/src/components/metrics/ListExpandedTable.tsx +++ b/src/components/metrics/ListExpandedTable.tsx @@ -2,7 +2,6 @@ import { useMessages } from '@/components/hooks'; import { formatShortTime } from '@/lib/format'; import { DataColumn, DataTable } from '@umami/react-zen'; import { ReactNode } from 'react'; -import styles from './ListExpandedTable.module.css'; export interface ListExpandedTableProps { data?: any[]; @@ -15,7 +14,7 @@ export function ListExpandedTable({ data = [], title, renderLabel }: ListExpande return ( - + {row => renderLabel ? renderLabel({ x: row?.['name'], country: row?.['country'] }, Number(row.id)) diff --git a/src/components/metrics/ListTable.tsx b/src/components/metrics/ListTable.tsx index 9c614d33..4e9ee904 100644 --- a/src/components/metrics/ListTable.tsx +++ b/src/components/metrics/ListTable.tsx @@ -9,13 +9,19 @@ import { formatLongCurrency, formatLongNumber } from '@/lib/format'; const ITEM_SIZE = 30; +interface ListData { + label: string; + count: number; + percent: number; +} + export interface ListTableProps { - data?: any[]; + data?: ListData[]; title?: string; metric?: string; className?: string; - renderLabel?: (row: any, index: number) => ReactNode; - renderChange?: (row: any, index: number) => ReactNode; + renderLabel?: (data: ListData, index: number) => ReactNode; + renderChange?: (data: ListData, index: number) => ReactNode; animate?: boolean; virtualize?: boolean; showPercentage?: boolean; @@ -37,14 +43,14 @@ export function ListTable({ }: ListTableProps) { const { formatMessage, labels } = useMessages(); - const getRow = (row: { x: any; y: any; z: any }, index: number) => { - const { x: label, y: value, z: percent } = row || {}; + const getRow = (row: ListData, index: number) => { + const { label, count, percent } = row; return ( void; +} + +export function MetricLabel({ type, data }: MetricLabelProps) { + const { formatMessage, labels } = useMessages(); + const { formatValue, formatCity } = useFormat(); + const { locale } = useLocale(); + const { countryNames } = useCountryNames(locale); + const { getRegionName } = useRegionNames(locale); + + const { label, country, domain } = data; + const isType = ['browser', 'country', 'device', 'os'].includes(type); + + switch (type) { + case 'browser': + return ( + } + /> + ); + + case 'channel': + return formatMessage(labels[label]); + + case 'city': + return ( + + ) + } + /> + ); + + case 'region': + return ( + } + /> + ); + + case 'country': + return ( + } + /> + ); + + case 'path': + case 'entry': + case 'exit': + return ( + + ); + + case 'device': + return ( + } + /> + ); + + case 'referrer': + return ( + } + /> + ); + + case 'grouped': + if (label === 'Other') { + return `(${formatMessage(labels.other)})`; + } else { + return ( + + + {GROUPED_DOMAINS.find(({ domain }) => domain === label)?.name} + + ); + } + + case 'language': + return formatValue(label, 'language'); + + default: + return ( + + ) + } + /> + ); + } +} diff --git a/src/components/metrics/MetricsExpandedTable.tsx b/src/components/metrics/MetricsExpandedTable.tsx new file mode 100644 index 00000000..f49cba36 --- /dev/null +++ b/src/components/metrics/MetricsExpandedTable.tsx @@ -0,0 +1,106 @@ +import { ReactNode, useState } from 'react'; +import { Button, Column, DataColumn, DataTable, Icon, Row, SearchField } from '@umami/react-zen'; +import { LoadingPanel } from '@/components/common/LoadingPanel'; +import { useMessages, useWebsiteExpandedMetricsQuery } from '@/components/hooks'; +import { Close } from '@/components/icons'; +import { DownloadButton } from '@/components/input/DownloadButton'; +import { formatShortTime } from '@/lib/format'; +import { MetricLabel } from '@/components/metrics/MetricLabel'; + +export interface MetricsExpandedTableProps { + websiteId: string; + type?: string; + title?: string; + dataFilter?: (data: any) => any; + onSearch?: (search: string) => void; + params?: { [key: string]: any }; + allowSearch?: boolean; + allowDownload?: boolean; + renderLabel?: (row: any, index: number) => ReactNode; + onClose?: () => void; + children?: ReactNode; +} + +export function MetricsExpandedTable({ + websiteId, + type, + title, + params, + allowSearch = true, + allowDownload = true, + onClose, + children, +}: MetricsExpandedTableProps) { + const [search, setSearch] = useState(''); + const { formatMessage, labels } = useMessages(); + const isType = ['browser', 'country', 'device', 'os'].includes(type); + + const { data, isLoading, isFetching, error } = useWebsiteExpandedMetricsQuery(websiteId, { + type, + search: isType ? undefined : search, + ...params, + }); + + const items = data?.map(({ name, ...props }) => ({ label: name, ...props })); + + return ( + <> + + {allowSearch && } + + {children} + {allowDownload && } + {onClose && ( + + )} + + + + + {items && ( + + + {row => } + + + {row => row?.['visitors']?.toLocaleString()} + + + {row => row?.['visits']?.toLocaleString()} + + + {row => row?.['pageviews']?.toLocaleString()} + + + {row => { + const n = (Math.min(row?.['visits'], row?.['bounces']) / row?.['visits']) * 100; + return Math.round(+n) + '%'; + }} + + + {row => { + const n = (row?.['totaltime'] / row?.['visits']) * 100; + return `${+n < 0 ? '-' : ''}${formatShortTime(Math.abs(~~n), ['m', 's'], ' ')}`; + }} + + + )} + + + + ); +} diff --git a/src/components/metrics/MetricsTable.tsx b/src/components/metrics/MetricsTable.tsx index 2e50ef77..0e545008 100644 --- a/src/components/metrics/MetricsTable.tsx +++ b/src/components/metrics/MetricsTable.tsx @@ -1,37 +1,20 @@ -import { ReactNode, useMemo, useState } from 'react'; -import { Button, Column, Icon, Row, SearchField, Text } from '@umami/react-zen'; +import { useMemo } from 'react'; +import { Icon, Row, Text } from '@umami/react-zen'; import { LinkButton } from '@/components/common/LinkButton'; import { LoadingPanel } from '@/components/common/LoadingPanel'; -import { - useFormat, - useMessages, - useNavigation, - useWebsiteExpandedMetricsQuery, - useWebsiteMetricsQuery, -} from '@/components/hooks'; -import { Close, Maximize } from '@/components/icons'; -import { DownloadButton } from '@/components/input/DownloadButton'; -import { DEFAULT_ANIMATION_DURATION } from '@/lib/constants'; +import { useMessages, useNavigation, useWebsiteMetricsQuery } from '@/components/hooks'; +import { Maximize } from '@/components/icons'; import { percentFilter } from '@/lib/filters'; - -import { ListExpandedTable, ListExpandedTableProps } from './ListExpandedTable'; import { ListTable, ListTableProps } from './ListTable'; +import { MetricLabel } from '@/components/metrics/MetricLabel'; export interface MetricsTableProps extends ListTableProps { websiteId: string; - type?: string; + type: string; dataFilter?: (data: any) => any; limit?: number; - delay?: number; - onSearch?: (search: string) => void; - allowSearch?: boolean; - searchFormattedValues?: boolean; showMore?: boolean; - params?: { [key: string]: any }; - allowDownload?: boolean; - isExpanded?: boolean; - onClose?: () => void; - children?: ReactNode; + params?: Record; } export function MetricsTable({ @@ -39,50 +22,17 @@ export function MetricsTable({ type, dataFilter, limit, - delay = null, - allowSearch = false, - searchFormattedValues = false, - showMore = true, + showMore = false, params, - allowDownload = true, - isExpanded = false, - onClose, - children, ...props }: MetricsTableProps) { - const [search, setSearch] = useState(''); - const { formatValue } = useFormat(); const { updateParams } = useNavigation(); const { formatMessage, labels } = useMessages(); - - const expandedQuery = useWebsiteExpandedMetricsQuery( - websiteId, - { - type, - search: searchFormattedValues ? undefined : search, - ...params, - }, - { - retryDelay: delay || DEFAULT_ANIMATION_DURATION, - enabled: isExpanded, - }, - ); - - const query = useWebsiteMetricsQuery( - websiteId, - { - type, - limit, - search: searchFormattedValues ? undefined : search, - ...params, - }, - { - retryDelay: delay || DEFAULT_ANIMATION_DURATION, - enabled: !isExpanded, - }, - ); - - const { data, isLoading, isFetching, error } = isExpanded ? expandedQuery : query; + const { data, isLoading, isFetching, error } = useWebsiteMetricsQuery(websiteId, { + type, + limit, + ...params, + }); const filteredData = useMemo(() => { if (data) { @@ -98,23 +48,16 @@ export function MetricsTable({ } } - if (searchFormattedValues && search) { - items = items.filter(({ x, ...data }) => { - const value = formatValue(x, type, data); - - return value?.toLowerCase().includes(search.toLowerCase()); - }); - } - items = percentFilter(items); - return items; + return items.map(({ x, y, z, ...props }) => ({ label: x, count: y, percent: z, ...props })); } return []; - }, [data, dataFilter, search, limit, formatValue, type]); + }, [data, dataFilter, limit, type]); - const downloadData = isExpanded ? data : filteredData; - const hasActions = data && (allowSearch || allowDownload || onClose || children); + const renderLabel = (data: any) => { + return ; + }; return ( - {hasActions && ( - - {allowSearch && } - - {children} - {allowDownload && } - {onClose && ( - - )} - - - )} - - {data && - (isExpanded ? ( - - ) : ( - - ))} - + {data && } {showMore && limit && ( diff --git a/src/components/metrics/OSTable.tsx b/src/components/metrics/OSTable.tsx deleted file mode 100644 index da2821f5..00000000 --- a/src/components/metrics/OSTable.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { MetricsTable, MetricsTableProps } from './MetricsTable'; -import { FilterLink } from '@/components/common/FilterLink'; -import { useMessages, useFormat } from '@/components/hooks'; -import { TypeIcon } from '@/components/common/TypeIcon'; - -export function OSTable(props: MetricsTableProps) { - const { formatMessage, labels } = useMessages(); - const { formatOS } = useFormat(); - - function renderLink({ x: os }) { - return ( - - - - ); - } - - return ( - - ); -} diff --git a/src/components/metrics/PagesTable.tsx b/src/components/metrics/PagesTable.tsx deleted file mode 100644 index 8d9ba2f1..00000000 --- a/src/components/metrics/PagesTable.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import { FilterButtons } from '@/components/input/FilterButtons'; -import { FilterLink } from '@/components/common/FilterLink'; -import { useMessages, useNavigation, useWebsite } from '@/components/hooks'; -import { emptyFilter } from '@/lib/filters'; -import { MetricsTable, MetricsTableProps } from './MetricsTable'; - -export interface PagesTableProps extends MetricsTableProps { - type: string; - allowFilter?: boolean; -} - -export function PagesTable({ type, allowFilter, ...props }: PagesTableProps) { - const { router, updateParams } = useNavigation(); - const { formatMessage, labels } = useMessages(); - const { domain } = useWebsite(); - - const handleChange = (id: any) => { - router.push(updateParams({ view: id })); - }; - - const buttons = [ - { - id: 'path', - label: formatMessage(labels.path), - }, - { - id: 'entry', - label: formatMessage(labels.entry), - }, - { - id: 'exit', - label: formatMessage(labels.exit), - }, - { - id: 'title', - label: formatMessage(labels.title), - }, - ]; - - const renderLink = ({ x }) => { - return ( - - ); - }; - - return ( - - {allowFilter && } - - ); -} diff --git a/src/components/metrics/QueryParametersTable.module.css b/src/components/metrics/QueryParametersTable.module.css deleted file mode 100644 index b21b9c96..00000000 --- a/src/components/metrics/QueryParametersTable.module.css +++ /dev/null @@ -1,16 +0,0 @@ -.item { - display: inline-flex; - align-items: center; - line-height: 26px; -} - -.param { - padding: 0 8px; - color: var(--primary-color); - background: var(--blue100); - border-radius: 4px; -} - -.value { - padding: 0 8px; -} diff --git a/src/components/metrics/QueryParametersTable.tsx b/src/components/metrics/QueryParametersTable.tsx deleted file mode 100644 index 3816a40b..00000000 --- a/src/components/metrics/QueryParametersTable.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { useState } from 'react'; -import { Row, Text } from '@umami/react-zen'; -import { FilterButtons } from '@/components/input/FilterButtons'; -import { emptyFilter, paramFilter } from '@/lib/filters'; -import { MetricsTable, MetricsTableProps } from './MetricsTable'; -import { useMessages } from '@/components/hooks'; - -const FILTER_COMBINED = 'filter-combined'; -const FILTER_RAW = 'filter-raw'; - -const filters = { - [FILTER_RAW]: emptyFilter, - [FILTER_COMBINED]: [emptyFilter, paramFilter], -}; - -export function QueryParametersTable({ - allowFilter, - ...props -}: { allowFilter?: boolean } & MetricsTableProps) { - const [filter, setFilter] = useState(FILTER_COMBINED); - const { formatMessage, labels } = useMessages(); - - const buttons = [ - { - id: FILTER_COMBINED, - label: formatMessage(labels.filterCombined), - }, - { id: FILTER_RAW, label: formatMessage(labels.filterRaw) }, - ]; - - const renderLabel = ({ x, p, v }) => { - return ( - - {filter === FILTER_RAW ? ( - - {x} - - ) : ( - <> - - {p} - - - {v} - - - )} - - ); - }; - - return ( - - {allowFilter && } - - ); -} diff --git a/src/components/metrics/ReferrersTable.tsx b/src/components/metrics/ReferrersTable.tsx deleted file mode 100644 index fdfc3469..00000000 --- a/src/components/metrics/ReferrersTable.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { Favicon } from '@/components/common/Favicon'; -import { FilterButtons } from '@/components/input/FilterButtons'; -import { FilterLink } from '@/components/common/FilterLink'; -import { useMessages, useNavigation } from '@/components/hooks'; -import { GROUPED_DOMAINS } from '@/lib/constants'; -import { emptyFilter } from '@/lib/filters'; -import { Row } from '@umami/react-zen'; -import { MetricsTable, MetricsTableProps } from './MetricsTable'; - -export interface ReferrersTableProps extends MetricsTableProps { - allowFilter?: boolean; -} - -export function ReferrersTable({ allowFilter, ...props }: ReferrersTableProps) { - const { - router, - updateParams, - query: { view = 'referrer' }, - } = useNavigation(); - const { formatMessage, labels } = useMessages(); - - const handleSelect = (key: any) => { - router.push(updateParams({ view: key })); - }; - - const buttons = [ - { - id: 'referrer', - label: formatMessage(labels.domain), - }, - { - id: 'grouped', - label: formatMessage(labels.grouped), - }, - ]; - - const renderLink = ({ x: referrer }) => { - if (view === 'grouped') { - if (referrer === 'Other') { - return `(${formatMessage(labels.other)})`; - } else { - return ( - - - {GROUPED_DOMAINS.find(({ domain }) => domain === referrer)?.name} - - ); - } - } - - return ( - - - - ); - }; - - return ( - - {allowFilter && } - - ); -} diff --git a/src/components/metrics/RegionsTable.tsx b/src/components/metrics/RegionsTable.tsx deleted file mode 100644 index 2a510a32..00000000 --- a/src/components/metrics/RegionsTable.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { FilterLink } from '@/components/common/FilterLink'; -import { emptyFilter } from '@/lib/filters'; -import { useMessages, useLocale, useRegionNames } from '@/components/hooks'; -import { MetricsTable, MetricsTableProps } from './MetricsTable'; -import { TypeIcon } from '@/components/common/TypeIcon'; - -export function RegionsTable(props: MetricsTableProps) { - const { locale } = useLocale(); - const { formatMessage, labels } = useMessages(); - const { getRegionName } = useRegionNames(locale); - - const renderLink = ({ x: code, country }) => { - return ( - - - - ); - }; - - return ( - - ); -} diff --git a/src/components/metrics/ScreenTable.tsx b/src/components/metrics/ScreenTable.tsx deleted file mode 100644 index 2e8c3fd3..00000000 --- a/src/components/metrics/ScreenTable.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { MetricsTable, MetricsTableProps } from './MetricsTable'; -import { useMessages } from '@/components/hooks'; - -export function ScreenTable(props: MetricsTableProps) { - const { formatMessage, labels } = useMessages(); - - return ( - - ); -} diff --git a/src/components/metrics/TagsTable.tsx b/src/components/metrics/TagsTable.tsx deleted file mode 100644 index 658484c8..00000000 --- a/src/components/metrics/TagsTable.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { MetricsTable, MetricsTableProps } from './MetricsTable'; -import { FilterLink } from '@/components/common/FilterLink'; -import { useMessages } from '@/components/hooks'; -import { Flexbox } from '@umami/react-zen'; - -export function TagsTable(props: MetricsTableProps) { - const { formatMessage, labels } = useMessages(); - - const renderLink = ({ x: tag }) => { - return ( - - - - ); - }; - - return ( - <> - - - ); -} diff --git a/src/app/(main)/websites/[websiteId]/sessions/SessionsWeekly.tsx b/src/components/metrics/WeeklyTraffic.tsx similarity index 82% rename from src/app/(main)/websites/[websiteId]/sessions/SessionsWeekly.tsx rename to src/components/metrics/WeeklyTraffic.tsx index a2055b80..f3c2adad 100644 --- a/src/app/(main)/websites/[websiteId]/sessions/SessionsWeekly.tsx +++ b/src/components/metrics/WeeklyTraffic.tsx @@ -1,12 +1,12 @@ import { Row, Grid, Text } from '@umami/react-zen'; import { format, startOfDay, addHours } from 'date-fns'; -import { useLocale, useMessages, useWebsiteSessionsWeeklyQuery } from '@/components/hooks'; +import { useLocale, useMessages, useWeeklyTrafficQuery } from '@/components/hooks'; import { LoadingPanel } from '@/components/common/LoadingPanel'; import { getDayOfWeekAsDate } from '@/lib/date'; import { Focusable, Tooltip, TooltipTrigger } from '@umami/react-zen'; -export function SessionsWeekly({ websiteId }: { websiteId: string }) { - const { data, isLoading, error } = useWebsiteSessionsWeeklyQuery(websiteId); +export function WeeklyTraffic({ websiteId }: { websiteId: string }) { + const { data, isLoading, error } = useWeeklyTrafficQuery(websiteId); const { dateLocale } = useLocale(); const { labels, formatMessage } = useMessages(); const { weekStartsOn } = dateLocale.options; @@ -40,19 +40,19 @@ export function SessionsWeekly({ websiteId }: { websiteId: string }) { {data && ( <> - +   {Array(24) .fill(null) .map((_, i) => { - const label = format(addHours(startOfDay(new Date()), i), 'p', { + const label = format(addHours(startOfDay(new Date()), i), 'haaa', { locale: dateLocale, - }) - .replace(/\D00 ?/, '') - .toLowerCase(); + }); return ( - {label} + + {label} + ); })} @@ -61,7 +61,7 @@ export function SessionsWeekly({ websiteId }: { websiteId: string }) { const day = data[index]; return ( diff --git a/src/lang/en-US.json b/src/lang/en-US.json index 7d5bf884..60627cca 100644 --- a/src/lang/en-US.json +++ b/src/lang/en-US.json @@ -86,13 +86,13 @@ "label.email": "Email", "label.enable-share-url": "Enable share URL", "label.end-step": "End Step", - "label.entry": "Entry path", + "label.entry": "Entry page", "label.event": "Event", "label.event-data": "Event data", "label.event-name": "Event name", "label.events": "Events", "label.exists": "Exists", - "label.exit": "Exit path", + "label.exit": "Exit page", "label.false": "False", "label.field": "Field", "label.fields": "Fields", diff --git a/src/queries/index.ts b/src/queries/index.ts index 1bb7703a..fa8e1b28 100644 --- a/src/queries/index.ts +++ b/src/queries/index.ts @@ -35,7 +35,6 @@ export * from '@/queries/sql/sessions/getSessionMetrics'; export * from '@/queries/sql/sessions/getSessionExpandedMetrics'; export * from '@/queries/sql/sessions/getWebsiteSessions'; export * from '@/queries/sql/sessions/getWebsiteSessionStats'; -export * from '@/queries/sql/sessions/getWebsiteSessionsWeekly'; export * from '@/queries/sql/sessions/getSessionActivity'; export * from '@/queries/sql/sessions/getSessionStats'; export * from '@/queries/sql/sessions/saveSessionData'; @@ -47,3 +46,4 @@ export * from '@/queries/sql/getRealtimeData'; export * from '@/queries/sql/getValues'; export * from '@/queries/sql/getWebsiteDateRange'; export * from '@/queries/sql/getWebsiteStats'; +export * from '@/queries/sql/getWeeklyTraffic'; diff --git a/src/queries/sql/sessions/getWebsiteSessionsWeekly.ts b/src/queries/sql/getWeeklyTraffic.ts similarity index 96% rename from src/queries/sql/sessions/getWebsiteSessionsWeekly.ts rename to src/queries/sql/getWeeklyTraffic.ts index 9bae5425..217f6fa7 100644 --- a/src/queries/sql/sessions/getWebsiteSessionsWeekly.ts +++ b/src/queries/sql/getWeeklyTraffic.ts @@ -4,9 +4,7 @@ import { runQuery, PRISMA, CLICKHOUSE } from '@/lib/db'; import { QueryFilters } from '@/lib/types'; import { EVENT_COLUMNS } from '@/lib/constants'; -export async function getWebsiteSessionsWeekly( - ...args: [websiteId: string, filters: QueryFilters] -) { +export async function getWeeklyTraffic(...args: [websiteId: string, filters: QueryFilters]) { return runQuery({ [PRISMA]: () => relationalQuery(...args), [CLICKHOUSE]: () => clickhouseQuery(...args),