mirror of
https://github.com/umami-software/umami.git
synced 2026-02-10 07:37:11 +01:00
Refactored tables.
This commit is contained in:
parent
600a3d28c3
commit
c8fe93dd9d
56 changed files with 643 additions and 1038 deletions
|
|
@ -82,7 +82,7 @@
|
||||||
"@react-spring/web": "^9.7.3",
|
"@react-spring/web": "^9.7.3",
|
||||||
"@svgr/cli": "^8.1.0",
|
"@svgr/cli": "^8.1.0",
|
||||||
"@tanstack/react-query": "^5.83.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",
|
"@umami/redis-client": "^0.27.0",
|
||||||
"bcryptjs": "^3.0.2",
|
"bcryptjs": "^3.0.2",
|
||||||
"chalk": "^5.4.1",
|
"chalk": "^5.4.1",
|
||||||
|
|
|
||||||
10
pnpm-lock.yaml
generated
10
pnpm-lock.yaml
generated
|
|
@ -45,8 +45,8 @@ importers:
|
||||||
specifier: ^5.83.0
|
specifier: ^5.83.0
|
||||||
version: 5.85.3(react@19.1.1)
|
version: 5.85.3(react@19.1.1)
|
||||||
'@umami/react-zen':
|
'@umami/react-zen':
|
||||||
specifier: ^0.163.0
|
specifier: ^0.164.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))
|
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':
|
'@umami/redis-client':
|
||||||
specifier: ^0.27.0
|
specifier: ^0.27.0
|
||||||
version: 0.27.0
|
version: 0.27.0
|
||||||
|
|
@ -2549,8 +2549,8 @@ packages:
|
||||||
resolution: {integrity: sha512-W8FQi6kEh2e8zVhQ0eeRnxdvIoOkAp/CPAahcNio6nO9dsIwb9b34z90KOlheoyuVf6LSOEdjlkxSkapNEc+4A==}
|
resolution: {integrity: sha512-W8FQi6kEh2e8zVhQ0eeRnxdvIoOkAp/CPAahcNio6nO9dsIwb9b34z90KOlheoyuVf6LSOEdjlkxSkapNEc+4A==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
|
||||||
'@umami/react-zen@0.163.0':
|
'@umami/react-zen@0.164.0':
|
||||||
resolution: {integrity: sha512-H+Z7sADljnBdzRQdOUIHXKphiPkzHKTLTNtBf/VbylzXg5A61e+OYoDG37eOkR+JFU9+KmJnF+zOiXyA33LW0A==}
|
resolution: {integrity: sha512-z27uy0W3ZL0MH2cdVuu0c4guInHJQC2rYcAXxwxOAdEMtkzWym9ODfK3v5ihqS6oct+6er/bS1yVJ8gNnRvXDw==}
|
||||||
|
|
||||||
'@umami/redis-client@0.27.0':
|
'@umami/redis-client@0.27.0':
|
||||||
resolution: {integrity: sha512-SbHTpxhgeZyTBUSp2zdZM+XUtpsaSL4Tad8QXIEhEtjWhvvfoornyT5kLuyYCVtzSAT4daALeGmOO1z6EE1KcA==}
|
resolution: {integrity: sha512-SbHTpxhgeZyTBUSp2zdZM+XUtpsaSL4Tad8QXIEhEtjWhvvfoornyT5kLuyYCVtzSAT4daALeGmOO1z6EE1KcA==}
|
||||||
|
|
@ -9849,7 +9849,7 @@ snapshots:
|
||||||
'@typescript-eslint/types': 8.39.1
|
'@typescript-eslint/types': 8.39.1
|
||||||
eslint-visitor-keys: 4.2.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:
|
dependencies:
|
||||||
'@fontsource/jetbrains-mono': 5.2.6
|
'@fontsource/jetbrains-mono': 5.2.6
|
||||||
'@internationalized/date': 3.8.2
|
'@internationalized/date': 3.8.2
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ import {
|
||||||
LayoutDashboard,
|
LayoutDashboard,
|
||||||
Link as LinkIcon,
|
Link as LinkIcon,
|
||||||
Logo,
|
Logo,
|
||||||
Grid2X2,
|
Pixel,
|
||||||
Settings,
|
Settings,
|
||||||
PanelLeft,
|
PanelLeft,
|
||||||
} from '@/components/icons';
|
} from '@/components/icons';
|
||||||
|
|
@ -21,6 +21,7 @@ import { useMessages, useNavigation, useGlobalState } from '@/components/hooks';
|
||||||
import { TeamsButton } from '@/components/input/TeamsButton';
|
import { TeamsButton } from '@/components/input/TeamsButton';
|
||||||
import { PanelButton } from '@/components/input/PanelButton';
|
import { PanelButton } from '@/components/input/PanelButton';
|
||||||
import { ProfileButton } from '@/components/input/ProfileButton';
|
import { ProfileButton } from '@/components/input/ProfileButton';
|
||||||
|
import { LanguageButton } from '@/components/input/LanguageButton';
|
||||||
|
|
||||||
export function SideNav(props: SidebarProps) {
|
export function SideNav(props: SidebarProps) {
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
|
|
@ -52,7 +53,7 @@ export function SideNav(props: SidebarProps) {
|
||||||
id: 'pixels',
|
id: 'pixels',
|
||||||
label: formatMessage(labels.pixels),
|
label: formatMessage(labels.pixels),
|
||||||
path: '/pixels',
|
path: '/pixels',
|
||||||
icon: <Grid2X2 />,
|
icon: <Pixel />,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -97,6 +98,7 @@ export function SideNav(props: SidebarProps) {
|
||||||
<ProfileButton />
|
<ProfileButton />
|
||||||
{!isCollapsed && !hasNav && (
|
{!isCollapsed && !hasNav && (
|
||||||
<Row>
|
<Row>
|
||||||
|
<LanguageButton />
|
||||||
<ThemeButton />
|
<ThemeButton />
|
||||||
</Row>
|
</Row>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { useLink, useMessages, useSlug } from '@/components/hooks';
|
import { useLink, useMessages, useSlug } from '@/components/hooks';
|
||||||
import { PageHeader } from '@/components/common/PageHeader';
|
import { PageHeader } from '@/components/common/PageHeader';
|
||||||
import { Icon, Text } from '@umami/react-zen';
|
import { Icon, Text } from '@umami/react-zen';
|
||||||
import { ExternalLink } from '@/components/icons';
|
import { ExternalLink, Link } from '@/components/icons';
|
||||||
import { LinkButton } from '@/components/common/LinkButton';
|
import { LinkButton } from '@/components/common/LinkButton';
|
||||||
|
|
||||||
export function LinkHeader() {
|
export function LinkHeader() {
|
||||||
|
|
@ -10,7 +10,7 @@ export function LinkHeader() {
|
||||||
const link = useLink();
|
const link = useLink();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageHeader title={link.name} description={link.url}>
|
<PageHeader title={link.name} description={link.url} icon={<Link />}>
|
||||||
<LinkButton href={getSlugUrl(link.slug)} target="_blank">
|
<LinkButton href={getSlugUrl(link.slug)} target="_blank">
|
||||||
<Icon>
|
<Icon>
|
||||||
<ExternalLink />
|
<ExternalLink />
|
||||||
|
|
|
||||||
|
|
@ -6,25 +6,9 @@ import { Panel } from '@/components/common/Panel';
|
||||||
import { WebsiteChart } from '@/app/(main)/websites/[websiteId]/WebsiteChart';
|
import { WebsiteChart } from '@/app/(main)/websites/[websiteId]/WebsiteChart';
|
||||||
import { LinkMetricsBar } from '@/app/(main)/links/[linkId]/LinkMetricsBar';
|
import { LinkMetricsBar } from '@/app/(main)/links/[linkId]/LinkMetricsBar';
|
||||||
import { LinkControls } from '@/app/(main)/links/[linkId]/LinkControls';
|
import { LinkControls } from '@/app/(main)/links/[linkId]/LinkControls';
|
||||||
import { Grid, Tab, TabList, TabPanel, Tabs } from '@umami/react-zen';
|
import { LinkPanels } from '@/app/(main)/links/[linkId]/LinkPanels';
|
||||||
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';
|
|
||||||
|
|
||||||
export function LinkPage({ linkId }: { linkId: string }) {
|
export function LinkPage({ linkId }: { linkId: string }) {
|
||||||
const { formatMessage, labels } = useMessages();
|
|
||||||
const tableProps = { websiteId: linkId, limit: 10, allowDownload: false };
|
|
||||||
const rowProps = { minHeight: 570 };
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LinkProvider linkId={linkId}>
|
<LinkProvider linkId={linkId}>
|
||||||
<PageBody gap>
|
<PageBody gap>
|
||||||
|
|
@ -34,70 +18,7 @@ export function LinkPage({ linkId }: { linkId: string }) {
|
||||||
<Panel>
|
<Panel>
|
||||||
<WebsiteChart websiteId={linkId} />
|
<WebsiteChart websiteId={linkId} />
|
||||||
</Panel>
|
</Panel>
|
||||||
<Grid gap>
|
<LinkPanels linkId={linkId} />
|
||||||
<GridRow layout="one" {...rowProps}>
|
|
||||||
<Panel>
|
|
||||||
<Tabs>
|
|
||||||
<TabList>
|
|
||||||
<Tab id="referrer">{formatMessage(labels.referrers)}</Tab>
|
|
||||||
<Tab id="channel">{formatMessage(labels.channels)}</Tab>
|
|
||||||
</TabList>
|
|
||||||
<TabPanel id="referrer">
|
|
||||||
<ReferrersTable {...tableProps} />
|
|
||||||
</TabPanel>
|
|
||||||
<TabPanel id="channel">
|
|
||||||
<ChannelsTable {...tableProps} />
|
|
||||||
</TabPanel>
|
|
||||||
</Tabs>
|
|
||||||
</Panel>
|
|
||||||
</GridRow>
|
|
||||||
<GridRow layout="two-one" {...rowProps}>
|
|
||||||
<Panel gridColumn="span 2" noPadding>
|
|
||||||
<WorldMap websiteId={linkId} />
|
|
||||||
</Panel>
|
|
||||||
<Panel>
|
|
||||||
<Tabs>
|
|
||||||
<TabList>
|
|
||||||
<Tab id="country">{formatMessage(labels.countries)}</Tab>
|
|
||||||
<Tab id="region">{formatMessage(labels.regions)}</Tab>
|
|
||||||
<Tab id="city">{formatMessage(labels.cities)}</Tab>
|
|
||||||
</TabList>
|
|
||||||
<TabPanel id="country">
|
|
||||||
<CountriesTable {...tableProps} />
|
|
||||||
</TabPanel>
|
|
||||||
<TabPanel id="region">
|
|
||||||
<RegionsTable {...tableProps} />
|
|
||||||
</TabPanel>
|
|
||||||
<TabPanel id="city">
|
|
||||||
<CitiesTable {...tableProps} />
|
|
||||||
</TabPanel>
|
|
||||||
</Tabs>
|
|
||||||
</Panel>
|
|
||||||
</GridRow>
|
|
||||||
<GridRow layout="two" {...rowProps}>
|
|
||||||
<Panel>
|
|
||||||
<Tabs>
|
|
||||||
<TabList>
|
|
||||||
<Tab id="browser">{formatMessage(labels.browsers)}</Tab>
|
|
||||||
<Tab id="os">{formatMessage(labels.os)}</Tab>
|
|
||||||
<Tab id="device">{formatMessage(labels.devices)}</Tab>
|
|
||||||
</TabList>
|
|
||||||
<TabPanel id="browser">
|
|
||||||
<BrowsersTable {...tableProps} />
|
|
||||||
</TabPanel>
|
|
||||||
<TabPanel id="os">
|
|
||||||
<OSTable {...tableProps} />
|
|
||||||
</TabPanel>
|
|
||||||
<TabPanel id="device">
|
|
||||||
<DevicesTable {...tableProps} />
|
|
||||||
</TabPanel>
|
|
||||||
</Tabs>
|
|
||||||
</Panel>
|
|
||||||
<Panel>
|
|
||||||
<SessionsWeekly websiteId={linkId} />
|
|
||||||
</Panel>
|
|
||||||
</GridRow>
|
|
||||||
</Grid>
|
|
||||||
</PageBody>
|
</PageBody>
|
||||||
</LinkProvider>
|
</LinkProvider>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
83
src/app/(main)/links/[linkId]/LinkPanels.tsx
Normal file
83
src/app/(main)/links/[linkId]/LinkPanels.tsx
Normal file
|
|
@ -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 (
|
||||||
|
<Grid gap="3">
|
||||||
|
<GridRow layout="two" {...rowProps}>
|
||||||
|
<Panel>
|
||||||
|
<Heading size="2">{formatMessage(labels.sources)}</Heading>
|
||||||
|
<Tabs>
|
||||||
|
<TabList>
|
||||||
|
<Tab id="referrer">{formatMessage(labels.referrers)}</Tab>
|
||||||
|
<Tab id="channel">{formatMessage(labels.channels)}</Tab>
|
||||||
|
</TabList>
|
||||||
|
<TabPanel id="referrer">
|
||||||
|
<MetricsTable type="referrer" title={formatMessage(labels.domain)} {...tableProps} />
|
||||||
|
</TabPanel>
|
||||||
|
<TabPanel id="channel">
|
||||||
|
<MetricsTable type="channel" title={formatMessage(labels.type)} {...tableProps} />
|
||||||
|
</TabPanel>
|
||||||
|
</Tabs>
|
||||||
|
</Panel>
|
||||||
|
<Panel>
|
||||||
|
<Heading size="2">{formatMessage(labels.environment)}</Heading>
|
||||||
|
<Tabs>
|
||||||
|
<TabList>
|
||||||
|
<Tab id="browser">{formatMessage(labels.browsers)}</Tab>
|
||||||
|
<Tab id="os">{formatMessage(labels.os)}</Tab>
|
||||||
|
<Tab id="device">{formatMessage(labels.devices)}</Tab>
|
||||||
|
</TabList>
|
||||||
|
<TabPanel id="browser">
|
||||||
|
<MetricsTable type="browser" title={formatMessage(labels.browser)} {...tableProps} />
|
||||||
|
</TabPanel>
|
||||||
|
<TabPanel id="os">
|
||||||
|
<MetricsTable type="os" title={formatMessage(labels.os)} {...tableProps} />
|
||||||
|
</TabPanel>
|
||||||
|
<TabPanel id="device">
|
||||||
|
<MetricsTable type="device" title={formatMessage(labels.device)} {...tableProps} />
|
||||||
|
</TabPanel>
|
||||||
|
</Tabs>
|
||||||
|
</Panel>
|
||||||
|
</GridRow>
|
||||||
|
<GridRow layout="two" {...rowProps}>
|
||||||
|
<Panel noPadding>
|
||||||
|
<WorldMap websiteId={linkId} />
|
||||||
|
</Panel>
|
||||||
|
<Panel>
|
||||||
|
<Heading size="2">{formatMessage(labels.location)}</Heading>
|
||||||
|
<Tabs>
|
||||||
|
<TabList>
|
||||||
|
<Tab id="country">{formatMessage(labels.countries)}</Tab>
|
||||||
|
<Tab id="region">{formatMessage(labels.regions)}</Tab>
|
||||||
|
<Tab id="city">{formatMessage(labels.cities)}</Tab>
|
||||||
|
</TabList>
|
||||||
|
<TabPanel id="country">
|
||||||
|
<MetricsTable type="country" title={formatMessage(labels.country)} {...tableProps} />
|
||||||
|
</TabPanel>
|
||||||
|
<TabPanel id="region">
|
||||||
|
<MetricsTable type="region" title={formatMessage(labels.region)} {...tableProps} />
|
||||||
|
</TabPanel>
|
||||||
|
<TabPanel id="city">
|
||||||
|
<MetricsTable type="city" title={formatMessage(labels.city)} {...tableProps} />
|
||||||
|
</TabPanel>
|
||||||
|
</Tabs>
|
||||||
|
</Panel>
|
||||||
|
</GridRow>
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { usePixel, useMessages, useSlug } from '@/components/hooks';
|
import { usePixel, useMessages, useSlug } from '@/components/hooks';
|
||||||
import { PageHeader } from '@/components/common/PageHeader';
|
import { PageHeader } from '@/components/common/PageHeader';
|
||||||
import { Icon, Text } from '@umami/react-zen';
|
import { Icon, Text } from '@umami/react-zen';
|
||||||
import { ExternalLink } from '@/components/icons';
|
import { ExternalLink, Pixel } from '@/components/icons';
|
||||||
import { LinkButton } from '@/components/common/LinkButton';
|
import { LinkButton } from '@/components/common/LinkButton';
|
||||||
|
|
||||||
export function PixelHeader() {
|
export function PixelHeader() {
|
||||||
|
|
@ -10,7 +10,7 @@ export function PixelHeader() {
|
||||||
const pixel = usePixel();
|
const pixel = usePixel();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageHeader title={pixel.name} description={pixel.url}>
|
<PageHeader title={pixel.name} description={pixel.url} icon={<Pixel />}>
|
||||||
<LinkButton href={getSlugUrl(pixel.slug)} target="_blank">
|
<LinkButton href={getSlugUrl(pixel.slug)} target="_blank">
|
||||||
<Icon>
|
<Icon>
|
||||||
<ExternalLink />
|
<ExternalLink />
|
||||||
|
|
|
||||||
|
|
@ -6,25 +6,9 @@ import { Panel } from '@/components/common/Panel';
|
||||||
import { WebsiteChart } from '@/app/(main)/websites/[websiteId]/WebsiteChart';
|
import { WebsiteChart } from '@/app/(main)/websites/[websiteId]/WebsiteChart';
|
||||||
import { PixelMetricsBar } from '@/app/(main)/pixels/[pixelId]/PixelMetricsBar';
|
import { PixelMetricsBar } from '@/app/(main)/pixels/[pixelId]/PixelMetricsBar';
|
||||||
import { PixelControls } from '@/app/(main)/pixels/[pixelId]/PixelControls';
|
import { PixelControls } from '@/app/(main)/pixels/[pixelId]/PixelControls';
|
||||||
import { Grid, Tab, TabList, TabPanel, Tabs } from '@umami/react-zen';
|
import { PixelPanels } from '@/app/(main)/pixels/[pixelId]/PixelPanels';
|
||||||
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';
|
|
||||||
|
|
||||||
export function PixelPage({ pixelId }: { pixelId: string }) {
|
export function PixelPage({ pixelId }: { pixelId: string }) {
|
||||||
const { formatMessage, labels } = useMessages();
|
|
||||||
const tableProps = { websiteId: pixelId, limit: 10, allowDownload: false };
|
|
||||||
const rowProps = { minHeight: 570 };
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PixelProvider pixelId={pixelId}>
|
<PixelProvider pixelId={pixelId}>
|
||||||
<PageBody gap>
|
<PageBody gap>
|
||||||
|
|
@ -34,70 +18,7 @@ export function PixelPage({ pixelId }: { pixelId: string }) {
|
||||||
<Panel>
|
<Panel>
|
||||||
<WebsiteChart websiteId={pixelId} />
|
<WebsiteChart websiteId={pixelId} />
|
||||||
</Panel>
|
</Panel>
|
||||||
<Grid gap>
|
<PixelPanels pixelId={pixelId} />
|
||||||
<GridRow layout="one" {...rowProps}>
|
|
||||||
<Panel>
|
|
||||||
<Tabs>
|
|
||||||
<TabList>
|
|
||||||
<Tab id="referrer">{formatMessage(labels.referrers)}</Tab>
|
|
||||||
<Tab id="channel">{formatMessage(labels.channels)}</Tab>
|
|
||||||
</TabList>
|
|
||||||
<TabPanel id="referrer">
|
|
||||||
<ReferrersTable {...tableProps} />
|
|
||||||
</TabPanel>
|
|
||||||
<TabPanel id="channel">
|
|
||||||
<ChannelsTable {...tableProps} />
|
|
||||||
</TabPanel>
|
|
||||||
</Tabs>
|
|
||||||
</Panel>
|
|
||||||
</GridRow>
|
|
||||||
<GridRow layout="two-one" {...rowProps}>
|
|
||||||
<Panel gridColumn="span 2" noPadding>
|
|
||||||
<WorldMap websiteId={pixelId} />
|
|
||||||
</Panel>
|
|
||||||
<Panel>
|
|
||||||
<Tabs>
|
|
||||||
<TabList>
|
|
||||||
<Tab id="country">{formatMessage(labels.countries)}</Tab>
|
|
||||||
<Tab id="region">{formatMessage(labels.regions)}</Tab>
|
|
||||||
<Tab id="city">{formatMessage(labels.cities)}</Tab>
|
|
||||||
</TabList>
|
|
||||||
<TabPanel id="country">
|
|
||||||
<CountriesTable {...tableProps} />
|
|
||||||
</TabPanel>
|
|
||||||
<TabPanel id="region">
|
|
||||||
<RegionsTable {...tableProps} />
|
|
||||||
</TabPanel>
|
|
||||||
<TabPanel id="city">
|
|
||||||
<CitiesTable {...tableProps} />
|
|
||||||
</TabPanel>
|
|
||||||
</Tabs>
|
|
||||||
</Panel>
|
|
||||||
</GridRow>
|
|
||||||
<GridRow layout="two" {...rowProps}>
|
|
||||||
<Panel>
|
|
||||||
<Tabs>
|
|
||||||
<TabList>
|
|
||||||
<Tab id="browser">{formatMessage(labels.browsers)}</Tab>
|
|
||||||
<Tab id="os">{formatMessage(labels.os)}</Tab>
|
|
||||||
<Tab id="device">{formatMessage(labels.devices)}</Tab>
|
|
||||||
</TabList>
|
|
||||||
<TabPanel id="browser">
|
|
||||||
<BrowsersTable {...tableProps} />
|
|
||||||
</TabPanel>
|
|
||||||
<TabPanel id="os">
|
|
||||||
<OSTable {...tableProps} />
|
|
||||||
</TabPanel>
|
|
||||||
<TabPanel id="device">
|
|
||||||
<DevicesTable {...tableProps} />
|
|
||||||
</TabPanel>
|
|
||||||
</Tabs>
|
|
||||||
</Panel>
|
|
||||||
<Panel>
|
|
||||||
<SessionsWeekly websiteId={pixelId} />
|
|
||||||
</Panel>
|
|
||||||
</GridRow>
|
|
||||||
</Grid>
|
|
||||||
</PageBody>
|
</PageBody>
|
||||||
</PixelProvider>
|
</PixelProvider>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
83
src/app/(main)/pixels/[pixelId]/PixelPanels.tsx
Normal file
83
src/app/(main)/pixels/[pixelId]/PixelPanels.tsx
Normal file
|
|
@ -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 (
|
||||||
|
<Grid gap="3">
|
||||||
|
<GridRow layout="two" {...rowProps}>
|
||||||
|
<Panel>
|
||||||
|
<Heading size="2">{formatMessage(labels.sources)}</Heading>
|
||||||
|
<Tabs>
|
||||||
|
<TabList>
|
||||||
|
<Tab id="referrer">{formatMessage(labels.referrers)}</Tab>
|
||||||
|
<Tab id="channel">{formatMessage(labels.channels)}</Tab>
|
||||||
|
</TabList>
|
||||||
|
<TabPanel id="referrer">
|
||||||
|
<MetricsTable type="referrer" title={formatMessage(labels.domain)} {...tableProps} />
|
||||||
|
</TabPanel>
|
||||||
|
<TabPanel id="channel">
|
||||||
|
<MetricsTable type="channel" title={formatMessage(labels.type)} {...tableProps} />
|
||||||
|
</TabPanel>
|
||||||
|
</Tabs>
|
||||||
|
</Panel>
|
||||||
|
<Panel>
|
||||||
|
<Heading size="2">{formatMessage(labels.environment)}</Heading>
|
||||||
|
<Tabs>
|
||||||
|
<TabList>
|
||||||
|
<Tab id="browser">{formatMessage(labels.browsers)}</Tab>
|
||||||
|
<Tab id="os">{formatMessage(labels.os)}</Tab>
|
||||||
|
<Tab id="device">{formatMessage(labels.devices)}</Tab>
|
||||||
|
</TabList>
|
||||||
|
<TabPanel id="browser">
|
||||||
|
<MetricsTable type="browser" title={formatMessage(labels.browser)} {...tableProps} />
|
||||||
|
</TabPanel>
|
||||||
|
<TabPanel id="os">
|
||||||
|
<MetricsTable type="os" title={formatMessage(labels.os)} {...tableProps} />
|
||||||
|
</TabPanel>
|
||||||
|
<TabPanel id="device">
|
||||||
|
<MetricsTable type="device" title={formatMessage(labels.device)} {...tableProps} />
|
||||||
|
</TabPanel>
|
||||||
|
</Tabs>
|
||||||
|
</Panel>
|
||||||
|
</GridRow>
|
||||||
|
<GridRow layout="two" {...rowProps}>
|
||||||
|
<Panel noPadding>
|
||||||
|
<WorldMap websiteId={pixelId} />
|
||||||
|
</Panel>
|
||||||
|
<Panel>
|
||||||
|
<Heading size="2">{formatMessage(labels.location)}</Heading>
|
||||||
|
<Tabs>
|
||||||
|
<TabList>
|
||||||
|
<Tab id="country">{formatMessage(labels.countries)}</Tab>
|
||||||
|
<Tab id="region">{formatMessage(labels.regions)}</Tab>
|
||||||
|
<Tab id="city">{formatMessage(labels.cities)}</Tab>
|
||||||
|
</TabList>
|
||||||
|
<TabPanel id="country">
|
||||||
|
<MetricsTable type="country" title={formatMessage(labels.country)} {...tableProps} />
|
||||||
|
</TabPanel>
|
||||||
|
<TabPanel id="region">
|
||||||
|
<MetricsTable type="region" title={formatMessage(labels.region)} {...tableProps} />
|
||||||
|
</TabPanel>
|
||||||
|
<TabPanel id="city">
|
||||||
|
<MetricsTable type="city" title={formatMessage(labels.city)} {...tableProps} />
|
||||||
|
</TabPanel>
|
||||||
|
</Tabs>
|
||||||
|
</Panel>
|
||||||
|
</GridRow>
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -47,7 +47,13 @@ export function WebsiteChart({
|
||||||
}, [data, startDate, endDate, unit]);
|
}, [data, startDate, endDate, unit]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LoadingPanel data={data} isFetching={isFetching} isLoading={isLoading} error={error}>
|
<LoadingPanel
|
||||||
|
data={data}
|
||||||
|
isFetching={isFetching}
|
||||||
|
isLoading={isLoading}
|
||||||
|
error={error}
|
||||||
|
minHeight="520px"
|
||||||
|
>
|
||||||
<PageviewsChart
|
<PageviewsChart
|
||||||
key={value}
|
key={value}
|
||||||
data={chartData}
|
data={chartData}
|
||||||
|
|
|
||||||
|
|
@ -1,42 +1,12 @@
|
||||||
import { Grid, Heading, Column, Row, NavMenu, NavMenuItem, Text } from '@umami/react-zen';
|
import { Grid, Heading, Column, Row, NavMenu, NavMenuItem, Text } from '@umami/react-zen';
|
||||||
import { useDateRange, useMessages, useNavigation } from '@/components/hooks';
|
import { useDateRange, useMessages, useNavigation } from '@/components/hooks';
|
||||||
import { BrowsersTable } from '@/components/metrics/BrowsersTable';
|
|
||||||
import { ChangeLabel } from '@/components/metrics/ChangeLabel';
|
|
||||||
import { CitiesTable } from '@/components/metrics/CitiesTable';
|
|
||||||
import { CountriesTable } from '@/components/metrics/CountriesTable';
|
|
||||||
import { DevicesTable } from '@/components/metrics/DevicesTable';
|
|
||||||
import { EventsTable } from '@/components/metrics/EventsTable';
|
|
||||||
import { LanguagesTable } from '@/components/metrics/LanguagesTable';
|
|
||||||
import { MetricsTable } from '@/components/metrics/MetricsTable';
|
import { MetricsTable } from '@/components/metrics/MetricsTable';
|
||||||
import { OSTable } from '@/components/metrics/OSTable';
|
|
||||||
import { PagesTable } from '@/components/metrics/PagesTable';
|
|
||||||
import { QueryParametersTable } from '@/components/metrics/QueryParametersTable';
|
|
||||||
import { ReferrersTable } from '@/components/metrics/ReferrersTable';
|
|
||||||
import { RegionsTable } from '@/components/metrics/RegionsTable';
|
|
||||||
import { ScreenTable } from '@/components/metrics/ScreenTable';
|
|
||||||
import { TagsTable } from '@/components/metrics/TagsTable';
|
|
||||||
import { getCompareDate } from '@/lib/date';
|
import { getCompareDate } from '@/lib/date';
|
||||||
import { formatNumber } from '@/lib/format';
|
import { formatNumber } from '@/lib/format';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Panel } from '@/components/common/Panel';
|
import { Panel } from '@/components/common/Panel';
|
||||||
import { DateDisplay } from '@/components/common/DateDisplay';
|
import { DateDisplay } from '@/components/common/DateDisplay';
|
||||||
|
import { ChangeLabel } from '@/components/metrics/ChangeLabel';
|
||||||
const views = {
|
|
||||||
path: PagesTable,
|
|
||||||
title: PagesTable,
|
|
||||||
referrer: ReferrersTable,
|
|
||||||
browser: BrowsersTable,
|
|
||||||
os: OSTable,
|
|
||||||
device: DevicesTable,
|
|
||||||
screen: ScreenTable,
|
|
||||||
country: CountriesTable,
|
|
||||||
region: RegionsTable,
|
|
||||||
city: CitiesTable,
|
|
||||||
language: LanguagesTable,
|
|
||||||
event: EventsTable,
|
|
||||||
query: QueryParametersTable,
|
|
||||||
tag: TagsTable,
|
|
||||||
};
|
|
||||||
|
|
||||||
export function WebsiteCompareTables({ websiteId }: { websiteId: string }) {
|
export function WebsiteCompareTables({ websiteId }: { websiteId: string }) {
|
||||||
const [data] = useState([]);
|
const [data] = useState([]);
|
||||||
|
|
@ -46,7 +16,6 @@ export function WebsiteCompareTables({ websiteId }: { websiteId: string }) {
|
||||||
updateParams,
|
updateParams,
|
||||||
query: { view },
|
query: { view },
|
||||||
} = useNavigation();
|
} = useNavigation();
|
||||||
const Component: typeof MetricsTable = views[view || 'path'] || (() => null);
|
|
||||||
|
|
||||||
const items = [
|
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 prev = data.find(d => d.x === x)?.y;
|
||||||
const value = y - prev;
|
const value = y - prev;
|
||||||
const change = Math.abs(((y - prev) / prev) * 100);
|
const change = Math.abs(((y - prev) / prev) * 100);
|
||||||
|
|
@ -163,15 +132,22 @@ export function WebsiteCompareTables({ websiteId }: { websiteId: string }) {
|
||||||
<Heading size="1">{formatMessage(labels.previous)}</Heading>
|
<Heading size="1">{formatMessage(labels.previous)}</Heading>
|
||||||
<DateDisplay startDate={startDate} endDate={endDate} />
|
<DateDisplay startDate={startDate} endDate={endDate} />
|
||||||
</Row>
|
</Row>
|
||||||
<Component websiteId={websiteId} limit={20} showMore={false} params={params} />
|
<MetricsTable
|
||||||
|
websiteId={websiteId}
|
||||||
|
type={view}
|
||||||
|
limit={20}
|
||||||
|
showMore={false}
|
||||||
|
params={params}
|
||||||
|
/>
|
||||||
</Column>
|
</Column>
|
||||||
<Column border="left" paddingLeft="6" gap="6">
|
<Column border="left" paddingLeft="6" gap="6">
|
||||||
<Row alignItems="center" justifyContent="space-between">
|
<Row alignItems="center" justifyContent="space-between">
|
||||||
<Heading size="1"> {formatMessage(labels.current)}</Heading>
|
<Heading size="1"> {formatMessage(labels.current)}</Heading>
|
||||||
<DateDisplay startDate={dateRange.startDate} endDate={dateRange.endDate} />
|
<DateDisplay startDate={dateRange.startDate} endDate={dateRange.endDate} />
|
||||||
</Row>
|
</Row>
|
||||||
<Component
|
<MetricsTable
|
||||||
websiteId={websiteId}
|
websiteId={websiteId}
|
||||||
|
type={view}
|
||||||
limit={20}
|
limit={20}
|
||||||
showMore={false}
|
showMore={false}
|
||||||
renderChange={renderChange}
|
renderChange={renderChange}
|
||||||
|
|
|
||||||
|
|
@ -1,43 +1,7 @@
|
||||||
|
import Link from 'next/link';
|
||||||
import { Grid, Column, NavMenu, NavMenuItem } from '@umami/react-zen';
|
import { Grid, Column, NavMenu, NavMenuItem } from '@umami/react-zen';
|
||||||
import { useMessages, useNavigation } from '@/components/hooks';
|
import { useMessages, useNavigation } from '@/components/hooks';
|
||||||
import { BrowsersTable } from '@/components/metrics/BrowsersTable';
|
import { MetricsExpandedTable } from '@/components/metrics/MetricsExpandedTable';
|
||||||
import { CitiesTable } from '@/components/metrics/CitiesTable';
|
|
||||||
import { CountriesTable } from '@/components/metrics/CountriesTable';
|
|
||||||
import { DevicesTable } from '@/components/metrics/DevicesTable';
|
|
||||||
import { EventsTable } from '@/components/metrics/EventsTable';
|
|
||||||
import { HostnamesTable } from '@/components/metrics/HostnamesTable';
|
|
||||||
import { LanguagesTable } from '@/components/metrics/LanguagesTable';
|
|
||||||
import { OSTable } from '@/components/metrics/OSTable';
|
|
||||||
import { PagesTable } from '@/components/metrics/PagesTable';
|
|
||||||
import { QueryParametersTable } from '@/components/metrics/QueryParametersTable';
|
|
||||||
import { ReferrersTable } from '@/components/metrics/ReferrersTable';
|
|
||||||
import { RegionsTable } from '@/components/metrics/RegionsTable';
|
|
||||||
import { ScreenTable } from '@/components/metrics/ScreenTable';
|
|
||||||
import { TagsTable } from '@/components/metrics/TagsTable';
|
|
||||||
import { ChannelsTable } from '@/components/metrics/ChannelsTable';
|
|
||||||
import Link from 'next/link';
|
|
||||||
|
|
||||||
const views = {
|
|
||||||
path: PagesTable,
|
|
||||||
entry: PagesTable,
|
|
||||||
exit: PagesTable,
|
|
||||||
title: PagesTable,
|
|
||||||
referrer: ReferrersTable,
|
|
||||||
grouped: ReferrersTable,
|
|
||||||
hostname: HostnamesTable,
|
|
||||||
browser: BrowsersTable,
|
|
||||||
os: OSTable,
|
|
||||||
device: DevicesTable,
|
|
||||||
screen: ScreenTable,
|
|
||||||
country: CountriesTable,
|
|
||||||
region: RegionsTable,
|
|
||||||
city: CitiesTable,
|
|
||||||
language: LanguagesTable,
|
|
||||||
event: EventsTable,
|
|
||||||
query: QueryParametersTable,
|
|
||||||
tag: TagsTable,
|
|
||||||
channel: ChannelsTable,
|
|
||||||
};
|
|
||||||
|
|
||||||
export function WebsiteExpandedView({
|
export function WebsiteExpandedView({
|
||||||
websiteId,
|
websiteId,
|
||||||
|
|
@ -130,8 +94,6 @@ export function WebsiteExpandedView({
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const DetailsComponent = views[view] || (() => null);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid columns="auto 1fr" gap="6" height="100%" overflow="hidden">
|
<Grid columns="auto 1fr" gap="6" height="100%" overflow="hidden">
|
||||||
<Column gap="6" width="200px" border="right" paddingRight="3">
|
<Column gap="6" width="200px" border="right" paddingRight="3">
|
||||||
|
|
@ -146,15 +108,13 @@ export function WebsiteExpandedView({
|
||||||
</NavMenu>
|
</NavMenu>
|
||||||
</Column>
|
</Column>
|
||||||
<Column overflow="hidden">
|
<Column overflow="hidden">
|
||||||
<DetailsComponent
|
<MetricsExpandedTable
|
||||||
|
title={formatMessage(labels[view])}
|
||||||
|
type={view}
|
||||||
websiteId={websiteId}
|
websiteId={websiteId}
|
||||||
animate={false}
|
|
||||||
virtualize={true}
|
|
||||||
itemCount={25}
|
|
||||||
allowFilter={true}
|
|
||||||
allowSearch={true}
|
|
||||||
isExpanded={true}
|
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
|
allowSearch
|
||||||
|
allowDownload
|
||||||
/>
|
/>
|
||||||
</Column>
|
</Column>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { Panel } from '@/components/common/Panel';
|
||||||
import { WebsiteChart } from './WebsiteChart';
|
import { WebsiteChart } from './WebsiteChart';
|
||||||
import { WebsiteExpandedView } from './WebsiteExpandedView';
|
import { WebsiteExpandedView } from './WebsiteExpandedView';
|
||||||
import { WebsiteMetricsBar } from './WebsiteMetricsBar';
|
import { WebsiteMetricsBar } from './WebsiteMetricsBar';
|
||||||
import { WebsiteTableView } from './WebsiteTableView';
|
import { WebsitePanels } from './WebsitePanels';
|
||||||
import { WebsiteControls } from './WebsiteControls';
|
import { WebsiteControls } from './WebsiteControls';
|
||||||
|
|
||||||
export function WebsitePage({ websiteId }: { websiteId: string }) {
|
export function WebsitePage({ websiteId }: { websiteId: string }) {
|
||||||
|
|
@ -32,7 +32,7 @@ export function WebsitePage({ websiteId }: { websiteId: string }) {
|
||||||
<Panel>
|
<Panel>
|
||||||
<WebsiteChart websiteId={websiteId} compareMode={compare} />
|
<WebsiteChart websiteId={websiteId} compareMode={compare} />
|
||||||
</Panel>
|
</Panel>
|
||||||
<WebsiteTableView websiteId={websiteId} />
|
<WebsitePanels websiteId={websiteId} />
|
||||||
<Modal isOpen={!!view} onOpenChange={handleOpenChange} isDismissable>
|
<Modal isOpen={!!view} onOpenChange={handleOpenChange} isDismissable>
|
||||||
<Dialog style={{ maxWidth: 1320, width: '100vw', height: 'calc(100vh - 40px)' }}>
|
<Dialog style={{ maxWidth: 1320, width: '100vw', height: 'calc(100vh - 40px)' }}>
|
||||||
{({ close }) => {
|
{({ close }) => {
|
||||||
|
|
|
||||||
|
|
@ -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 { GridRow } from '@/components/common/GridRow';
|
||||||
import { Panel } from '@/components/common/Panel';
|
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 { 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 { 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 { 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 };
|
const rowProps = { minHeight: 570 };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid gap="3">
|
<Grid gap="3">
|
||||||
<GridRow layout="two" {...rowProps}>
|
<GridRow layout="two" {...rowProps}>
|
||||||
<Panel>
|
<Panel>
|
||||||
|
<Heading size="2">{formatMessage(labels.pages)}</Heading>
|
||||||
<Tabs>
|
<Tabs>
|
||||||
<TabList>
|
<TabList>
|
||||||
<Tab id="page">{formatMessage(labels.pages)}</Tab>
|
<Tab id="page">{formatMessage(labels.pages)}</Tab>
|
||||||
|
|
@ -30,27 +29,28 @@ export function WebsiteTableView({ websiteId }: { websiteId: string }) {
|
||||||
<Tab id="exit">{formatMessage(labels.exit)}</Tab>
|
<Tab id="exit">{formatMessage(labels.exit)}</Tab>
|
||||||
</TabList>
|
</TabList>
|
||||||
<TabPanel id="page">
|
<TabPanel id="page">
|
||||||
<PagesTable type="path" {...tableProps} />
|
<MetricsTable type="path" title={formatMessage(labels.path)} {...tableProps} />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
<TabPanel id="entry">
|
<TabPanel id="entry">
|
||||||
<PagesTable type="entry" {...tableProps} />
|
<MetricsTable type="entry" title={formatMessage(labels.path)} {...tableProps} />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
<TabPanel id="exit">
|
<TabPanel id="exit">
|
||||||
<PagesTable type="exit" {...tableProps} />
|
<MetricsTable type="exit" title={formatMessage(labels.path)} {...tableProps} />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</Panel>
|
</Panel>
|
||||||
<Panel>
|
<Panel>
|
||||||
|
<Heading size="2">{formatMessage(labels.sources)}</Heading>
|
||||||
<Tabs>
|
<Tabs>
|
||||||
<TabList>
|
<TabList>
|
||||||
<Tab id="referrer">{formatMessage(labels.referrers)}</Tab>
|
<Tab id="referrer">{formatMessage(labels.referrers)}</Tab>
|
||||||
<Tab id="channel">{formatMessage(labels.channels)}</Tab>
|
<Tab id="channel">{formatMessage(labels.channels)}</Tab>
|
||||||
</TabList>
|
</TabList>
|
||||||
<TabPanel id="referrer">
|
<TabPanel id="referrer">
|
||||||
<ReferrersTable {...tableProps} />
|
<MetricsTable type="referrer" title={formatMessage(labels.domain)} {...tableProps} />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
<TabPanel id="channel">
|
<TabPanel id="channel">
|
||||||
<ChannelsTable {...tableProps} />
|
<MetricsTable type="channel" title={formatMessage(labels.type)} {...tableProps} />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|
@ -60,6 +60,7 @@ export function WebsiteTableView({ websiteId }: { websiteId: string }) {
|
||||||
<WorldMap websiteId={websiteId} />
|
<WorldMap websiteId={websiteId} />
|
||||||
</Panel>
|
</Panel>
|
||||||
<Panel>
|
<Panel>
|
||||||
|
<Heading size="2">{formatMessage(labels.location)}</Heading>
|
||||||
<Tabs>
|
<Tabs>
|
||||||
<TabList>
|
<TabList>
|
||||||
<Tab id="country">{formatMessage(labels.countries)}</Tab>
|
<Tab id="country">{formatMessage(labels.countries)}</Tab>
|
||||||
|
|
@ -67,19 +68,20 @@ export function WebsiteTableView({ websiteId }: { websiteId: string }) {
|
||||||
<Tab id="city">{formatMessage(labels.cities)}</Tab>
|
<Tab id="city">{formatMessage(labels.cities)}</Tab>
|
||||||
</TabList>
|
</TabList>
|
||||||
<TabPanel id="country">
|
<TabPanel id="country">
|
||||||
<CountriesTable {...tableProps} />
|
<MetricsTable type="country" title={formatMessage(labels.country)} {...tableProps} />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
<TabPanel id="region">
|
<TabPanel id="region">
|
||||||
<RegionsTable {...tableProps} />
|
<MetricsTable type="region" title={formatMessage(labels.region)} {...tableProps} />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
<TabPanel id="city">
|
<TabPanel id="city">
|
||||||
<CitiesTable {...tableProps} />
|
<MetricsTable type="city" title={formatMessage(labels.city)} {...tableProps} />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</Panel>
|
</Panel>
|
||||||
</GridRow>
|
</GridRow>
|
||||||
<GridRow layout="two" {...rowProps}>
|
<GridRow layout="two" {...rowProps}>
|
||||||
<Panel>
|
<Panel>
|
||||||
|
<Heading size="2">{formatMessage(labels.environment)}</Heading>
|
||||||
<Tabs>
|
<Tabs>
|
||||||
<TabList>
|
<TabList>
|
||||||
<Tab id="browser">{formatMessage(labels.browsers)}</Tab>
|
<Tab id="browser">{formatMessage(labels.browsers)}</Tab>
|
||||||
|
|
@ -87,18 +89,20 @@ export function WebsiteTableView({ websiteId }: { websiteId: string }) {
|
||||||
<Tab id="device">{formatMessage(labels.devices)}</Tab>
|
<Tab id="device">{formatMessage(labels.devices)}</Tab>
|
||||||
</TabList>
|
</TabList>
|
||||||
<TabPanel id="browser">
|
<TabPanel id="browser">
|
||||||
<BrowsersTable {...tableProps} />
|
<MetricsTable type="browser" title={formatMessage(labels.browser)} {...tableProps} />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
<TabPanel id="os">
|
<TabPanel id="os">
|
||||||
<OSTable {...tableProps} />
|
<MetricsTable type="os" title={formatMessage(labels.os)} {...tableProps} />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
<TabPanel id="device">
|
<TabPanel id="device">
|
||||||
<DevicesTable {...tableProps} />
|
<MetricsTable type="device" title={formatMessage(labels.device)} {...tableProps} />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</Panel>
|
</Panel>
|
||||||
<Panel>
|
<Panel>
|
||||||
<SessionsWeekly websiteId={websiteId} />
|
<Heading size="2">{formatMessage(labels.traffic)}</Heading>
|
||||||
|
<Row border="bottom" marginBottom="4" />
|
||||||
|
<WeeklyTraffic websiteId={websiteId} />
|
||||||
</Panel>
|
</Panel>
|
||||||
</GridRow>
|
</GridRow>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
@ -5,10 +5,11 @@ import { DateDistance } from '@/components/common/DateDistance';
|
||||||
import { filtersObjectToArray } from '@/lib/params';
|
import { filtersObjectToArray } from '@/lib/params';
|
||||||
import { CohortEditButton } from '@/app/(main)/websites/[websiteId]/cohorts/CohortEditButton';
|
import { CohortEditButton } from '@/app/(main)/websites/[websiteId]/cohorts/CohortEditButton';
|
||||||
import { CohortDeleteButton } from '@/app/(main)/websites/[websiteId]/cohorts/CohortDeleteButton';
|
import { CohortDeleteButton } from '@/app/(main)/websites/[websiteId]/cohorts/CohortDeleteButton';
|
||||||
|
import Link from 'next/link';
|
||||||
|
|
||||||
export function CohortsTable({ data = [] }) {
|
export function CohortsTable({ data = [] }) {
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
const { websiteId } = useNavigation();
|
const { websiteId, renderUrl } = useNavigation();
|
||||||
|
|
||||||
if (data.length === 0) {
|
if (data.length === 0) {
|
||||||
return <Empty />;
|
return <Empty />;
|
||||||
|
|
@ -16,7 +17,11 @@ export function CohortsTable({ data = [] }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DataTable data={data}>
|
<DataTable data={data}>
|
||||||
<DataColumn id="name" label={formatMessage(labels.name)} />
|
<DataColumn id="name" label={formatMessage(labels.name)}>
|
||||||
|
{(row: any) => (
|
||||||
|
<Link href={renderUrl(`/websites/${websiteId}?cohort=${row.id}`)}>{row.name}</Link>
|
||||||
|
)}
|
||||||
|
</DataColumn>
|
||||||
<DataColumn id="created" label={formatMessage(labels.created)}>
|
<DataColumn id="created" label={formatMessage(labels.created)}>
|
||||||
{(row: any) => <DateDistance date={new Date(row.createdAt)} />}
|
{(row: any) => <DateDistance date={new Date(row.createdAt)} />}
|
||||||
</DataColumn>
|
</DataColumn>
|
||||||
|
|
|
||||||
|
|
@ -100,9 +100,9 @@ const EventValues = ({ websiteId, eventName, propertyName }) => {
|
||||||
const tableData = useMemo(() => {
|
const tableData = useMemo(() => {
|
||||||
if (!propertyName || !values || propertySum === 0) return [];
|
if (!propertyName || !values || propertySum === 0) return [];
|
||||||
return values.map(({ value, total }) => ({
|
return values.map(({ value, total }) => ({
|
||||||
x: value,
|
label: value,
|
||||||
y: total,
|
count: total,
|
||||||
z: 100 * (total / propertySum),
|
percent: 100 * (total / propertySum),
|
||||||
}));
|
}));
|
||||||
}, [propertyName, values, propertySum]);
|
}, [propertyName, values, propertySum]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
'use client';
|
'use client';
|
||||||
import { TabList, Tab, Tabs, TabPanel, Column } from '@umami/react-zen';
|
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 { useState, Key } from 'react';
|
||||||
import { EventsDataTable } from './EventsDataTable';
|
import { EventsDataTable } from './EventsDataTable';
|
||||||
import { Panel } from '@/components/common/Panel';
|
import { Panel } from '@/components/common/Panel';
|
||||||
|
|
@ -13,14 +13,9 @@ import { getItem, setItem } from '@/lib/storage';
|
||||||
const KEY_NAME = 'umami.events.tab';
|
const KEY_NAME = 'umami.events.tab';
|
||||||
|
|
||||||
export function EventsPage({ websiteId }) {
|
export function EventsPage({ websiteId }) {
|
||||||
const [label, setLabel] = useState(null);
|
|
||||||
const [tab, setTab] = useState(getItem(KEY_NAME) || 'chart');
|
const [tab, setTab] = useState(getItem(KEY_NAME) || 'chart');
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
|
|
||||||
const handleLabelClick = (value: string) => {
|
|
||||||
setLabel(value !== label ? value : '');
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSelect = (value: Key) => {
|
const handleSelect = (value: Key) => {
|
||||||
setItem(KEY_NAME, value);
|
setItem(KEY_NAME, value);
|
||||||
setTab(value);
|
setTab(value);
|
||||||
|
|
@ -42,14 +37,13 @@ export function EventsPage({ websiteId }) {
|
||||||
<TabPanel id="chart">
|
<TabPanel id="chart">
|
||||||
<Column gap="6">
|
<Column gap="6">
|
||||||
<Column border="bottom" paddingBottom="6">
|
<Column border="bottom" paddingBottom="6">
|
||||||
<EventsChart websiteId={websiteId} focusLabel={label} />
|
<EventsChart websiteId={websiteId} />
|
||||||
</Column>
|
</Column>
|
||||||
<EventsTable
|
<MetricsTable
|
||||||
websiteId={websiteId}
|
websiteId={websiteId}
|
||||||
type="event"
|
type="event"
|
||||||
title={formatMessage(labels.events)}
|
title={formatMessage(labels.events)}
|
||||||
metric={formatMessage(labels.actions)}
|
metric={formatMessage(labels.actions)}
|
||||||
onLabelClick={handleLabelClick}
|
|
||||||
/>
|
/>
|
||||||
</Column>
|
</Column>
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,11 @@ import { DateDistance } from '@/components/common/DateDistance';
|
||||||
import { filtersObjectToArray } from '@/lib/params';
|
import { filtersObjectToArray } from '@/lib/params';
|
||||||
import { SegmentEditButton } from '@/app/(main)/websites/[websiteId]/segments/SegmentEditButton';
|
import { SegmentEditButton } from '@/app/(main)/websites/[websiteId]/segments/SegmentEditButton';
|
||||||
import { SegmentDeleteButton } from '@/app/(main)/websites/[websiteId]/segments/SegmentDeleteButton';
|
import { SegmentDeleteButton } from '@/app/(main)/websites/[websiteId]/segments/SegmentDeleteButton';
|
||||||
|
import Link from 'next/link';
|
||||||
|
|
||||||
export function SegmentsTable({ data = [] }) {
|
export function SegmentsTable({ data = [] }) {
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
const { websiteId } = useNavigation();
|
const { websiteId, renderUrl } = useNavigation();
|
||||||
|
|
||||||
if (data.length === 0) {
|
if (data.length === 0) {
|
||||||
return <Empty />;
|
return <Empty />;
|
||||||
|
|
@ -16,7 +17,11 @@ export function SegmentsTable({ data = [] }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DataTable data={data}>
|
<DataTable data={data}>
|
||||||
<DataColumn id="name" label={formatMessage(labels.name)} />
|
<DataColumn id="name" label={formatMessage(labels.name)}>
|
||||||
|
{(row: any) => (
|
||||||
|
<Link href={renderUrl(`/websites/${websiteId}?segment=${row.id}`)}>{row.name}</Link>
|
||||||
|
)}
|
||||||
|
</DataColumn>
|
||||||
<DataColumn id="created" label={formatMessage(labels.created)}>
|
<DataColumn id="created" label={formatMessage(labels.created)}>
|
||||||
{(row: any) => <DateDistance date={new Date(row.createdAt)} />}
|
{(row: any) => <DateDistance date={new Date(row.createdAt)} />}
|
||||||
</DataColumn>
|
</DataColumn>
|
||||||
|
|
|
||||||
|
|
@ -71,9 +71,9 @@ const SessionValues = ({ websiteId, propertyName }) => {
|
||||||
const tableData = useMemo(() => {
|
const tableData = useMemo(() => {
|
||||||
if (!propertyName || !data || propertySum === 0) return [];
|
if (!propertyName || !data || propertySum === 0) return [];
|
||||||
return data.map(({ value, total }) => ({
|
return data.map(({ value, total }) => ({
|
||||||
x: value,
|
label: value,
|
||||||
y: total,
|
count: total,
|
||||||
z: 100 * (total / propertySum),
|
percent: 100 * (total / propertySum),
|
||||||
}));
|
}));
|
||||||
}, [propertyName, data, propertySum]);
|
}, [propertyName, data, propertySum]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,9 @@ import {
|
||||||
Text,
|
Text,
|
||||||
} from '@umami/react-zen';
|
} from '@umami/react-zen';
|
||||||
import {
|
import {
|
||||||
useApi,
|
|
||||||
useLoginQuery,
|
useLoginQuery,
|
||||||
useMessages,
|
useMessages,
|
||||||
|
useUpdateQuery,
|
||||||
useUserTeamsQuery,
|
useUserTeamsQuery,
|
||||||
useWebsite,
|
useWebsite,
|
||||||
} from '@/components/hooks';
|
} from '@/components/hooks';
|
||||||
|
|
@ -32,10 +32,7 @@ export function WebsiteTransferForm({
|
||||||
const website = useWebsite();
|
const website = useWebsite();
|
||||||
const [teamId, setTeamId] = useState<string>(null);
|
const [teamId, setTeamId] = useState<string>(null);
|
||||||
const { formatMessage, labels, messages } = useMessages();
|
const { formatMessage, labels, messages } = useMessages();
|
||||||
const { post, useMutation } = useApi();
|
const { mutate, error, isPending } = useUpdateQuery(`/websites/${websiteId}/transfer`);
|
||||||
const { mutate, error } = useMutation({
|
|
||||||
mutationFn: (data: any) => post(`/websites/${websiteId}/transfer`, data),
|
|
||||||
});
|
|
||||||
const { data: teams, isLoading } = useUserTeamsQuery(user.id);
|
const { data: teams, isLoading } = useUserTeamsQuery(user.id);
|
||||||
const isTeamWebsite = !!website?.teamId;
|
const isTeamWebsite = !!website?.teamId;
|
||||||
|
|
||||||
|
|
@ -92,7 +89,11 @@ export function WebsiteTransferForm({
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormButtons>
|
<FormButtons>
|
||||||
<Button onPress={onClose}>{formatMessage(labels.cancel)}</Button>
|
<Button onPress={onClose}>{formatMessage(labels.cancel)}</Button>
|
||||||
<FormSubmitButton variant="primary" isDisabled={!isTeamWebsite && !teamId}>
|
<FormSubmitButton
|
||||||
|
variant="primary"
|
||||||
|
isPending={isPending}
|
||||||
|
isDisabled={!isTeamWebsite && !teamId}
|
||||||
|
>
|
||||||
{formatMessage(labels.transfer)}
|
{formatMessage(labels.transfer)}
|
||||||
</FormSubmitButton>
|
</FormSubmitButton>
|
||||||
</FormButtons>
|
</FormButtons>
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { getQueryFilters, parseRequest } from '@/lib/request';
|
||||||
import { unauthorized, json } from '@/lib/response';
|
import { unauthorized, json } from '@/lib/response';
|
||||||
import { canViewWebsite } from '@/validations';
|
import { canViewWebsite } from '@/validations';
|
||||||
import { pagingParams, timezoneParam } from '@/lib/schema';
|
import { pagingParams, timezoneParam } from '@/lib/schema';
|
||||||
import { getWebsiteSessionsWeekly } from '@/queries';
|
import { getWeeklyTraffic } from '@/queries';
|
||||||
|
|
||||||
export async function GET(
|
export async function GET(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
|
@ -30,7 +30,7 @@ export async function GET(
|
||||||
|
|
||||||
const filters = await getQueryFilters(query, websiteId);
|
const filters = await getQueryFilters(query, websiteId);
|
||||||
|
|
||||||
const data = await getWebsiteSessionsWeekly(websiteId, filters);
|
const data = await getWeeklyTraffic(websiteId, filters);
|
||||||
|
|
||||||
return json(data);
|
return json(data);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,54 +1,47 @@
|
||||||
import { ReactNode } from 'react';
|
import { HTMLAttributes, ReactNode, useState } from 'react';
|
||||||
import classNames from 'classnames';
|
|
||||||
import Link from 'next/link';
|
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 { useMessages, useNavigation } from '@/components/hooks';
|
||||||
import { ExternalLink } from '@/components/icons';
|
import { ExternalLink } from '@/components/icons';
|
||||||
import styles from './FilterLink.module.css';
|
|
||||||
|
|
||||||
export interface FilterLinkProps {
|
export interface FilterLinkProps extends HTMLAttributes<HTMLDivElement> {
|
||||||
id: string;
|
type: string;
|
||||||
value: string;
|
value: string;
|
||||||
label?: string;
|
label?: string;
|
||||||
|
icon?: ReactNode;
|
||||||
externalUrl?: string;
|
externalUrl?: string;
|
||||||
className?: string;
|
|
||||||
children?: ReactNode;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FilterLink({
|
export function FilterLink({ type, value, label, externalUrl, icon }: FilterLinkProps) {
|
||||||
id,
|
const [showLink, setShowLink] = useState(false);
|
||||||
value,
|
|
||||||
label,
|
|
||||||
externalUrl,
|
|
||||||
children,
|
|
||||||
className,
|
|
||||||
}: FilterLinkProps) {
|
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
const { updateParams, query } = useNavigation();
|
const { updateParams, query } = useNavigation();
|
||||||
const active = query[id] !== undefined;
|
const active = query[type] !== undefined;
|
||||||
const selected = query[id] === value;
|
const selected = query[type] === value;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<Row
|
||||||
className={classNames(styles.row, className, {
|
alignItems="center"
|
||||||
[styles.inactive]: active && !selected,
|
gap
|
||||||
[styles.active]: active && selected,
|
fontWeight={active && selected ? 'bold' : undefined}
|
||||||
})}
|
color={active && !selected ? 'muted' : undefined}
|
||||||
|
onMouseOver={() => setShowLink(true)}
|
||||||
|
onMouseOut={() => setShowLink(false)}
|
||||||
>
|
>
|
||||||
{children}
|
{icon}
|
||||||
{!value && `(${label || formatMessage(labels.unknown)})`}
|
{!value && `(${label || formatMessage(labels.unknown)})`}
|
||||||
{value && (
|
{value && (
|
||||||
<Link href={updateParams({ [id]: `eq.${value}` })} className={styles.label} replace>
|
<Link href={updateParams({ [type]: `eq.${value}` })} replace>
|
||||||
{label || value}
|
{label || value}
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
{externalUrl && (
|
{externalUrl && showLink && (
|
||||||
<a className={styles.link} href={externalUrl} target="_blank" rel="noreferrer noopener">
|
<a href={externalUrl} target="_blank" rel="noreferrer noopener">
|
||||||
<Icon className={styles.icon}>
|
<Icon color="muted">
|
||||||
<ExternalLink />
|
<ExternalLink />
|
||||||
</Icon>
|
</Icon>
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
</div>
|
</Row>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ export function LoadingPanel({
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Show loading spinner only if no data exists */}
|
{/* Show loading spinner only if no data exists */}
|
||||||
{(isLoading || isFetching) && !data && (
|
{(isLoading || isFetching) && (
|
||||||
<Column position="relative" height="100%" {...props}>
|
<Column position="relative" height="100%" {...props}>
|
||||||
<Loading icon={loadingIcon} position="page" />
|
<Loading icon={loadingIcon} position="page" />
|
||||||
</Column>
|
</Column>
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,11 @@ export function PageHeader({
|
||||||
>
|
>
|
||||||
<Column>
|
<Column>
|
||||||
<Row alignItems="center" gap="3">
|
<Row alignItems="center" gap="3">
|
||||||
{icon && <Icon size="md">{icon}</Icon>}
|
{icon && (
|
||||||
|
<Icon size="md" color="muted">
|
||||||
|
{icon}
|
||||||
|
</Icon>
|
||||||
|
)}
|
||||||
{title && <Heading size="4">{title}</Heading>}
|
{title && <Heading size="4">{title}</Heading>}
|
||||||
</Row>
|
</Row>
|
||||||
{description && <Text color="muted">{description}</Text>}
|
{description && <Text color="muted">{description}</Text>}
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ export * from './queries/useWebsiteSegmentsQuery';
|
||||||
export * from './queries/useWebsiteSessionQuery';
|
export * from './queries/useWebsiteSessionQuery';
|
||||||
export * from './queries/useWebsiteSessionStatsQuery';
|
export * from './queries/useWebsiteSessionStatsQuery';
|
||||||
export * from './queries/useWebsiteSessionsQuery';
|
export * from './queries/useWebsiteSessionsQuery';
|
||||||
export * from './queries/useWebsiteSessionsWeeklyQuery';
|
export * from './queries/useWeeklyTrafficQuery';
|
||||||
export * from './queries/useWebsiteStatsQuery';
|
export * from './queries/useWebsiteStatsQuery';
|
||||||
export * from './queries/useWebsiteValuesQuery';
|
export * from './queries/useWebsiteValuesQuery';
|
||||||
export * from './queries/useWebsitesQuery';
|
export * from './queries/useWebsitesQuery';
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,7 @@ import { useModified } from '../useModified';
|
||||||
import { useDateParameters } from '../useDateParameters';
|
import { useDateParameters } from '../useDateParameters';
|
||||||
import { useFilterParameters } from '@/components/hooks/useFilterParameters';
|
import { useFilterParameters } from '@/components/hooks/useFilterParameters';
|
||||||
|
|
||||||
export function useWebsiteSessionsWeeklyQuery(
|
export function useWeeklyTrafficQuery(websiteId: string, params?: Record<string, string | number>) {
|
||||||
websiteId: string,
|
|
||||||
params?: Record<string, string | number>,
|
|
||||||
) {
|
|
||||||
const { get, useQuery } = useApi();
|
const { get, useQuery } = useApi();
|
||||||
const { modified } = useModified(`sessions`);
|
const { modified } = useModified(`sessions`);
|
||||||
const date = useDateParameters(websiteId);
|
const date = useDateParameters(websiteId);
|
||||||
|
|
@ -9,7 +9,12 @@ export function useRegionNames(locale: string) {
|
||||||
return regions[regionCode];
|
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;
|
return regions[region] ? `${regions[region]}, ${countryNames[countryCode]}` : region;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ export {
|
||||||
FileJson,
|
FileJson,
|
||||||
FileText,
|
FileText,
|
||||||
Globe,
|
Globe,
|
||||||
Grid2X2,
|
Grid2X2 as Pixel,
|
||||||
KeyRound,
|
KeyRound,
|
||||||
LayoutDashboard,
|
LayoutDashboard,
|
||||||
Link,
|
Link,
|
||||||
|
|
|
||||||
|
|
@ -78,16 +78,18 @@ export function FilterBar({ websiteId }: { websiteId: string }) {
|
||||||
</Row>
|
</Row>
|
||||||
<Row alignItems="center">
|
<Row alignItems="center">
|
||||||
<DialogTrigger>
|
<DialogTrigger>
|
||||||
<TooltipTrigger delay={0}>
|
{!!filters.length && (
|
||||||
<Button variant="zero">
|
<TooltipTrigger delay={0}>
|
||||||
<Icon>
|
<Button variant="zero">
|
||||||
<Bookmark />
|
<Icon>
|
||||||
</Icon>
|
<Bookmark />
|
||||||
</Button>
|
</Icon>
|
||||||
<Tooltip>
|
</Button>
|
||||||
<Text>{formatMessage(labels.saveSegment)}</Text>
|
<Tooltip>
|
||||||
</Tooltip>
|
<Text>{formatMessage(labels.saveSegment)}</Text>
|
||||||
</TooltipTrigger>
|
</Tooltip>
|
||||||
|
</TooltipTrigger>
|
||||||
|
)}
|
||||||
<Modal>
|
<Modal>
|
||||||
<Dialog title={formatMessage(labels.segment)} style={{ width: 400 }}>
|
<Dialog title={formatMessage(labels.segment)} style={{ width: 400 }}>
|
||||||
{({ close }) => {
|
{({ close }) => {
|
||||||
|
|
|
||||||
|
|
@ -91,8 +91,8 @@ export const labels = defineMessages({
|
||||||
refresh: { id: 'label.refresh', defaultMessage: 'Refresh' },
|
refresh: { id: 'label.refresh', defaultMessage: 'Refresh' },
|
||||||
page: { id: 'label.page', defaultMessage: 'Page' },
|
page: { id: 'label.page', defaultMessage: 'Page' },
|
||||||
pages: { id: 'label.pages', defaultMessage: 'Pages' },
|
pages: { id: 'label.pages', defaultMessage: 'Pages' },
|
||||||
entry: { id: 'label.entry', defaultMessage: 'Entry path' },
|
entry: { id: 'label.entry', defaultMessage: 'Entry' },
|
||||||
exit: { id: 'label.exit', defaultMessage: 'Exit path' },
|
exit: { id: 'label.exit', defaultMessage: 'Exit' },
|
||||||
referrers: { id: 'label.referrers', defaultMessage: 'Referrers' },
|
referrers: { id: 'label.referrers', defaultMessage: 'Referrers' },
|
||||||
screens: { id: 'label.screens', defaultMessage: 'Screens' },
|
screens: { id: 'label.screens', defaultMessage: 'Screens' },
|
||||||
browsers: { id: 'label.browsers', defaultMessage: 'Browsers' },
|
browsers: { id: 'label.browsers', defaultMessage: 'Browsers' },
|
||||||
|
|
@ -354,6 +354,8 @@ export const labels = defineMessages({
|
||||||
destinationUrl: { id: 'label.destination-url', defaultMessage: 'Destination URL' },
|
destinationUrl: { id: 'label.destination-url', defaultMessage: 'Destination URL' },
|
||||||
audience: { id: 'label.audience', defaultMessage: 'Audience' },
|
audience: { id: 'label.audience', defaultMessage: 'Audience' },
|
||||||
invalidUrl: { id: 'label.invalid-url', defaultMessage: 'Invalid URL' },
|
invalidUrl: { id: 'label.invalid-url', defaultMessage: 'Invalid URL' },
|
||||||
|
environment: { id: 'label.environment', defaultMessage: 'Environment' },
|
||||||
|
weekly: { id: 'label.weekly', defaultMessage: 'Weekly' },
|
||||||
});
|
});
|
||||||
|
|
||||||
export const messages = defineMessages({
|
export const messages = defineMessages({
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
|
||||||
<FilterLink id="browser" value={browser} label={formatBrowser(browser)}>
|
|
||||||
<TypeIcon type="browser" value={browser} />
|
|
||||||
</FilterLink>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<MetricsTable
|
|
||||||
{...props}
|
|
||||||
title={formatMessage(labels.browsers)}
|
|
||||||
type="browser"
|
|
||||||
metric={formatMessage(labels.visitors)}
|
|
||||||
renderLabel={renderLink}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -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 (
|
|
||||||
<MetricsTable
|
|
||||||
{...props}
|
|
||||||
title={formatMessage(labels.channels)}
|
|
||||||
type="channel"
|
|
||||||
renderLabel={renderLabel}
|
|
||||||
metric={formatMessage(labels.visitors)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -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 (
|
|
||||||
<FilterLink id="city" value={city} label={formatCity(city, country)}>
|
|
||||||
{country && (
|
|
||||||
<img
|
|
||||||
src={`${process.env.basePath || ''}/images/country/${
|
|
||||||
country?.toLowerCase() || 'xx'
|
|
||||||
}.png`}
|
|
||||||
alt={country}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</FilterLink>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<MetricsTable
|
|
||||||
{...props}
|
|
||||||
title={formatMessage(labels.cities)}
|
|
||||||
type="city"
|
|
||||||
metric={formatMessage(labels.visitors)}
|
|
||||||
dataFilter={emptyFilter}
|
|
||||||
renderLabel={renderLink}
|
|
||||||
searchFormattedValues={true}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -3,17 +3,22 @@ import { useCountryNames } from '@/components/hooks';
|
||||||
import { useLocale, useMessages, useFormat } from '@/components/hooks';
|
import { useLocale, useMessages, useFormat } from '@/components/hooks';
|
||||||
import { MetricsTable, MetricsTableProps } from './MetricsTable';
|
import { MetricsTable, MetricsTableProps } from './MetricsTable';
|
||||||
import { TypeIcon } from '@/components/common/TypeIcon';
|
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 { locale } = useLocale();
|
||||||
const { countryNames } = useCountryNames(locale);
|
const { countryNames } = useCountryNames(locale);
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
const { formatCountry } = useFormat();
|
const { formatCountry } = useFormat();
|
||||||
|
|
||||||
const renderLink = ({ x: code }) => {
|
const renderLabel = ({ label: code }) => {
|
||||||
return (
|
return (
|
||||||
<FilterLink
|
<FilterLink
|
||||||
id="country"
|
type="country"
|
||||||
value={(countryNames[code] && code) || code}
|
value={(countryNames[code] && code) || code}
|
||||||
label={formatCountry(code)}
|
label={formatCountry(code)}
|
||||||
>
|
>
|
||||||
|
|
@ -22,13 +27,15 @@ export function CountriesTable({ ...props }: MetricsTableProps) {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const Component = isExpanded ? MetricsExpandedTable : MetricsTable;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MetricsTable
|
<Component
|
||||||
{...props}
|
{...props}
|
||||||
title={formatMessage(labels.countries)}
|
title={formatMessage(labels.countries)}
|
||||||
type="country"
|
type="country"
|
||||||
metric={formatMessage(labels.visitors)}
|
metric={formatMessage(labels.visitors)}
|
||||||
renderLabel={renderLink}
|
renderLabel={renderLabel}
|
||||||
searchFormattedValues={true}
|
searchFormattedValues={true}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
|
||||||
<FilterLink id="device" value={labels[device] && device} label={formatDevice(device)}>
|
|
||||||
<TypeIcon type="device" value={device?.toLowerCase()} />
|
|
||||||
</FilterLink>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<MetricsTable
|
|
||||||
{...props}
|
|
||||||
title={formatMessage(labels.devices)}
|
|
||||||
type="device"
|
|
||||||
metric={formatMessage(labels.visitors)}
|
|
||||||
renderLabel={renderLink}
|
|
||||||
searchFormattedValues={true}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -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 (
|
|
||||||
<div onClick={() => onLabelClick(label)} style={{ cursor: 'pointer' }}>
|
|
||||||
{label}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return label;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<MetricsTable
|
|
||||||
{...props}
|
|
||||||
title={formatMessage(labels.events)}
|
|
||||||
type="event"
|
|
||||||
metric={formatMessage(labels.actions)}
|
|
||||||
renderLabel={renderLabel}
|
|
||||||
allowDownload={false}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -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 (
|
|
||||||
<Flexbox alignItems="center">
|
|
||||||
<FilterLink
|
|
||||||
id="hostname"
|
|
||||||
value={hostname}
|
|
||||||
externalUrl={`https://${hostname}`}
|
|
||||||
label={!hostname && formatMessage(labels.none)}
|
|
||||||
/>
|
|
||||||
</Flexbox>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<MetricsTable
|
|
||||||
{...props}
|
|
||||||
title={formatMessage(labels.hostname)}
|
|
||||||
type="hostname"
|
|
||||||
metric={formatMessage(labels.visitors)}
|
|
||||||
renderLabel={renderLink}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -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 <div className={locale}>{formatLanguage(x)}</div>;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<MetricsTable
|
|
||||||
{...props}
|
|
||||||
title={formatMessage(labels.languages)}
|
|
||||||
type="language"
|
|
||||||
metric={formatMessage(labels.visitors)}
|
|
||||||
renderLabel={renderLabel}
|
|
||||||
searchFormattedValues={true}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
.truncate {
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
width: 300px;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
@ -2,7 +2,6 @@ import { useMessages } from '@/components/hooks';
|
||||||
import { formatShortTime } from '@/lib/format';
|
import { formatShortTime } from '@/lib/format';
|
||||||
import { DataColumn, DataTable } from '@umami/react-zen';
|
import { DataColumn, DataTable } from '@umami/react-zen';
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
import styles from './ListExpandedTable.module.css';
|
|
||||||
|
|
||||||
export interface ListExpandedTableProps {
|
export interface ListExpandedTableProps {
|
||||||
data?: any[];
|
data?: any[];
|
||||||
|
|
@ -15,7 +14,7 @@ export function ListExpandedTable({ data = [], title, renderLabel }: ListExpande
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DataTable data={data}>
|
<DataTable data={data}>
|
||||||
<DataColumn id="label" label={title} align="start" className={styles.truncate}>
|
<DataColumn id="label" label={title} align="start">
|
||||||
{row =>
|
{row =>
|
||||||
renderLabel
|
renderLabel
|
||||||
? renderLabel({ x: row?.['name'], country: row?.['country'] }, Number(row.id))
|
? renderLabel({ x: row?.['name'], country: row?.['country'] }, Number(row.id))
|
||||||
|
|
|
||||||
|
|
@ -9,13 +9,19 @@ import { formatLongCurrency, formatLongNumber } from '@/lib/format';
|
||||||
|
|
||||||
const ITEM_SIZE = 30;
|
const ITEM_SIZE = 30;
|
||||||
|
|
||||||
|
interface ListData {
|
||||||
|
label: string;
|
||||||
|
count: number;
|
||||||
|
percent: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ListTableProps {
|
export interface ListTableProps {
|
||||||
data?: any[];
|
data?: ListData[];
|
||||||
title?: string;
|
title?: string;
|
||||||
metric?: string;
|
metric?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
renderLabel?: (row: any, index: number) => ReactNode;
|
renderLabel?: (data: ListData, index: number) => ReactNode;
|
||||||
renderChange?: (row: any, index: number) => ReactNode;
|
renderChange?: (data: ListData, index: number) => ReactNode;
|
||||||
animate?: boolean;
|
animate?: boolean;
|
||||||
virtualize?: boolean;
|
virtualize?: boolean;
|
||||||
showPercentage?: boolean;
|
showPercentage?: boolean;
|
||||||
|
|
@ -37,14 +43,14 @@ export function ListTable({
|
||||||
}: ListTableProps) {
|
}: ListTableProps) {
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
|
|
||||||
const getRow = (row: { x: any; y: any; z: any }, index: number) => {
|
const getRow = (row: ListData, index: number) => {
|
||||||
const { x: label, y: value, z: percent } = row || {};
|
const { label, count, percent } = row;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AnimatedRow
|
<AnimatedRow
|
||||||
key={label}
|
key={`${label}${index}`}
|
||||||
label={renderLabel ? renderLabel(row, index) : (label ?? formatMessage(labels.unknown))}
|
label={renderLabel ? renderLabel(row, index) : (label ?? formatMessage(labels.unknown))}
|
||||||
value={value}
|
value={count}
|
||||||
percent={percent}
|
percent={percent}
|
||||||
animate={animate && !virtualize}
|
animate={animate && !virtualize}
|
||||||
showPercentage={showPercentage}
|
showPercentage={showPercentage}
|
||||||
|
|
|
||||||
149
src/components/metrics/MetricLabel.tsx
Normal file
149
src/components/metrics/MetricLabel.tsx
Normal file
|
|
@ -0,0 +1,149 @@
|
||||||
|
import { Row } from '@umami/react-zen';
|
||||||
|
import {
|
||||||
|
useCountryNames,
|
||||||
|
useLocale,
|
||||||
|
useMessages,
|
||||||
|
useRegionNames,
|
||||||
|
useFormat,
|
||||||
|
} from '@/components/hooks';
|
||||||
|
import { FilterLink } from '@/components/common/FilterLink';
|
||||||
|
import { TypeIcon } from '@/components/common/TypeIcon';
|
||||||
|
import { Favicon } from '@/components/common/Favicon';
|
||||||
|
import { GROUPED_DOMAINS } from '@/lib/constants';
|
||||||
|
|
||||||
|
export interface MetricLabelProps {
|
||||||
|
type: string;
|
||||||
|
data: any;
|
||||||
|
onClick?: () => 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 (
|
||||||
|
<FilterLink
|
||||||
|
type="browser"
|
||||||
|
value={label}
|
||||||
|
label={formatValue(label, 'browser')}
|
||||||
|
icon={<TypeIcon type="browser" value={label} />}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'channel':
|
||||||
|
return formatMessage(labels[label]);
|
||||||
|
|
||||||
|
case 'city':
|
||||||
|
return (
|
||||||
|
<FilterLink
|
||||||
|
type="city"
|
||||||
|
value={label}
|
||||||
|
label={formatCity(label, country)}
|
||||||
|
icon={
|
||||||
|
country && (
|
||||||
|
<img
|
||||||
|
src={`${process.env.basePath || ''}/images/country/${
|
||||||
|
country?.toLowerCase() || 'xx'
|
||||||
|
}.png`}
|
||||||
|
alt={country}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'region':
|
||||||
|
return (
|
||||||
|
<FilterLink
|
||||||
|
type="region"
|
||||||
|
value={label}
|
||||||
|
label={getRegionName(label, country)}
|
||||||
|
icon={<TypeIcon type="country" value={country} />}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'country':
|
||||||
|
return (
|
||||||
|
<FilterLink
|
||||||
|
type="country"
|
||||||
|
value={(countryNames[label] && label) || label}
|
||||||
|
label={formatValue(label, 'country')}
|
||||||
|
icon={<TypeIcon type="country" value={label} />}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'path':
|
||||||
|
case 'entry':
|
||||||
|
case 'exit':
|
||||||
|
return (
|
||||||
|
<FilterLink
|
||||||
|
type={type === 'entry' || type === 'exit' ? 'path' : type}
|
||||||
|
value={label}
|
||||||
|
label={!label && formatMessage(labels.none)}
|
||||||
|
externalUrl={
|
||||||
|
domain ? `${domain?.startsWith('http') ? domain : `https://${domain}`}${label}` : null
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'device':
|
||||||
|
return (
|
||||||
|
<FilterLink
|
||||||
|
type="device"
|
||||||
|
value={labels[label] && label}
|
||||||
|
label={formatValue(label, 'device')}
|
||||||
|
icon={<TypeIcon type="device" value={label?.toLowerCase()} />}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'referrer':
|
||||||
|
return (
|
||||||
|
<FilterLink
|
||||||
|
type="referrer"
|
||||||
|
value={label}
|
||||||
|
externalUrl={`https://${label}`}
|
||||||
|
label={!label && formatMessage(labels.none)}
|
||||||
|
icon={<Favicon domain={label} />}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'grouped':
|
||||||
|
if (label === 'Other') {
|
||||||
|
return `(${formatMessage(labels.other)})`;
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<Row alignItems="center" gap="3">
|
||||||
|
<Favicon domain={label} />
|
||||||
|
{GROUPED_DOMAINS.find(({ domain }) => domain === label)?.name}
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'language':
|
||||||
|
return formatValue(label, 'language');
|
||||||
|
|
||||||
|
default:
|
||||||
|
return (
|
||||||
|
<FilterLink
|
||||||
|
type={type}
|
||||||
|
value={label}
|
||||||
|
icon={
|
||||||
|
isType && (
|
||||||
|
<TypeIcon
|
||||||
|
type={type as 'browser' | 'country' | 'device' | 'os'}
|
||||||
|
value={label?.toLowerCase()?.replaceAll(/\W/g, '-')}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
106
src/components/metrics/MetricsExpandedTable.tsx
Normal file
106
src/components/metrics/MetricsExpandedTable.tsx
Normal file
|
|
@ -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 (
|
||||||
|
<>
|
||||||
|
<Row alignItems="center" paddingBottom="3">
|
||||||
|
{allowSearch && <SearchField value={search} onSearch={setSearch} delay={300} />}
|
||||||
|
<Row justifyContent="flex-end" flexGrow={1} gap>
|
||||||
|
{children}
|
||||||
|
{allowDownload && <DownloadButton filename={type} data={data} />}
|
||||||
|
{onClose && (
|
||||||
|
<Button onPress={onClose} variant="quiet">
|
||||||
|
<Icon>
|
||||||
|
<Close />
|
||||||
|
</Icon>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Row>
|
||||||
|
</Row>
|
||||||
|
<LoadingPanel
|
||||||
|
data={data}
|
||||||
|
isFetching={isFetching}
|
||||||
|
isLoading={isLoading}
|
||||||
|
error={error}
|
||||||
|
height="100%"
|
||||||
|
>
|
||||||
|
<Column overflowY="auto" minHeight="0" height="100%" paddingRight="3" overflow="hidden">
|
||||||
|
{items && (
|
||||||
|
<DataTable data={items}>
|
||||||
|
<DataColumn id="label" label={title} width="2fr" align="start">
|
||||||
|
{row => <MetricLabel type={type} data={row} />}
|
||||||
|
</DataColumn>
|
||||||
|
<DataColumn id="visitors" label={formatMessage(labels.visitors)} align="end">
|
||||||
|
{row => row?.['visitors']?.toLocaleString()}
|
||||||
|
</DataColumn>
|
||||||
|
<DataColumn id="visits" label={formatMessage(labels.visits)} align="end">
|
||||||
|
{row => row?.['visits']?.toLocaleString()}
|
||||||
|
</DataColumn>
|
||||||
|
<DataColumn id="pageviews" label={formatMessage(labels.views)} align="end">
|
||||||
|
{row => row?.['pageviews']?.toLocaleString()}
|
||||||
|
</DataColumn>
|
||||||
|
<DataColumn id="bounceRate" label={formatMessage(labels.bounceRate)} align="end">
|
||||||
|
{row => {
|
||||||
|
const n = (Math.min(row?.['visits'], row?.['bounces']) / row?.['visits']) * 100;
|
||||||
|
return Math.round(+n) + '%';
|
||||||
|
}}
|
||||||
|
</DataColumn>
|
||||||
|
<DataColumn
|
||||||
|
id="visitDuration"
|
||||||
|
label={formatMessage(labels.visitDuration)}
|
||||||
|
align="end"
|
||||||
|
>
|
||||||
|
{row => {
|
||||||
|
const n = (row?.['totaltime'] / row?.['visits']) * 100;
|
||||||
|
return `${+n < 0 ? '-' : ''}${formatShortTime(Math.abs(~~n), ['m', 's'], ' ')}`;
|
||||||
|
}}
|
||||||
|
</DataColumn>
|
||||||
|
</DataTable>
|
||||||
|
)}
|
||||||
|
</Column>
|
||||||
|
</LoadingPanel>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,37 +1,20 @@
|
||||||
import { ReactNode, useMemo, useState } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { Button, Column, Icon, Row, SearchField, Text } from '@umami/react-zen';
|
import { Icon, Row, Text } from '@umami/react-zen';
|
||||||
import { LinkButton } from '@/components/common/LinkButton';
|
import { LinkButton } from '@/components/common/LinkButton';
|
||||||
import { LoadingPanel } from '@/components/common/LoadingPanel';
|
import { LoadingPanel } from '@/components/common/LoadingPanel';
|
||||||
import {
|
import { useMessages, useNavigation, useWebsiteMetricsQuery } from '@/components/hooks';
|
||||||
useFormat,
|
import { Maximize } from '@/components/icons';
|
||||||
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 { percentFilter } from '@/lib/filters';
|
import { percentFilter } from '@/lib/filters';
|
||||||
|
|
||||||
import { ListExpandedTable, ListExpandedTableProps } from './ListExpandedTable';
|
|
||||||
import { ListTable, ListTableProps } from './ListTable';
|
import { ListTable, ListTableProps } from './ListTable';
|
||||||
|
import { MetricLabel } from '@/components/metrics/MetricLabel';
|
||||||
|
|
||||||
export interface MetricsTableProps extends ListTableProps {
|
export interface MetricsTableProps extends ListTableProps {
|
||||||
websiteId: string;
|
websiteId: string;
|
||||||
type?: string;
|
type: string;
|
||||||
dataFilter?: (data: any) => any;
|
dataFilter?: (data: any) => any;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
delay?: number;
|
|
||||||
onSearch?: (search: string) => void;
|
|
||||||
allowSearch?: boolean;
|
|
||||||
searchFormattedValues?: boolean;
|
|
||||||
showMore?: boolean;
|
showMore?: boolean;
|
||||||
params?: { [key: string]: any };
|
params?: Record<string, any>;
|
||||||
allowDownload?: boolean;
|
|
||||||
isExpanded?: boolean;
|
|
||||||
onClose?: () => void;
|
|
||||||
children?: ReactNode;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MetricsTable({
|
export function MetricsTable({
|
||||||
|
|
@ -39,50 +22,17 @@ export function MetricsTable({
|
||||||
type,
|
type,
|
||||||
dataFilter,
|
dataFilter,
|
||||||
limit,
|
limit,
|
||||||
delay = null,
|
showMore = false,
|
||||||
allowSearch = false,
|
|
||||||
searchFormattedValues = false,
|
|
||||||
showMore = true,
|
|
||||||
params,
|
params,
|
||||||
allowDownload = true,
|
|
||||||
isExpanded = false,
|
|
||||||
onClose,
|
|
||||||
children,
|
|
||||||
...props
|
...props
|
||||||
}: MetricsTableProps) {
|
}: MetricsTableProps) {
|
||||||
const [search, setSearch] = useState('');
|
|
||||||
const { formatValue } = useFormat();
|
|
||||||
const { updateParams } = useNavigation();
|
const { updateParams } = useNavigation();
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
|
const { data, isLoading, isFetching, error } = useWebsiteMetricsQuery(websiteId, {
|
||||||
const expandedQuery = useWebsiteExpandedMetricsQuery(
|
type,
|
||||||
websiteId,
|
limit,
|
||||||
{
|
...params,
|
||||||
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 filteredData = useMemo(() => {
|
const filteredData = useMemo(() => {
|
||||||
if (data) {
|
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);
|
items = percentFilter(items);
|
||||||
|
|
||||||
return items;
|
return items.map(({ x, y, z, ...props }) => ({ label: x, count: y, percent: z, ...props }));
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
}, [data, dataFilter, search, limit, formatValue, type]);
|
}, [data, dataFilter, limit, type]);
|
||||||
|
|
||||||
const downloadData = isExpanded ? data : filteredData;
|
const renderLabel = (data: any) => {
|
||||||
const hasActions = data && (allowSearch || allowDownload || onClose || children);
|
return <MetricLabel type={type} data={data} />;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LoadingPanel
|
<LoadingPanel
|
||||||
|
|
@ -124,37 +67,7 @@ export function MetricsTable({
|
||||||
error={error}
|
error={error}
|
||||||
height="100%"
|
height="100%"
|
||||||
>
|
>
|
||||||
{hasActions && (
|
{data && <ListTable {...props} data={filteredData} renderLabel={renderLabel} />}
|
||||||
<Row alignItems="center" paddingBottom="3">
|
|
||||||
{allowSearch && <SearchField value={search} onSearch={setSearch} delay={300} />}
|
|
||||||
<Row justifyContent="flex-end" flexGrow={1} gap>
|
|
||||||
{children}
|
|
||||||
{allowDownload && <DownloadButton filename={type} data={downloadData} />}
|
|
||||||
{onClose && (
|
|
||||||
<Button onPress={onClose} variant="quiet">
|
|
||||||
<Icon>
|
|
||||||
<Close />
|
|
||||||
</Icon>
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</Row>
|
|
||||||
</Row>
|
|
||||||
)}
|
|
||||||
<Column
|
|
||||||
overflowY="auto"
|
|
||||||
minHeight="0"
|
|
||||||
height="100%"
|
|
||||||
paddingRight="3"
|
|
||||||
overflow="hidden"
|
|
||||||
flexGrow={1}
|
|
||||||
>
|
|
||||||
{data &&
|
|
||||||
(isExpanded ? (
|
|
||||||
<ListExpandedTable {...(props as ListExpandedTableProps)} data={data} />
|
|
||||||
) : (
|
|
||||||
<ListTable {...(props as ListTableProps)} data={filteredData} />
|
|
||||||
))}
|
|
||||||
</Column>
|
|
||||||
{showMore && limit && (
|
{showMore && limit && (
|
||||||
<Row justifyContent="center">
|
<Row justifyContent="center">
|
||||||
<LinkButton href={updateParams({ view: type })} variant="quiet">
|
<LinkButton href={updateParams({ view: type })} variant="quiet">
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
|
||||||
<FilterLink id="os" value={os} label={formatOS(os)}>
|
|
||||||
<TypeIcon type="os" value={os?.toLowerCase()?.replaceAll(/\W/g, '-')} />
|
|
||||||
</FilterLink>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<MetricsTable
|
|
||||||
{...props}
|
|
||||||
type="os"
|
|
||||||
title={formatMessage(labels.os)}
|
|
||||||
metric={formatMessage(labels.visitors)}
|
|
||||||
renderLabel={renderLink}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -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 (
|
|
||||||
<FilterLink
|
|
||||||
id={type === 'entry' || type === 'exit' ? 'path' : type}
|
|
||||||
value={x}
|
|
||||||
label={!x && formatMessage(labels.none)}
|
|
||||||
externalUrl={
|
|
||||||
type !== 'title'
|
|
||||||
? `${domain.startsWith('http') ? domain : `https://${domain}`}${x}`
|
|
||||||
: null
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<MetricsTable
|
|
||||||
{...props}
|
|
||||||
title={formatMessage(labels.pages)}
|
|
||||||
type={type}
|
|
||||||
metric={formatMessage(labels.visitors)}
|
|
||||||
dataFilter={emptyFilter}
|
|
||||||
renderLabel={renderLink}
|
|
||||||
>
|
|
||||||
{allowFilter && <FilterButtons items={buttons} value={type} onChange={handleChange} />}
|
|
||||||
</MetricsTable>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
@ -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 (
|
|
||||||
<Row alignItems="center" maxWidth="600px" gap>
|
|
||||||
{filter === FILTER_RAW ? (
|
|
||||||
<Text truncate title={x}>
|
|
||||||
{x}
|
|
||||||
</Text>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Text color="primary" weight="bold">
|
|
||||||
{p}
|
|
||||||
</Text>
|
|
||||||
<Text truncate title={v}>
|
|
||||||
{v}
|
|
||||||
</Text>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Row>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<MetricsTable
|
|
||||||
{...props}
|
|
||||||
title={formatMessage(labels.query)}
|
|
||||||
type="query"
|
|
||||||
metric={formatMessage(labels.views)}
|
|
||||||
dataFilter={filters[filter]}
|
|
||||||
renderLabel={renderLabel}
|
|
||||||
delay={0}
|
|
||||||
isExpanded={false}
|
|
||||||
>
|
|
||||||
{allowFilter && <FilterButtons items={buttons} value={filter} onChange={setFilter} />}
|
|
||||||
</MetricsTable>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -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 (
|
|
||||||
<Row alignItems="center" gap="3">
|
|
||||||
<Favicon domain={referrer} />
|
|
||||||
{GROUPED_DOMAINS.find(({ domain }) => domain === referrer)?.name}
|
|
||||||
</Row>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FilterLink
|
|
||||||
id="referrer"
|
|
||||||
value={referrer}
|
|
||||||
externalUrl={`https://${referrer}`}
|
|
||||||
label={!referrer && formatMessage(labels.none)}
|
|
||||||
>
|
|
||||||
<Favicon domain={referrer} />
|
|
||||||
</FilterLink>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<MetricsTable
|
|
||||||
{...props}
|
|
||||||
title={formatMessage(labels.referrers)}
|
|
||||||
type={view}
|
|
||||||
metric={formatMessage(labels.visitors)}
|
|
||||||
dataFilter={emptyFilter}
|
|
||||||
renderLabel={renderLink}
|
|
||||||
>
|
|
||||||
{allowFilter && <FilterButtons items={buttons} value={view} onChange={handleSelect} />}
|
|
||||||
</MetricsTable>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -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 (
|
|
||||||
<FilterLink id="region" value={code} label={getRegionName(code, country)}>
|
|
||||||
<TypeIcon type="country" value={country?.toLowerCase()} />
|
|
||||||
</FilterLink>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<MetricsTable
|
|
||||||
{...props}
|
|
||||||
title={formatMessage(labels.regions)}
|
|
||||||
type="region"
|
|
||||||
metric={formatMessage(labels.visitors)}
|
|
||||||
dataFilter={emptyFilter}
|
|
||||||
renderLabel={renderLink}
|
|
||||||
searchFormattedValues={true}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
import { MetricsTable, MetricsTableProps } from './MetricsTable';
|
|
||||||
import { useMessages } from '@/components/hooks';
|
|
||||||
|
|
||||||
export function ScreenTable(props: MetricsTableProps) {
|
|
||||||
const { formatMessage, labels } = useMessages();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<MetricsTable
|
|
||||||
{...props}
|
|
||||||
title={formatMessage(labels.screens)}
|
|
||||||
type="screen"
|
|
||||||
metric={formatMessage(labels.visitors)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -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 (
|
|
||||||
<Flexbox alignItems="center">
|
|
||||||
<FilterLink id="tag" value={tag} label={!tag && formatMessage(labels.none)} />
|
|
||||||
</Flexbox>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<MetricsTable
|
|
||||||
{...props}
|
|
||||||
title={formatMessage(labels.tags)}
|
|
||||||
type="tag"
|
|
||||||
metric={formatMessage(labels.views)}
|
|
||||||
renderLabel={renderLink}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
import { Row, Grid, Text } from '@umami/react-zen';
|
import { Row, Grid, Text } from '@umami/react-zen';
|
||||||
import { format, startOfDay, addHours } from 'date-fns';
|
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 { LoadingPanel } from '@/components/common/LoadingPanel';
|
||||||
import { getDayOfWeekAsDate } from '@/lib/date';
|
import { getDayOfWeekAsDate } from '@/lib/date';
|
||||||
import { Focusable, Tooltip, TooltipTrigger } from '@umami/react-zen';
|
import { Focusable, Tooltip, TooltipTrigger } from '@umami/react-zen';
|
||||||
|
|
||||||
export function SessionsWeekly({ websiteId }: { websiteId: string }) {
|
export function WeeklyTraffic({ websiteId }: { websiteId: string }) {
|
||||||
const { data, isLoading, error } = useWebsiteSessionsWeeklyQuery(websiteId);
|
const { data, isLoading, error } = useWeeklyTrafficQuery(websiteId);
|
||||||
const { dateLocale } = useLocale();
|
const { dateLocale } = useLocale();
|
||||||
const { labels, formatMessage } = useMessages();
|
const { labels, formatMessage } = useMessages();
|
||||||
const { weekStartsOn } = dateLocale.options;
|
const { weekStartsOn } = dateLocale.options;
|
||||||
|
|
@ -40,19 +40,19 @@ export function SessionsWeekly({ websiteId }: { websiteId: string }) {
|
||||||
<Grid columns="repeat(8, 1fr)" gap>
|
<Grid columns="repeat(8, 1fr)" gap>
|
||||||
{data && (
|
{data && (
|
||||||
<>
|
<>
|
||||||
<Grid rows="repeat(25, 20px)" gap="1">
|
<Grid rows="repeat(25, 16px)" gap="1">
|
||||||
<Row> </Row>
|
<Row> </Row>
|
||||||
{Array(24)
|
{Array(24)
|
||||||
.fill(null)
|
.fill(null)
|
||||||
.map((_, i) => {
|
.map((_, i) => {
|
||||||
const label = format(addHours(startOfDay(new Date()), i), 'p', {
|
const label = format(addHours(startOfDay(new Date()), i), 'haaa', {
|
||||||
locale: dateLocale,
|
locale: dateLocale,
|
||||||
})
|
});
|
||||||
.replace(/\D00 ?/, '')
|
|
||||||
.toLowerCase();
|
|
||||||
return (
|
return (
|
||||||
<Row key={i} justifyContent="flex-end">
|
<Row key={i} justifyContent="flex-end">
|
||||||
<Text color="muted">{label}</Text>
|
<Text color="muted" size="2">
|
||||||
|
{label}
|
||||||
|
</Text>
|
||||||
</Row>
|
</Row>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
@ -61,7 +61,7 @@ export function SessionsWeekly({ websiteId }: { websiteId: string }) {
|
||||||
const day = data[index];
|
const day = data[index];
|
||||||
return (
|
return (
|
||||||
<Grid
|
<Grid
|
||||||
rows="repeat(24, 20px)"
|
rows="repeat(24, 16px)"
|
||||||
justifyContent="center"
|
justifyContent="center"
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
key={index}
|
key={index}
|
||||||
|
|
@ -81,15 +81,15 @@ export function SessionsWeekly({ websiteId }: { websiteId: string }) {
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
justifyContent="center"
|
justifyContent="center"
|
||||||
backgroundColor="2"
|
backgroundColor="2"
|
||||||
width="20px"
|
width="16px"
|
||||||
height="20px"
|
height="16px"
|
||||||
borderRadius="full"
|
borderRadius="full"
|
||||||
style={{ margin: '0 auto' }}
|
style={{ margin: '0 auto' }}
|
||||||
>
|
>
|
||||||
<Row
|
<Row
|
||||||
backgroundColor="primary"
|
backgroundColor="primary"
|
||||||
width="20px"
|
width="16px"
|
||||||
height="20px"
|
height="16px"
|
||||||
borderRadius="full"
|
borderRadius="full"
|
||||||
style={{ opacity: pct, transform: `scale(${pct})` }}
|
style={{ opacity: pct, transform: `scale(${pct})` }}
|
||||||
/>
|
/>
|
||||||
|
|
@ -86,13 +86,13 @@
|
||||||
"label.email": "Email",
|
"label.email": "Email",
|
||||||
"label.enable-share-url": "Enable share URL",
|
"label.enable-share-url": "Enable share URL",
|
||||||
"label.end-step": "End Step",
|
"label.end-step": "End Step",
|
||||||
"label.entry": "Entry path",
|
"label.entry": "Entry page",
|
||||||
"label.event": "Event",
|
"label.event": "Event",
|
||||||
"label.event-data": "Event data",
|
"label.event-data": "Event data",
|
||||||
"label.event-name": "Event name",
|
"label.event-name": "Event name",
|
||||||
"label.events": "Events",
|
"label.events": "Events",
|
||||||
"label.exists": "Exists",
|
"label.exists": "Exists",
|
||||||
"label.exit": "Exit path",
|
"label.exit": "Exit page",
|
||||||
"label.false": "False",
|
"label.false": "False",
|
||||||
"label.field": "Field",
|
"label.field": "Field",
|
||||||
"label.fields": "Fields",
|
"label.fields": "Fields",
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,6 @@ export * from '@/queries/sql/sessions/getSessionMetrics';
|
||||||
export * from '@/queries/sql/sessions/getSessionExpandedMetrics';
|
export * from '@/queries/sql/sessions/getSessionExpandedMetrics';
|
||||||
export * from '@/queries/sql/sessions/getWebsiteSessions';
|
export * from '@/queries/sql/sessions/getWebsiteSessions';
|
||||||
export * from '@/queries/sql/sessions/getWebsiteSessionStats';
|
export * from '@/queries/sql/sessions/getWebsiteSessionStats';
|
||||||
export * from '@/queries/sql/sessions/getWebsiteSessionsWeekly';
|
|
||||||
export * from '@/queries/sql/sessions/getSessionActivity';
|
export * from '@/queries/sql/sessions/getSessionActivity';
|
||||||
export * from '@/queries/sql/sessions/getSessionStats';
|
export * from '@/queries/sql/sessions/getSessionStats';
|
||||||
export * from '@/queries/sql/sessions/saveSessionData';
|
export * from '@/queries/sql/sessions/saveSessionData';
|
||||||
|
|
@ -47,3 +46,4 @@ export * from '@/queries/sql/getRealtimeData';
|
||||||
export * from '@/queries/sql/getValues';
|
export * from '@/queries/sql/getValues';
|
||||||
export * from '@/queries/sql/getWebsiteDateRange';
|
export * from '@/queries/sql/getWebsiteDateRange';
|
||||||
export * from '@/queries/sql/getWebsiteStats';
|
export * from '@/queries/sql/getWebsiteStats';
|
||||||
|
export * from '@/queries/sql/getWeeklyTraffic';
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,7 @@ import { runQuery, PRISMA, CLICKHOUSE } from '@/lib/db';
|
||||||
import { QueryFilters } from '@/lib/types';
|
import { QueryFilters } from '@/lib/types';
|
||||||
import { EVENT_COLUMNS } from '@/lib/constants';
|
import { EVENT_COLUMNS } from '@/lib/constants';
|
||||||
|
|
||||||
export async function getWebsiteSessionsWeekly(
|
export async function getWeeklyTraffic(...args: [websiteId: string, filters: QueryFilters]) {
|
||||||
...args: [websiteId: string, filters: QueryFilters]
|
|
||||||
) {
|
|
||||||
return runQuery({
|
return runQuery({
|
||||||
[PRISMA]: () => relationalQuery(...args),
|
[PRISMA]: () => relationalQuery(...args),
|
||||||
[CLICKHOUSE]: () => clickhouseQuery(...args),
|
[CLICKHOUSE]: () => clickhouseQuery(...args),
|
||||||
Loading…
Add table
Add a link
Reference in a new issue