mirror of
https://github.com/umami-software/umami.git
synced 2026-02-17 11:05:36 +01:00
Compare commits
7 commits
4d06b0ca5b
...
03adb6b7e1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
03adb6b7e1 | ||
|
|
ed013d5d58 | ||
|
|
eabfec9075 | ||
|
|
fdfa8b08f9 | ||
|
|
8971f23e72 | ||
|
|
904c313a64 | ||
|
|
92ee44756c |
45 changed files with 290 additions and 268 deletions
|
|
@ -11,7 +11,7 @@
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev -p 3001 --turbopack",
|
"dev": "next dev -p 3001 --turbo",
|
||||||
"build": "npm-run-all check-env build-db check-db build-tracker build-geo build-app",
|
"build": "npm-run-all check-env build-db check-db build-tracker build-geo build-app",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"build-docker": "npm-run-all build-db build-tracker build-geo build-app",
|
"build-docker": "npm-run-all build-db build-tracker build-geo build-app",
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { Column, Row } from '@umami/react-zen';
|
import { Column, Row } from '@umami/react-zen';
|
||||||
import { WebsiteFilterButton } from '@/app/(main)/websites/[websiteId]/WebsiteFilterButton';
|
import { WebsiteFilterButton } from '@/components/input/WebsiteFilterButton';
|
||||||
import { WebsiteDateFilter } from '@/components/input/WebsiteDateFilter';
|
import { WebsiteDateFilter } from '@/components/input/WebsiteDateFilter';
|
||||||
import { FilterBar } from '@/components/input/FilterBar';
|
import { FilterBar } from '@/components/input/FilterBar';
|
||||||
import { WebsiteMonthSelect } from '@/components/input/WebsiteMonthSelect';
|
import { MonthFilter } from '@/components/input/MonthFilter';
|
||||||
import { ExportButton } from '@/components/input/ExportButton';
|
import { ExportButton } from '@/components/input/ExportButton';
|
||||||
|
|
||||||
export function LinkControls({
|
export function LinkControls({
|
||||||
|
|
@ -24,7 +24,7 @@ export function LinkControls({
|
||||||
{allowFilter ? <WebsiteFilterButton websiteId={websiteId} /> : <div />}
|
{allowFilter ? <WebsiteFilterButton websiteId={websiteId} /> : <div />}
|
||||||
{allowDateFilter && <WebsiteDateFilter websiteId={websiteId} showAllTime={false} />}
|
{allowDateFilter && <WebsiteDateFilter websiteId={websiteId} showAllTime={false} />}
|
||||||
{allowDownload && <ExportButton websiteId={websiteId} />}
|
{allowDownload && <ExportButton websiteId={websiteId} />}
|
||||||
{allowMonthFilter && <WebsiteMonthSelect websiteId={websiteId} />}
|
{allowMonthFilter && <MonthFilter />}
|
||||||
</Row>
|
</Row>
|
||||||
{allowFilter && <FilterBar websiteId={websiteId} />}
|
{allowFilter && <FilterBar websiteId={websiteId} />}
|
||||||
</Column>
|
</Column>
|
||||||
|
|
|
||||||
|
|
@ -12,10 +12,9 @@ export function LinkMetricsBar({
|
||||||
showChange?: boolean;
|
showChange?: boolean;
|
||||||
compareMode?: boolean;
|
compareMode?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const { dateRange } = useDateRange(linkId);
|
const { isAllTime } = useDateRange();
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
const { data, isLoading, isFetching, error } = useWebsiteStatsQuery(linkId);
|
const { data, isLoading, isFetching, error } = useWebsiteStatsQuery(linkId);
|
||||||
const isAllTime = dateRange.value === 'all';
|
|
||||||
|
|
||||||
const { pageviews, visitors, visits, comparison } = data || {};
|
const { pageviews, visitors, visits, comparison } = data || {};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,30 @@ 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 { LinkPanels } from '@/app/(main)/links/[linkId]/LinkPanels';
|
import { LinkPanels } from '@/app/(main)/links/[linkId]/LinkPanels';
|
||||||
import { Column, Grid } from '@umami/react-zen';
|
import { Column, Dialog, Grid, Modal } from '@umami/react-zen';
|
||||||
|
import { WebsiteExpandedView } from '@/app/(main)/websites/[websiteId]/WebsiteExpandedView';
|
||||||
|
import { useNavigation } from '@/components/hooks';
|
||||||
|
|
||||||
|
const excludedIds = ['path', 'entry', 'exit', 'title', 'language', 'screen', 'event'];
|
||||||
|
|
||||||
export function LinkPage({ linkId }: { linkId: string }) {
|
export function LinkPage({ linkId }: { linkId: string }) {
|
||||||
|
const {
|
||||||
|
router,
|
||||||
|
query: { view },
|
||||||
|
updateParams,
|
||||||
|
} = useNavigation();
|
||||||
|
|
||||||
|
const handleClose = (close: () => void) => {
|
||||||
|
router.push(updateParams({ view: undefined }));
|
||||||
|
close();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOpenChange = (isOpen: boolean) => {
|
||||||
|
if (!isOpen) {
|
||||||
|
router.push(updateParams({ view: undefined }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LinkProvider linkId={linkId}>
|
<LinkProvider linkId={linkId}>
|
||||||
<Grid width="100%" height="100%">
|
<Grid width="100%" height="100%">
|
||||||
|
|
@ -23,6 +44,19 @@ export function LinkPage({ linkId }: { linkId: string }) {
|
||||||
</Panel>
|
</Panel>
|
||||||
<LinkPanels linkId={linkId} />
|
<LinkPanels linkId={linkId} />
|
||||||
</PageBody>
|
</PageBody>
|
||||||
|
<Modal isOpen={!!view} onOpenChange={handleOpenChange} isDismissable>
|
||||||
|
<Dialog style={{ maxWidth: 1320, width: '100vw', height: 'calc(100vh - 40px)' }}>
|
||||||
|
{({ close }) => {
|
||||||
|
return (
|
||||||
|
<WebsiteExpandedView
|
||||||
|
websiteId={linkId}
|
||||||
|
excludedIds={excludedIds}
|
||||||
|
onClose={() => handleClose(close)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Dialog>
|
||||||
|
</Modal>
|
||||||
</Column>
|
</Column>
|
||||||
</Grid>
|
</Grid>
|
||||||
</LinkProvider>
|
</LinkProvider>
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { Column, Row } from '@umami/react-zen';
|
import { Column, Row } from '@umami/react-zen';
|
||||||
import { WebsiteFilterButton } from '@/app/(main)/websites/[websiteId]/WebsiteFilterButton';
|
import { WebsiteFilterButton } from '@/components/input/WebsiteFilterButton';
|
||||||
import { WebsiteDateFilter } from '@/components/input/WebsiteDateFilter';
|
import { WebsiteDateFilter } from '@/components/input/WebsiteDateFilter';
|
||||||
import { FilterBar } from '@/components/input/FilterBar';
|
import { FilterBar } from '@/components/input/FilterBar';
|
||||||
import { WebsiteMonthSelect } from '@/components/input/WebsiteMonthSelect';
|
import { MonthFilter } from '@/components/input/MonthFilter';
|
||||||
import { ExportButton } from '@/components/input/ExportButton';
|
import { ExportButton } from '@/components/input/ExportButton';
|
||||||
|
|
||||||
export function PixelControls({
|
export function PixelControls({
|
||||||
|
|
@ -24,7 +24,7 @@ export function PixelControls({
|
||||||
{allowFilter ? <WebsiteFilterButton websiteId={websiteId} /> : <div />}
|
{allowFilter ? <WebsiteFilterButton websiteId={websiteId} /> : <div />}
|
||||||
{allowDateFilter && <WebsiteDateFilter websiteId={websiteId} showAllTime={false} />}
|
{allowDateFilter && <WebsiteDateFilter websiteId={websiteId} showAllTime={false} />}
|
||||||
{allowDownload && <ExportButton websiteId={websiteId} />}
|
{allowDownload && <ExportButton websiteId={websiteId} />}
|
||||||
{allowMonthFilter && <WebsiteMonthSelect websiteId={websiteId} />}
|
{allowMonthFilter && <MonthFilter />}
|
||||||
</Row>
|
</Row>
|
||||||
{allowFilter && <FilterBar websiteId={websiteId} />}
|
{allowFilter && <FilterBar websiteId={websiteId} />}
|
||||||
</Column>
|
</Column>
|
||||||
|
|
|
||||||
|
|
@ -12,10 +12,9 @@ export function PixelMetricsBar({
|
||||||
showChange?: boolean;
|
showChange?: boolean;
|
||||||
compareMode?: boolean;
|
compareMode?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const { dateRange } = useDateRange(pixelId);
|
const { isAllTime } = useDateRange();
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
const { data, isLoading, isFetching, error } = useWebsiteStatsQuery(pixelId);
|
const { data, isLoading, isFetching, error } = useWebsiteStatsQuery(pixelId);
|
||||||
const isAllTime = dateRange.value === 'all';
|
|
||||||
|
|
||||||
const { pageviews, visitors, visits, comparison } = data || {};
|
const { pageviews, visitors, visits, comparison } = data || {};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,30 @@ 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 { PixelPanels } from '@/app/(main)/pixels/[pixelId]/PixelPanels';
|
import { PixelPanels } from '@/app/(main)/pixels/[pixelId]/PixelPanels';
|
||||||
import { Column, Grid } from '@umami/react-zen';
|
import { Column, Dialog, Grid, Modal } from '@umami/react-zen';
|
||||||
|
import { WebsiteExpandedView } from '@/app/(main)/websites/[websiteId]/WebsiteExpandedView';
|
||||||
|
import { useNavigation } from '@/components/hooks';
|
||||||
|
|
||||||
|
const excludedIds = ['path', 'entry', 'exit', 'title', 'language', 'screen', 'event'];
|
||||||
|
|
||||||
export function PixelPage({ pixelId }: { pixelId: string }) {
|
export function PixelPage({ pixelId }: { pixelId: string }) {
|
||||||
|
const {
|
||||||
|
router,
|
||||||
|
query: { view },
|
||||||
|
updateParams,
|
||||||
|
} = useNavigation();
|
||||||
|
|
||||||
|
const handleClose = (close: () => void) => {
|
||||||
|
router.push(updateParams({ view: undefined }));
|
||||||
|
close();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOpenChange = (isOpen: boolean) => {
|
||||||
|
if (!isOpen) {
|
||||||
|
router.push(updateParams({ view: undefined }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PixelProvider pixelId={pixelId}>
|
<PixelProvider pixelId={pixelId}>
|
||||||
<Grid width="100%" height="100%">
|
<Grid width="100%" height="100%">
|
||||||
|
|
@ -23,6 +44,19 @@ export function PixelPage({ pixelId }: { pixelId: string }) {
|
||||||
</Panel>
|
</Panel>
|
||||||
<PixelPanels pixelId={pixelId} />
|
<PixelPanels pixelId={pixelId} />
|
||||||
</PageBody>
|
</PageBody>
|
||||||
|
<Modal isOpen={!!view} onOpenChange={handleOpenChange} isDismissable>
|
||||||
|
<Dialog style={{ maxWidth: 1320, width: '100vw', height: 'calc(100vh - 40px)' }}>
|
||||||
|
{({ close }) => {
|
||||||
|
return (
|
||||||
|
<WebsiteExpandedView
|
||||||
|
websiteId={pixelId}
|
||||||
|
excludedIds={excludedIds}
|
||||||
|
onClose={() => handleClose(close)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Dialog>
|
||||||
|
</Modal>
|
||||||
</Column>
|
</Column>
|
||||||
</Grid>
|
</Grid>
|
||||||
</PixelProvider>
|
</PixelProvider>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
'use client';
|
'use client';
|
||||||
import { Column } from '@umami/react-zen';
|
|
||||||
import { WebsiteProvider } from '@/app/(main)/websites/WebsiteProvider';
|
import { WebsiteProvider } from '@/app/(main)/websites/WebsiteProvider';
|
||||||
import { WebsiteSettings } from '@/app/(main)/websites/[websiteId]/settings/WebsiteSettings';
|
import { WebsiteSettings } from '@/app/(main)/websites/[websiteId]/settings/WebsiteSettings';
|
||||||
import { WebsiteSettingsHeader } from '@/app/(main)/websites/[websiteId]/settings/WebsiteSettingsHeader';
|
import { WebsiteSettingsHeader } from '@/app/(main)/websites/[websiteId]/settings/WebsiteSettingsHeader';
|
||||||
|
|
@ -7,10 +6,8 @@ import { WebsiteSettingsHeader } from '@/app/(main)/websites/[websiteId]/setting
|
||||||
export function WebsiteSettingsPage({ websiteId }: { websiteId: string }) {
|
export function WebsiteSettingsPage({ websiteId }: { websiteId: string }) {
|
||||||
return (
|
return (
|
||||||
<WebsiteProvider websiteId={websiteId}>
|
<WebsiteProvider websiteId={websiteId}>
|
||||||
<Column gap="6" margin="2">
|
<WebsiteSettingsHeader />
|
||||||
<WebsiteSettingsHeader />
|
<WebsiteSettings websiteId={websiteId} />
|
||||||
<WebsiteSettings websiteId={websiteId} />
|
|
||||||
</Column>
|
|
||||||
</WebsiteProvider>
|
</WebsiteProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,13 @@
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
import { Row, Text, Icon, DataTable, DataColumn, MenuItem } from '@umami/react-zen';
|
import { Icon, DataTable, DataColumn } from '@umami/react-zen';
|
||||||
|
import { LinkButton } from '@/components/common/LinkButton';
|
||||||
import { useMessages, useNavigation } from '@/components/hooks';
|
import { useMessages, useNavigation } from '@/components/hooks';
|
||||||
import { MenuButton } from '@/components/input/MenuButton';
|
import { SquarePen } from '@/components/icons';
|
||||||
import { Eye, SquarePen } from '@/components/icons';
|
|
||||||
import { Empty } from '@/components/common/Empty';
|
import { Empty } from '@/components/common/Empty';
|
||||||
|
|
||||||
export function WebsitesTable({
|
export function WebsitesTable({
|
||||||
data = [],
|
data = [],
|
||||||
showActions,
|
showActions,
|
||||||
allowEdit,
|
|
||||||
allowView,
|
|
||||||
renderLink,
|
renderLink,
|
||||||
}: {
|
}: {
|
||||||
data: Record<string, any>[];
|
data: Record<string, any>[];
|
||||||
|
|
@ -37,28 +35,11 @@ export function WebsitesTable({
|
||||||
const websiteId = row.id;
|
const websiteId = row.id;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MenuButton>
|
<LinkButton href={renderUrl(`/websites/${websiteId}/settings`)} variant="quiet">
|
||||||
{allowView && (
|
<Icon>
|
||||||
<MenuItem href={renderUrl(`/websites/${websiteId}`)}>
|
<SquarePen />
|
||||||
<Row alignItems="center" gap>
|
</Icon>
|
||||||
<Icon data-test="link-button-view">
|
</LinkButton>
|
||||||
<Eye />
|
|
||||||
</Icon>
|
|
||||||
<Text>{formatMessage(labels.view)}</Text>
|
|
||||||
</Row>
|
|
||||||
</MenuItem>
|
|
||||||
)}
|
|
||||||
{allowEdit && (
|
|
||||||
<MenuItem href={renderUrl(`/websites/${websiteId}/settings`)}>
|
|
||||||
<Row alignItems="center" gap>
|
|
||||||
<Icon data-test="link-button-edit">
|
|
||||||
<SquarePen />
|
|
||||||
</Icon>
|
|
||||||
<Text>{formatMessage(labels.edit)}</Text>
|
|
||||||
</Row>
|
|
||||||
</MenuItem>
|
|
||||||
)}
|
|
||||||
</MenuButton>
|
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
</DataColumn>
|
</DataColumn>
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ export function AttributionPage({ websiteId }: { websiteId: string }) {
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
const {
|
const {
|
||||||
dateRange: { startDate, endDate },
|
dateRange: { startDate, endDate },
|
||||||
} = useDateRange(websiteId);
|
} = useDateRange();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column gap="6">
|
<Column gap="6">
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,23 @@
|
||||||
'use client';
|
'use client';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Button, Column, Box, Text, Icon, DialogTrigger, Modal, Dialog } from '@umami/react-zen';
|
import { Button, Column, Box, DialogTrigger, Popover, Dialog, IconLabel } from '@umami/react-zen';
|
||||||
import { useDateRange, useMessages } from '@/components/hooks';
|
import { useDateRange, useMessages } from '@/components/hooks';
|
||||||
import { ListCheck } from '@/components/icons';
|
import { ListCheck } from '@/components/icons';
|
||||||
import { Panel } from '@/components/common/Panel';
|
import { Panel } from '@/components/common/Panel';
|
||||||
import { Breakdown } from './Breakdown';
|
import { Breakdown } from './Breakdown';
|
||||||
import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteControls';
|
import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteControls';
|
||||||
import { FieldSelectForm } from '@/app/(main)/websites/[websiteId]/(reports)/breakdown/FieldSelectForm';
|
import { FieldSelectForm } from '@/app/(main)/websites/[websiteId]/(reports)/breakdown/FieldSelectForm';
|
||||||
import { SectionHeader } from '@/components/common/SectionHeader';
|
|
||||||
|
|
||||||
export function BreakdownPage({ websiteId }: { websiteId: string }) {
|
export function BreakdownPage({ websiteId }: { websiteId: string }) {
|
||||||
const {
|
const {
|
||||||
dateRange: { startDate, endDate },
|
dateRange: { startDate, endDate },
|
||||||
} = useDateRange(websiteId);
|
} = useDateRange();
|
||||||
const [fields, setFields] = useState(['path']);
|
const [fields, setFields] = useState(['path']);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column gap>
|
<Column gap>
|
||||||
<WebsiteControls websiteId={websiteId} />
|
<WebsiteControls websiteId={websiteId} />
|
||||||
<SectionHeader>
|
<FieldsButton value={fields} onChange={setFields} />
|
||||||
<FieldsButton value={fields} onChange={setFields} />
|
|
||||||
</SectionHeader>
|
|
||||||
<Panel height="900px" overflow="auto" allowFullscreen>
|
<Panel height="900px" overflow="auto" allowFullscreen>
|
||||||
<Breakdown
|
<Breakdown
|
||||||
websiteId={websiteId}
|
websiteId={websiteId}
|
||||||
|
|
@ -39,19 +36,16 @@ const FieldsButton = ({ value, onChange }) => {
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<DialogTrigger>
|
<DialogTrigger>
|
||||||
<Button variant="primary">
|
<Button>
|
||||||
<Icon>
|
<IconLabel icon={<ListCheck />}>{formatMessage(labels.fields)}</IconLabel>
|
||||||
<ListCheck />
|
|
||||||
</Icon>
|
|
||||||
<Text>Fields</Text>
|
|
||||||
</Button>
|
</Button>
|
||||||
<Modal>
|
<Popover>
|
||||||
<Dialog title={formatMessage(labels.fields)} style={{ width: 400 }}>
|
<Dialog title={formatMessage(labels.fields)} style={{ width: 400 }}>
|
||||||
{({ close }) => (
|
{({ close }) => (
|
||||||
<FieldSelectForm selectedFields={value} onChange={onChange} onClose={close} />
|
<FieldSelectForm selectedFields={value} onChange={onChange} onClose={close} />
|
||||||
)}
|
)}
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</Modal>
|
</Popover>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ export function FunnelsPage({ websiteId }: { websiteId: string }) {
|
||||||
const { data, isLoading, error } = useReportsQuery({ websiteId, type: 'funnel' });
|
const { data, isLoading, error } = useReportsQuery({ websiteId, type: 'funnel' });
|
||||||
const {
|
const {
|
||||||
dateRange: { startDate, endDate },
|
dateRange: { startDate, endDate },
|
||||||
} = useDateRange(websiteId);
|
} = useDateRange();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column gap>
|
<Column gap>
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ export function GoalsPage({ websiteId }: { websiteId: string }) {
|
||||||
const { data, isLoading, error } = useReportsQuery({ websiteId, type: 'goal' });
|
const { data, isLoading, error } = useReportsQuery({ websiteId, type: 'goal' });
|
||||||
const {
|
const {
|
||||||
dateRange: { startDate, endDate },
|
dateRange: { startDate, endDate },
|
||||||
} = useDateRange(websiteId);
|
} = useDateRange();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column gap>
|
<Column gap>
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ export function JourneysPage({ websiteId }: { websiteId: string }) {
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
const {
|
const {
|
||||||
dateRange: { startDate, endDate },
|
dateRange: { startDate, endDate },
|
||||||
} = useDateRange(websiteId);
|
} = useDateRange();
|
||||||
const [steps, setSteps] = useState(DEFAULT_STEP);
|
const [steps, setSteps] = useState(DEFAULT_STEP);
|
||||||
const [startStep, setStartStep] = useState('');
|
const [startStep, setStartStep] = useState('');
|
||||||
const [endStep, setEndStep] = useState('');
|
const [endStep, setEndStep] = useState('');
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import { useDateRange } from '@/components/hooks';
|
||||||
export function RevenuePage({ websiteId }: { websiteId: string }) {
|
export function RevenuePage({ websiteId }: { websiteId: string }) {
|
||||||
const {
|
const {
|
||||||
dateRange: { startDate, endDate, unit },
|
dateRange: { startDate, endDate, unit },
|
||||||
} = useDateRange(websiteId);
|
} = useDateRange();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column gap>
|
<Column gap>
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteContro
|
||||||
export function UTMPage({ websiteId }: { websiteId: string }) {
|
export function UTMPage({ websiteId }: { websiteId: string }) {
|
||||||
const {
|
const {
|
||||||
dateRange: { startDate, endDate },
|
dateRange: { startDate, endDate },
|
||||||
} = useDateRange(websiteId);
|
} = useDateRange();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column gap>
|
<Column gap>
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ export function WebsiteChart({
|
||||||
websiteId: string;
|
websiteId: string;
|
||||||
compareMode?: boolean;
|
compareMode?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const { dateRange, dateCompare } = useDateRange(websiteId);
|
const { dateRange, dateCompare } = useDateRange();
|
||||||
const { startDate, endDate, unit, value } = dateRange;
|
const { startDate, endDate, unit, value } = dateRange;
|
||||||
const { data, isLoading, isFetching, error } = useWebsitePageviewsQuery({
|
const { data, isLoading, isFetching, error } = useWebsitePageviewsQuery({
|
||||||
websiteId,
|
websiteId,
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { Column, Row } from '@umami/react-zen';
|
import { Column, Row } from '@umami/react-zen';
|
||||||
import { WebsiteFilterButton } from '@/app/(main)/websites/[websiteId]/WebsiteFilterButton';
|
import { WebsiteFilterButton } from '@/components/input/WebsiteFilterButton';
|
||||||
import { WebsiteDateFilter } from '@/components/input/WebsiteDateFilter';
|
import { WebsiteDateFilter } from '@/components/input/WebsiteDateFilter';
|
||||||
import { FilterBar } from '@/components/input/FilterBar';
|
import { FilterBar } from '@/components/input/FilterBar';
|
||||||
import { WebsiteMonthSelect } from '@/components/input/WebsiteMonthSelect';
|
import { MonthFilter } from '@/components/input/MonthFilter';
|
||||||
import { ExportButton } from '@/components/input/ExportButton';
|
import { ExportButton } from '@/components/input/ExportButton';
|
||||||
|
|
||||||
export function WebsiteControls({
|
export function WebsiteControls({
|
||||||
|
|
@ -26,7 +26,7 @@ export function WebsiteControls({
|
||||||
{allowFilter ? <WebsiteFilterButton websiteId={websiteId} /> : <div />}
|
{allowFilter ? <WebsiteFilterButton websiteId={websiteId} /> : <div />}
|
||||||
{allowDateFilter && <WebsiteDateFilter websiteId={websiteId} allowCompare={allowCompare} />}
|
{allowDateFilter && <WebsiteDateFilter websiteId={websiteId} allowCompare={allowCompare} />}
|
||||||
{allowDownload && <ExportButton websiteId={websiteId} />}
|
{allowDownload && <ExportButton websiteId={websiteId} />}
|
||||||
{allowMonthFilter && <WebsiteMonthSelect websiteId={websiteId} />}
|
{allowMonthFilter && <MonthFilter />}
|
||||||
</Row>
|
</Row>
|
||||||
{allowFilter && <FilterBar websiteId={websiteId} />}
|
{allowFilter && <FilterBar websiteId={websiteId} />}
|
||||||
</Column>
|
</Column>
|
||||||
|
|
|
||||||
|
|
@ -26,9 +26,11 @@ import { Lightning } from '@/components/svg';
|
||||||
|
|
||||||
export function WebsiteExpandedView({
|
export function WebsiteExpandedView({
|
||||||
websiteId,
|
websiteId,
|
||||||
|
excludedIds = [],
|
||||||
onClose,
|
onClose,
|
||||||
}: {
|
}: {
|
||||||
websiteId: string;
|
websiteId: string;
|
||||||
|
excludedIds?: string[];
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
}) {
|
}) {
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
|
|
@ -37,9 +39,11 @@ export function WebsiteExpandedView({
|
||||||
query: { view },
|
query: { view },
|
||||||
} = useNavigation();
|
} = useNavigation();
|
||||||
|
|
||||||
|
const filterExcluded = (item: { id: string }) => !excludedIds.includes(item.id);
|
||||||
|
|
||||||
const items = [
|
const items = [
|
||||||
{
|
{
|
||||||
label: formatMessage(labels.pages),
|
label: 'URL',
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
id: 'path',
|
id: 'path',
|
||||||
|
|
@ -71,7 +75,7 @@ export function WebsiteExpandedView({
|
||||||
path: updateParams({ view: 'query' }),
|
path: updateParams({ view: 'query' }),
|
||||||
icon: <Search />,
|
icon: <Search />,
|
||||||
},
|
},
|
||||||
],
|
].filter(filterExcluded),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: formatMessage(labels.sources),
|
label: formatMessage(labels.sources),
|
||||||
|
|
@ -94,7 +98,7 @@ export function WebsiteExpandedView({
|
||||||
path: updateParams({ view: 'domain' }),
|
path: updateParams({ view: 'domain' }),
|
||||||
icon: <Globe />,
|
icon: <Globe />,
|
||||||
},
|
},
|
||||||
],
|
].filter(filterExcluded),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: formatMessage(labels.location),
|
label: formatMessage(labels.location),
|
||||||
|
|
@ -117,7 +121,7 @@ export function WebsiteExpandedView({
|
||||||
path: updateParams({ view: 'city' }),
|
path: updateParams({ view: 'city' }),
|
||||||
icon: <Landmark />,
|
icon: <Landmark />,
|
||||||
},
|
},
|
||||||
],
|
].filter(filterExcluded),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: formatMessage(labels.environment),
|
label: formatMessage(labels.environment),
|
||||||
|
|
@ -152,7 +156,7 @@ export function WebsiteExpandedView({
|
||||||
path: updateParams({ view: 'screen' }),
|
path: updateParams({ view: 'screen' }),
|
||||||
icon: <Monitor />,
|
icon: <Monitor />,
|
||||||
},
|
},
|
||||||
],
|
].filter(filterExcluded),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: formatMessage(labels.other),
|
label: formatMessage(labels.other),
|
||||||
|
|
@ -175,7 +179,7 @@ export function WebsiteExpandedView({
|
||||||
path: updateParams({ view: 'tag' }),
|
path: updateParams({ view: 'tag' }),
|
||||||
icon: <Tag />,
|
icon: <Tag />,
|
||||||
},
|
},
|
||||||
],
|
].filter(filterExcluded),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,17 +2,24 @@
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
import { Column, Grid } from '@umami/react-zen';
|
import { Column, Grid } from '@umami/react-zen';
|
||||||
import { WebsiteProvider } from '@/app/(main)/websites/WebsiteProvider';
|
import { WebsiteProvider } from '@/app/(main)/websites/WebsiteProvider';
|
||||||
|
import { useNavigation } from '@/components/hooks';
|
||||||
import { PageBody } from '@/components/common/PageBody';
|
import { PageBody } from '@/components/common/PageBody';
|
||||||
import { WebsiteHeader } from './WebsiteHeader';
|
import { WebsiteHeader } from './WebsiteHeader';
|
||||||
import { WebsiteNav } from './WebsiteNav';
|
import { WebsiteNav } from './WebsiteNav';
|
||||||
|
|
||||||
export function WebsiteLayout({ websiteId, children }: { websiteId: string; children: ReactNode }) {
|
export function WebsiteLayout({ websiteId, children }: { websiteId: string; children: ReactNode }) {
|
||||||
|
const { pathname } = useNavigation();
|
||||||
|
|
||||||
|
const isSettings = pathname.endsWith('/settings');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<WebsiteProvider websiteId={websiteId}>
|
<WebsiteProvider websiteId={websiteId}>
|
||||||
<Grid columns="auto 1fr" width="100%" height="100%">
|
<Grid columns={isSettings ? '1fr' : 'auto 1fr'} width="100%" height="100%">
|
||||||
<Column height="100%" border="right" backgroundColor marginRight="2">
|
{!isSettings && (
|
||||||
<WebsiteNav websiteId={websiteId} />
|
<Column height="100%" border="right" backgroundColor marginRight="2">
|
||||||
</Column>
|
<WebsiteNav websiteId={websiteId} />
|
||||||
|
</Column>
|
||||||
|
)}
|
||||||
<PageBody gap>
|
<PageBody gap>
|
||||||
<WebsiteHeader />
|
<WebsiteHeader />
|
||||||
<Column>{children}</Column>
|
<Column>{children}</Column>
|
||||||
|
|
|
||||||
|
|
@ -12,10 +12,9 @@ export function WebsiteMetricsBar({
|
||||||
showChange?: boolean;
|
showChange?: boolean;
|
||||||
compareMode?: boolean;
|
compareMode?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const { dateRange } = useDateRange(websiteId);
|
const { isAllTime } = useDateRange();
|
||||||
const { formatMessage, labels, getErrorMessage } = useMessages();
|
const { formatMessage, labels, getErrorMessage } = useMessages();
|
||||||
const { data, isLoading, isFetching, error } = useWebsiteStatsQuery(websiteId);
|
const { data, isLoading, isFetching, error } = useWebsiteStatsQuery(websiteId);
|
||||||
const isAllTime = dateRange.value === 'all';
|
|
||||||
|
|
||||||
const { pageviews, visitors, visits, bounces, totaltime, comparison } = data || {};
|
const { pageviews, visitors, visits, bounces, totaltime, comparison } = data || {};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,18 @@
|
||||||
import { Eye, User, Clock, Sheet, Tag, ChartPie, UserPlus } from '@/components/icons';
|
import { Text } from '@umami/react-zen';
|
||||||
import { Lightning, Path, Money, Compare, Target, Funnel, Magnet, Network } from '@/components/svg';
|
import {
|
||||||
|
Eye,
|
||||||
|
User,
|
||||||
|
Clock,
|
||||||
|
Sheet,
|
||||||
|
Tag,
|
||||||
|
ChartPie,
|
||||||
|
UserPlus,
|
||||||
|
GitCompareArrows,
|
||||||
|
} from '@/components/icons';
|
||||||
|
import { Lightning, Path, Money, Target, Funnel, Magnet, Network } from '@/components/svg';
|
||||||
import { useMessages, useNavigation } from '@/components/hooks';
|
import { useMessages, useNavigation } from '@/components/hooks';
|
||||||
import { SideMenu } from '@/components/common/SideMenu';
|
import { SideMenu } from '@/components/common/SideMenu';
|
||||||
import { WebsiteSelect } from '@/components/input/WebsiteSelect';
|
import { WebsiteSelect } from '@/components/input/WebsiteSelect';
|
||||||
import { Text } from '@umami/react-zen';
|
|
||||||
|
|
||||||
export function WebsiteNav({ websiteId }: { websiteId: string }) {
|
export function WebsiteNav({ websiteId }: { websiteId: string }) {
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
|
|
@ -47,7 +56,7 @@ export function WebsiteNav({ websiteId }: { websiteId: string }) {
|
||||||
{
|
{
|
||||||
id: 'compare',
|
id: 'compare',
|
||||||
label: formatMessage(labels.compare),
|
label: formatMessage(labels.compare),
|
||||||
icon: <Compare />,
|
icon: <GitCompareArrows />,
|
||||||
path: renderPath('/compare'),
|
path: renderPath('/compare'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import { useState } from 'react';
|
||||||
|
|
||||||
export function CompareTables({ websiteId }: { websiteId: string }) {
|
export function CompareTables({ websiteId }: { websiteId: string }) {
|
||||||
const [data, setData] = useState([]);
|
const [data, setData] = useState([]);
|
||||||
const { dateRange, dateCompare } = useDateRange(websiteId);
|
const { dateRange, dateCompare } = useDateRange();
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
const {
|
const {
|
||||||
router,
|
router,
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,22 @@
|
||||||
|
import Link from 'next/link';
|
||||||
import { PageHeader } from '@/components/common/PageHeader';
|
import { PageHeader } from '@/components/common/PageHeader';
|
||||||
import { Globe } from '@/components/icons';
|
import { Globe, ArrowLeft } from '@/components/icons';
|
||||||
import { useWebsite } from '@/components/hooks';
|
import { useMessages, useNavigation, useWebsite } from '@/components/hooks';
|
||||||
|
import { IconLabel, Row } from '@umami/react-zen';
|
||||||
|
|
||||||
export function WebsiteSettingsHeader() {
|
export function WebsiteSettingsHeader() {
|
||||||
const website = useWebsite();
|
const website = useWebsite();
|
||||||
|
const { formatMessage, labels } = useMessages();
|
||||||
|
const { renderUrl } = useNavigation();
|
||||||
|
|
||||||
return <PageHeader title={website?.name} icon={<Globe />} />;
|
return (
|
||||||
|
<>
|
||||||
|
<Row marginTop="6">
|
||||||
|
<Link href={renderUrl(`/websites/${website.id}`)}>
|
||||||
|
<IconLabel icon={<ArrowLeft />} label={formatMessage(labels.website)} />
|
||||||
|
</Link>
|
||||||
|
</Row>
|
||||||
|
<PageHeader title={website?.name} description={website?.domain} icon={<Globe />} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,60 +0,0 @@
|
||||||
import { z } from 'zod';
|
|
||||||
import { json, unauthorized } from '@/lib/response';
|
|
||||||
import { getAllUserWebsitesIncludingTeamOwner } from '@/queries/prisma/website';
|
|
||||||
import { getEventUsage } from '@/queries/sql/events/getEventUsage';
|
|
||||||
import { getEventDataUsage } from '@/queries/sql/events/getEventDataUsage';
|
|
||||||
import { parseRequest, getQueryFilters } from '@/lib/request';
|
|
||||||
|
|
||||||
export async function GET(request: Request, { params }: { params: Promise<{ userId: string }> }) {
|
|
||||||
const schema = z.object({
|
|
||||||
startAt: z.coerce.number().int(),
|
|
||||||
endAt: z.coerce.number().int(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const { auth, query, error } = await parseRequest(request, schema);
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return error();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!auth.user.isAdmin) {
|
|
||||||
return unauthorized();
|
|
||||||
}
|
|
||||||
|
|
||||||
const { userId } = await params;
|
|
||||||
const filters = await getQueryFilters(query);
|
|
||||||
|
|
||||||
const websites = await getAllUserWebsitesIncludingTeamOwner(userId);
|
|
||||||
|
|
||||||
const websiteIds = websites.map(a => a.id);
|
|
||||||
|
|
||||||
const websiteEventUsage = await getEventUsage(websiteIds, filters);
|
|
||||||
const eventDataUsage = await getEventDataUsage(websiteIds, filters);
|
|
||||||
|
|
||||||
const websiteUsage = websites.map(a => ({
|
|
||||||
websiteId: a.id,
|
|
||||||
websiteName: a.name,
|
|
||||||
websiteEventUsage: websiteEventUsage.find(b => a.id === b.websiteId)?.count || 0,
|
|
||||||
eventDataUsage: eventDataUsage.find(b => a.id === b.websiteId)?.count || 0,
|
|
||||||
deletedAt: a.deletedAt,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const usage = websiteUsage.reduce(
|
|
||||||
(acc, cv) => {
|
|
||||||
acc.websiteEventUsage += cv.websiteEventUsage;
|
|
||||||
acc.eventDataUsage += cv.eventDataUsage;
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
{ websiteEventUsage: 0, eventDataUsage: 0 },
|
|
||||||
);
|
|
||||||
|
|
||||||
const filteredWebsiteUsage = websiteUsage.filter(
|
|
||||||
a => !a.deletedAt && (a.websiteEventUsage > 0 || a.eventDataUsage > 0),
|
|
||||||
);
|
|
||||||
|
|
||||||
return json({
|
|
||||||
...usage,
|
|
||||||
websites: filteredWebsiteUsage,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
@ -4,9 +4,11 @@ import { json, unauthorized } from '@/lib/response';
|
||||||
import { uuid } from '@/lib/crypto';
|
import { uuid } from '@/lib/crypto';
|
||||||
import { getQueryFilters, parseRequest } from '@/lib/request';
|
import { getQueryFilters, parseRequest } from '@/lib/request';
|
||||||
import { pagingParams, searchParams } from '@/lib/schema';
|
import { pagingParams, searchParams } from '@/lib/schema';
|
||||||
import { createWebsite } from '@/queries/prisma';
|
import { createWebsite, getWebsiteCount } from '@/queries/prisma';
|
||||||
import { getAllUserWebsitesIncludingTeamOwner, getUserWebsites } from '@/queries/prisma/website';
|
import { getAllUserWebsitesIncludingTeamOwner, getUserWebsites } from '@/queries/prisma/website';
|
||||||
|
|
||||||
|
const CLOUD_WEBSITE_LIMIT = 3;
|
||||||
|
|
||||||
export async function GET(request: Request) {
|
export async function GET(request: Request) {
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
...pagingParams,
|
...pagingParams,
|
||||||
|
|
@ -36,7 +38,7 @@ export async function POST(request: Request) {
|
||||||
name: z.string().max(100),
|
name: z.string().max(100),
|
||||||
domain: z.string().max(500),
|
domain: z.string().max(500),
|
||||||
shareId: z.string().max(50).nullable().optional(),
|
shareId: z.string().max(50).nullable().optional(),
|
||||||
teamId: z.string().nullable().optional(),
|
teamId: z.uuid().nullable().optional(),
|
||||||
id: z.uuid().nullable().optional(),
|
id: z.uuid().nullable().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -48,6 +50,14 @@ export async function POST(request: Request) {
|
||||||
|
|
||||||
const { id, name, domain, shareId, teamId } = body;
|
const { id, name, domain, shareId, teamId } = body;
|
||||||
|
|
||||||
|
if (process.env.CLOUD_MODE && !teamId && !auth.user.hasSubscription) {
|
||||||
|
const count = await getWebsiteCount(auth.user.id);
|
||||||
|
|
||||||
|
if (count >= CLOUD_WEBSITE_LIMIT) {
|
||||||
|
return unauthorized({ message: 'Website limit reached.' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ((teamId && !(await canCreateTeamWebsite(auth, teamId))) || !(await canCreateWebsite(auth))) {
|
if ((teamId && !(await canCreateTeamWebsite(auth, teamId))) || !(await canCreateWebsite(auth))) {
|
||||||
return unauthorized();
|
return unauthorized();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
import { Row, Button, FormSubmitButton, Form, FormButtons } from '@umami/react-zen';
|
import { Box, Button, FormSubmitButton, Form, FormButtons } from '@umami/react-zen';
|
||||||
import { useMessages } from '@/components/hooks';
|
import { useMessages } from '@/components/hooks';
|
||||||
|
|
||||||
export interface ConfirmationFormProps {
|
export interface ConfirmationFormProps {
|
||||||
|
|
@ -25,7 +25,7 @@ export function ConfirmationForm({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form onSubmit={onConfirm} error={getErrorMessage(error)}>
|
<Form onSubmit={onConfirm} error={getErrorMessage(error)}>
|
||||||
<Row marginY="4">{message}</Row>
|
<Box marginY="4">{message}</Box>
|
||||||
<FormButtons>
|
<FormButtons>
|
||||||
<Button onPress={onClose}>{formatMessage(labels.cancel)}</Button>
|
<Button onPress={onClose}>{formatMessage(labels.cancel)}</Button>
|
||||||
<FormSubmitButton
|
<FormSubmitButton
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ export function PageHeader({
|
||||||
justifyContent="space-between"
|
justifyContent="space-between"
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
paddingY="6"
|
paddingY="6"
|
||||||
|
marginBottom="6"
|
||||||
border={showBorder ? 'bottom' : undefined}
|
border={showBorder ? 'bottom' : undefined}
|
||||||
width="100%"
|
width="100%"
|
||||||
{...props}
|
{...props}
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,13 @@
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
import {
|
import {
|
||||||
Text,
|
|
||||||
Heading,
|
Heading,
|
||||||
NavMenu,
|
NavMenu,
|
||||||
NavMenuItem,
|
NavMenuItem,
|
||||||
Icon,
|
|
||||||
Row,
|
Row,
|
||||||
Column,
|
Column,
|
||||||
NavMenuGroup,
|
NavMenuGroup,
|
||||||
NavMenuProps,
|
NavMenuProps,
|
||||||
|
IconLabel,
|
||||||
} from '@umami/react-zen';
|
} from '@umami/react-zen';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
|
|
@ -47,10 +46,7 @@ export function SideMenu({
|
||||||
return (
|
return (
|
||||||
<Link key={id} href={path}>
|
<Link key={id} href={path}>
|
||||||
<NavMenuItem isSelected={isSelected}>
|
<NavMenuItem isSelected={isSelected}>
|
||||||
<Row alignItems="center" gap>
|
<IconLabel icon={icon}>{label}</IconLabel>
|
||||||
<Icon>{icon}</Icon>
|
|
||||||
<Text>{label}</Text>
|
|
||||||
</Row>
|
|
||||||
</NavMenuItem>
|
</NavMenuItem>
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,6 @@ export * from './useModified';
|
||||||
export * from './useNavigation';
|
export * from './useNavigation';
|
||||||
export * from './usePagedQuery';
|
export * from './usePagedQuery';
|
||||||
export * from './usePageParameters';
|
export * from './usePageParameters';
|
||||||
export * from './useQueryStringDate';
|
|
||||||
export * from './useRegionNames';
|
export * from './useRegionNames';
|
||||||
export * from './useSlug';
|
export * from './useSlug';
|
||||||
export * from './useSticky';
|
export * from './useSticky';
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,23 @@
|
||||||
import { useApi } from '../useApi';
|
import { useApi } from '../useApi';
|
||||||
import { ReactQueryOptions } from '@/lib/types';
|
import { ReactQueryOptions } from '@/lib/types';
|
||||||
|
|
||||||
|
type DateRange = {
|
||||||
|
startDate?: string;
|
||||||
|
endDate?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export function useDateRangeQuery(websiteId: string, options?: ReactQueryOptions) {
|
export function useDateRangeQuery(websiteId: string, options?: ReactQueryOptions) {
|
||||||
const { get, useQuery } = useApi();
|
const { get, useQuery } = useApi();
|
||||||
return useQuery<any>({
|
|
||||||
|
const { data } = useQuery<DateRange>({
|
||||||
queryKey: ['date-range', websiteId],
|
queryKey: ['date-range', websiteId],
|
||||||
queryFn: () => get(`/websites/${websiteId}/daterange`),
|
queryFn: () => get(`/websites/${websiteId}/daterange`),
|
||||||
enabled: !!websiteId,
|
enabled: !!websiteId,
|
||||||
...options,
|
...options,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
startDate: data?.startDate ? new Date(data.startDate) : null,
|
||||||
|
endDate: data?.endDate ? new Date(data.endDate) : null,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import { useDateRange } from './useDateRange';
|
import { useDateRange } from './useDateRange';
|
||||||
import { useTimezone } from './useTimezone';
|
import { useTimezone } from './useTimezone';
|
||||||
|
|
||||||
export function useDateParameters(websiteId: string) {
|
export function useDateParameters() {
|
||||||
const {
|
const {
|
||||||
dateRange: { startDate, endDate, unit },
|
dateRange: { startDate, endDate, unit },
|
||||||
} = useDateRange(websiteId);
|
} = useDateRange();
|
||||||
const { timezone, toUtc } = useTimezone();
|
const { timezone, toUtc } = useTimezone();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -1,35 +1,32 @@
|
||||||
import { getMinimumUnit, parseDateRange } from '@/lib/date';
|
import { useNavigation } from '@/components/hooks/useNavigation';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { getCompareDate, getOffsetDateRange, parseDateRange } from '@/lib/date';
|
||||||
import { useLocale } from '@/components/hooks/useLocale';
|
import { useLocale } from '@/components/hooks/useLocale';
|
||||||
import { useApi } from '@/components/hooks//useApi';
|
import { DEFAULT_DATE_RANGE_VALUE } from '@/lib/constants';
|
||||||
import { useQueryStringDate } from '@/components/hooks/useQueryStringDate';
|
|
||||||
import { useGlobalState } from '@/components/hooks/useGlobalState';
|
|
||||||
|
|
||||||
export function useDateRange(websiteId: string) {
|
export function useDateRange(options: { ignoreOffset?: boolean } = {}) {
|
||||||
const { get } = useApi();
|
const {
|
||||||
|
query: { date = DEFAULT_DATE_RANGE_VALUE, offset = 0, compare = 'prev', all },
|
||||||
|
} = useNavigation();
|
||||||
const { locale } = useLocale();
|
const { locale } = useLocale();
|
||||||
const { dateRange: defaultDateRange, dateCompare } = useQueryStringDate();
|
|
||||||
|
|
||||||
const [dateRange, setDateRange] = useGlobalState(`date-range:${websiteId}`, defaultDateRange);
|
const dateRange = useMemo(() => {
|
||||||
|
const dateRangeObject = parseDateRange(date, locale);
|
||||||
|
|
||||||
const setDateRangeValue = async (value: string) => {
|
return !options.ignoreOffset && offset
|
||||||
if (value === 'all') {
|
? getOffsetDateRange(dateRangeObject, +offset)
|
||||||
const result = await get(`/websites/${websiteId}/daterange`);
|
: dateRangeObject;
|
||||||
const { mindate, maxdate } = result;
|
}, [date, offset, options]);
|
||||||
|
|
||||||
const startDate = new Date(mindate);
|
const dateCompare = getCompareDate(compare, dateRange.startDate, dateRange.endDate);
|
||||||
const endDate = new Date(maxdate);
|
|
||||||
const unit = getMinimumUnit(startDate, endDate);
|
|
||||||
|
|
||||||
setDateRange({
|
return {
|
||||||
startDate,
|
date,
|
||||||
endDate,
|
offset,
|
||||||
unit,
|
compare,
|
||||||
value,
|
isAllTime: !!all,
|
||||||
});
|
isCustomRange: date.startsWith('range:'),
|
||||||
} else {
|
dateRange,
|
||||||
setDateRange(parseDateRange(value, locale));
|
dateCompare,
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return { dateRange, dateCompare, setDateRange, setDateRangeValue };
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
import { useNavigation } from '@/components/hooks/useNavigation';
|
|
||||||
import { useMemo } from 'react';
|
|
||||||
import { getCompareDate, getOffsetDateRange, parseDateRange } from '@/lib/date';
|
|
||||||
import { useLocale } from '@/components/hooks/useLocale';
|
|
||||||
import { DEFAULT_DATE_RANGE_VALUE } from '@/lib/constants';
|
|
||||||
|
|
||||||
export function useQueryStringDate(options: { ignoreOffset?: boolean } = {}) {
|
|
||||||
const {
|
|
||||||
query: { date = DEFAULT_DATE_RANGE_VALUE, offset = 0, compare = 'prev' },
|
|
||||||
} = useNavigation();
|
|
||||||
const { locale } = useLocale();
|
|
||||||
|
|
||||||
const dateRange = useMemo(() => {
|
|
||||||
const dateRangeObject = parseDateRange(date, locale);
|
|
||||||
|
|
||||||
return !options.ignoreOffset && offset
|
|
||||||
? getOffsetDateRange(dateRangeObject, +offset)
|
|
||||||
: dateRangeObject;
|
|
||||||
}, [date, offset, options]);
|
|
||||||
|
|
||||||
const dateCompare = getCompareDate(compare, dateRange.startDate, dateRange.endDate);
|
|
||||||
|
|
||||||
return { date, offset, dateRange, dateCompare };
|
|
||||||
}
|
|
||||||
18
src/components/input/MonthFilter.tsx
Normal file
18
src/components/input/MonthFilter.tsx
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { useDateRange, useNavigation } from '@/components/hooks';
|
||||||
|
import { getMonthDateRangeValue } from '@/lib/date';
|
||||||
|
import { MonthSelect } from './MonthSelect';
|
||||||
|
|
||||||
|
export function MonthFilter() {
|
||||||
|
const { router, updateParams } = useNavigation();
|
||||||
|
const {
|
||||||
|
dateRange: { startDate },
|
||||||
|
} = useDateRange();
|
||||||
|
|
||||||
|
const handleMonthSelect = (date: Date) => {
|
||||||
|
const range = getMonthDateRangeValue(date);
|
||||||
|
|
||||||
|
router.push(updateParams({ date: range, offset: undefined }));
|
||||||
|
};
|
||||||
|
|
||||||
|
return <MonthSelect date={startDate} onChange={handleMonthSelect} />;
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { LoadingButton, Icon, Tooltip, TooltipTrigger } from '@umami/react-zen';
|
import { LoadingButton, Icon, Tooltip, TooltipTrigger } from '@umami/react-zen';
|
||||||
import { setWebsiteDateRange } from '@/store/websites';
|
import { setWebsiteDateRange } from '@/store/websites';
|
||||||
import { useDateRange } from '@/components/hooks';
|
import { useDateRange } from '@/components/hooks';
|
||||||
import { Refresh } from '@/components/icons';
|
import { RefreshCw } from '@/components/icons';
|
||||||
import { useMessages } from '@/components/hooks';
|
import { useMessages } from '@/components/hooks';
|
||||||
|
|
||||||
export function RefreshButton({
|
export function RefreshButton({
|
||||||
|
|
@ -12,7 +12,7 @@ export function RefreshButton({
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
const { dateRange } = useDateRange(websiteId);
|
const { dateRange } = useDateRange();
|
||||||
|
|
||||||
function handleClick() {
|
function handleClick() {
|
||||||
if (!isLoading && dateRange) {
|
if (!isLoading && dateRange) {
|
||||||
|
|
@ -24,7 +24,7 @@ export function RefreshButton({
|
||||||
<TooltipTrigger>
|
<TooltipTrigger>
|
||||||
<LoadingButton isLoading={isLoading} onPress={handleClick}>
|
<LoadingButton isLoading={isLoading} onPress={handleClick}>
|
||||||
<Icon>
|
<Icon>
|
||||||
<Refresh />
|
<RefreshCw />
|
||||||
</Icon>
|
</Icon>
|
||||||
</LoadingButton>
|
</LoadingButton>
|
||||||
<Tooltip>{formatMessage(labels.refresh)}</Tooltip>
|
<Tooltip>{formatMessage(labels.refresh)}</Tooltip>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import { List, ListItem } from '@umami/react-zen';
|
import { IconLabel, List, ListItem } from '@umami/react-zen';
|
||||||
import { useWebsiteSegmentsQuery } from '@/components/hooks';
|
import { useWebsiteSegmentsQuery } from '@/components/hooks';
|
||||||
import { LoadingPanel } from '@/components/common/LoadingPanel';
|
import { LoadingPanel } from '@/components/common/LoadingPanel';
|
||||||
import { Empty } from '@/components/common/Empty';
|
import { Empty } from '@/components/common/Empty';
|
||||||
|
import { ChartPie, UserPlus } from '@/components/icons';
|
||||||
|
|
||||||
export interface SegmentFiltersProps {
|
export interface SegmentFiltersProps {
|
||||||
websiteId: string;
|
websiteId: string;
|
||||||
|
|
@ -29,7 +30,9 @@ export function SegmentFilters({
|
||||||
{data?.data?.map(item => {
|
{data?.data?.map(item => {
|
||||||
return (
|
return (
|
||||||
<ListItem key={item.id} id={item.id}>
|
<ListItem key={item.id} id={item.id}>
|
||||||
{item.name}
|
<IconLabel icon={type === 'segment' ? <ChartPie /> : <UserPlus />}>
|
||||||
|
{item.name}
|
||||||
|
</IconLabel>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
|
import { useCallback, useMemo } from 'react';
|
||||||
import { Button, Icon, Row, Text, Select, ListItem } from '@umami/react-zen';
|
import { Button, Icon, Row, Text, Select, ListItem } from '@umami/react-zen';
|
||||||
import { isAfter } from 'date-fns';
|
import { isAfter } from 'date-fns';
|
||||||
import { ChevronRight } from '@/components/icons';
|
import { ChevronRight } from '@/components/icons';
|
||||||
import { useDateRange, useMessages, useNavigation } from '@/components/hooks';
|
import { useDateRange, useDateRangeQuery, useMessages, useNavigation } from '@/components/hooks';
|
||||||
|
import { getDateRangeValue } from '@/lib/date';
|
||||||
import { DateFilter } from './DateFilter';
|
import { DateFilter } from './DateFilter';
|
||||||
import { getOffsetDateRange } from '@/lib/date';
|
|
||||||
import { useCallback } from 'react';
|
|
||||||
|
|
||||||
export interface WebsiteDateFilterProps {
|
export interface WebsiteDateFilterProps {
|
||||||
websiteId: string;
|
websiteId: string;
|
||||||
|
|
@ -20,30 +20,33 @@ export function WebsiteDateFilter({
|
||||||
showButtons = true,
|
showButtons = true,
|
||||||
allowCompare,
|
allowCompare,
|
||||||
}: WebsiteDateFilterProps) {
|
}: WebsiteDateFilterProps) {
|
||||||
const { dateRange, setDateRange, setDateRangeValue } = useDateRange(websiteId);
|
const { dateRange, isAllTime, isCustomRange } = useDateRange();
|
||||||
const { value, endDate } = dateRange;
|
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
const {
|
const {
|
||||||
router,
|
router,
|
||||||
updateParams,
|
updateParams,
|
||||||
query: { compare = 'prev', offset = 0 },
|
query: { compare = 'prev', offset = 0 },
|
||||||
} = useNavigation();
|
} = useNavigation();
|
||||||
const isAllTime = value === 'all';
|
const disableForward = isAllTime || isAfter(dateRange.endDate, new Date());
|
||||||
|
|
||||||
const isCustomRange = value.startsWith('range');
|
const websiteDateRange = useDateRangeQuery(websiteId);
|
||||||
|
|
||||||
const disableForward = value === 'all' || isAfter(endDate, new Date());
|
|
||||||
|
|
||||||
const handleChange = (date: string) => {
|
const handleChange = (date: string) => {
|
||||||
setDateRangeValue(date);
|
if (date === 'all') {
|
||||||
router.push(updateParams({ date, offset: undefined }));
|
router.push(
|
||||||
|
updateParams({
|
||||||
|
date: getDateRangeValue(websiteDateRange.startDate, websiteDateRange.endDate),
|
||||||
|
offset: undefined,
|
||||||
|
all: 1,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
router.push(updateParams({ date, offset: undefined, all: undefined }));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleIncrement = useCallback(
|
const handleIncrement = useCallback(
|
||||||
(increment: number) => {
|
(increment: number) => {
|
||||||
const offsetDate = getOffsetDateRange(dateRange, +offset + increment);
|
|
||||||
|
|
||||||
setDateRange(offsetDate);
|
|
||||||
router.push(updateParams({ offset: +offset + increment }));
|
router.push(updateParams({ offset: +offset + increment }));
|
||||||
},
|
},
|
||||||
[offset],
|
[offset],
|
||||||
|
|
@ -53,6 +56,12 @@ export function WebsiteDateFilter({
|
||||||
router.push(updateParams({ compare }));
|
router.push(updateParams({ compare }));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const dateValue = useMemo(() => {
|
||||||
|
return offset !== 0
|
||||||
|
? getDateRangeValue(dateRange.startDate, dateRange.endDate)
|
||||||
|
: dateRange.value;
|
||||||
|
}, [dateRange]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row gap>
|
<Row gap>
|
||||||
{showButtons && !isAllTime && !isCustomRange && (
|
{showButtons && !isAllTime && !isCustomRange && (
|
||||||
|
|
@ -71,7 +80,7 @@ export function WebsiteDateFilter({
|
||||||
)}
|
)}
|
||||||
<Row minWidth="200px">
|
<Row minWidth="200px">
|
||||||
<DateFilter
|
<DateFilter
|
||||||
value={value}
|
value={dateValue}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
showAllTime={showAllTime}
|
showAllTime={showAllTime}
|
||||||
renderDate={+offset !== 0}
|
renderDate={+offset !== 0}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Button, Icon, DialogTrigger, Dialog, Modal, Text } from '@umami/react-zen';
|
import { Button, Icon, DialogTrigger, Dialog, Popover, Text } from '@umami/react-zen';
|
||||||
import { ListFilter } from '@/components/icons';
|
import { ListFilter } from '@/components/icons';
|
||||||
import { FilterEditForm } from '@/components/input/FilterEditForm';
|
import { FilterEditForm } from '@/components/input/FilterEditForm';
|
||||||
import { useMessages, useNavigation } from '@/components/hooks';
|
import { useMessages, useNavigation } from '@/components/hooks';
|
||||||
|
|
@ -32,13 +32,13 @@ export function WebsiteFilterButton({
|
||||||
</Icon>
|
</Icon>
|
||||||
{showText && <Text>{formatMessage(labels.filter)}</Text>}
|
{showText && <Text>{formatMessage(labels.filter)}</Text>}
|
||||||
</Button>
|
</Button>
|
||||||
<Modal>
|
<Popover placement="bottom start">
|
||||||
<Dialog title={formatMessage(labels.filters)} style={{ width: 800, minHeight: 600 }}>
|
<Dialog title={formatMessage(labels.filters)} style={{ width: 800, minHeight: 600 }}>
|
||||||
{({ close }) => {
|
{({ close }) => {
|
||||||
return <FilterEditForm websiteId={websiteId} onChange={handleChange} onClose={close} />;
|
return <FilterEditForm websiteId={websiteId} onChange={handleChange} onClose={close} />;
|
||||||
}}
|
}}
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</Modal>
|
</Popover>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
import { useDateRange } from '@/components/hooks';
|
|
||||||
import { dateToRangeValue } from '@/lib/date';
|
|
||||||
import { MonthSelect } from './MonthSelect';
|
|
||||||
|
|
||||||
export function WebsiteMonthSelect({ websiteId }: { websiteId: string }) {
|
|
||||||
const {
|
|
||||||
dateRange: { startDate },
|
|
||||||
saveDateRange,
|
|
||||||
} = useDateRange(websiteId);
|
|
||||||
|
|
||||||
const handleMonthSelect = (date: Date) => {
|
|
||||||
const range = dateToRangeValue(date);
|
|
||||||
saveDateRange(range);
|
|
||||||
};
|
|
||||||
|
|
||||||
return <MonthSelect date={startDate} onChange={handleMonthSelect} />;
|
|
||||||
}
|
|
||||||
|
|
@ -14,7 +14,7 @@ export interface EventsChartProps extends BarChartProps {
|
||||||
export function EventsChart({ websiteId, focusLabel }: EventsChartProps) {
|
export function EventsChart({ websiteId, focusLabel }: EventsChartProps) {
|
||||||
const {
|
const {
|
||||||
dateRange: { startDate, endDate, unit },
|
dateRange: { startDate, endDate, unit },
|
||||||
} = useDateRange(websiteId);
|
} = useDateRange();
|
||||||
const { locale } = useLocale();
|
const { locale } = useLocale();
|
||||||
const { data, isLoading, error } = useWebsiteEventsSeriesQuery(websiteId);
|
const { data, isLoading, error } = useWebsiteEventsSeriesQuery(websiteId);
|
||||||
const [label, setLabel] = useState<string>(focusLabel);
|
const [label, setLabel] = useState<string>(focusLabel);
|
||||||
|
|
|
||||||
|
|
@ -131,14 +131,6 @@ export function parseDateRange(value: string, locale = 'en-US'): DateRange {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (value === 'all') {
|
|
||||||
return {
|
|
||||||
startDate: new Date(0),
|
|
||||||
endDate: new Date(1),
|
|
||||||
value,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value.startsWith('range')) {
|
if (value.startsWith('range')) {
|
||||||
const [, startTime, endTime] = value.split(':');
|
const [, startTime, endTime] = value.split(':');
|
||||||
|
|
||||||
|
|
@ -225,24 +217,28 @@ export function getOffsetDateRange(dateRange: DateRange, offset: number) {
|
||||||
case 'day':
|
case 'day':
|
||||||
return {
|
return {
|
||||||
...dateRange,
|
...dateRange,
|
||||||
|
offset,
|
||||||
startDate: addDays(startDate, change),
|
startDate: addDays(startDate, change),
|
||||||
endDate: addDays(endDate, change),
|
endDate: addDays(endDate, change),
|
||||||
};
|
};
|
||||||
case 'week':
|
case 'week':
|
||||||
return {
|
return {
|
||||||
...dateRange,
|
...dateRange,
|
||||||
|
offset,
|
||||||
startDate: addWeeks(startDate, change),
|
startDate: addWeeks(startDate, change),
|
||||||
endDate: addWeeks(endDate, change),
|
endDate: addWeeks(endDate, change),
|
||||||
};
|
};
|
||||||
case 'month':
|
case 'month':
|
||||||
return {
|
return {
|
||||||
...dateRange,
|
...dateRange,
|
||||||
|
offset,
|
||||||
startDate: addMonths(startDate, change),
|
startDate: addMonths(startDate, change),
|
||||||
endDate: addMonths(endDate, change),
|
endDate: addMonths(endDate, change),
|
||||||
};
|
};
|
||||||
case 'year':
|
case 'year':
|
||||||
return {
|
return {
|
||||||
...dateRange,
|
...dateRange,
|
||||||
|
offset,
|
||||||
startDate: addYears(startDate, change),
|
startDate: addYears(startDate, change),
|
||||||
endDate: addYears(endDate, change),
|
endDate: addYears(endDate, change),
|
||||||
};
|
};
|
||||||
|
|
@ -250,6 +246,7 @@ export function getOffsetDateRange(dateRange: DateRange, offset: number) {
|
||||||
return {
|
return {
|
||||||
startDate: add(startDate, change),
|
startDate: add(startDate, change),
|
||||||
endDate: add(endDate, change),
|
endDate: add(endDate, change),
|
||||||
|
offset,
|
||||||
value,
|
value,
|
||||||
unit,
|
unit,
|
||||||
num,
|
num,
|
||||||
|
|
@ -354,6 +351,10 @@ export function generateTimeSeries(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function dateToRangeValue(date: Date) {
|
export function getDateRangeValue(startDate: Date, endDate: Date) {
|
||||||
return `range:${startOfMonth(date).getTime()}:${endOfMonth(date).getTime()}`;
|
return `range:${startDate.getTime()}:${endDate.getTime()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMonthDateRangeValue(date: Date) {
|
||||||
|
return getDateRangeValue(startOfMonth(date), endOfMonth(date));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -284,7 +284,7 @@ function getClient() {
|
||||||
replicaUrl: process.env.DATABASE_REPLICA_URL,
|
replicaUrl: process.env.DATABASE_REPLICA_URL,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (process.env.NODE_ENV !== 'production') {
|
if (!globalThis[PRISMA]) {
|
||||||
globalThis[PRISMA] = prisma.client;
|
globalThis[PRISMA] = prisma.client;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -203,3 +203,11 @@ export async function deleteWebsite(websiteId: string) {
|
||||||
return data;
|
return data;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getWebsiteCount(userId: string) {
|
||||||
|
return prisma.client.website.count({
|
||||||
|
where: {
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,8 @@ async function relationalQuery(websiteId: string) {
|
||||||
const result = await rawQuery(
|
const result = await rawQuery(
|
||||||
`
|
`
|
||||||
select
|
select
|
||||||
min(created_at) as mindate,
|
min(created_at) as startDate,
|
||||||
max(created_at) as maxdate
|
max(created_at) as endDate
|
||||||
from website_event
|
from website_event
|
||||||
where website_id = {{websiteId::uuid}}
|
where website_id = {{websiteId::uuid}}
|
||||||
and created_at >= {{startDate}}
|
and created_at >= {{startDate}}
|
||||||
|
|
@ -42,8 +42,8 @@ async function clickhouseQuery(websiteId: string) {
|
||||||
const result = await rawQuery(
|
const result = await rawQuery(
|
||||||
`
|
`
|
||||||
select
|
select
|
||||||
min(created_at) as mindate,
|
min(created_at) as startDate,
|
||||||
max(created_at) as maxdate
|
max(created_at) as endDate
|
||||||
from website_event_stats_hourly
|
from website_event_stats_hourly
|
||||||
where website_id = {websiteId:UUID}
|
where website_id = {websiteId:UUID}
|
||||||
and created_at >= {startDate:DateTime64}
|
and created_at >= {startDate:DateTime64}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue