mirror of
https://github.com/umami-software/umami.git
synced 2026-02-15 18:15:35 +01:00
Compare commits
7 commits
df3ae72ab7
...
9df012084d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9df012084d | ||
|
|
5c3a6ce8ce | ||
|
|
10bc2895eb | ||
|
|
be5f0494cc | ||
|
|
d9b08d9491 | ||
|
|
ba45972bd3 | ||
|
|
9fb38f6970 |
31 changed files with 269 additions and 129 deletions
|
|
@ -1,4 +1 @@
|
||||||
#!/bin/sh
|
|
||||||
. "$(dirname "$0")/_/husky.sh"
|
|
||||||
|
|
||||||
npx lint-staged
|
npx lint-staged
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@umami/components",
|
"name": "@umami/components",
|
||||||
"version": "0.128.0",
|
"version": "0.129.0",
|
||||||
"description": "Umami React components.",
|
"description": "Umami React components.",
|
||||||
"author": "Mike Cao <mike@mikecao.com>",
|
"author": "Mike Cao <mike@mikecao.com>",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,7 @@
|
||||||
"@react-spring/web": "^10.0.3",
|
"@react-spring/web": "^10.0.3",
|
||||||
"@svgr/cli": "^8.1.0",
|
"@svgr/cli": "^8.1.0",
|
||||||
"@tanstack/react-query": "^5.90.2",
|
"@tanstack/react-query": "^5.90.2",
|
||||||
"@umami/react-zen": "^0.189.0",
|
"@umami/react-zen": "^0.196.0",
|
||||||
"@umami/redis-client": "^0.29.0",
|
"@umami/redis-client": "^0.29.0",
|
||||||
"bcryptjs": "^3.0.2",
|
"bcryptjs": "^3.0.2",
|
||||||
"chalk": "^5.6.2",
|
"chalk": "^5.6.2",
|
||||||
|
|
|
||||||
20
pnpm-lock.yaml
generated
20
pnpm-lock.yaml
generated
|
|
@ -45,8 +45,8 @@ importers:
|
||||||
specifier: ^5.90.2
|
specifier: ^5.90.2
|
||||||
version: 5.90.2(react@19.1.1)
|
version: 5.90.2(react@19.1.1)
|
||||||
'@umami/react-zen':
|
'@umami/react-zen':
|
||||||
specifier: ^0.189.0
|
specifier: ^0.196.0
|
||||||
version: 0.189.0(@babel/core@7.28.3)(@types/react@19.1.16)(babel-plugin-react-compiler@19.1.0-rc.2)(immer@10.1.3)(use-sync-external-store@1.6.0(react@19.1.1))
|
version: 0.196.0(@babel/core@7.28.3)(@types/react@19.1.16)(babel-plugin-react-compiler@19.1.0-rc.2)(immer@10.1.3)(use-sync-external-store@1.6.0(react@19.1.1))
|
||||||
'@umami/redis-client':
|
'@umami/redis-client':
|
||||||
specifier: ^0.29.0
|
specifier: ^0.29.0
|
||||||
version: 0.29.0
|
version: 0.29.0
|
||||||
|
|
@ -364,8 +364,6 @@ importers:
|
||||||
specifier: ^5.9.3
|
specifier: ^5.9.3
|
||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
|
|
||||||
dist: {}
|
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
'@ampproject/remapping@2.3.0':
|
'@ampproject/remapping@2.3.0':
|
||||||
|
|
@ -2756,8 +2754,8 @@ packages:
|
||||||
resolution: {integrity: sha512-qsaFBA3e09MIDAGFUrTk+dzqtfv1XPVz8t8d1f0ybTzrCY7BKiMC5cjrl1O/P7UmHsNyW90EYSkU/ZWpmXelag==}
|
resolution: {integrity: sha512-qsaFBA3e09MIDAGFUrTk+dzqtfv1XPVz8t8d1f0ybTzrCY7BKiMC5cjrl1O/P7UmHsNyW90EYSkU/ZWpmXelag==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
|
||||||
'@umami/react-zen@0.189.0':
|
'@umami/react-zen@0.196.0':
|
||||||
resolution: {integrity: sha512-E5t5HvMrGfuilrnF6LJV+jeooC4qXpwUC4VGhnTPV24B1vdMC2W9ByzZreNaomgZy8XOVAk1wZf8QX1elloUjA==}
|
resolution: {integrity: sha512-CLxrDAJOdo+0aJAclOq7naIDg+2I5wP9wXxAFhxhQVPXHV8yUHqH9Ula632cLMo51JYp0l+eEtOtuimpuKX3jg==}
|
||||||
|
|
||||||
'@umami/redis-client@0.29.0':
|
'@umami/redis-client@0.29.0':
|
||||||
resolution: {integrity: sha512-Jaqh++jskqDB7ny75pfC02OvKp1JTS4asGDsFrRL3qy8sxL3PAl9+/mybCJe4/6vWrXDJKqpgkSfUDJq2bFjyw==}
|
resolution: {integrity: sha512-Jaqh++jskqDB7ny75pfC02OvKp1JTS4asGDsFrRL3qy8sxL3PAl9+/mybCJe4/6vWrXDJKqpgkSfUDJq2bFjyw==}
|
||||||
|
|
@ -6350,8 +6348,8 @@ packages:
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: '>=16.13.1'
|
react: '>=16.13.1'
|
||||||
|
|
||||||
react-hook-form@7.64.0:
|
react-hook-form@7.65.0:
|
||||||
resolution: {integrity: sha512-fnN+vvTiMLnRqKNTVhDysdrUay0kUUAymQnFIznmgDvapjveUWOOPqMNzPg+A+0yf9DuE2h6xzBjN1s+Qx8wcg==}
|
resolution: {integrity: sha512-xtOzDz063WcXvGWaHgLNrNzlsdFgtUWcb32E6WFaGTd7kPZG3EeDusjdZfUsPwKCKVXy1ZlntifaHZ4l8pAsmw==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^16.8.0 || ^17 || ^18 || ^19
|
react: ^16.8.0 || ^17 || ^18 || ^19
|
||||||
|
|
@ -10387,7 +10385,7 @@ snapshots:
|
||||||
'@typescript-eslint/types': 8.45.0
|
'@typescript-eslint/types': 8.45.0
|
||||||
eslint-visitor-keys: 4.2.1
|
eslint-visitor-keys: 4.2.1
|
||||||
|
|
||||||
'@umami/react-zen@0.189.0(@babel/core@7.28.3)(@types/react@19.1.16)(babel-plugin-react-compiler@19.1.0-rc.2)(immer@10.1.3)(use-sync-external-store@1.6.0(react@19.1.1))':
|
'@umami/react-zen@0.196.0(@babel/core@7.28.3)(@types/react@19.1.16)(babel-plugin-react-compiler@19.1.0-rc.2)(immer@10.1.3)(use-sync-external-store@1.6.0(react@19.1.1))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@fontsource/jetbrains-mono': 5.2.8
|
'@fontsource/jetbrains-mono': 5.2.8
|
||||||
'@internationalized/date': 3.10.0
|
'@internationalized/date': 3.10.0
|
||||||
|
|
@ -10401,7 +10399,7 @@ snapshots:
|
||||||
react: 19.1.1
|
react: 19.1.1
|
||||||
react-aria-components: 1.13.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
react-aria-components: 1.13.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
||||||
react-dom: 19.1.1(react@19.1.1)
|
react-dom: 19.1.1(react@19.1.1)
|
||||||
react-hook-form: 7.64.0(react@19.1.1)
|
react-hook-form: 7.65.0(react@19.1.1)
|
||||||
react-icons: 5.5.0(react@19.1.1)
|
react-icons: 5.5.0(react@19.1.1)
|
||||||
thenby: 1.3.4
|
thenby: 1.3.4
|
||||||
zustand: 5.0.8(@types/react@19.1.16)(immer@10.1.3)(react@19.1.1)(use-sync-external-store@1.6.0(react@19.1.1))
|
zustand: 5.0.8(@types/react@19.1.16)(immer@10.1.3)(react@19.1.1)(use-sync-external-store@1.6.0(react@19.1.1))
|
||||||
|
|
@ -14534,7 +14532,7 @@ snapshots:
|
||||||
'@babel/runtime': 7.28.3
|
'@babel/runtime': 7.28.3
|
||||||
react: 19.1.1
|
react: 19.1.1
|
||||||
|
|
||||||
react-hook-form@7.64.0(react@19.1.1):
|
react-hook-form@7.65.0(react@19.1.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 19.1.1
|
react: 19.1.1
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import Script from 'next/script';
|
||||||
import { UpdateNotice } from './UpdateNotice';
|
import { UpdateNotice } from './UpdateNotice';
|
||||||
import { SideNav } from '@/app/(main)/SideNav';
|
import { SideNav } from '@/app/(main)/SideNav';
|
||||||
import { useLoginQuery, useConfig, useNavigation } from '@/components/hooks';
|
import { useLoginQuery, useConfig, useNavigation } from '@/components/hooks';
|
||||||
|
import { MobileNav } from '@/app/(main)/MobileNav';
|
||||||
|
|
||||||
export function App({ children }) {
|
export function App({ children }) {
|
||||||
const { user, isLoading, error } = useLoginQuery();
|
const { user, isLoading, error } = useLoginQuery();
|
||||||
|
|
@ -28,9 +29,16 @@ export function App({ children }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid columns={{ xs: '1fr', md: 'auto 1fr' }} height="100vh" width="100%" backgroundColor="2">
|
<Grid
|
||||||
<Row display={{ xs: 'flex', md: 'none' }} alignItems="center" gap></Row>
|
columns={{ xs: '1fr', lg: 'auto 1fr' }}
|
||||||
<Column display={{ xs: 'none', md: 'flex' }}>
|
rows={{ xs: 'auto 1fr', lg: '1fr' }}
|
||||||
|
height={{ xs: 'auto', lg: '100vh' }}
|
||||||
|
width="100%"
|
||||||
|
>
|
||||||
|
<Row display={{ xs: 'flex', lg: 'none' }} alignItems="center" gap padding="3">
|
||||||
|
<MobileNav />
|
||||||
|
</Row>
|
||||||
|
<Column display={{ xs: 'none', lg: 'flex' }}>
|
||||||
<SideNav />
|
<SideNav />
|
||||||
</Column>
|
</Column>
|
||||||
<Column alignItems="center" overflowY="auto" overflowX="hidden" position="relative">
|
<Column alignItems="center" overflowY="auto" overflowX="hidden" position="relative">
|
||||||
|
|
|
||||||
79
src/app/(main)/MobileNav.tsx
Normal file
79
src/app/(main)/MobileNav.tsx
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
import {
|
||||||
|
Row,
|
||||||
|
Dialog,
|
||||||
|
DialogTrigger,
|
||||||
|
Button,
|
||||||
|
Icon,
|
||||||
|
Modal,
|
||||||
|
NavMenu,
|
||||||
|
NavMenuItem,
|
||||||
|
IconLabel,
|
||||||
|
Text,
|
||||||
|
Grid,
|
||||||
|
} from '@umami/react-zen';
|
||||||
|
import { Globe, Grid2x2, LinkIcon, Menu } from '@/components/icons';
|
||||||
|
import { useMessages, useNavigation } from '@/components/hooks';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { WebsiteNav } from '@/app/(main)/websites/[websiteId]/WebsiteNav';
|
||||||
|
import { Logo } from '@/components/svg';
|
||||||
|
import { NavButton } from '@/components/input/NavButton';
|
||||||
|
|
||||||
|
export function MobileNav() {
|
||||||
|
const { formatMessage, labels } = useMessages();
|
||||||
|
const { websiteId } = useNavigation();
|
||||||
|
|
||||||
|
const links = [
|
||||||
|
{
|
||||||
|
id: 'websites',
|
||||||
|
label: formatMessage(labels.websites),
|
||||||
|
path: '/websites',
|
||||||
|
icon: <Globe />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'links',
|
||||||
|
label: formatMessage(labels.links),
|
||||||
|
path: '/links',
|
||||||
|
icon: <LinkIcon />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'pixels',
|
||||||
|
label: formatMessage(labels.pixels),
|
||||||
|
path: '/pixels',
|
||||||
|
icon: <Grid2x2 />,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Grid columns="auto 1fr" flexGrow={1}>
|
||||||
|
<DialogTrigger>
|
||||||
|
<Button>
|
||||||
|
<Icon>
|
||||||
|
<Menu />
|
||||||
|
</Icon>
|
||||||
|
</Button>
|
||||||
|
<Modal position="left" offset="80px">
|
||||||
|
<Dialog variant="sheet">
|
||||||
|
<NavMenu padding="3">
|
||||||
|
<NavButton />
|
||||||
|
{links.map(link => {
|
||||||
|
return (
|
||||||
|
<Link key={link.id} href={link.path}>
|
||||||
|
<NavMenuItem>
|
||||||
|
<IconLabel icon={link.icon} label={link.label} />
|
||||||
|
</NavMenuItem>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</NavMenu>
|
||||||
|
{websiteId && <WebsiteNav websiteId={websiteId} />}
|
||||||
|
</Dialog>
|
||||||
|
</Modal>
|
||||||
|
</DialogTrigger>
|
||||||
|
<Row alignItems="center" justifyContent="center" flexGrow={1}>
|
||||||
|
<IconLabel icon={<Logo />} style={{ width: 'auto' }}>
|
||||||
|
<Text weight="bold">umami</Text>
|
||||||
|
</IconLabel>
|
||||||
|
</Row>
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,23 +1,18 @@
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { DataTable, DataColumn, Row } from '@umami/react-zen';
|
import { DataTable, DataColumn, Row, DataTableProps } from '@umami/react-zen';
|
||||||
import { useMessages, useNavigation, useSlug } from '@/components/hooks';
|
import { useMessages, useNavigation, useSlug } from '@/components/hooks';
|
||||||
import { Empty } from '@/components/common/Empty';
|
|
||||||
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 { LinkEditButton } from './LinkEditButton';
|
import { LinkEditButton } from './LinkEditButton';
|
||||||
import { LinkDeleteButton } from './LinkDeleteButton';
|
import { LinkDeleteButton } from './LinkDeleteButton';
|
||||||
|
|
||||||
export function LinksTable({ data = [] }) {
|
export function LinksTable(props: DataTableProps) {
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
const { websiteId, renderUrl } = useNavigation();
|
const { websiteId, renderUrl } = useNavigation();
|
||||||
const { getSlugUrl } = useSlug('link');
|
const { getSlugUrl } = useSlug('link');
|
||||||
|
|
||||||
if (data.length === 0) {
|
|
||||||
return <Empty />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DataTable data={data}>
|
<DataTable {...props}>
|
||||||
<DataColumn id="name" label={formatMessage(labels.name)}>
|
<DataColumn id="name" label={formatMessage(labels.name)}>
|
||||||
{({ id, name }: any) => {
|
{({ id, name }: any) => {
|
||||||
return <Link href={renderUrl(`/links/${id}`)}>{name}</Link>;
|
return <Link href={renderUrl(`/links/${id}`)}>{name}</Link>;
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,18 @@
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { DataTable, DataColumn, Row } from '@umami/react-zen';
|
import { DataTable, DataColumn, Row, DataTableProps } from '@umami/react-zen';
|
||||||
import { useMessages, useNavigation, useSlug } from '@/components/hooks';
|
import { useMessages, useNavigation, useSlug } from '@/components/hooks';
|
||||||
import { Empty } from '@/components/common/Empty';
|
|
||||||
import { DateDistance } from '@/components/common/DateDistance';
|
import { DateDistance } from '@/components/common/DateDistance';
|
||||||
import { PixelEditButton } from './PixelEditButton';
|
import { PixelEditButton } from './PixelEditButton';
|
||||||
import { PixelDeleteButton } from './PixelDeleteButton';
|
import { PixelDeleteButton } from './PixelDeleteButton';
|
||||||
import { ExternalLink } from '@/components/common/ExternalLink';
|
import { ExternalLink } from '@/components/common/ExternalLink';
|
||||||
|
|
||||||
export function PixelsTable({ data = [] }) {
|
export function PixelsTable(props: DataTableProps) {
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
const { renderUrl } = useNavigation();
|
const { renderUrl } = useNavigation();
|
||||||
const { getSlugUrl } = useSlug('pixel');
|
const { getSlugUrl } = useSlug('pixel');
|
||||||
|
|
||||||
if (data.length === 0) {
|
|
||||||
return <Empty />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DataTable data={data}>
|
<DataTable {...props}>
|
||||||
<DataColumn id="name" label={formatMessage(labels.name)}>
|
<DataColumn id="name" label={formatMessage(labels.name)}>
|
||||||
{({ id, name }: any) => {
|
{({ id, name }: any) => {
|
||||||
return <Link href={renderUrl(`/pixels/${id}`)}>{name}</Link>;
|
return <Link href={renderUrl(`/pixels/${id}`)}>{name}</Link>;
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,17 @@
|
||||||
import { DataColumn, DataTable } from '@umami/react-zen';
|
import { DataColumn, DataTable, DataTableProps } from '@umami/react-zen';
|
||||||
import { useMessages } from '@/components/hooks';
|
import { useMessages } from '@/components/hooks';
|
||||||
import { ROLES } from '@/lib/constants';
|
import { ROLES } from '@/lib/constants';
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
|
|
||||||
export function TeamsTable({
|
export interface TeamsTableProps extends DataTableProps {
|
||||||
data = [],
|
|
||||||
renderLink,
|
|
||||||
}: {
|
|
||||||
data: any[];
|
|
||||||
renderLink?: (row: any) => ReactNode;
|
renderLink?: (row: any) => ReactNode;
|
||||||
}) {
|
}
|
||||||
|
|
||||||
|
export function TeamsTable({ renderLink, ...props }: TeamsTableProps) {
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DataTable data={data}>
|
<DataTable {...props}>
|
||||||
<DataColumn id="name" label={formatMessage(labels.name)}>
|
<DataColumn id="name" label={formatMessage(labels.name)}>
|
||||||
{renderLink}
|
{renderLink}
|
||||||
</DataColumn>
|
</DataColumn>
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,22 @@
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
import { Icon, DataTable, DataColumn } from '@umami/react-zen';
|
import { Icon, DataTable, DataColumn, DataTableProps } from '@umami/react-zen';
|
||||||
import { LinkButton } from '@/components/common/LinkButton';
|
import { LinkButton } from '@/components/common/LinkButton';
|
||||||
import { useMessages, useNavigation } from '@/components/hooks';
|
import { useMessages, useNavigation } from '@/components/hooks';
|
||||||
import { SquarePen } from '@/components/icons';
|
import { SquarePen } from '@/components/icons';
|
||||||
import { Empty } from '@/components/common/Empty';
|
|
||||||
|
|
||||||
export function WebsitesTable({
|
export interface WebsitesTableProps extends DataTableProps {
|
||||||
data = [],
|
|
||||||
showActions,
|
|
||||||
renderLink,
|
|
||||||
}: {
|
|
||||||
data: Record<string, any>[];
|
|
||||||
showActions?: boolean;
|
showActions?: boolean;
|
||||||
allowEdit?: boolean;
|
allowEdit?: boolean;
|
||||||
allowView?: boolean;
|
allowView?: boolean;
|
||||||
renderLink?: (row: any) => ReactNode;
|
renderLink?: (row: any) => ReactNode;
|
||||||
}) {
|
}
|
||||||
|
|
||||||
|
export function WebsitesTable({ showActions, renderLink, ...props }: WebsitesTableProps) {
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
const { renderUrl } = useNavigation();
|
const { renderUrl } = useNavigation();
|
||||||
|
|
||||||
if (data.length === 0) {
|
|
||||||
return <Empty />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DataTable data={data}>
|
<DataTable {...props}>
|
||||||
<DataColumn id="name" label={formatMessage(labels.name)}>
|
<DataColumn id="name" label={formatMessage(labels.name)}>
|
||||||
{renderLink}
|
{renderLink}
|
||||||
</DataColumn>
|
</DataColumn>
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ export function WebsiteLayout({ websiteId, children }: { websiteId: string; chil
|
||||||
<Grid columns={{ xs: '1fr', lg: 'auto 1fr' }} width="100%" height="100%">
|
<Grid columns={{ xs: '1fr', lg: 'auto 1fr' }} width="100%" height="100%">
|
||||||
<Column
|
<Column
|
||||||
display={{ xs: 'none', lg: 'flex' }}
|
display={{ xs: 'none', lg: 'flex' }}
|
||||||
|
width="240px"
|
||||||
height="100%"
|
height="100%"
|
||||||
border="right"
|
border="right"
|
||||||
backgroundColor
|
backgroundColor
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { DataTable, DataColumn, Icon, Row, Text } from '@umami/react-zen';
|
import { DataTable, DataColumn, Row, Text, DataTableProps, IconLabel } from '@umami/react-zen';
|
||||||
import { useFormat, useMessages, useNavigation } from '@/components/hooks';
|
import { useFormat, useMessages, useNavigation } from '@/components/hooks';
|
||||||
import { Empty } from '@/components/common/Empty';
|
|
||||||
import { Avatar } from '@/components/common/Avatar';
|
import { Avatar } from '@/components/common/Avatar';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { Eye } from '@/components/icons';
|
import { Eye } from '@/components/icons';
|
||||||
|
|
@ -8,35 +7,44 @@ import { Lightning } from '@/components/svg';
|
||||||
import { DateDistance } from '@/components/common/DateDistance';
|
import { DateDistance } from '@/components/common/DateDistance';
|
||||||
import { TypeIcon } from '@/components/common/TypeIcon';
|
import { TypeIcon } from '@/components/common/TypeIcon';
|
||||||
|
|
||||||
export function EventsTable({ data = [] }) {
|
export function EventsTable(props: DataTableProps) {
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
const { updateParams } = useNavigation();
|
const { updateParams } = useNavigation();
|
||||||
const { formatValue } = useFormat();
|
const { formatValue } = useFormat();
|
||||||
|
|
||||||
if (data.length === 0) {
|
|
||||||
return <Empty />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DataTable data={data}>
|
<DataTable {...props}>
|
||||||
<DataColumn id="event" label={formatMessage(labels.event)} width="2fr">
|
<DataColumn id="event" label={formatMessage(labels.event)} width="2fr">
|
||||||
{(row: any) => {
|
{(row: any) => {
|
||||||
return (
|
return (
|
||||||
<Row alignItems="center" gap="2">
|
<Row alignItems="center" wrap="wrap" gap>
|
||||||
<Link href={updateParams({ session: row.sessionId })}>
|
<Row>
|
||||||
<Avatar seed={row.sessionId} size={32} />
|
<IconLabel
|
||||||
</Link>
|
icon={row.eventName ? <Lightning /> : <Eye />}
|
||||||
<Icon>{row.eventName ? <Lightning /> : <Eye />}</Icon>
|
label={formatMessage(row.eventName ? labels.triggeredEvent : labels.viewedPage)}
|
||||||
<Text>
|
/>
|
||||||
{formatMessage(row.eventName ? labels.triggeredEvent : labels.viewedPage)}
|
</Row>
|
||||||
</Text>
|
<Text
|
||||||
<Text weight="bold" style={{ maxWidth: '300px' }} truncate>
|
weight="bold"
|
||||||
|
style={{ maxWidth: '300px' }}
|
||||||
|
title={row.eventName || row.urlPath}
|
||||||
|
truncate
|
||||||
|
>
|
||||||
{row.eventName || row.urlPath}
|
{row.eventName || row.urlPath}
|
||||||
</Text>
|
</Text>
|
||||||
</Row>
|
</Row>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
</DataColumn>
|
</DataColumn>
|
||||||
|
<DataColumn id="session" label={formatMessage(labels.session)} width="80px">
|
||||||
|
{(row: any) => {
|
||||||
|
return (
|
||||||
|
<Link href={updateParams({ session: row.sessionId })}>
|
||||||
|
<Avatar seed={row.sessionId} size={32} />
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</DataColumn>
|
||||||
<DataColumn id="location" label={formatMessage(labels.location)}>
|
<DataColumn id="location" label={formatMessage(labels.location)}>
|
||||||
{(row: any) => (
|
{(row: any) => (
|
||||||
<TypeIcon type="country" value={row.country}>
|
<TypeIcon type="country" value={row.country}>
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ export function SessionsDataTable({ websiteId }: { websiteId?: string; teamId?:
|
||||||
return (
|
return (
|
||||||
<DataGrid query={queryResult} allowPaging allowSearch>
|
<DataGrid query={queryResult} allowPaging allowSearch>
|
||||||
{({ data }) => {
|
{({ data }) => {
|
||||||
return <SessionsTable data={data} showDomain={!websiteId} />;
|
return <SessionsTable data={data} />;
|
||||||
}}
|
}}
|
||||||
</DataGrid>
|
</DataGrid>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,17 @@
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { DataColumn, DataTable } from '@umami/react-zen';
|
import { DataColumn, DataTable, DataTableProps } from '@umami/react-zen';
|
||||||
import { useFormat, useMessages, useNavigation } from '@/components/hooks';
|
import { useFormat, useMessages, useNavigation } from '@/components/hooks';
|
||||||
import { Avatar } from '@/components/common/Avatar';
|
import { Avatar } from '@/components/common/Avatar';
|
||||||
import { TypeIcon } from '@/components/common/TypeIcon';
|
import { TypeIcon } from '@/components/common/TypeIcon';
|
||||||
import { DateDistance } from '@/components/common/DateDistance';
|
import { DateDistance } from '@/components/common/DateDistance';
|
||||||
|
|
||||||
export function SessionsTable({ data = [] }: { data: any[]; showDomain?: boolean }) {
|
export function SessionsTable(props: DataTableProps) {
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
const { formatValue } = useFormat();
|
const { formatValue } = useFormat();
|
||||||
const { updateParams } = useNavigation();
|
const { updateParams } = useNavigation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DataTable data={data}>
|
<DataTable {...props}>
|
||||||
<DataColumn id="id" label={formatMessage(labels.session)} width="100px">
|
<DataColumn id="id" label={formatMessage(labels.session)} width="100px">
|
||||||
{(row: any) => (
|
{(row: any) => (
|
||||||
<Link href={updateParams({ session: row.id })}>
|
<Link href={updateParams({ session: row.id })}>
|
||||||
|
|
|
||||||
|
|
@ -24,22 +24,26 @@ export async function GET(request: Request) {
|
||||||
const teams = await getTeams(
|
const teams = await getTeams(
|
||||||
{
|
{
|
||||||
include: {
|
include: {
|
||||||
_count: {
|
|
||||||
select: {
|
|
||||||
members: true,
|
|
||||||
websites: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
members: {
|
members: {
|
||||||
select: {
|
include: {
|
||||||
user: {
|
user: {
|
||||||
omit: {
|
select: {
|
||||||
password: true,
|
id: true,
|
||||||
|
username: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
where: {
|
},
|
||||||
role: 'team-owner',
|
_count: {
|
||||||
|
select: {
|
||||||
|
websites: {
|
||||||
|
where: { deletedAt: null },
|
||||||
|
},
|
||||||
|
members: {
|
||||||
|
where: {
|
||||||
|
user: { deletedAt: null },
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { uuid } from '@/lib/crypto';
|
import { uuid } from '@/lib/crypto';
|
||||||
import { pagingParams, reportSchema } from '@/lib/schema';
|
import { pagingParams, reportSchema, reportTypeParam } from '@/lib/schema';
|
||||||
import { parseRequest } from '@/lib/request';
|
import { parseRequest } from '@/lib/request';
|
||||||
import { canViewWebsite, canUpdateWebsite } from '@/permissions';
|
import { canViewWebsite, canUpdateWebsite } from '@/permissions';
|
||||||
import { unauthorized, json } from '@/lib/response';
|
import { unauthorized, json } from '@/lib/response';
|
||||||
|
|
@ -9,7 +9,7 @@ import { getReports, createReport } from '@/queries/prisma';
|
||||||
export async function GET(request: Request) {
|
export async function GET(request: Request) {
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
websiteId: z.uuid(),
|
websiteId: z.uuid(),
|
||||||
type: z.string().optional(),
|
type: reportTypeParam.optional(),
|
||||||
...pagingParams,
|
...pagingParams,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,9 +26,9 @@ export async function GET(request: Request, { params }: { params: Promise<{ user
|
||||||
|
|
||||||
export async function POST(request: Request, { params }: { params: Promise<{ userId: string }> }) {
|
export async function POST(request: Request, { params }: { params: Promise<{ userId: string }> }) {
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
username: z.string().max(255),
|
username: z.string().max(255).optional(),
|
||||||
password: z.string().max(255).optional(),
|
password: z.string().max(255).optional(),
|
||||||
role: userRoleParam,
|
role: userRoleParam.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const { auth, body, error } = await parseRequest(request, schema);
|
const { auth, body, error } = await parseRequest(request, schema);
|
||||||
|
|
|
||||||
|
|
@ -3,15 +3,16 @@ import { getQueryFilters, parseRequest } from '@/lib/request';
|
||||||
import { unauthorized, json } from '@/lib/response';
|
import { unauthorized, json } from '@/lib/response';
|
||||||
import { canViewWebsite } from '@/permissions';
|
import { canViewWebsite } from '@/permissions';
|
||||||
import { getEventDataProperties } from '@/queries/sql';
|
import { getEventDataProperties } from '@/queries/sql';
|
||||||
|
import { dateRangeParams, filterParams } from '@/lib/schema';
|
||||||
|
|
||||||
export async function GET(
|
export async function GET(
|
||||||
request: Request,
|
request: Request,
|
||||||
{ params }: { params: Promise<{ websiteId: string }> },
|
{ params }: { params: Promise<{ websiteId: string }> },
|
||||||
) {
|
) {
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
startAt: z.coerce.number().int(),
|
|
||||||
endAt: z.coerce.number().int(),
|
|
||||||
propertyName: z.string().optional(),
|
propertyName: z.string().optional(),
|
||||||
|
...dateRangeParams,
|
||||||
|
...filterParams,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { auth, query, error } = await parseRequest(request, schema);
|
const { auth, query, error } = await parseRequest(request, schema);
|
||||||
|
|
|
||||||
|
|
@ -3,16 +3,17 @@ import { getQueryFilters, parseRequest } from '@/lib/request';
|
||||||
import { unauthorized, json } from '@/lib/response';
|
import { unauthorized, json } from '@/lib/response';
|
||||||
import { canViewWebsite } from '@/permissions';
|
import { canViewWebsite } from '@/permissions';
|
||||||
import { getEventDataValues } from '@/queries/sql';
|
import { getEventDataValues } from '@/queries/sql';
|
||||||
|
import { dateRangeParams, filterParams } from '@/lib/schema';
|
||||||
|
|
||||||
export async function GET(
|
export async function GET(
|
||||||
request: Request,
|
request: Request,
|
||||||
{ params }: { params: Promise<{ websiteId: string }> },
|
{ params }: { params: Promise<{ websiteId: string }> },
|
||||||
) {
|
) {
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
startAt: z.coerce.number().int(),
|
|
||||||
endAt: z.coerce.number().int(),
|
|
||||||
eventName: z.string().optional(),
|
eventName: z.string().optional(),
|
||||||
propertyName: z.string().optional(),
|
propertyName: z.string().optional(),
|
||||||
|
...dateRangeParams,
|
||||||
|
...filterParams,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { auth, query, error } = await parseRequest(request, schema);
|
const { auth, query, error } = await parseRequest(request, schema);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,12 @@
|
||||||
import { ReactNode, useState, useCallback } from 'react';
|
import {
|
||||||
import { SearchField, Row, Column } from '@umami/react-zen';
|
ReactNode,
|
||||||
|
useState,
|
||||||
|
useCallback,
|
||||||
|
ReactElement,
|
||||||
|
cloneElement,
|
||||||
|
isValidElement,
|
||||||
|
} from 'react';
|
||||||
|
import { SearchField, Row, Column, useBreakpoint } from '@umami/react-zen';
|
||||||
import { UseQueryResult } from '@tanstack/react-query';
|
import { UseQueryResult } from '@tanstack/react-query';
|
||||||
import { useMessages, useNavigation } from '@/components/hooks';
|
import { useMessages, useNavigation } from '@/components/hooks';
|
||||||
import { Pager } from '@/components/common/Pager';
|
import { Pager } from '@/components/common/Pager';
|
||||||
|
|
@ -35,6 +42,8 @@ export function DataGrid({
|
||||||
const { router, updateParams, query: queryParams } = useNavigation();
|
const { router, updateParams, query: queryParams } = useNavigation();
|
||||||
const [search, setSearch] = useState(queryParams?.search || data?.search || '');
|
const [search, setSearch] = useState(queryParams?.search || data?.search || '');
|
||||||
const showPager = allowPaging && data && data.count > data.pageSize;
|
const showPager = allowPaging && data && data.count > data.pageSize;
|
||||||
|
const breakpoint = useBreakpoint();
|
||||||
|
const displayMode = ['xs', 'sm', 'md', 'lg'].includes(breakpoint) ? 'cards' : undefined;
|
||||||
|
|
||||||
const handleSearch = (value: string) => {
|
const handleSearch = (value: string) => {
|
||||||
if (value !== search) {
|
if (value !== search) {
|
||||||
|
|
@ -50,6 +59,8 @@ export function DataGrid({
|
||||||
[search],
|
[search],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const child = data ? (typeof children === 'function' ? children(data) : children) : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column gap="4" minHeight="300px">
|
<Column gap="4" minHeight="300px">
|
||||||
{allowSearch && (
|
{allowSearch && (
|
||||||
|
|
@ -73,7 +84,11 @@ export function DataGrid({
|
||||||
>
|
>
|
||||||
{data && (
|
{data && (
|
||||||
<>
|
<>
|
||||||
<Column>{typeof children === 'function' ? children(data) : children}</Column>
|
<Column>
|
||||||
|
{isValidElement(child)
|
||||||
|
? cloneElement(child as ReactElement<any>, { displayMode })
|
||||||
|
: child}
|
||||||
|
</Column>
|
||||||
{showPager && (
|
{showPager && (
|
||||||
<Row marginTop="6">
|
<Row marginTop="6">
|
||||||
<Pager
|
<Pager
|
||||||
|
|
|
||||||
|
|
@ -31,9 +31,9 @@ export function PageBody({
|
||||||
<Column
|
<Column
|
||||||
{...props}
|
{...props}
|
||||||
width="100%"
|
width="100%"
|
||||||
paddingBottom="9"
|
paddingBottom="6"
|
||||||
maxWidth={maxWidth}
|
maxWidth={maxWidth}
|
||||||
paddingX="4"
|
paddingX={{ xs: '3', md: '6' }}
|
||||||
style={{ margin: '0 auto' }}
|
style={{ margin: '0 auto' }}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ export function PageHeader({
|
||||||
{icon}
|
{icon}
|
||||||
</Icon>
|
</Icon>
|
||||||
)}
|
)}
|
||||||
{title && <Heading size="4">{title}</Heading>}
|
{title && <Heading size={{ xs: '2', md: '3', lg: '4' }}>{title}</Heading>}
|
||||||
</Row>
|
</Row>
|
||||||
{description && (
|
{description && (
|
||||||
<Text color="muted" truncate style={{ maxWidth: 600 }} title={description}>
|
<Text color="muted" truncate style={{ maxWidth: 600 }} title={description}>
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,6 @@ export function SideMenu({
|
||||||
<Column
|
<Column
|
||||||
gap
|
gap
|
||||||
padding
|
padding
|
||||||
width="240px"
|
|
||||||
overflowY="auto"
|
overflowY="auto"
|
||||||
justifyContent="space-between"
|
justifyContent="space-between"
|
||||||
position="sticky"
|
position="sticky"
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,22 @@ import { ReactQueryOptions } from '@/lib/types';
|
||||||
|
|
||||||
export function useEventDataEventsQuery(websiteId: string, options?: ReactQueryOptions) {
|
export function useEventDataEventsQuery(websiteId: string, options?: ReactQueryOptions) {
|
||||||
const { get, useQuery } = useApi();
|
const { get, useQuery } = useApi();
|
||||||
const date = useDateParameters();
|
const { startAt, endAt, unit, timezone } = useDateParameters();
|
||||||
const filters = useFilterParameters();
|
const filters = useFilterParameters();
|
||||||
|
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['websites:event-data:events', { websiteId, ...date, ...filters }],
|
queryKey: [
|
||||||
queryFn: () => get(`/websites/${websiteId}/event-data/events`, { ...date, ...filters }),
|
'websites:event-data:events',
|
||||||
|
{ websiteId, startAt, endAt, unit, timezone, ...filters },
|
||||||
|
],
|
||||||
|
queryFn: () =>
|
||||||
|
get(`/websites/${websiteId}/event-data/events`, {
|
||||||
|
startAt,
|
||||||
|
endAt,
|
||||||
|
unit,
|
||||||
|
timezone,
|
||||||
|
...filters,
|
||||||
|
}),
|
||||||
enabled: !!websiteId,
|
enabled: !!websiteId,
|
||||||
...options,
|
...options,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,22 @@ import { ReactQueryOptions } from '@/lib/types';
|
||||||
|
|
||||||
export function useEventDataPropertiesQuery(websiteId: string, options?: ReactQueryOptions) {
|
export function useEventDataPropertiesQuery(websiteId: string, options?: ReactQueryOptions) {
|
||||||
const { get, useQuery } = useApi();
|
const { get, useQuery } = useApi();
|
||||||
const date = useDateParameters();
|
const { startAt, endAt, unit, timezone } = useDateParameters();
|
||||||
const filters = useFilterParameters();
|
const filters = useFilterParameters();
|
||||||
|
|
||||||
return useQuery<any>({
|
return useQuery<any>({
|
||||||
queryKey: ['websites:event-data:properties', { websiteId, ...date, ...filters }],
|
queryKey: [
|
||||||
queryFn: () => get(`/websites/${websiteId}/event-data/properties`, { ...date, ...filters }),
|
'websites:event-data:properties',
|
||||||
|
{ websiteId, startAt, endAt, unit, timezone, ...filters },
|
||||||
|
],
|
||||||
|
queryFn: () =>
|
||||||
|
get(`/websites/${websiteId}/event-data/properties`, {
|
||||||
|
startAt,
|
||||||
|
endAt,
|
||||||
|
unit,
|
||||||
|
timezone,
|
||||||
|
...filters,
|
||||||
|
}),
|
||||||
enabled: !!websiteId,
|
enabled: !!websiteId,
|
||||||
...options,
|
...options,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,22 @@ import { ReactQueryOptions } from '@/lib/types';
|
||||||
|
|
||||||
export function useEventDataQuery(websiteId: string, eventId: string, options?: ReactQueryOptions) {
|
export function useEventDataQuery(websiteId: string, eventId: string, options?: ReactQueryOptions) {
|
||||||
const { get, useQuery } = useApi();
|
const { get, useQuery } = useApi();
|
||||||
const date = useDateParameters();
|
const { startAt, endAt, unit, timezone } = useDateParameters();
|
||||||
const params = useFilterParameters();
|
const params = useFilterParameters();
|
||||||
|
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['websites:event-data', { websiteId, eventId, ...date, ...params }],
|
queryKey: [
|
||||||
queryFn: () => get(`/websites/${websiteId}/event-data/${eventId}`, { ...date, ...params }),
|
'websites:event-data',
|
||||||
|
{ websiteId, eventId, startAt, endAt, unit, timezone, ...params },
|
||||||
|
],
|
||||||
|
queryFn: () =>
|
||||||
|
get(`/websites/${websiteId}/event-data/${eventId}`, {
|
||||||
|
startAt,
|
||||||
|
endAt,
|
||||||
|
unit,
|
||||||
|
timezone,
|
||||||
|
...params,
|
||||||
|
}),
|
||||||
enabled: !!(websiteId && eventId),
|
enabled: !!(websiteId && eventId),
|
||||||
...options,
|
...options,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { useApi } from '../useApi';
|
|
||||||
import { useFilterParameters } from '../useFilterParameters';
|
|
||||||
import { useDateParameters } from '../useDateParameters';
|
|
||||||
import { ReactQueryOptions } from '@/lib/types';
|
import { ReactQueryOptions } from '@/lib/types';
|
||||||
|
import { useApi } from '../useApi';
|
||||||
|
import { useDateParameters } from '../useDateParameters';
|
||||||
|
import { useFilterParameters } from '../useFilterParameters';
|
||||||
|
|
||||||
export function useEventDataValuesQuery(
|
export function useEventDataValuesQuery(
|
||||||
websiteId: string,
|
websiteId: string,
|
||||||
|
|
@ -10,17 +10,20 @@ export function useEventDataValuesQuery(
|
||||||
options?: ReactQueryOptions,
|
options?: ReactQueryOptions,
|
||||||
) {
|
) {
|
||||||
const { get, useQuery } = useApi();
|
const { get, useQuery } = useApi();
|
||||||
const date = useDateParameters();
|
const { startAt, endAt, unit, timezone } = useDateParameters();
|
||||||
const filters = useFilterParameters();
|
const filters = useFilterParameters();
|
||||||
|
|
||||||
return useQuery<any>({
|
return useQuery<any>({
|
||||||
queryKey: [
|
queryKey: [
|
||||||
'websites:event-data:values',
|
'websites:event-data:values',
|
||||||
{ websiteId, eventName, propertyName, ...date, ...filters },
|
{ websiteId, eventName, propertyName, startAt, endAt, unit, timezone, ...filters },
|
||||||
],
|
],
|
||||||
queryFn: () =>
|
queryFn: () =>
|
||||||
get(`/websites/${websiteId}/event-data/values`, {
|
get(`/websites/${websiteId}/event-data/values`, {
|
||||||
...date,
|
startAt,
|
||||||
|
endAt,
|
||||||
|
unit,
|
||||||
|
timezone,
|
||||||
...filters,
|
...filters,
|
||||||
eventName,
|
eventName,
|
||||||
propertyName,
|
propertyName,
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,7 @@ export function NavButton({ showText = true }: TeamsButtonProps) {
|
||||||
borderRadius
|
borderRadius
|
||||||
shadow="1"
|
shadow="1"
|
||||||
maxHeight="40px"
|
maxHeight="40px"
|
||||||
|
role="button"
|
||||||
style={{ cursor: 'pointer', textWrap: 'nowrap', outline: 'none' }}
|
style={{ cursor: 'pointer', textWrap: 'nowrap', outline: 'none' }}
|
||||||
>
|
>
|
||||||
<Row alignItems="center" position="relative" gap maxHeight="40px">
|
<Row alignItems="center" position="relative" gap maxHeight="40px">
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,11 @@
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Select, SelectProps, ListItem, Text, Row } from '@umami/react-zen';
|
import { Select, SelectProps, ListItem, Text, Row } from '@umami/react-zen';
|
||||||
import { useUserWebsitesQuery, useMessages, useLoginQuery, useWebsite } from '@/components/hooks';
|
import {
|
||||||
|
useUserWebsitesQuery,
|
||||||
|
useMessages,
|
||||||
|
useLoginQuery,
|
||||||
|
useWebsiteQuery,
|
||||||
|
} from '@/components/hooks';
|
||||||
import { Empty } from '@/components/common/Empty';
|
import { Empty } from '@/components/common/Empty';
|
||||||
|
|
||||||
export function WebsiteSelect({
|
export function WebsiteSelect({
|
||||||
|
|
@ -15,7 +20,7 @@ export function WebsiteSelect({
|
||||||
includeTeams?: boolean;
|
includeTeams?: boolean;
|
||||||
} & SelectProps) {
|
} & SelectProps) {
|
||||||
const { formatMessage, messages } = useMessages();
|
const { formatMessage, messages } = useMessages();
|
||||||
const website = useWebsite();
|
const { data: website } = useWebsiteQuery(websiteId);
|
||||||
const [name, setName] = useState<string>(website?.name);
|
const [name, setName] = useState<string>(website?.name);
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
const { user } = useLoginQuery();
|
const { user } = useLoginQuery();
|
||||||
|
|
|
||||||
|
|
@ -68,10 +68,15 @@ async function clickhouseQuery(
|
||||||
event_name as eventName,
|
event_name as eventName,
|
||||||
data_key as propertyName,
|
data_key as propertyName,
|
||||||
count(*) as total
|
count(*) as total
|
||||||
from event_data website_event
|
from event_data
|
||||||
|
join website_event
|
||||||
|
on website_event.event_id = event_data.event_id
|
||||||
|
and website_event.website_id = event_data.website_id
|
||||||
|
and website_event.website_id = {websiteId:UUID}
|
||||||
|
and website_event.created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||||
${cohortQuery}
|
${cohortQuery}
|
||||||
where website_id = {websiteId:UUID}
|
where event_data.website_id = {websiteId:UUID}
|
||||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
and event_data.created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||||
${filterQuery}
|
${filterQuery}
|
||||||
group by event_name, data_key
|
group by event_name, data_key
|
||||||
order by 1, 3 desc
|
order by 1, 3 desc
|
||||||
|
|
|
||||||
|
|
@ -75,12 +75,17 @@ async function clickhouseQuery(
|
||||||
data_type = 4, toString(date_trunc('hour', date_value)),
|
data_type = 4, toString(date_trunc('hour', date_value)),
|
||||||
string_value) as "value",
|
string_value) as "value",
|
||||||
count(*) as "total"
|
count(*) as "total"
|
||||||
from event_data website_event
|
from event_data
|
||||||
|
join website_event
|
||||||
|
on website_event.event_id = event_data.event_id
|
||||||
|
and website_event.website_id = event_data.website_id
|
||||||
|
and website_event.website_id = {websiteId:UUID}
|
||||||
|
and website_event.created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||||
${cohortQuery}
|
${cohortQuery}
|
||||||
where website_id = {websiteId:UUID}
|
where event_data.website_id = {websiteId:UUID}
|
||||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
and event_data.created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||||
and data_key = {propertyName:String}
|
and event_data.data_key = {propertyName:String}
|
||||||
and event_name = {eventName:String}
|
and event_data.event_name = {eventName:String}
|
||||||
${filterQuery}
|
${filterQuery}
|
||||||
group by value
|
group by value
|
||||||
order by 2 desc
|
order by 2 desc
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue