mirror of
https://github.com/umami-software/umami.git
synced 2026-02-04 04:37:11 +01:00
Compare commits
47 commits
a37de757a0
...
abfb78bb98
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
abfb78bb98 | ||
|
|
9b310dacef | ||
|
|
489c2712d1 | ||
|
|
b43e7fd3a7 | ||
|
|
5880eae4e4 | ||
|
|
6169a58e86 | ||
|
|
452a385c4e | ||
|
|
482d6c1e47 | ||
|
|
4e8be724ac | ||
|
|
c9e14f3bce | ||
|
|
d028bfa1f5 | ||
|
|
78d467b478 | ||
|
|
9d3f5ad0fd | ||
|
|
018e76b067 | ||
|
|
2df24a78ca | ||
|
|
9339383497 | ||
|
|
b84942b6da | ||
|
|
3d75246802 | ||
|
|
376c570d26 | ||
|
|
558cb3b041 | ||
|
|
9e7285cf2b | ||
|
|
c5aa8be15c | ||
|
|
daccd22ab2 | ||
|
|
b958403224 | ||
|
|
752f395d83 | ||
|
|
c7a0d65590 | ||
|
|
c3dad5b7ef | ||
|
|
9426de90f7 | ||
|
|
c527819fd4 | ||
|
|
f7bdd5c54e | ||
|
|
f32ab11785 | ||
|
|
dde1c3a57a | ||
|
|
dd3649bbf7 | ||
|
|
67cdfdfb7e | ||
|
|
a0886c0594 | ||
|
|
4680c89e28 | ||
|
|
fdafe13c35 | ||
|
|
e782c2e627 | ||
|
|
801a3ec6bb | ||
|
|
4a09f2bff6 | ||
|
|
c9f6653b62 | ||
|
|
af7f7adf5b | ||
|
|
4c9b2f10da | ||
|
|
adea3e9b1c | ||
|
|
518f0b66c6 | ||
|
|
f84e67b0e6 | ||
|
|
52d9dd2871 |
52 changed files with 809 additions and 855 deletions
94
.gitignore
vendored
94
.gitignore
vendored
|
|
@ -1,46 +1,48 @@
|
||||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
# dependencies
|
# dependencies
|
||||||
node_modules
|
node_modules
|
||||||
.pnp
|
.pnp
|
||||||
.pnp.js
|
.pnp.js
|
||||||
.pnpm-store
|
.pnpm-store
|
||||||
package-lock.json
|
package-lock.json
|
||||||
|
|
||||||
# testing
|
# testing
|
||||||
/coverage
|
/coverage
|
||||||
|
|
||||||
# next.js
|
# next.js
|
||||||
/.next
|
/.next
|
||||||
/out
|
/out
|
||||||
|
|
||||||
# production
|
# production
|
||||||
/build
|
/build
|
||||||
/public/script.js
|
/public/script.js
|
||||||
/geo
|
/geo
|
||||||
/dist
|
/dist
|
||||||
/generated
|
/generated
|
||||||
/src/generated
|
/src/generated
|
||||||
pm2.yml
|
pm2.yml
|
||||||
|
|
||||||
# misc
|
# misc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.idea
|
.idea
|
||||||
.yarn
|
.yarn
|
||||||
*.iml
|
*.iml
|
||||||
*.log
|
*.log
|
||||||
.vscode
|
.vscode
|
||||||
.tool-versions
|
.tool-versions
|
||||||
|
.claude
|
||||||
# debug
|
nul
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
# debug
|
||||||
yarn-error.log*
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
# local env files
|
yarn-error.log*
|
||||||
.env
|
|
||||||
.env.*
|
# local env files
|
||||||
*.env.*
|
.env
|
||||||
|
.env.*
|
||||||
*.dev.yml
|
*.env.*
|
||||||
|
|
||||||
|
*.dev.yml
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev -p 3001 --turbo",
|
"dev": "next dev -p 3002 --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",
|
||||||
|
|
@ -102,7 +102,7 @@
|
||||||
"kafkajs": "^2.1.0",
|
"kafkajs": "^2.1.0",
|
||||||
"lucide-react": "^0.543.0",
|
"lucide-react": "^0.543.0",
|
||||||
"maxmind": "^5.0.0",
|
"maxmind": "^5.0.0",
|
||||||
"next": "^15.5.9",
|
"next": "^15.5.10",
|
||||||
"node-fetch": "^3.2.8",
|
"node-fetch": "^3.2.8",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"papaparse": "^5.5.3",
|
"papaparse": "^5.5.3",
|
||||||
|
|
@ -164,7 +164,7 @@
|
||||||
"stylelint-config-css-modules": "^4.5.1",
|
"stylelint-config-css-modules": "^4.5.1",
|
||||||
"stylelint-config-prettier": "^9.0.3",
|
"stylelint-config-prettier": "^9.0.3",
|
||||||
"stylelint-config-recommended": "^14.0.0",
|
"stylelint-config-recommended": "^14.0.0",
|
||||||
"tar": "^7.5.4",
|
"tar": "^7.5.7",
|
||||||
"ts-jest": "^29.4.6",
|
"ts-jest": "^29.4.6",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
"tsup": "^8.5.0",
|
"tsup": "^8.5.0",
|
||||||
|
|
|
||||||
617
pnpm-lock.yaml
generated
617
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
|
@ -42,7 +42,7 @@ export function MobileNav() {
|
||||||
{({ close }) => {
|
{({ close }) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<NavMenu padding="3" onItemClick={close} border="bottom">
|
<NavMenu padding="3" onItemClick={close} border="bottom" width="100%">
|
||||||
<NavButton />
|
<NavButton />
|
||||||
{links.map(link => {
|
{links.map(link => {
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,6 @@ export function PixelsTable({ showActions, ...props }: PixelsTableProps) {
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
const { renderUrl } = useNavigation();
|
const { renderUrl } = useNavigation();
|
||||||
const { getSlugUrl } = useSlug('pixel');
|
const { getSlugUrl } = useSlug('pixel');
|
||||||
console.log(showActions);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DataTable {...props}>
|
<DataTable {...props}>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Box, Column, Dialog, Grid, Icon, ProgressBar, Row, Text } from '@umami/react-zen';
|
import { Box, Column, Dialog, Grid, Icon, ProgressBar, Row, Text } from '@umami/react-zen';
|
||||||
import { LoadingPanel } from '@/components/common/LoadingPanel';
|
import { LoadingPanel } from '@/components/common/LoadingPanel';
|
||||||
import { useMessages, useResultQuery } from '@/components/hooks';
|
import { useMessages, useNavigation, useResultQuery } from '@/components/hooks';
|
||||||
import { File, User } from '@/components/icons';
|
import { File, User } from '@/components/icons';
|
||||||
import { ReportEditButton } from '@/components/input/ReportEditButton';
|
import { ReportEditButton } from '@/components/input/ReportEditButton';
|
||||||
import { ChangeLabel } from '@/components/metrics/ChangeLabel';
|
import { ChangeLabel } from '@/components/metrics/ChangeLabel';
|
||||||
|
|
@ -20,6 +20,8 @@ type FunnelResult = {
|
||||||
|
|
||||||
export function Funnel({ id, name, type, parameters, websiteId }) {
|
export function Funnel({ id, name, type, parameters, websiteId }) {
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
|
const { pathname } = useNavigation();
|
||||||
|
const isSharePage = pathname.includes('/share/');
|
||||||
const { data, error, isLoading } = useResultQuery(type, {
|
const { data, error, isLoading } = useResultQuery(type, {
|
||||||
websiteId,
|
websiteId,
|
||||||
...parameters,
|
...parameters,
|
||||||
|
|
@ -36,21 +38,22 @@ export function Funnel({ id, name, type, parameters, websiteId }) {
|
||||||
</Text>
|
</Text>
|
||||||
</Row>
|
</Row>
|
||||||
</Column>
|
</Column>
|
||||||
<Column>
|
{!isSharePage && (
|
||||||
<ReportEditButton id={id} name={name} type={type}>
|
<Column>
|
||||||
{({ close }) => {
|
<ReportEditButton id={id} name={name} type={type}>
|
||||||
return (
|
{({ close }) => {
|
||||||
<Dialog
|
return (
|
||||||
title={formatMessage(labels.funnel)}
|
<Dialog
|
||||||
variant="modal"
|
title={formatMessage(labels.funnel)}
|
||||||
style={{ minHeight: 300, minWidth: 400 }}
|
style={{ minHeight: 300, minWidth: 400 }}
|
||||||
>
|
>
|
||||||
<FunnelEditForm id={id} websiteId={websiteId} onClose={close} />
|
<FunnelEditForm id={id} websiteId={websiteId} onClose={close} />
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
</ReportEditButton>
|
</ReportEditButton>
|
||||||
</Column>
|
</Column>
|
||||||
|
)}
|
||||||
</Grid>
|
</Grid>
|
||||||
{data?.map(
|
{data?.map(
|
||||||
(
|
(
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteContro
|
||||||
import { LoadingPanel } from '@/components/common/LoadingPanel';
|
import { LoadingPanel } from '@/components/common/LoadingPanel';
|
||||||
import { Panel } from '@/components/common/Panel';
|
import { Panel } from '@/components/common/Panel';
|
||||||
import { SectionHeader } from '@/components/common/SectionHeader';
|
import { SectionHeader } from '@/components/common/SectionHeader';
|
||||||
import { useDateRange, useReportsQuery } from '@/components/hooks';
|
import { useDateRange, useNavigation, useReportsQuery } from '@/components/hooks';
|
||||||
import { Funnel } from './Funnel';
|
import { Funnel } from './Funnel';
|
||||||
import { FunnelAddButton } from './FunnelAddButton';
|
import { FunnelAddButton } from './FunnelAddButton';
|
||||||
|
|
||||||
|
|
@ -13,13 +13,17 @@ export function FunnelsPage({ websiteId }: { websiteId: string }) {
|
||||||
const {
|
const {
|
||||||
dateRange: { startDate, endDate },
|
dateRange: { startDate, endDate },
|
||||||
} = useDateRange();
|
} = useDateRange();
|
||||||
|
const { pathname } = useNavigation();
|
||||||
|
const isSharePage = pathname.includes('/share/');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column gap>
|
<Column gap>
|
||||||
<WebsiteControls websiteId={websiteId} />
|
<WebsiteControls websiteId={websiteId} />
|
||||||
<SectionHeader>
|
{!isSharePage && (
|
||||||
<FunnelAddButton websiteId={websiteId} />
|
<SectionHeader>
|
||||||
</SectionHeader>
|
<FunnelAddButton websiteId={websiteId} />
|
||||||
|
</SectionHeader>
|
||||||
|
)}
|
||||||
<LoadingPanel data={data} isLoading={isLoading} error={error}>
|
<LoadingPanel data={data} isLoading={isLoading} error={error}>
|
||||||
{data && (
|
{data && (
|
||||||
<Grid gap>
|
<Grid gap>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Column, Dialog, Grid, Icon, ProgressBar, Row, Text } from '@umami/react-zen';
|
import { Column, Dialog, Grid, Icon, ProgressBar, Row, Text } from '@umami/react-zen';
|
||||||
import { LoadingPanel } from '@/components/common/LoadingPanel';
|
import { LoadingPanel } from '@/components/common/LoadingPanel';
|
||||||
import { useMessages, useResultQuery } from '@/components/hooks';
|
import { useMessages, useNavigation, useResultQuery } from '@/components/hooks';
|
||||||
import { File, User } from '@/components/icons';
|
import { File, User } from '@/components/icons';
|
||||||
import { ReportEditButton } from '@/components/input/ReportEditButton';
|
import { ReportEditButton } from '@/components/input/ReportEditButton';
|
||||||
import { Lightning } from '@/components/svg';
|
import { Lightning } from '@/components/svg';
|
||||||
|
|
@ -25,6 +25,8 @@ export type GoalData = { num: number; total: number };
|
||||||
|
|
||||||
export function Goal({ id, name, type, parameters, websiteId, startDate, endDate }: GoalProps) {
|
export function Goal({ id, name, type, parameters, websiteId, startDate, endDate }: GoalProps) {
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
|
const { pathname } = useNavigation();
|
||||||
|
const isSharePage = pathname.includes('/share/');
|
||||||
const { data, error, isLoading, isFetching } = useResultQuery<GoalData>(type, {
|
const { data, error, isLoading, isFetching } = useResultQuery<GoalData>(type, {
|
||||||
websiteId,
|
websiteId,
|
||||||
startDate,
|
startDate,
|
||||||
|
|
@ -45,21 +47,23 @@ export function Goal({ id, name, type, parameters, websiteId, startDate, endDate
|
||||||
</Text>
|
</Text>
|
||||||
</Row>
|
</Row>
|
||||||
</Column>
|
</Column>
|
||||||
<Column>
|
{!isSharePage && (
|
||||||
<ReportEditButton id={id} name={name} type={type}>
|
<Column>
|
||||||
{({ close }) => {
|
<ReportEditButton id={id} name={name} type={type}>
|
||||||
return (
|
{({ close }) => {
|
||||||
<Dialog
|
return (
|
||||||
title={formatMessage(labels.goal)}
|
<Dialog
|
||||||
variant="modal"
|
title={formatMessage(labels.goal)}
|
||||||
style={{ minHeight: 300, minWidth: 400 }}
|
variant="modal"
|
||||||
>
|
style={{ minHeight: 300, minWidth: 400 }}
|
||||||
<GoalEditForm id={id} websiteId={websiteId} onClose={close} />
|
>
|
||||||
</Dialog>
|
<GoalEditForm id={id} websiteId={websiteId} onClose={close} />
|
||||||
);
|
</Dialog>
|
||||||
}}
|
);
|
||||||
</ReportEditButton>
|
}}
|
||||||
</Column>
|
</ReportEditButton>
|
||||||
|
</Column>
|
||||||
|
)}
|
||||||
</Grid>
|
</Grid>
|
||||||
<Row alignItems="center" justifyContent="space-between" gap>
|
<Row alignItems="center" justifyContent="space-between" gap>
|
||||||
<Text color="muted">
|
<Text color="muted">
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteContro
|
||||||
import { LoadingPanel } from '@/components/common/LoadingPanel';
|
import { LoadingPanel } from '@/components/common/LoadingPanel';
|
||||||
import { Panel } from '@/components/common/Panel';
|
import { Panel } from '@/components/common/Panel';
|
||||||
import { SectionHeader } from '@/components/common/SectionHeader';
|
import { SectionHeader } from '@/components/common/SectionHeader';
|
||||||
import { useDateRange, useReportsQuery } from '@/components/hooks';
|
import { useDateRange, useNavigation, useReportsQuery } from '@/components/hooks';
|
||||||
import { Goal } from './Goal';
|
import { Goal } from './Goal';
|
||||||
import { GoalAddButton } from './GoalAddButton';
|
import { GoalAddButton } from './GoalAddButton';
|
||||||
|
|
||||||
|
|
@ -13,13 +13,17 @@ export function GoalsPage({ websiteId }: { websiteId: string }) {
|
||||||
const {
|
const {
|
||||||
dateRange: { startDate, endDate },
|
dateRange: { startDate, endDate },
|
||||||
} = useDateRange();
|
} = useDateRange();
|
||||||
|
const { pathname } = useNavigation();
|
||||||
|
const isSharePage = pathname.includes('/share/');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column gap>
|
<Column gap>
|
||||||
<WebsiteControls websiteId={websiteId} />
|
<WebsiteControls websiteId={websiteId} />
|
||||||
<SectionHeader>
|
{!isSharePage && (
|
||||||
<GoalAddButton websiteId={websiteId} />
|
<SectionHeader>
|
||||||
</SectionHeader>
|
<GoalAddButton websiteId={websiteId} />
|
||||||
|
</SectionHeader>
|
||||||
|
)}
|
||||||
<LoadingPanel data={data} isLoading={isLoading} error={error}>
|
<LoadingPanel data={data} isLoading={isLoading} error={error}>
|
||||||
{data && (
|
{data && (
|
||||||
<Grid columns={{ xs: '1fr', md: '1fr 1fr' }} gap>
|
<Grid columns={{ xs: '1fr', md: '1fr 1fr' }} gap>
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,13 @@ import { useMessages, useNavigation, useWebsite } from '@/components/hooks';
|
||||||
import { Edit } from '@/components/icons';
|
import { Edit } from '@/components/icons';
|
||||||
import { ActiveUsers } from '@/components/metrics/ActiveUsers';
|
import { ActiveUsers } from '@/components/metrics/ActiveUsers';
|
||||||
|
|
||||||
export function WebsiteHeader({ showActions }: { showActions?: boolean }) {
|
export function WebsiteHeader({
|
||||||
|
showActions,
|
||||||
|
allowLink = true,
|
||||||
|
}: {
|
||||||
|
showActions?: boolean;
|
||||||
|
allowLink?: boolean;
|
||||||
|
}) {
|
||||||
const website = useWebsite();
|
const website = useWebsite();
|
||||||
const { renderUrl, pathname } = useNavigation();
|
const { renderUrl, pathname } = useNavigation();
|
||||||
const isSettings = pathname.endsWith('/settings');
|
const isSettings = pathname.endsWith('/settings');
|
||||||
|
|
@ -21,7 +27,7 @@ export function WebsiteHeader({ showActions }: { showActions?: boolean }) {
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title={website.name}
|
title={website.name}
|
||||||
icon={<Favicon domain={website.domain} />}
|
icon={<Favicon domain={website.domain} />}
|
||||||
titleHref={renderUrl(`/websites/${website.id}`, false)}
|
titleHref={allowLink ? renderUrl(`/websites/${website.id}`, false) : undefined}
|
||||||
>
|
>
|
||||||
<Row alignItems="center" gap="6" wrap="wrap">
|
<Row alignItems="center" gap="6" wrap="wrap">
|
||||||
<ActiveUsers websiteId={website.id} />
|
<ActiveUsers websiteId={website.id} />
|
||||||
|
|
|
||||||
|
|
@ -9,11 +9,10 @@ import { WebsiteNav } from './WebsiteNav';
|
||||||
export function WebsiteLayout({ websiteId, children }: { websiteId: string; children: ReactNode }) {
|
export function WebsiteLayout({ websiteId, children }: { websiteId: string; children: ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<WebsiteProvider websiteId={websiteId}>
|
<WebsiteProvider websiteId={websiteId}>
|
||||||
<Grid columns={{ xs: '1fr', lg: 'auto 1fr' }} width="100%" height="100%">
|
<Grid columns={{ xs: '1fr', lg: 'auto 1fr' }} width="100%">
|
||||||
<Column
|
<Column
|
||||||
display={{ xs: 'none', lg: 'flex' }}
|
display={{ xs: 'none', lg: 'flex' }}
|
||||||
width="240px"
|
width="240px"
|
||||||
height="100%"
|
|
||||||
border="right"
|
border="right"
|
||||||
backgroundColor
|
backgroundColor
|
||||||
marginRight="2"
|
marginRight="2"
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,13 @@
|
||||||
import { Grid, Heading, Row, Tab, TabList, TabPanel, Tabs } from '@umami/react-zen';
|
import { Grid, Heading, Row, Tab, TabList, TabPanel, Tabs } 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 { useMessages, useNavigation } from '@/components/hooks';
|
import { useMessages } from '@/components/hooks';
|
||||||
import { EventsChart } from '@/components/metrics/EventsChart';
|
|
||||||
import { MetricsTable } from '@/components/metrics/MetricsTable';
|
import { MetricsTable } from '@/components/metrics/MetricsTable';
|
||||||
import { WeeklyTraffic } from '@/components/metrics/WeeklyTraffic';
|
import { WeeklyTraffic } from '@/components/metrics/WeeklyTraffic';
|
||||||
import { WorldMap } from '@/components/metrics/WorldMap';
|
import { WorldMap } from '@/components/metrics/WorldMap';
|
||||||
|
|
||||||
export function WebsitePanels({ websiteId }: { websiteId: string }) {
|
export function WebsitePanels({ websiteId }: { websiteId: string }) {
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
const { pathname } = useNavigation();
|
|
||||||
const tableProps = {
|
const tableProps = {
|
||||||
websiteId,
|
websiteId,
|
||||||
limit: 10,
|
limit: 10,
|
||||||
|
|
@ -18,7 +16,6 @@ export function WebsitePanels({ websiteId }: { websiteId: string }) {
|
||||||
metric: formatMessage(labels.visitors),
|
metric: formatMessage(labels.visitors),
|
||||||
};
|
};
|
||||||
const rowProps = { minHeight: '570px' };
|
const rowProps = { minHeight: '570px' };
|
||||||
const isSharePage = pathname.includes('/share/');
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid gap="3">
|
<Grid gap="3">
|
||||||
|
|
@ -116,25 +113,6 @@ export function WebsitePanels({ websiteId }: { websiteId: string }) {
|
||||||
<WeeklyTraffic websiteId={websiteId} />
|
<WeeklyTraffic websiteId={websiteId} />
|
||||||
</Panel>
|
</Panel>
|
||||||
</GridRow>
|
</GridRow>
|
||||||
{isSharePage && (
|
|
||||||
<GridRow layout="two-one" {...rowProps}>
|
|
||||||
<Panel>
|
|
||||||
<Heading size="2">{formatMessage(labels.events)}</Heading>
|
|
||||||
<Row border="bottom" marginBottom="4" />
|
|
||||||
<MetricsTable
|
|
||||||
websiteId={websiteId}
|
|
||||||
type="event"
|
|
||||||
title={formatMessage(labels.event)}
|
|
||||||
metric={formatMessage(labels.count)}
|
|
||||||
limit={15}
|
|
||||||
filterLink={false}
|
|
||||||
/>
|
|
||||||
</Panel>
|
|
||||||
<Panel gridColumn={{ xs: 'span 1', md: 'span 2' }}>
|
|
||||||
<EventsChart websiteId={websiteId} />
|
|
||||||
</Panel>
|
|
||||||
</GridRow>
|
|
||||||
)}
|
|
||||||
</Grid>
|
</Grid>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,81 +0,0 @@
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
Checkbox,
|
|
||||||
Column,
|
|
||||||
Form,
|
|
||||||
FormField,
|
|
||||||
FormSubmitButton,
|
|
||||||
Row,
|
|
||||||
Text,
|
|
||||||
} from '@umami/react-zen';
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { useApi, useMessages, useModified } from '@/components/hooks';
|
|
||||||
import { SHARE_NAV_ITEMS } from './constants';
|
|
||||||
|
|
||||||
export interface ShareCreateFormProps {
|
|
||||||
websiteId: string;
|
|
||||||
onSave?: () => void;
|
|
||||||
onClose?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ShareCreateForm({ websiteId, onSave, onClose }: ShareCreateFormProps) {
|
|
||||||
const { formatMessage, labels } = useMessages();
|
|
||||||
const { post } = useApi();
|
|
||||||
const { touch } = useModified();
|
|
||||||
const [isPending, setIsPending] = useState(false);
|
|
||||||
|
|
||||||
// Build default values - only overview and events enabled by default
|
|
||||||
const defaultValues: Record<string, boolean> = {};
|
|
||||||
SHARE_NAV_ITEMS.forEach(section => {
|
|
||||||
section.items.forEach(item => {
|
|
||||||
defaultValues[item.id] = item.id === 'overview' || item.id === 'events';
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleSubmit = async (data: any) => {
|
|
||||||
setIsPending(true);
|
|
||||||
try {
|
|
||||||
const parameters: Record<string, boolean> = {};
|
|
||||||
SHARE_NAV_ITEMS.forEach(section => {
|
|
||||||
section.items.forEach(item => {
|
|
||||||
parameters[item.id] = data[item.id] ?? false;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
await post(`/websites/${websiteId}/shares`, { parameters });
|
|
||||||
touch('shares');
|
|
||||||
onSave?.();
|
|
||||||
onClose?.();
|
|
||||||
} finally {
|
|
||||||
setIsPending(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Form onSubmit={handleSubmit} defaultValues={defaultValues}>
|
|
||||||
<Column gap="3">
|
|
||||||
{SHARE_NAV_ITEMS.map(section => (
|
|
||||||
<Column key={section.section} gap="1">
|
|
||||||
<Text size="2" weight="bold">
|
|
||||||
{formatMessage((labels as any)[section.section])}
|
|
||||||
</Text>
|
|
||||||
<Column gap="1">
|
|
||||||
{section.items.map(item => (
|
|
||||||
<FormField key={item.id} name={item.id}>
|
|
||||||
<Checkbox>{formatMessage((labels as any)[item.label])}</Checkbox>
|
|
||||||
</FormField>
|
|
||||||
))}
|
|
||||||
</Column>
|
|
||||||
</Column>
|
|
||||||
))}
|
|
||||||
<Row justifyContent="flex-end" paddingTop="3" gap="3">
|
|
||||||
{onClose && (
|
|
||||||
<Button isDisabled={isPending} onPress={onClose}>
|
|
||||||
{formatMessage(labels.cancel)}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<FormSubmitButton isDisabled={isPending}>{formatMessage(labels.save)}</FormSubmitButton>
|
|
||||||
</Row>
|
|
||||||
</Column>
|
|
||||||
</Form>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -5,6 +5,7 @@ import {
|
||||||
Form,
|
Form,
|
||||||
FormField,
|
FormField,
|
||||||
FormSubmitButton,
|
FormSubmitButton,
|
||||||
|
Grid,
|
||||||
Label,
|
Label,
|
||||||
Loading,
|
Loading,
|
||||||
Row,
|
Row,
|
||||||
|
|
@ -13,25 +14,30 @@ import {
|
||||||
} from '@umami/react-zen';
|
} from '@umami/react-zen';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useApi, useConfig, useMessages, useModified } from '@/components/hooks';
|
import { useApi, useConfig, useMessages, useModified } from '@/components/hooks';
|
||||||
import { useUpdateQuery } from '@/components/hooks/queries/useUpdateQuery';
|
|
||||||
import { SHARE_NAV_ITEMS } from './constants';
|
import { SHARE_NAV_ITEMS } from './constants';
|
||||||
|
|
||||||
export function ShareEditForm({
|
export function ShareEditForm({
|
||||||
shareId,
|
shareId,
|
||||||
|
websiteId,
|
||||||
onSave,
|
onSave,
|
||||||
onClose,
|
onClose,
|
||||||
}: {
|
}: {
|
||||||
shareId: string;
|
shareId?: string;
|
||||||
|
websiteId?: string;
|
||||||
onSave?: () => void;
|
onSave?: () => void;
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
}) {
|
}) {
|
||||||
const { formatMessage, labels, messages, getErrorMessage } = useMessages();
|
const { formatMessage, labels, getErrorMessage } = useMessages();
|
||||||
const { mutateAsync, error, isPending, touch, toast } = useUpdateQuery(`/share/id/${shareId}`);
|
|
||||||
const { cloudMode } = useConfig();
|
const { cloudMode } = useConfig();
|
||||||
const { get } = useApi();
|
const { get, post } = useApi();
|
||||||
|
const { touch } = useModified();
|
||||||
const { modified } = useModified('shares');
|
const { modified } = useModified('shares');
|
||||||
const [share, setShare] = useState<any>(null);
|
const [share, setShare] = useState<any>(null);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(!!shareId);
|
||||||
|
const [isPending, setIsPending] = useState(false);
|
||||||
|
const [error, setError] = useState<any>(null);
|
||||||
|
|
||||||
|
const isEditing = !!shareId;
|
||||||
|
|
||||||
const getUrl = (slug: string) => {
|
const getUrl = (slug: string) => {
|
||||||
if (cloudMode) {
|
if (cloudMode) {
|
||||||
|
|
@ -41,6 +47,8 @@ export function ShareEditForm({
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!shareId) return;
|
||||||
|
|
||||||
const loadShare = async () => {
|
const loadShare = async () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
|
|
@ -61,27 +69,35 @@ export function ShareEditForm({
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
await mutateAsync(
|
setIsPending(true);
|
||||||
{ slug: share.slug, parameters },
|
setError(null);
|
||||||
{
|
|
||||||
onSuccess: async () => {
|
try {
|
||||||
toast(formatMessage(messages.saved));
|
if (isEditing) {
|
||||||
touch('shares');
|
await post(`/share/id/${shareId}`, { name: data.name, slug: share.slug, parameters });
|
||||||
onSave?.();
|
} else {
|
||||||
onClose?.();
|
await post(`/websites/${websiteId}/shares`, { name: data.name, parameters });
|
||||||
},
|
}
|
||||||
},
|
touch('shares');
|
||||||
);
|
onSave?.();
|
||||||
|
onClose?.();
|
||||||
|
} catch (e) {
|
||||||
|
setError(e);
|
||||||
|
} finally {
|
||||||
|
setIsPending(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <Loading placement="absolute" />;
|
return <Loading placement="absolute" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = getUrl(share?.slug || '');
|
const url = isEditing ? getUrl(share?.slug || '') : null;
|
||||||
|
|
||||||
// Build default values from share parameters
|
// Build default values from share parameters
|
||||||
const defaultValues: Record<string, boolean> = {};
|
const defaultValues: Record<string, any> = {
|
||||||
|
name: share?.name || '',
|
||||||
|
};
|
||||||
SHARE_NAV_ITEMS.forEach(section => {
|
SHARE_NAV_ITEMS.forEach(section => {
|
||||||
section.items.forEach(item => {
|
section.items.forEach(item => {
|
||||||
const defaultSelected = item.id === 'overview' || item.id === 'events';
|
const defaultSelected = item.id === 'overview' || item.id === 'events';
|
||||||
|
|
@ -89,34 +105,60 @@ export function ShareEditForm({
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Get all item ids for validation
|
||||||
|
const allItemIds = SHARE_NAV_ITEMS.flatMap(section => section.items.map(item => item.id));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form onSubmit={handleSubmit} error={getErrorMessage(error)} defaultValues={defaultValues}>
|
<Form onSubmit={handleSubmit} error={getErrorMessage(error)} defaultValues={defaultValues}>
|
||||||
<Column gap="3">
|
{({ watch }) => {
|
||||||
<Column>
|
const values = watch();
|
||||||
<Label>{formatMessage(labels.shareUrl)}</Label>
|
const hasSelection = allItemIds.some(id => values[id]);
|
||||||
<TextField value={url} isReadOnly allowCopy />
|
|
||||||
</Column>
|
return (
|
||||||
{SHARE_NAV_ITEMS.map(section => (
|
<Column gap="6">
|
||||||
<Column key={section.section} gap="1">
|
{url && (
|
||||||
<Text weight="bold">{formatMessage((labels as any)[section.section])}</Text>
|
<Column>
|
||||||
<Column gap="1">
|
<Label>{formatMessage(labels.shareUrl)}</Label>
|
||||||
{section.items.map(item => (
|
<TextField value={url} isReadOnly allowCopy />
|
||||||
<FormField key={item.id} name={item.id}>
|
</Column>
|
||||||
<Checkbox>{formatMessage((labels as any)[item.label])}</Checkbox>
|
)}
|
||||||
</FormField>
|
<FormField
|
||||||
|
label={formatMessage(labels.name)}
|
||||||
|
name="name"
|
||||||
|
rules={{ required: formatMessage(labels.required) }}
|
||||||
|
>
|
||||||
|
<TextField autoComplete="off" autoFocus={!isEditing} />
|
||||||
|
</FormField>
|
||||||
|
<Grid columns="repeat(auto-fit, minmax(150px, 1fr))" gap="3">
|
||||||
|
{SHARE_NAV_ITEMS.map(section => (
|
||||||
|
<Column key={section.section} gap="3">
|
||||||
|
<Text weight="bold">{formatMessage((labels as any)[section.section])}</Text>
|
||||||
|
<Column gap="1">
|
||||||
|
{section.items.map(item => (
|
||||||
|
<FormField key={item.id} name={item.id}>
|
||||||
|
<Checkbox>{formatMessage((labels as any)[item.label])}</Checkbox>
|
||||||
|
</FormField>
|
||||||
|
))}
|
||||||
|
</Column>
|
||||||
|
</Column>
|
||||||
))}
|
))}
|
||||||
</Column>
|
</Grid>
|
||||||
|
<Row justifyContent="flex-end" paddingTop="3" gap="3">
|
||||||
|
{onClose && (
|
||||||
|
<Button isDisabled={isPending} onPress={onClose}>
|
||||||
|
{formatMessage(labels.cancel)}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<FormSubmitButton
|
||||||
|
variant="primary"
|
||||||
|
isDisabled={isPending || !hasSelection || !values.name}
|
||||||
|
>
|
||||||
|
{formatMessage(labels.save)}
|
||||||
|
</FormSubmitButton>
|
||||||
|
</Row>
|
||||||
</Column>
|
</Column>
|
||||||
))}
|
);
|
||||||
<Row justifyContent="flex-end" paddingTop="3" gap="3">
|
}}
|
||||||
{onClose && (
|
|
||||||
<Button isDisabled={isPending} onPress={onClose}>
|
|
||||||
{formatMessage(labels.cancel)}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<FormSubmitButton variant="primary">{formatMessage(labels.save)}</FormSubmitButton>
|
|
||||||
</Row>
|
|
||||||
</Column>
|
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,25 @@
|
||||||
import { DataColumn, DataTable, type DataTableProps, Row } from '@umami/react-zen';
|
import { DataColumn, DataTable, type DataTableProps, Row } from '@umami/react-zen';
|
||||||
import { DateDistance } from '@/components/common/DateDistance';
|
import { DateDistance } from '@/components/common/DateDistance';
|
||||||
import { ExternalLink } from '@/components/common/ExternalLink';
|
import { ExternalLink } from '@/components/common/ExternalLink';
|
||||||
import { useConfig, useMessages } from '@/components/hooks';
|
import { useConfig, useMessages, useMobile } from '@/components/hooks';
|
||||||
import { ShareDeleteButton } from './ShareDeleteButton';
|
import { ShareDeleteButton } from './ShareDeleteButton';
|
||||||
import { ShareEditButton } from './ShareEditButton';
|
import { ShareEditButton } from './ShareEditButton';
|
||||||
|
|
||||||
export function SharesTable(props: DataTableProps) {
|
export function SharesTable(props: DataTableProps) {
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
const { cloudMode } = useConfig();
|
const { cloudMode } = useConfig();
|
||||||
|
const { isMobile } = useMobile();
|
||||||
|
|
||||||
const getUrl = (slug: string) => {
|
const getUrl = (slug: string) => {
|
||||||
if (cloudMode) {
|
return `${cloudMode ? process.env.cloudUrl : window?.location.origin}${process.env.basePath || ''}/share/${slug}`;
|
||||||
return `${process.env.cloudUrl}/share/${slug}`;
|
|
||||||
}
|
|
||||||
return `${window?.location.origin}${process.env.basePath || ''}/share/${slug}`;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DataTable {...props}>
|
<DataTable {...props}>
|
||||||
<DataColumn id="slug" label={formatMessage(labels.shareUrl)}>
|
<DataColumn id="name" label={formatMessage(labels.name)}>
|
||||||
|
{({ name }: any) => name}
|
||||||
|
</DataColumn>
|
||||||
|
<DataColumn id="slug" label={formatMessage(labels.shareUrl)} width="2fr">
|
||||||
{({ slug }: any) => {
|
{({ slug }: any) => {
|
||||||
const url = getUrl(slug);
|
const url = getUrl(slug);
|
||||||
return (
|
return (
|
||||||
|
|
@ -28,9 +29,11 @@ export function SharesTable(props: DataTableProps) {
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
</DataColumn>
|
</DataColumn>
|
||||||
<DataColumn id="created" label={formatMessage(labels.created)} width="200px">
|
{!isMobile && (
|
||||||
{(row: any) => <DateDistance date={new Date(row.createdAt)} />}
|
<DataColumn id="created" label={formatMessage(labels.created)}>
|
||||||
</DataColumn>
|
{(row: any) => <DateDistance date={new Date(row.createdAt)} />}
|
||||||
|
</DataColumn>
|
||||||
|
)}
|
||||||
<DataColumn id="action" align="end" width="100px">
|
<DataColumn id="action" align="end" width="100px">
|
||||||
{({ id, slug }: any) => {
|
{({ id, slug }: any) => {
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { Column, Heading, Row, Text } from '@umami/react-zen';
|
||||||
import { Plus } from 'lucide-react';
|
import { Plus } from 'lucide-react';
|
||||||
import { useMessages, useWebsiteSharesQuery } from '@/components/hooks';
|
import { useMessages, useWebsiteSharesQuery } from '@/components/hooks';
|
||||||
import { DialogButton } from '@/components/input/DialogButton';
|
import { DialogButton } from '@/components/input/DialogButton';
|
||||||
import { ShareCreateForm } from './ShareCreateForm';
|
import { ShareEditForm } from './ShareEditForm';
|
||||||
import { SharesTable } from './SharesTable';
|
import { SharesTable } from './SharesTable';
|
||||||
|
|
||||||
export interface WebsiteShareFormProps {
|
export interface WebsiteShareFormProps {
|
||||||
|
|
@ -11,7 +11,7 @@ export interface WebsiteShareFormProps {
|
||||||
|
|
||||||
export function WebsiteShareForm({ websiteId }: WebsiteShareFormProps) {
|
export function WebsiteShareForm({ websiteId }: WebsiteShareFormProps) {
|
||||||
const { formatMessage, labels, messages } = useMessages();
|
const { formatMessage, labels, messages } = useMessages();
|
||||||
const { data, isLoading } = useWebsiteSharesQuery({ websiteId });
|
const { data } = useWebsiteSharesQuery({ websiteId });
|
||||||
|
|
||||||
const shares = data?.data || [];
|
const shares = data?.data || [];
|
||||||
const hasShares = shares.length > 0;
|
const hasShares = shares.length > 0;
|
||||||
|
|
@ -25,9 +25,9 @@ export function WebsiteShareForm({ websiteId }: WebsiteShareFormProps) {
|
||||||
label={formatMessage(labels.add)}
|
label={formatMessage(labels.add)}
|
||||||
title={formatMessage(labels.share)}
|
title={formatMessage(labels.share)}
|
||||||
variant="primary"
|
variant="primary"
|
||||||
width="400px"
|
width="600px"
|
||||||
>
|
>
|
||||||
{({ close }) => <ShareCreateForm websiteId={websiteId} onClose={close} />}
|
{({ close }) => <ShareEditForm websiteId={websiteId} onClose={close} />}
|
||||||
</DialogButton>
|
</DialogButton>
|
||||||
</Row>
|
</Row>
|
||||||
{hasShares ? (
|
{hasShares ? (
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,47 @@
|
||||||
|
import { ROLES } from '@/lib/constants';
|
||||||
import { secret } from '@/lib/crypto';
|
import { secret } from '@/lib/crypto';
|
||||||
import { createToken } from '@/lib/jwt';
|
import { createToken } from '@/lib/jwt';
|
||||||
|
import prisma from '@/lib/prisma';
|
||||||
|
import redis from '@/lib/redis';
|
||||||
import { json, notFound } from '@/lib/response';
|
import { json, notFound } from '@/lib/response';
|
||||||
import { getShareByCode } from '@/queries/prisma';
|
import type { WhiteLabel } from '@/lib/types';
|
||||||
|
import { getShareByCode, getWebsite } from '@/queries/prisma';
|
||||||
|
|
||||||
|
async function getAccountId(website: { userId?: string; teamId?: string }): Promise<string | null> {
|
||||||
|
if (website.userId) {
|
||||||
|
return website.userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (website.teamId) {
|
||||||
|
const teamOwner = await prisma.client.teamUser.findFirst({
|
||||||
|
where: {
|
||||||
|
teamId: website.teamId,
|
||||||
|
role: ROLES.teamOwner,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
userId: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return teamOwner?.userId || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getWhiteLabel(accountId: string): Promise<WhiteLabel | null> {
|
||||||
|
if (!redis.enabled) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await redis.client.get(`white-label:${accountId}`);
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
return data as WhiteLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
export async function GET(_request: Request, { params }: { params: Promise<{ slug: string }> }) {
|
export async function GET(_request: Request, { params }: { params: Promise<{ slug: string }> }) {
|
||||||
const { slug } = await params;
|
const { slug } = await params;
|
||||||
|
|
@ -12,12 +52,25 @@ export async function GET(_request: Request, { params }: { params: Promise<{ slu
|
||||||
return notFound();
|
return notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = {
|
const website = await getWebsite(share.entityId);
|
||||||
|
|
||||||
|
const data: Record<string, any> = {
|
||||||
shareId: share.id,
|
shareId: share.id,
|
||||||
websiteId: share.entityId,
|
websiteId: share.entityId,
|
||||||
parameters: share.parameters,
|
parameters: share.parameters,
|
||||||
};
|
};
|
||||||
const token = createToken(data, secret());
|
|
||||||
|
|
||||||
return json({ ...data, token });
|
data.token = createToken(data, secret());
|
||||||
|
|
||||||
|
const accountId = await getAccountId(website);
|
||||||
|
|
||||||
|
if (accountId) {
|
||||||
|
const whiteLabel = await getWhiteLabel(accountId);
|
||||||
|
|
||||||
|
if (whiteLabel) {
|
||||||
|
data.whiteLabel = whiteLabel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return json(data);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ export async function GET(request: Request, { params }: { params: Promise<{ shar
|
||||||
|
|
||||||
export async function POST(request: Request, { params }: { params: Promise<{ shareId: string }> }) {
|
export async function POST(request: Request, { params }: { params: Promise<{ shareId: string }> }) {
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
|
name: z.string().max(200),
|
||||||
slug: z.string().max(100),
|
slug: z.string().max(100),
|
||||||
parameters: anyObjectParam,
|
parameters: anyObjectParam,
|
||||||
});
|
});
|
||||||
|
|
@ -36,7 +37,7 @@ export async function POST(request: Request, { params }: { params: Promise<{ sha
|
||||||
}
|
}
|
||||||
|
|
||||||
const { shareId } = await params;
|
const { shareId } = await params;
|
||||||
const { slug, parameters } = body;
|
const { name, slug, parameters } = body;
|
||||||
|
|
||||||
const share = await getShare(shareId);
|
const share = await getShare(shareId);
|
||||||
|
|
||||||
|
|
@ -49,6 +50,7 @@ export async function POST(request: Request, { params }: { params: Promise<{ sha
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await updateShare(shareId, {
|
const result = await updateShare(shareId, {
|
||||||
|
name,
|
||||||
slug,
|
slug,
|
||||||
parameters,
|
parameters,
|
||||||
} as any);
|
} as any);
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,7 @@ export async function POST(
|
||||||
{ params }: { params: Promise<{ websiteId: string }> },
|
{ params }: { params: Promise<{ websiteId: string }> },
|
||||||
) {
|
) {
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
|
name: z.string().max(200),
|
||||||
parameters: anyObjectParam.optional(),
|
parameters: anyObjectParam.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -54,7 +55,8 @@ export async function POST(
|
||||||
}
|
}
|
||||||
|
|
||||||
const { websiteId } = await params;
|
const { websiteId } = await params;
|
||||||
const { parameters = {} } = body;
|
const { name, parameters } = body;
|
||||||
|
const shareParameters = parameters ?? {};
|
||||||
|
|
||||||
if (!(await canUpdateWebsite(auth, websiteId))) {
|
if (!(await canUpdateWebsite(auth, websiteId))) {
|
||||||
return unauthorized();
|
return unauthorized();
|
||||||
|
|
@ -66,8 +68,9 @@ export async function POST(
|
||||||
id: uuid(),
|
id: uuid(),
|
||||||
entityId: websiteId,
|
entityId: websiteId,
|
||||||
shareType: ENTITY_TYPE.website,
|
shareType: ENTITY_TYPE.website,
|
||||||
|
name,
|
||||||
slug,
|
slug,
|
||||||
parameters,
|
parameters: shareParameters,
|
||||||
});
|
});
|
||||||
|
|
||||||
return json(share);
|
return json(share);
|
||||||
|
|
|
||||||
77
src/app/share/ShareProvider.tsx
Normal file
77
src/app/share/ShareProvider.tsx
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
'use client';
|
||||||
|
import { Loading } from '@umami/react-zen';
|
||||||
|
import { usePathname, useRouter } from 'next/navigation';
|
||||||
|
import { createContext, type ReactNode, useEffect } from 'react';
|
||||||
|
import { useShareTokenQuery } from '@/components/hooks';
|
||||||
|
import type { WhiteLabel } from '@/lib/types';
|
||||||
|
|
||||||
|
export interface ShareData {
|
||||||
|
shareId: string;
|
||||||
|
slug: string;
|
||||||
|
websiteId: string;
|
||||||
|
parameters: any;
|
||||||
|
token: string;
|
||||||
|
whiteLabel?: WhiteLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ShareContext = createContext<ShareData>(null);
|
||||||
|
|
||||||
|
const ALL_SECTION_IDS = [
|
||||||
|
'overview',
|
||||||
|
'events',
|
||||||
|
'sessions',
|
||||||
|
'realtime',
|
||||||
|
'compare',
|
||||||
|
'breakdown',
|
||||||
|
'goals',
|
||||||
|
'funnels',
|
||||||
|
'journeys',
|
||||||
|
'retention',
|
||||||
|
'utm',
|
||||||
|
'revenue',
|
||||||
|
'attribution',
|
||||||
|
];
|
||||||
|
|
||||||
|
function getSharePath(pathname: string) {
|
||||||
|
const segments = pathname.split('/');
|
||||||
|
const firstSegment = segments[3];
|
||||||
|
|
||||||
|
// If first segment looks like a domain name, skip it
|
||||||
|
if (firstSegment?.includes('.')) {
|
||||||
|
return segments[4];
|
||||||
|
}
|
||||||
|
|
||||||
|
return firstSegment;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ShareProvider({ slug, children }: { slug: string; children: ReactNode }) {
|
||||||
|
const { share, isLoading, isFetching } = useShareTokenQuery(slug);
|
||||||
|
const router = useRouter();
|
||||||
|
const pathname = usePathname();
|
||||||
|
const path = getSharePath(pathname);
|
||||||
|
|
||||||
|
const allowedSections = share?.parameters
|
||||||
|
? ALL_SECTION_IDS.filter(id => share.parameters[id] !== false)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const shouldRedirect =
|
||||||
|
allowedSections.length === 1 &&
|
||||||
|
allowedSections[0] !== 'overview' &&
|
||||||
|
(path === undefined || path === '' || path === 'overview');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (shouldRedirect) {
|
||||||
|
router.replace(`/share/${slug}/${allowedSections[0]}`);
|
||||||
|
}
|
||||||
|
}, [shouldRedirect, slug, allowedSections, router]);
|
||||||
|
|
||||||
|
if (isFetching && isLoading) {
|
||||||
|
return <Loading placement="absolute" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!share || shouldRedirect) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <ShareContext.Provider value={{ ...share, slug }}>{children}</ShareContext.Provider>;
|
||||||
|
}
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
import { Row, Text } from '@umami/react-zen';
|
|
||||||
import { CURRENT_VERSION, HOMEPAGE_URL } from '@/lib/constants';
|
|
||||||
|
|
||||||
export function Footer() {
|
|
||||||
return (
|
|
||||||
<Row as="footer" paddingY="6" justifyContent="flex-end">
|
|
||||||
<a href={HOMEPAGE_URL} target="_blank">
|
|
||||||
<Text weight="bold">umami</Text> {`v${CURRENT_VERSION}`}
|
|
||||||
</a>
|
|
||||||
</Row>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
import { Icon, Row, Text, ThemeButton } from '@umami/react-zen';
|
|
||||||
import { LanguageButton } from '@/components/input/LanguageButton';
|
|
||||||
import { PreferencesButton } from '@/components/input/PreferencesButton';
|
|
||||||
import { Logo } from '@/components/svg';
|
|
||||||
|
|
||||||
export function Header() {
|
|
||||||
return (
|
|
||||||
<Row as="header" justifyContent="space-between" alignItems="center" paddingY="3">
|
|
||||||
<a href="https://umami.is" target="_blank" rel="noopener">
|
|
||||||
<Row alignItems="center" gap>
|
|
||||||
<Icon>
|
|
||||||
<Logo />
|
|
||||||
</Icon>
|
|
||||||
<Text weight="bold">umami</Text>
|
|
||||||
</Row>
|
|
||||||
</a>
|
|
||||||
<Row alignItems="center" gap>
|
|
||||||
<ThemeButton />
|
|
||||||
<LanguageButton />
|
|
||||||
<PreferencesButton />
|
|
||||||
</Row>
|
|
||||||
</Row>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
import { SharePage } from './SharePage';
|
|
||||||
|
|
||||||
export default async function ({ params }: { params: Promise<{ shareId: string[] }> }) {
|
|
||||||
const { shareId } = await params;
|
|
||||||
const [slug, ...path] = shareId;
|
|
||||||
|
|
||||||
return <SharePage shareId={slug} path={path.join('/')} />;
|
|
||||||
}
|
|
||||||
|
|
@ -1,23 +1,30 @@
|
||||||
'use client';
|
import { Button, Column, Icon, Row, Text, ThemeButton } from '@umami/react-zen';
|
||||||
import { Column } from '@umami/react-zen';
|
|
||||||
import { SideMenu } from '@/components/common/SideMenu';
|
import { SideMenu } from '@/components/common/SideMenu';
|
||||||
import { useMessages, useNavigation } from '@/components/hooks';
|
import { useMessages, useNavigation, useShare } from '@/components/hooks';
|
||||||
import { AlignEndHorizontal, Clock, Eye, Sheet, Tag, User } from '@/components/icons';
|
import { AlignEndHorizontal, Clock, Eye, PanelLeft, Sheet, Tag, User } from '@/components/icons';
|
||||||
import { Funnel, Lightning, Magnet, Money, Network, Path, Target } from '@/components/svg';
|
import { LanguageButton } from '@/components/input/LanguageButton';
|
||||||
|
import { PreferencesButton } from '@/components/input/PreferencesButton';
|
||||||
|
import { Funnel, Lightning, Logo, Magnet, Money, Network, Path, Target } from '@/components/svg';
|
||||||
|
|
||||||
export function ShareNav({
|
export function ShareNav({
|
||||||
shareId,
|
collapsed,
|
||||||
parameters,
|
onCollapse,
|
||||||
onItemClick,
|
onItemClick,
|
||||||
}: {
|
}: {
|
||||||
shareId: string;
|
collapsed?: boolean;
|
||||||
parameters: Record<string, boolean>;
|
onCollapse?: (collapsed: boolean) => void;
|
||||||
onItemClick?: () => void;
|
onItemClick?: () => void;
|
||||||
}) {
|
}) {
|
||||||
|
const share = useShare();
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
const { pathname } = useNavigation();
|
const { pathname } = useNavigation();
|
||||||
|
const { slug, parameters, whiteLabel } = share;
|
||||||
|
|
||||||
const renderPath = (path: string) => `/share/${shareId}${path}`;
|
const logoUrl = whiteLabel?.url || 'https://umami.is';
|
||||||
|
const logoName = whiteLabel?.name || 'umami';
|
||||||
|
const logoImage = whiteLabel?.image;
|
||||||
|
|
||||||
|
const renderPath = (path: string) => `/share/${slug}${path}`;
|
||||||
|
|
||||||
const allItems = [
|
const allItems = [
|
||||||
{
|
{
|
||||||
|
|
@ -130,14 +137,70 @@ export function ShareNav({
|
||||||
.flatMap(e => e.items)
|
.flatMap(e => e.items)
|
||||||
.find(({ path }) => path && pathname.endsWith(path.split('?')[0]))?.id;
|
.find(({ path }) => path && pathname.endsWith(path.split('?')[0]))?.id;
|
||||||
|
|
||||||
|
const isMobile = !!onItemClick;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column padding="3" position="sticky" top="0" gap>
|
<Column
|
||||||
<SideMenu
|
position={isMobile ? undefined : 'fixed'}
|
||||||
items={items}
|
padding="3"
|
||||||
selectedKey={selectedKey}
|
width={isMobile ? '100%' : collapsed ? '60px' : '240px'}
|
||||||
allowMinimize={false}
|
maxHeight="100dvh"
|
||||||
onItemClick={onItemClick}
|
height="100dvh"
|
||||||
/>
|
border={isMobile ? undefined : 'right'}
|
||||||
|
borderColor={isMobile ? undefined : '4'}
|
||||||
|
>
|
||||||
|
<Row as="header" gap alignItems="center" justifyContent="space-between">
|
||||||
|
{!collapsed && (
|
||||||
|
<a href={logoUrl} target="_blank" rel="noopener" style={{ marginLeft: 12 }}>
|
||||||
|
<Row alignItems="center" gap>
|
||||||
|
{logoImage ? (
|
||||||
|
<img src={logoImage} alt={logoName} style={{ height: 24 }} />
|
||||||
|
) : (
|
||||||
|
<Icon>
|
||||||
|
<Logo />
|
||||||
|
</Icon>
|
||||||
|
)}
|
||||||
|
<Text weight="bold">{logoName}</Text>
|
||||||
|
</Row>
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
{!onItemClick && (
|
||||||
|
<Button variant="quiet" onPress={() => onCollapse?.(!collapsed)}>
|
||||||
|
<Icon color="muted">
|
||||||
|
<PanelLeft />
|
||||||
|
</Icon>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Row>
|
||||||
|
{!collapsed && (
|
||||||
|
<Column flexGrow={1} overflowY="auto">
|
||||||
|
<SideMenu
|
||||||
|
items={items}
|
||||||
|
selectedKey={selectedKey}
|
||||||
|
allowMinimize={false}
|
||||||
|
onItemClick={onItemClick}
|
||||||
|
/>
|
||||||
|
</Column>
|
||||||
|
)}
|
||||||
|
<Column
|
||||||
|
flexGrow={collapsed ? 1 : undefined}
|
||||||
|
justifyContent="flex-end"
|
||||||
|
alignItems={collapsed ? 'center' : undefined}
|
||||||
|
>
|
||||||
|
{collapsed ? (
|
||||||
|
<Column gap="2" alignItems="center">
|
||||||
|
<ThemeButton />
|
||||||
|
<LanguageButton />
|
||||||
|
<PreferencesButton />
|
||||||
|
</Column>
|
||||||
|
) : (
|
||||||
|
<Row>
|
||||||
|
<ThemeButton />
|
||||||
|
<LanguageButton />
|
||||||
|
<PreferencesButton />
|
||||||
|
</Row>
|
||||||
|
)}
|
||||||
|
</Column>
|
||||||
</Column>
|
</Column>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
'use client';
|
'use client';
|
||||||
import { Column, Grid, useTheme } from '@umami/react-zen';
|
import { Column, Grid, Row, useTheme } from '@umami/react-zen';
|
||||||
import { useEffect } from 'react';
|
import { usePathname } from 'next/navigation';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
import { AttributionPage } from '@/app/(main)/websites/[websiteId]/(reports)/attribution/AttributionPage';
|
import { AttributionPage } from '@/app/(main)/websites/[websiteId]/(reports)/attribution/AttributionPage';
|
||||||
import { BreakdownPage } from '@/app/(main)/websites/[websiteId]/(reports)/breakdown/BreakdownPage';
|
import { BreakdownPage } from '@/app/(main)/websites/[websiteId]/(reports)/breakdown/BreakdownPage';
|
||||||
import { FunnelsPage } from '@/app/(main)/websites/[websiteId]/(reports)/funnels/FunnelsPage';
|
import { FunnelsPage } from '@/app/(main)/websites/[websiteId]/(reports)/funnels/FunnelsPage';
|
||||||
|
|
@ -17,9 +18,8 @@ import { WebsiteHeader } from '@/app/(main)/websites/[websiteId]/WebsiteHeader';
|
||||||
import { WebsitePage } from '@/app/(main)/websites/[websiteId]/WebsitePage';
|
import { WebsitePage } from '@/app/(main)/websites/[websiteId]/WebsitePage';
|
||||||
import { WebsiteProvider } from '@/app/(main)/websites/WebsiteProvider';
|
import { WebsiteProvider } from '@/app/(main)/websites/WebsiteProvider';
|
||||||
import { PageBody } from '@/components/common/PageBody';
|
import { PageBody } from '@/components/common/PageBody';
|
||||||
import { useShareTokenQuery } from '@/components/hooks';
|
import { useShare } from '@/components/hooks';
|
||||||
import { Footer } from './Footer';
|
import { MobileMenuButton } from '@/components/input/MobileMenuButton';
|
||||||
import { Header } from './Header';
|
|
||||||
import { ShareNav } from './ShareNav';
|
import { ShareNav } from './ShareNav';
|
||||||
|
|
||||||
const PAGE_COMPONENTS: Record<string, React.ComponentType<{ websiteId: string }>> = {
|
const PAGE_COMPONENTS: Record<string, React.ComponentType<{ websiteId: string }>> = {
|
||||||
|
|
@ -39,9 +39,25 @@ const PAGE_COMPONENTS: Record<string, React.ComponentType<{ websiteId: string }>
|
||||||
attribution: AttributionPage,
|
attribution: AttributionPage,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function SharePage({ shareId, path = '' }: { shareId: string; path?: string }) {
|
function getSharePath(pathname: string) {
|
||||||
const { shareToken, isLoading } = useShareTokenQuery(shareId);
|
const segments = pathname.split('/');
|
||||||
|
const firstSegment = segments[3];
|
||||||
|
|
||||||
|
// If first segment looks like a domain name, skip it
|
||||||
|
if (firstSegment?.includes('.')) {
|
||||||
|
return segments[4];
|
||||||
|
}
|
||||||
|
|
||||||
|
return firstSegment;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SharePage() {
|
||||||
|
const [navCollapsed, setNavCollapsed] = useState(false);
|
||||||
|
const share = useShare();
|
||||||
const { setTheme } = useTheme();
|
const { setTheme } = useTheme();
|
||||||
|
const pathname = usePathname();
|
||||||
|
const path = getSharePath(pathname);
|
||||||
|
const { websiteId, parameters = {} } = share;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const url = new URL(window?.location?.href);
|
const url = new URL(window?.location?.href);
|
||||||
|
|
@ -52,12 +68,6 @@ export function SharePage({ shareId, path = '' }: { shareId: string; path?: stri
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (isLoading || !shareToken) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { websiteId, parameters = {} } = shareToken;
|
|
||||||
|
|
||||||
// Check if the requested path is allowed
|
// Check if the requested path is allowed
|
||||||
const pageKey = path || '';
|
const pageKey = path || '';
|
||||||
const isAllowed = pageKey === '' || pageKey === 'overview' || parameters[pageKey] !== false;
|
const isAllowed = pageKey === '' || pageKey === 'overview' || parameters[pageKey] !== false;
|
||||||
|
|
@ -69,29 +79,25 @@ export function SharePage({ shareId, path = '' }: { shareId: string; path?: stri
|
||||||
const PageComponent = PAGE_COMPONENTS[pageKey] || WebsitePage;
|
const PageComponent = PAGE_COMPONENTS[pageKey] || WebsitePage;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column backgroundColor="2">
|
<Grid columns={{ xs: '1fr', lg: `${navCollapsed ? '60px' : '240px'} 1fr` }} width="100%">
|
||||||
<Header />
|
<Row display={{ xs: 'flex', lg: 'none' }} alignItems="center" gap padding="3">
|
||||||
<Grid columns={{ xs: '1fr', lg: 'auto 1fr' }} width="100%" height="100%">
|
<MobileMenuButton>
|
||||||
<Column
|
{({ close }) => {
|
||||||
display={{ xs: 'none', lg: 'flex' }}
|
return <ShareNav onItemClick={close} />;
|
||||||
width="240px"
|
}}
|
||||||
height="100%"
|
</MobileMenuButton>
|
||||||
border="right"
|
</Row>
|
||||||
backgroundColor
|
<Column display={{ xs: 'none', lg: 'flex' }} marginRight="2">
|
||||||
marginRight="2"
|
<ShareNav collapsed={navCollapsed} onCollapse={setNavCollapsed} />
|
||||||
>
|
</Column>
|
||||||
<ShareNav shareId={shareId} parameters={parameters} />
|
<PageBody gap>
|
||||||
</Column>
|
<WebsiteProvider websiteId={websiteId}>
|
||||||
<PageBody gap>
|
<Column>
|
||||||
<WebsiteProvider websiteId={websiteId}>
|
|
||||||
<WebsiteHeader showActions={false} />
|
<WebsiteHeader showActions={false} />
|
||||||
<Column>
|
<PageComponent websiteId={websiteId} />
|
||||||
<PageComponent websiteId={websiteId} />
|
</Column>
|
||||||
</Column>
|
</WebsiteProvider>
|
||||||
</WebsiteProvider>
|
</PageBody>
|
||||||
</PageBody>
|
</Grid>
|
||||||
</Grid>
|
|
||||||
<Footer />
|
|
||||||
</Column>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
5
src/app/share/[slug]/[[...path]]/page.tsx
Normal file
5
src/app/share/[slug]/[[...path]]/page.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { SharePage } from './SharePage';
|
||||||
|
|
||||||
|
export default function () {
|
||||||
|
return <SharePage />;
|
||||||
|
}
|
||||||
13
src/app/share/[slug]/layout.tsx
Normal file
13
src/app/share/[slug]/layout.tsx
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { ShareProvider } from '@/app/share/ShareProvider';
|
||||||
|
|
||||||
|
export default async function ({
|
||||||
|
params,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
params: Promise<{ slug: string }>;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
const { slug } = await params;
|
||||||
|
|
||||||
|
return <ShareProvider slug={slug}>{children}</ShareProvider>;
|
||||||
|
}
|
||||||
|
|
@ -31,6 +31,7 @@ export function PageBody({
|
||||||
<Column
|
<Column
|
||||||
{...props}
|
{...props}
|
||||||
width="100%"
|
width="100%"
|
||||||
|
minHeight="100vh"
|
||||||
paddingBottom="6"
|
paddingBottom="6"
|
||||||
maxWidth={maxWidth}
|
maxWidth={maxWidth}
|
||||||
paddingX={{ xs: '3', md: '6' }}
|
paddingX={{ xs: '3', md: '6' }}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import {
|
||||||
NavMenuItem,
|
NavMenuItem,
|
||||||
type NavMenuProps,
|
type NavMenuProps,
|
||||||
Row,
|
Row,
|
||||||
|
Text,
|
||||||
} from '@umami/react-zen';
|
} from '@umami/react-zen';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
|
|
@ -42,9 +43,11 @@ export function SideMenu({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link key={id} href={path}>
|
<Link key={id} href={path}>
|
||||||
<NavMenuItem isSelected={isSelected}>
|
<Row padding hoverBackgroundColor="3">
|
||||||
<IconLabel icon={icon}>{label}</IconLabel>
|
<IconLabel icon={icon}>
|
||||||
</NavMenuItem>
|
<Text weight={isSelected ? 'bold' : undefined}>{label}</Text>
|
||||||
|
</IconLabel>
|
||||||
|
</Row>
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
6
src/components/hooks/context/useShare.ts
Normal file
6
src/components/hooks/context/useShare.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { useContext } from 'react';
|
||||||
|
import { ShareContext } from '@/app/share/ShareProvider';
|
||||||
|
|
||||||
|
export function useShare() {
|
||||||
|
return useContext(ShareContext);
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
// Context hooks
|
// Context hooks
|
||||||
export * from './context/useLink';
|
export * from './context/useLink';
|
||||||
export * from './context/usePixel';
|
export * from './context/usePixel';
|
||||||
|
export * from './context/useShare';
|
||||||
export * from './context/useTeam';
|
export * from './context/useTeam';
|
||||||
export * from './context/useUser';
|
export * from './context/useUser';
|
||||||
export * from './context/useWebsite';
|
export * from './context/useWebsite';
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,21 @@
|
||||||
import { setShareToken, useApp } from '@/store/app';
|
import { setShare, useApp } from '@/store/app';
|
||||||
import { useApi } from '../useApi';
|
import { useApi } from '../useApi';
|
||||||
|
|
||||||
const selector = (state: { shareToken: string }) => state.shareToken;
|
const selector = state => state.share;
|
||||||
|
|
||||||
export function useShareTokenQuery(slug: string): {
|
export function useShareTokenQuery(slug: string) {
|
||||||
shareToken: any;
|
const share = useApp(selector);
|
||||||
isLoading?: boolean;
|
|
||||||
error?: Error;
|
|
||||||
} {
|
|
||||||
const shareToken = useApp(selector);
|
|
||||||
const { get, useQuery } = useApi();
|
const { get, useQuery } = useApi();
|
||||||
const { isLoading, error } = useQuery({
|
const query = useQuery({
|
||||||
queryKey: ['share', slug],
|
queryKey: ['share', slug],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const data = await get(`/share/${slug}`);
|
const data = await get(`/share/${slug}`);
|
||||||
|
|
||||||
setShareToken(data);
|
setShare(data);
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return { shareToken, isLoading, error };
|
return { share, ...query };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ export function MobileMenuButton(props: DialogProps) {
|
||||||
</Icon>
|
</Icon>
|
||||||
</Button>
|
</Button>
|
||||||
<Modal placement="left" offset="80px">
|
<Modal placement="left" offset="80px">
|
||||||
<Dialog variant="sheet" {...props} />
|
<Dialog variant="sheet" {...props} style={{ width: 'auto' }} />
|
||||||
</Modal>
|
</Modal>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -31,9 +31,11 @@ export function WebsiteDateFilter({
|
||||||
const showCompare = allowCompare && !isAllTime;
|
const showCompare = allowCompare && !isAllTime;
|
||||||
|
|
||||||
const websiteDateRange = useDateRangeQuery(websiteId);
|
const websiteDateRange = useDateRangeQuery(websiteId);
|
||||||
|
const { startDate, endDate } = websiteDateRange;
|
||||||
|
const hasData = startDate && endDate;
|
||||||
|
|
||||||
const handleChange = (date: string) => {
|
const handleChange = (date: string) => {
|
||||||
if (date === 'all') {
|
if (date === 'all' && hasData) {
|
||||||
router.push(
|
router.push(
|
||||||
updateParams({
|
updateParams({
|
||||||
date: `${getDateRangeValue(websiteDateRange.startDate, websiteDateRange.endDate)}:all`,
|
date: `${getDateRangeValue(websiteDateRange.startDate, websiteDateRange.endDate)}:all`,
|
||||||
|
|
@ -78,7 +80,7 @@ export function WebsiteDateFilter({
|
||||||
<DateFilter
|
<DateFilter
|
||||||
value={dateValue}
|
value={dateValue}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
showAllTime={showAllTime}
|
showAllTime={hasData && showAllTime}
|
||||||
renderDate={+offset !== 0}
|
renderDate={+offset !== 0}
|
||||||
/>
|
/>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ export function WebsiteSelect({
|
||||||
const { user } = useLoginQuery();
|
const { user } = useLoginQuery();
|
||||||
const { data, isLoading } = useUserWebsitesQuery(
|
const { data, isLoading } = useUserWebsitesQuery(
|
||||||
{ userId: user?.id, teamId },
|
{ userId: user?.id, teamId },
|
||||||
{ search, pageSize: 10, includeTeams },
|
{ search, pageSize: 20, includeTeams },
|
||||||
);
|
);
|
||||||
const listItems: { id: string; name: string }[] = data?.data || [];
|
const listItems: { id: string; name: string }[] = data?.data || [];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,5 +18,5 @@ test('getIpAddress: Standard header', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('getIpAddress: No header', () => {
|
test('getIpAddress: No header', () => {
|
||||||
expect(getIpAddress(new Headers())).toEqual(null);
|
expect(getIpAddress(new Headers())).toEqual(undefined);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ export function renderDateLabels(unit: string, locale: string) {
|
||||||
|
|
||||||
switch (unit) {
|
switch (unit) {
|
||||||
case 'minute':
|
case 'minute':
|
||||||
return formatDate(d, 'h:mm', locale);
|
|
||||||
case 'hour':
|
case 'hour':
|
||||||
return formatDate(d, 'p', locale);
|
return formatDate(d, 'p', locale);
|
||||||
case 'day':
|
case 'day':
|
||||||
|
|
|
||||||
|
|
@ -108,7 +108,7 @@ function getFilterQuery(filters: Record<string, any>, options: QueryOptions = {}
|
||||||
|
|
||||||
if (name === 'referrer') {
|
if (name === 'referrer') {
|
||||||
arr.push(
|
arr.push(
|
||||||
`and (website_event.referrer_domain != website_event.hostname or website_event.referrer_domain is null)`,
|
`and (website_event.referrer_domain != regexp_replace(website_event.hostname, '^www.', '') or website_event.referrer_domain is null)`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -141,3 +141,9 @@ export interface ApiError extends Error {
|
||||||
code?: string;
|
code?: string;
|
||||||
message: string;
|
message: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface WhiteLabel {
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
image: string;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,11 @@ import type { Auth } from '@/lib/types';
|
||||||
import { getTeamUser } from '@/queries/prisma';
|
import { getTeamUser } from '@/queries/prisma';
|
||||||
|
|
||||||
export async function canViewEntity({ user }: Auth, entityId: string) {
|
export async function canViewEntity({ user }: Auth, entityId: string) {
|
||||||
if (user?.isAdmin) {
|
if (!user) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.isAdmin) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -25,6 +29,10 @@ export async function canViewEntity({ user }: Auth, entityId: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function canUpdateEntity({ user }: Auth, entityId: string) {
|
export async function canUpdateEntity({ user }: Auth, entityId: string) {
|
||||||
|
if (!user) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (user.isAdmin) {
|
if (user.isAdmin) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -45,6 +53,10 @@ export async function canUpdateEntity({ user }: Auth, entityId: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function canDeleteEntity({ user }: Auth, entityId: string) {
|
export async function canDeleteEntity({ user }: Auth, entityId: string) {
|
||||||
|
if (!user) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (user.isAdmin) {
|
if (user.isAdmin) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,11 @@ import type { Auth } from '@/lib/types';
|
||||||
import { getLink, getTeamUser } from '@/queries/prisma';
|
import { getLink, getTeamUser } from '@/queries/prisma';
|
||||||
|
|
||||||
export async function canViewLink({ user }: Auth, linkId: string) {
|
export async function canViewLink({ user }: Auth, linkId: string) {
|
||||||
if (user?.isAdmin) {
|
if (!user) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.isAdmin) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -24,6 +28,10 @@ export async function canViewLink({ user }: Auth, linkId: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function canUpdateLink({ user }: Auth, linkId: string) {
|
export async function canUpdateLink({ user }: Auth, linkId: string) {
|
||||||
|
if (!user) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (user.isAdmin) {
|
if (user.isAdmin) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -44,6 +52,10 @@ export async function canUpdateLink({ user }: Auth, linkId: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function canDeleteLink({ user }: Auth, linkId: string) {
|
export async function canDeleteLink({ user }: Auth, linkId: string) {
|
||||||
|
if (!user) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (user.isAdmin) {
|
if (user.isAdmin) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,11 @@ import type { Auth } from '@/lib/types';
|
||||||
import { getPixel, getTeamUser } from '@/queries/prisma';
|
import { getPixel, getTeamUser } from '@/queries/prisma';
|
||||||
|
|
||||||
export async function canViewPixel({ user }: Auth, pixelId: string) {
|
export async function canViewPixel({ user }: Auth, pixelId: string) {
|
||||||
if (user?.isAdmin) {
|
if (!user) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.isAdmin) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -24,6 +28,10 @@ export async function canViewPixel({ user }: Auth, pixelId: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function canUpdatePixel({ user }: Auth, pixelId: string) {
|
export async function canUpdatePixel({ user }: Auth, pixelId: string) {
|
||||||
|
if (!user) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (user.isAdmin) {
|
if (user.isAdmin) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -44,6 +52,10 @@ export async function canUpdatePixel({ user }: Auth, pixelId: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function canDeletePixel({ user }: Auth, pixelId: string) {
|
export async function canDeletePixel({ user }: Auth, pixelId: string) {
|
||||||
|
if (!user) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (user.isAdmin) {
|
if (user.isAdmin) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,11 @@ import type { Auth } from '@/lib/types';
|
||||||
import { canViewWebsite } from './website';
|
import { canViewWebsite } from './website';
|
||||||
|
|
||||||
export async function canViewReport(auth: Auth, report: Report) {
|
export async function canViewReport(auth: Auth, report: Report) {
|
||||||
if (auth.user.isAdmin) {
|
if (auth.user?.isAdmin) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (auth.user.id === report.userId) {
|
if (auth.user?.id === report.userId) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -15,6 +15,10 @@ export async function canViewReport(auth: Auth, report: Report) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function canUpdateReport({ user }: Auth, report: Report) {
|
export async function canUpdateReport({ user }: Auth, report: Report) {
|
||||||
|
if (!user) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (user.isAdmin) {
|
if (user.isAdmin) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,10 @@ import type { Auth } from '@/lib/types';
|
||||||
import { getTeamUser } from '@/queries/prisma';
|
import { getTeamUser } from '@/queries/prisma';
|
||||||
|
|
||||||
export async function canViewTeam({ user }: Auth, teamId: string) {
|
export async function canViewTeam({ user }: Auth, teamId: string) {
|
||||||
|
if (!user) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (user.isAdmin) {
|
if (user.isAdmin) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -12,6 +16,10 @@ export async function canViewTeam({ user }: Auth, teamId: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function canCreateTeam({ user }: Auth) {
|
export async function canCreateTeam({ user }: Auth) {
|
||||||
|
if (!user) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (user.isAdmin) {
|
if (user.isAdmin) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -20,6 +28,10 @@ export async function canCreateTeam({ user }: Auth) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function canUpdateTeam({ user }: Auth, teamId: string) {
|
export async function canUpdateTeam({ user }: Auth, teamId: string) {
|
||||||
|
if (!user) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (user.isAdmin) {
|
if (user.isAdmin) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -30,6 +42,10 @@ export async function canUpdateTeam({ user }: Auth, teamId: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function canDeleteTeam({ user }: Auth, teamId: string) {
|
export async function canDeleteTeam({ user }: Auth, teamId: string) {
|
||||||
|
if (!user) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (user.isAdmin) {
|
if (user.isAdmin) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -40,6 +56,10 @@ export async function canDeleteTeam({ user }: Auth, teamId: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function canDeleteTeamUser({ user }: Auth, teamId: string, removeUserId: string) {
|
export async function canDeleteTeamUser({ user }: Auth, teamId: string, removeUserId: string) {
|
||||||
|
if (!user) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (user.isAdmin) {
|
if (user.isAdmin) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -54,6 +74,10 @@ export async function canDeleteTeamUser({ user }: Auth, teamId: string, removeUs
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function canCreateTeamWebsite({ user }: Auth, teamId: string) {
|
export async function canCreateTeamWebsite({ user }: Auth, teamId: string) {
|
||||||
|
if (!user) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (user.isAdmin) {
|
if (user.isAdmin) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -64,5 +88,5 @@ export async function canCreateTeamWebsite({ user }: Auth, teamId: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function canViewAllTeams({ user }: Auth) {
|
export async function canViewAllTeams({ user }: Auth) {
|
||||||
return user.isAdmin;
|
return user?.isAdmin ?? false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,14 @@
|
||||||
import type { Auth } from '@/lib/types';
|
import type { Auth } from '@/lib/types';
|
||||||
|
|
||||||
export async function canCreateUser({ user }: Auth) {
|
export async function canCreateUser({ user }: Auth) {
|
||||||
return user.isAdmin;
|
return user?.isAdmin ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function canViewUser({ user }: Auth, viewedUserId: string) {
|
export async function canViewUser({ user }: Auth, viewedUserId: string) {
|
||||||
|
if (!user) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (user.isAdmin) {
|
if (user.isAdmin) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -13,10 +17,14 @@ export async function canViewUser({ user }: Auth, viewedUserId: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function canViewUsers({ user }: Auth) {
|
export async function canViewUsers({ user }: Auth) {
|
||||||
return user.isAdmin;
|
return user?.isAdmin ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function canUpdateUser({ user }: Auth, viewedUserId: string) {
|
export async function canUpdateUser({ user }: Auth, viewedUserId: string) {
|
||||||
|
if (!user) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (user.isAdmin) {
|
if (user.isAdmin) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -25,5 +33,5 @@ export async function canUpdateUser({ user }: Auth, viewedUserId: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function canDeleteUser({ user }: Auth) {
|
export async function canDeleteUser({ user }: Auth) {
|
||||||
return user.isAdmin;
|
return user?.isAdmin ?? false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ export async function canViewWebsite({ user, shareToken }: Auth, websiteId: stri
|
||||||
|
|
||||||
const entity = await getEntity(websiteId);
|
const entity = await getEntity(websiteId);
|
||||||
|
|
||||||
if (!entity) {
|
if (!entity || !user) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -33,10 +33,14 @@ export async function canViewWebsite({ user, shareToken }: Auth, websiteId: stri
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function canViewAllWebsites({ user }: Auth) {
|
export async function canViewAllWebsites({ user }: Auth) {
|
||||||
return user.isAdmin;
|
return user?.isAdmin ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function canCreateWebsite({ user }: Auth) {
|
export async function canCreateWebsite({ user }: Auth) {
|
||||||
|
if (!user) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (user.isAdmin) {
|
if (user.isAdmin) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -45,6 +49,10 @@ export async function canCreateWebsite({ user }: Auth) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function canUpdateWebsite({ user }: Auth, websiteId: string) {
|
export async function canUpdateWebsite({ user }: Auth, websiteId: string) {
|
||||||
|
if (!user) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (user.isAdmin) {
|
if (user.isAdmin) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -69,6 +77,10 @@ export async function canUpdateWebsite({ user }: Auth, websiteId: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function canDeleteWebsite({ user }: Auth, websiteId: string) {
|
export async function canDeleteWebsite({ user }: Auth, websiteId: string) {
|
||||||
|
if (!user) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (user.isAdmin) {
|
if (user.isAdmin) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -93,6 +105,10 @@ export async function canDeleteWebsite({ user }: Auth, websiteId: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function canTransferWebsiteToUser({ user }: Auth, websiteId: string, userId: string) {
|
export async function canTransferWebsiteToUser({ user }: Auth, websiteId: string, userId: string) {
|
||||||
|
if (!user) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const website = await getWebsite(websiteId);
|
const website = await getWebsite(websiteId);
|
||||||
|
|
||||||
if (!website) {
|
if (!website) {
|
||||||
|
|
@ -109,6 +125,10 @@ export async function canTransferWebsiteToUser({ user }: Auth, websiteId: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function canTransferWebsiteToTeam({ user }: Auth, websiteId: string, teamId: string) {
|
export async function canTransferWebsiteToTeam({ user }: Auth, websiteId: string, teamId: string) {
|
||||||
|
if (!user) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const website = await getWebsite(websiteId);
|
const website = await getWebsite(websiteId);
|
||||||
|
|
||||||
if (!website) {
|
if (!website) {
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ async function relationalQuery(websiteId: string, column: string, filters: Query
|
||||||
let excludeDomain = '';
|
let excludeDomain = '';
|
||||||
|
|
||||||
if (column === 'referrer_domain') {
|
if (column === 'referrer_domain') {
|
||||||
excludeDomain = `and website_event.referrer_domain != website_event.hostname
|
excludeDomain = `and website_event.referrer_domain != regexp_replace(website_event.hostname, '^www.', '')
|
||||||
and website_event.referrer_domain != ''`;
|
and website_event.referrer_domain != ''`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ export async function getWeeklyTraffic(...args: [websiteId: string, filters: Que
|
||||||
}
|
}
|
||||||
|
|
||||||
async function relationalQuery(websiteId: string, filters: QueryFilters) {
|
async function relationalQuery(websiteId: string, filters: QueryFilters) {
|
||||||
const timezone = 'utc';
|
const { timezone = 'utc' } = filters;
|
||||||
const { rawQuery, getDateWeeklySQL, parseFilters } = prisma;
|
const { rawQuery, getDateWeeklySQL, parseFilters } = prisma;
|
||||||
const { filterQuery, joinSessionQuery, cohortQuery, queryParams } = parseFilters({
|
const { filterQuery, joinSessionQuery, cohortQuery, queryParams } = parseFilters({
|
||||||
...filters,
|
...filters,
|
||||||
|
|
@ -33,7 +33,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
|
||||||
and website_event.created_at between {{startDate}} and {{endDate}}
|
and website_event.created_at between {{startDate}} and {{endDate}}
|
||||||
${filterQuery}
|
${filterQuery}
|
||||||
group by time
|
group by time
|
||||||
order by 2
|
order by 1
|
||||||
`,
|
`,
|
||||||
queryParams,
|
queryParams,
|
||||||
FUNCTION_NAME,
|
FUNCTION_NAME,
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ async function relationalQuery(
|
||||||
let excludeDomain = '';
|
let excludeDomain = '';
|
||||||
|
|
||||||
if (column === 'referrer_domain') {
|
if (column === 'referrer_domain') {
|
||||||
excludeDomain = `and website_event.referrer_domain != website_event.hostname
|
excludeDomain = `and website_event.referrer_domain != regexp_replace(website_event.hostname, '^www.', '')
|
||||||
and website_event.referrer_domain != ''`;
|
and website_event.referrer_domain != ''`;
|
||||||
if (type === 'domain') {
|
if (type === 'domain') {
|
||||||
column = toPostgresGroupedReferrer(GROUPED_DOMAINS);
|
column = toPostgresGroupedReferrer(GROUPED_DOMAINS);
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ async function relationalQuery(
|
||||||
let excludeDomain = '';
|
let excludeDomain = '';
|
||||||
|
|
||||||
if (column === 'referrer_domain') {
|
if (column === 'referrer_domain') {
|
||||||
excludeDomain = `and website_event.referrer_domain != website_event.hostname
|
excludeDomain = `and website_event.referrer_domain != regexp_replace(website_event.hostname, '^www.', '')
|
||||||
and website_event.referrer_domain != ''`;
|
and website_event.referrer_domain != ''`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -142,7 +142,7 @@ async function relationalQuery(
|
||||||
${
|
${
|
||||||
currency
|
currency
|
||||||
? ''
|
? ''
|
||||||
: `and we.referrer_domain != hostname
|
: `and we.referrer_domain != regexp_replace(we.hostname, '^www.', '')
|
||||||
and we.referrer_domain != ''`
|
and we.referrer_domain != ''`
|
||||||
}
|
}
|
||||||
group by 1
|
group by 1
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ const initialState = {
|
||||||
theme: getItem(THEME_CONFIG) || DEFAULT_THEME,
|
theme: getItem(THEME_CONFIG) || DEFAULT_THEME,
|
||||||
timezone: getItem(TIMEZONE_CONFIG) || getTimezone(),
|
timezone: getItem(TIMEZONE_CONFIG) || getTimezone(),
|
||||||
dateRangeValue: getItem(DATE_RANGE_CONFIG) || DEFAULT_DATE_RANGE_VALUE,
|
dateRangeValue: getItem(DATE_RANGE_CONFIG) || DEFAULT_DATE_RANGE_VALUE,
|
||||||
shareToken: null,
|
share: null,
|
||||||
user: null,
|
user: null,
|
||||||
config: null,
|
config: null,
|
||||||
};
|
};
|
||||||
|
|
@ -31,8 +31,8 @@ export function setLocale(locale: string) {
|
||||||
store.setState({ locale });
|
store.setState({ locale });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setShareToken(shareToken: string) {
|
export function setShare(share: object) {
|
||||||
store.setState({ shareToken });
|
store.setState({ share });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setUser(user: object) {
|
export function setUser(user: object) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue