mirror of
https://github.com/umami-software/umami.git
synced 2026-02-10 23:57:12 +01:00
Pixel/link metrics pages.
This commit is contained in:
parent
789b8b36d8
commit
8e766e2db7
42 changed files with 530 additions and 49 deletions
|
|
@ -31,8 +31,8 @@ export function PixelEditForm({
|
|||
onSave?: () => void;
|
||||
onClose?: () => void;
|
||||
}) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { mutate, error, isPending, touch } = useUpdateQuery(
|
||||
const { formatMessage, labels, messages } = useMessages();
|
||||
const { mutate, error, isPending, touch, toast } = useUpdateQuery(
|
||||
pixelId ? `/pixels/${pixelId}` : '/pixels',
|
||||
{
|
||||
id: pixelId,
|
||||
|
|
@ -47,6 +47,7 @@ export function PixelEditForm({
|
|||
const handleSubmit = async (data: any) => {
|
||||
mutate(data, {
|
||||
onSuccess: async () => {
|
||||
toast(formatMessage(messages.saved));
|
||||
touch('pixels');
|
||||
onSave?.();
|
||||
onClose?.();
|
||||
|
|
|
|||
20
src/app/(main)/pixels/PixelProvider.tsx
Normal file
20
src/app/(main)/pixels/PixelProvider.tsx
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
'use client';
|
||||
import { createContext, ReactNode } from 'react';
|
||||
import { usePixelQuery } from '@/components/hooks';
|
||||
import { Loading } from '@umami/react-zen';
|
||||
|
||||
export const PixelContext = createContext(null);
|
||||
|
||||
export function PixelProvider({ pixelId, children }: { pixelId?: string; children: ReactNode }) {
|
||||
const { data: pixel, isLoading, isFetching } = usePixelQuery(pixelId);
|
||||
|
||||
if (isFetching && isLoading) {
|
||||
return <Loading position="page" />;
|
||||
}
|
||||
|
||||
if (!pixel) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <PixelContext.Provider value={pixel}>{children}</PixelContext.Provider>;
|
||||
}
|
||||
|
|
@ -1,16 +1,16 @@
|
|||
import Link from 'next/link';
|
||||
import { DataTable, DataColumn, Row } from '@umami/react-zen';
|
||||
import { useConfig, useMessages } from '@/components/hooks';
|
||||
import { useMessages, useNavigation, useSlug } from '@/components/hooks';
|
||||
import { Empty } from '@/components/common/Empty';
|
||||
import { DateDistance } from '@/components/common/DateDistance';
|
||||
import { PixelEditButton } from './PixelEditButton';
|
||||
import { PixelDeleteButton } from './PixelDeleteButton';
|
||||
import { PIXELS_URL } from '@/lib/constants';
|
||||
import { ExternalLink } from '@/components/common/ExternalLink';
|
||||
|
||||
export function PixelsTable({ data = [] }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { pixelsUrl } = useConfig();
|
||||
const hostUrl = pixelsUrl || PIXELS_URL;
|
||||
const { renderUrl } = useNavigation();
|
||||
const { getSlugUrl } = useSlug('pixel');
|
||||
|
||||
if (data.length === 0) {
|
||||
return <Empty />;
|
||||
|
|
@ -18,10 +18,14 @@ export function PixelsTable({ data = [] }) {
|
|||
|
||||
return (
|
||||
<DataTable data={data}>
|
||||
<DataColumn id="name" label={formatMessage(labels.name)} />
|
||||
<DataColumn id="name" label={formatMessage(labels.name)}>
|
||||
{({ id, name }: any) => {
|
||||
return <Link href={renderUrl(`/pixels/${id}`)}>{name}</Link>;
|
||||
}}
|
||||
</DataColumn>
|
||||
<DataColumn id="url" label="URL">
|
||||
{({ slug }: any) => {
|
||||
const url = `${hostUrl}/${slug}`;
|
||||
const url = getSlugUrl(slug);
|
||||
return <ExternalLink href={url}>{url}</ExternalLink>;
|
||||
}}
|
||||
</DataColumn>
|
||||
|
|
|
|||
34
src/app/(main)/pixels/[pixelId]/PixelControls.tsx
Normal file
34
src/app/(main)/pixels/[pixelId]/PixelControls.tsx
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import { Column, Row } from '@umami/react-zen';
|
||||
import { WebsiteFilterButton } from '@/app/(main)/websites/[websiteId]/WebsiteFilterButton';
|
||||
import { WebsiteDateFilter } from '@/components/input/WebsiteDateFilter';
|
||||
import { FilterBar } from '@/components/input/FilterBar';
|
||||
import { WebsiteMonthSelect } from '@/components/input/WebsiteMonthSelect';
|
||||
import { ExportButton } from '@/components/input/ExportButton';
|
||||
|
||||
export function PixelControls({
|
||||
pixelId: websiteId,
|
||||
allowFilter = true,
|
||||
allowDateFilter = true,
|
||||
allowMonthFilter,
|
||||
allowCompare,
|
||||
allowDownload = false,
|
||||
}: {
|
||||
pixelId: string;
|
||||
allowFilter?: boolean;
|
||||
allowCompare?: boolean;
|
||||
allowDateFilter?: boolean;
|
||||
allowMonthFilter?: boolean;
|
||||
allowDownload?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<Column gap>
|
||||
<Row alignItems="center" justifyContent="space-between" gap="3">
|
||||
{allowFilter ? <WebsiteFilterButton websiteId={websiteId} /> : <div />}
|
||||
{allowDateFilter && <WebsiteDateFilter websiteId={websiteId} allowCompare={allowCompare} />}
|
||||
{allowDownload && <ExportButton websiteId={websiteId} />}
|
||||
{allowMonthFilter && <WebsiteMonthSelect websiteId={websiteId} />}
|
||||
</Row>
|
||||
{allowFilter && <FilterBar websiteId={websiteId} />}
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
22
src/app/(main)/pixels/[pixelId]/PixelHeader.tsx
Normal file
22
src/app/(main)/pixels/[pixelId]/PixelHeader.tsx
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import { usePixel, useMessages, useSlug } from '@/components/hooks';
|
||||
import { PageHeader } from '@/components/common/PageHeader';
|
||||
import { Icon, Text } from '@umami/react-zen';
|
||||
import { ExternalLink } from '@/components/icons';
|
||||
import { LinkButton } from '@/components/common/LinkButton';
|
||||
|
||||
export function PixelHeader() {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { getSlugUrl } = useSlug('pixel');
|
||||
const pixel = usePixel();
|
||||
|
||||
return (
|
||||
<PageHeader title={pixel.name} description={pixel.url}>
|
||||
<LinkButton href={getSlugUrl(pixel.slug)}>
|
||||
<Icon>
|
||||
<ExternalLink />
|
||||
</Icon>
|
||||
<Text>{formatMessage(labels.view)}</Text>
|
||||
</LinkButton>
|
||||
</PageHeader>
|
||||
);
|
||||
}
|
||||
71
src/app/(main)/pixels/[pixelId]/PixelMetricsBar.tsx
Normal file
71
src/app/(main)/pixels/[pixelId]/PixelMetricsBar.tsx
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import { useDateRange, useMessages } from '@/components/hooks';
|
||||
import { MetricCard } from '@/components/metrics/MetricCard';
|
||||
import { MetricsBar } from '@/components/metrics/MetricsBar';
|
||||
import { formatLongNumber } from '@/lib/format';
|
||||
import { useWebsiteStatsQuery } from '@/components/hooks/queries/useWebsiteStatsQuery';
|
||||
import { LoadingPanel } from '@/components/common/LoadingPanel';
|
||||
|
||||
export function PixelMetricsBar({
|
||||
pixelId,
|
||||
}: {
|
||||
pixelId: string;
|
||||
showChange?: boolean;
|
||||
compareMode?: boolean;
|
||||
}) {
|
||||
const { dateRange } = useDateRange(pixelId);
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { data, isLoading, isFetching, error } = useWebsiteStatsQuery(pixelId);
|
||||
const isAllTime = dateRange.value === 'all';
|
||||
|
||||
const { pageviews, visitors, visits, comparison } = data || {};
|
||||
|
||||
const metrics = data
|
||||
? [
|
||||
{
|
||||
value: visitors,
|
||||
label: formatMessage(labels.visitors),
|
||||
change: visitors - comparison.visitors,
|
||||
formatValue: formatLongNumber,
|
||||
},
|
||||
{
|
||||
value: visits,
|
||||
label: formatMessage(labels.visits),
|
||||
change: visits - comparison.visits,
|
||||
formatValue: formatLongNumber,
|
||||
},
|
||||
{
|
||||
value: pageviews,
|
||||
label: formatMessage(labels.views),
|
||||
change: pageviews - comparison.pageviews,
|
||||
formatValue: formatLongNumber,
|
||||
},
|
||||
]
|
||||
: null;
|
||||
|
||||
return (
|
||||
<LoadingPanel
|
||||
data={metrics}
|
||||
isLoading={isLoading}
|
||||
isFetching={isFetching}
|
||||
error={error}
|
||||
minHeight="136px"
|
||||
>
|
||||
<MetricsBar>
|
||||
{metrics?.map(({ label, value, prev, change, formatValue, reverseColors }: any) => {
|
||||
return (
|
||||
<MetricCard
|
||||
key={label}
|
||||
value={value}
|
||||
previousValue={prev}
|
||||
label={label}
|
||||
change={change}
|
||||
formatValue={formatValue}
|
||||
reverseColors={reverseColors}
|
||||
showChange={!isAllTime}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</MetricsBar>
|
||||
</LoadingPanel>
|
||||
);
|
||||
}
|
||||
59
src/app/(main)/pixels/[pixelId]/PixelPage.tsx
Normal file
59
src/app/(main)/pixels/[pixelId]/PixelPage.tsx
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
'use client';
|
||||
import { PageBody } from '@/components/common/PageBody';
|
||||
import { PixelProvider } from '@/app/(main)/pixels/PixelProvider';
|
||||
import { PixelHeader } from '@/app/(main)/pixels/[pixelId]/PixelHeader';
|
||||
import { Panel } from '@/components/common/Panel';
|
||||
import { WebsiteChart } from '@/app/(main)/websites/[websiteId]/WebsiteChart';
|
||||
import { PixelMetricsBar } from '@/app/(main)/pixels/[pixelId]/PixelMetricsBar';
|
||||
import { PixelControls } from '@/app/(main)/pixels/[pixelId]/PixelControls';
|
||||
import { Grid } from '@umami/react-zen';
|
||||
import { GridRow } from '@/components/common/GridRow';
|
||||
import { ReferrersTable } from '@/components/metrics/ReferrersTable';
|
||||
import { BrowsersTable } from '@/components/metrics/BrowsersTable';
|
||||
import { OSTable } from '@/components/metrics/OSTable';
|
||||
import { DevicesTable } from '@/components/metrics/DevicesTable';
|
||||
import { WorldMap } from '@/components/metrics/WorldMap';
|
||||
import { CountriesTable } from '@/components/metrics/CountriesTable';
|
||||
|
||||
export function PixelPage({ pixelId }: { pixelId: string }) {
|
||||
const props = { websiteId: pixelId, limit: 10, allowDownload: false };
|
||||
|
||||
return (
|
||||
<PixelProvider pixelId={pixelId}>
|
||||
<PageBody gap>
|
||||
<PixelHeader />
|
||||
<PixelControls pixelId={pixelId} />
|
||||
<PixelMetricsBar pixelId={pixelId} showChange={true} />
|
||||
<Panel>
|
||||
<WebsiteChart websiteId={pixelId} />
|
||||
</Panel>
|
||||
<GridRow layout="two">
|
||||
<Panel>
|
||||
<ReferrersTable {...props} />
|
||||
</Panel>
|
||||
</GridRow>
|
||||
<Grid gap="3">
|
||||
<GridRow layout="three">
|
||||
<Panel>
|
||||
<BrowsersTable {...props} />
|
||||
</Panel>
|
||||
<Panel>
|
||||
<OSTable {...props} />
|
||||
</Panel>
|
||||
<Panel>
|
||||
<DevicesTable {...props} />
|
||||
</Panel>
|
||||
</GridRow>
|
||||
<GridRow layout="two-one">
|
||||
<Panel gridColumn="span 2" noPadding>
|
||||
<WorldMap websiteId={pixelId} />
|
||||
</Panel>
|
||||
<Panel>
|
||||
<CountriesTable {...props} />
|
||||
</Panel>
|
||||
</GridRow>
|
||||
</Grid>
|
||||
</PageBody>
|
||||
</PixelProvider>
|
||||
);
|
||||
}
|
||||
12
src/app/(main)/pixels/[pixelId]/page.tsx
Normal file
12
src/app/(main)/pixels/[pixelId]/page.tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { PixelPage } from './PixelPage';
|
||||
import { Metadata } from 'next';
|
||||
|
||||
export default async function ({ params }: { params: Promise<{ pixelId: string }> }) {
|
||||
const { pixelId } = await params;
|
||||
|
||||
return <PixelPage pixelId={pixelId} />;
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Pixel',
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue