From 9fb38f6970be6e4179009371c0b17d99534de5aa Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Tue, 14 Oct 2025 09:41:02 -0700 Subject: [PATCH 1/6] fix zod validations and prisma query for admin teams --- src/app/api/admin/teams/route.ts | 26 +++++++++++++++----------- src/app/api/reports/route.ts | 4 ++-- src/app/api/users/[userId]/route.ts | 4 ++-- 3 files changed, 19 insertions(+), 15 deletions(-) diff --git a/src/app/api/admin/teams/route.ts b/src/app/api/admin/teams/route.ts index fa203a299..ceb16ab11 100644 --- a/src/app/api/admin/teams/route.ts +++ b/src/app/api/admin/teams/route.ts @@ -24,22 +24,26 @@ export async function GET(request: Request) { const teams = await getTeams( { include: { - _count: { - select: { - members: true, - websites: true, - }, - }, members: { - select: { + include: { user: { - omit: { - password: true, + select: { + id: true, + username: true, }, }, }, - where: { - role: 'team-owner', + }, + _count: { + select: { + websites: { + where: { deletedAt: null }, + }, + members: { + where: { + user: { deletedAt: null }, + }, + }, }, }, }, diff --git a/src/app/api/reports/route.ts b/src/app/api/reports/route.ts index ec8a4d239..123a7e66a 100644 --- a/src/app/api/reports/route.ts +++ b/src/app/api/reports/route.ts @@ -1,6 +1,6 @@ import { z } from 'zod'; import { uuid } from '@/lib/crypto'; -import { pagingParams, reportSchema } from '@/lib/schema'; +import { pagingParams, reportSchema, reportTypeParam } from '@/lib/schema'; import { parseRequest } from '@/lib/request'; import { canViewWebsite, canUpdateWebsite } from '@/permissions'; import { unauthorized, json } from '@/lib/response'; @@ -9,7 +9,7 @@ import { getReports, createReport } from '@/queries/prisma'; export async function GET(request: Request) { const schema = z.object({ websiteId: z.uuid(), - type: z.string().optional(), + type: reportTypeParam.optional(), ...pagingParams, }); diff --git a/src/app/api/users/[userId]/route.ts b/src/app/api/users/[userId]/route.ts index fbb794746..c15b4b71b 100644 --- a/src/app/api/users/[userId]/route.ts +++ b/src/app/api/users/[userId]/route.ts @@ -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 }> }) { const schema = z.object({ - username: z.string().max(255), + username: z.string().max(255).optional(), password: z.string().max(255).optional(), - role: userRoleParam, + role: userRoleParam.optional(), }); const { auth, body, error } = await parseRequest(request, schema); From ba45972bd3e9e92990e16bbdf0d686f018c8d176 Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Tue, 14 Oct 2025 11:22:21 -0700 Subject: [PATCH 2/6] fix filter passthrough into event-data endpoints --- pnpm-lock.yaml | 2 -- .../[websiteId]/event-data/properties/route.ts | 5 +++-- .../[websiteId]/event-data/values/route.ts | 5 +++-- .../hooks/queries/useEventDataEventsQuery.ts | 16 +++++++++++++--- .../hooks/queries/useEventDataPropertiesQuery.ts | 16 +++++++++++++--- .../hooks/queries/useEventDataQuery.ts | 16 +++++++++++++--- .../hooks/queries/useEventDataValuesQuery.ts | 15 +++++++++------ src/queries/sql/events/getEventDataProperties.ts | 11 ++++++++--- src/queries/sql/events/getEventDataValues.ts | 15 ++++++++++----- 9 files changed, 72 insertions(+), 29 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d3ad08bc8..8be50f1de 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -364,8 +364,6 @@ importers: specifier: ^5.9.3 version: 5.9.3 - dist: {} - packages: '@ampproject/remapping@2.3.0': diff --git a/src/app/api/websites/[websiteId]/event-data/properties/route.ts b/src/app/api/websites/[websiteId]/event-data/properties/route.ts index 0028d7477..c3b884ae7 100644 --- a/src/app/api/websites/[websiteId]/event-data/properties/route.ts +++ b/src/app/api/websites/[websiteId]/event-data/properties/route.ts @@ -3,15 +3,16 @@ import { getQueryFilters, parseRequest } from '@/lib/request'; import { unauthorized, json } from '@/lib/response'; import { canViewWebsite } from '@/permissions'; import { getEventDataProperties } from '@/queries/sql'; +import { dateRangeParams, filterParams } from '@/lib/schema'; export async function GET( request: Request, { params }: { params: Promise<{ websiteId: string }> }, ) { const schema = z.object({ - startAt: z.coerce.number().int(), - endAt: z.coerce.number().int(), propertyName: z.string().optional(), + ...dateRangeParams, + ...filterParams, }); const { auth, query, error } = await parseRequest(request, schema); diff --git a/src/app/api/websites/[websiteId]/event-data/values/route.ts b/src/app/api/websites/[websiteId]/event-data/values/route.ts index 4966764b6..5cf406658 100644 --- a/src/app/api/websites/[websiteId]/event-data/values/route.ts +++ b/src/app/api/websites/[websiteId]/event-data/values/route.ts @@ -3,16 +3,17 @@ import { getQueryFilters, parseRequest } from '@/lib/request'; import { unauthorized, json } from '@/lib/response'; import { canViewWebsite } from '@/permissions'; import { getEventDataValues } from '@/queries/sql'; +import { dateRangeParams, filterParams } from '@/lib/schema'; export async function GET( request: Request, { params }: { params: Promise<{ websiteId: string }> }, ) { const schema = z.object({ - startAt: z.coerce.number().int(), - endAt: z.coerce.number().int(), eventName: z.string().optional(), propertyName: z.string().optional(), + ...dateRangeParams, + ...filterParams, }); const { auth, query, error } = await parseRequest(request, schema); diff --git a/src/components/hooks/queries/useEventDataEventsQuery.ts b/src/components/hooks/queries/useEventDataEventsQuery.ts index 2c91dece5..26a825e3c 100644 --- a/src/components/hooks/queries/useEventDataEventsQuery.ts +++ b/src/components/hooks/queries/useEventDataEventsQuery.ts @@ -5,12 +5,22 @@ import { ReactQueryOptions } from '@/lib/types'; export function useEventDataEventsQuery(websiteId: string, options?: ReactQueryOptions) { const { get, useQuery } = useApi(); - const date = useDateParameters(); + const { startAt, endAt, unit, timezone } = useDateParameters(); const filters = useFilterParameters(); return useQuery({ - queryKey: ['websites:event-data:events', { websiteId, ...date, ...filters }], - queryFn: () => get(`/websites/${websiteId}/event-data/events`, { ...date, ...filters }), + queryKey: [ + 'websites:event-data:events', + { websiteId, startAt, endAt, unit, timezone, ...filters }, + ], + queryFn: () => + get(`/websites/${websiteId}/event-data/events`, { + startAt, + endAt, + unit, + timezone, + ...filters, + }), enabled: !!websiteId, ...options, }); diff --git a/src/components/hooks/queries/useEventDataPropertiesQuery.ts b/src/components/hooks/queries/useEventDataPropertiesQuery.ts index 19a2eb61d..74dd58ef2 100644 --- a/src/components/hooks/queries/useEventDataPropertiesQuery.ts +++ b/src/components/hooks/queries/useEventDataPropertiesQuery.ts @@ -5,12 +5,22 @@ import { ReactQueryOptions } from '@/lib/types'; export function useEventDataPropertiesQuery(websiteId: string, options?: ReactQueryOptions) { const { get, useQuery } = useApi(); - const date = useDateParameters(); + const { startAt, endAt, unit, timezone } = useDateParameters(); const filters = useFilterParameters(); return useQuery({ - queryKey: ['websites:event-data:properties', { websiteId, ...date, ...filters }], - queryFn: () => get(`/websites/${websiteId}/event-data/properties`, { ...date, ...filters }), + queryKey: [ + 'websites:event-data:properties', + { websiteId, startAt, endAt, unit, timezone, ...filters }, + ], + queryFn: () => + get(`/websites/${websiteId}/event-data/properties`, { + startAt, + endAt, + unit, + timezone, + ...filters, + }), enabled: !!websiteId, ...options, }); diff --git a/src/components/hooks/queries/useEventDataQuery.ts b/src/components/hooks/queries/useEventDataQuery.ts index 7e6f66ec2..5e21081ac 100644 --- a/src/components/hooks/queries/useEventDataQuery.ts +++ b/src/components/hooks/queries/useEventDataQuery.ts @@ -5,12 +5,22 @@ import { ReactQueryOptions } from '@/lib/types'; export function useEventDataQuery(websiteId: string, eventId: string, options?: ReactQueryOptions) { const { get, useQuery } = useApi(); - const date = useDateParameters(); + const { startAt, endAt, unit, timezone } = useDateParameters(); const params = useFilterParameters(); return useQuery({ - queryKey: ['websites:event-data', { websiteId, eventId, ...date, ...params }], - queryFn: () => get(`/websites/${websiteId}/event-data/${eventId}`, { ...date, ...params }), + queryKey: [ + '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), ...options, }); diff --git a/src/components/hooks/queries/useEventDataValuesQuery.ts b/src/components/hooks/queries/useEventDataValuesQuery.ts index de3b0590b..6394a1bb4 100644 --- a/src/components/hooks/queries/useEventDataValuesQuery.ts +++ b/src/components/hooks/queries/useEventDataValuesQuery.ts @@ -1,7 +1,7 @@ -import { useApi } from '../useApi'; -import { useFilterParameters } from '../useFilterParameters'; -import { useDateParameters } from '../useDateParameters'; import { ReactQueryOptions } from '@/lib/types'; +import { useApi } from '../useApi'; +import { useDateParameters } from '../useDateParameters'; +import { useFilterParameters } from '../useFilterParameters'; export function useEventDataValuesQuery( websiteId: string, @@ -10,17 +10,20 @@ export function useEventDataValuesQuery( options?: ReactQueryOptions, ) { const { get, useQuery } = useApi(); - const date = useDateParameters(); + const { startAt, endAt, unit, timezone } = useDateParameters(); const filters = useFilterParameters(); return useQuery({ queryKey: [ 'websites:event-data:values', - { websiteId, eventName, propertyName, ...date, ...filters }, + { websiteId, eventName, propertyName, startAt, endAt, unit, timezone, ...filters }, ], queryFn: () => get(`/websites/${websiteId}/event-data/values`, { - ...date, + startAt, + endAt, + unit, + timezone, ...filters, eventName, propertyName, diff --git a/src/queries/sql/events/getEventDataProperties.ts b/src/queries/sql/events/getEventDataProperties.ts index 60b46bcce..7a618fd2e 100644 --- a/src/queries/sql/events/getEventDataProperties.ts +++ b/src/queries/sql/events/getEventDataProperties.ts @@ -68,10 +68,15 @@ async function clickhouseQuery( event_name as eventName, data_key as propertyName, 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} - where website_id = {websiteId:UUID} - and created_at between {startDate:DateTime64} and {endDate:DateTime64} + where event_data.website_id = {websiteId:UUID} + and event_data.created_at between {startDate:DateTime64} and {endDate:DateTime64} ${filterQuery} group by event_name, data_key order by 1, 3 desc diff --git a/src/queries/sql/events/getEventDataValues.ts b/src/queries/sql/events/getEventDataValues.ts index bdfe7482e..0af938304 100644 --- a/src/queries/sql/events/getEventDataValues.ts +++ b/src/queries/sql/events/getEventDataValues.ts @@ -75,12 +75,17 @@ async function clickhouseQuery( data_type = 4, toString(date_trunc('hour', date_value)), string_value) as "value", 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} - where website_id = {websiteId:UUID} - and created_at between {startDate:DateTime64} and {endDate:DateTime64} - and data_key = {propertyName:String} - and event_name = {eventName:String} + where event_data.website_id = {websiteId:UUID} + and event_data.created_at between {startDate:DateTime64} and {endDate:DateTime64} + and event_data.data_key = {propertyName:String} + and event_data.event_name = {eventName:String} ${filterQuery} group by value order by 2 desc From d9b08d94914fff84461d77c58b51d5163a4d1d09 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Mon, 13 Oct 2025 13:01:01 -0700 Subject: [PATCH 3/6] Card mode for tables. --- .husky/pre-commit | 3 -- package.json | 2 +- pnpm-lock.yaml | 18 ++++---- src/app/(main)/App.tsx | 6 +-- src/app/(main)/links/LinksTable.tsx | 11 ++--- src/app/(main)/pixels/PixelsTable.tsx | 11 ++--- src/app/(main)/teams/TeamsTable.tsx | 14 +++---- src/app/(main)/websites/WebsitesTable.tsx | 20 +++------ .../[websiteId]/events/EventsTable.tsx | 42 +++++++++++-------- .../sessions/SessionsDataTable.tsx | 2 +- .../[websiteId]/sessions/SessionsTable.tsx | 6 +-- src/components/common/DataGrid.tsx | 21 ++++++++-- src/components/common/PageBody.tsx | 4 +- src/components/input/NavButton.tsx | 1 + 14 files changed, 81 insertions(+), 80 deletions(-) diff --git a/.husky/pre-commit b/.husky/pre-commit index 36af21989..2312dc587 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1 @@ -#!/bin/sh -. "$(dirname "$0")/_/husky.sh" - npx lint-staged diff --git a/package.json b/package.json index d52a7777a..db28ef615 100644 --- a/package.json +++ b/package.json @@ -78,7 +78,7 @@ "@react-spring/web": "^10.0.3", "@svgr/cli": "^8.1.0", "@tanstack/react-query": "^5.90.2", - "@umami/react-zen": "^0.189.0", + "@umami/react-zen": "^0.195.0", "@umami/redis-client": "^0.29.0", "bcryptjs": "^3.0.2", "chalk": "^5.6.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d3ad08bc8..5b3d91033 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -45,8 +45,8 @@ importers: specifier: ^5.90.2 version: 5.90.2(react@19.1.1) '@umami/react-zen': - specifier: ^0.189.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)) + specifier: ^0.195.0 + version: 0.195.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': specifier: ^0.29.0 version: 0.29.0 @@ -2756,8 +2756,8 @@ packages: resolution: {integrity: sha512-qsaFBA3e09MIDAGFUrTk+dzqtfv1XPVz8t8d1f0ybTzrCY7BKiMC5cjrl1O/P7UmHsNyW90EYSkU/ZWpmXelag==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@umami/react-zen@0.189.0': - resolution: {integrity: sha512-E5t5HvMrGfuilrnF6LJV+jeooC4qXpwUC4VGhnTPV24B1vdMC2W9ByzZreNaomgZy8XOVAk1wZf8QX1elloUjA==} + '@umami/react-zen@0.195.0': + resolution: {integrity: sha512-DI/o0AOwq6wfWEx+PgXFQ8xV0NJFP9xY1qd0+cv2Bme9Bho0U5+vxyFPkHawJyC7bfMDi3BgC7JHgTqeCqE99A==} '@umami/redis-client@0.29.0': resolution: {integrity: sha512-Jaqh++jskqDB7ny75pfC02OvKp1JTS4asGDsFrRL3qy8sxL3PAl9+/mybCJe4/6vWrXDJKqpgkSfUDJq2bFjyw==} @@ -6350,8 +6350,8 @@ packages: peerDependencies: react: '>=16.13.1' - react-hook-form@7.64.0: - resolution: {integrity: sha512-fnN+vvTiMLnRqKNTVhDysdrUay0kUUAymQnFIznmgDvapjveUWOOPqMNzPg+A+0yf9DuE2h6xzBjN1s+Qx8wcg==} + react-hook-form@7.65.0: + resolution: {integrity: sha512-xtOzDz063WcXvGWaHgLNrNzlsdFgtUWcb32E6WFaGTd7kPZG3EeDusjdZfUsPwKCKVXy1ZlntifaHZ4l8pAsmw==} engines: {node: '>=18.0.0'} peerDependencies: react: ^16.8.0 || ^17 || ^18 || ^19 @@ -10387,7 +10387,7 @@ snapshots: '@typescript-eslint/types': 8.45.0 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.195.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: '@fontsource/jetbrains-mono': 5.2.8 '@internationalized/date': 3.10.0 @@ -10401,7 +10401,7 @@ snapshots: 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-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) 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)) @@ -14534,7 +14534,7 @@ snapshots: '@babel/runtime': 7.28.3 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: react: 19.1.1 diff --git a/src/app/(main)/App.tsx b/src/app/(main)/App.tsx index 52c9257a3..b32b27852 100644 --- a/src/app/(main)/App.tsx +++ b/src/app/(main)/App.tsx @@ -28,9 +28,9 @@ export function App({ children }) { } return ( - - - + + + diff --git a/src/app/(main)/links/LinksTable.tsx b/src/app/(main)/links/LinksTable.tsx index b84718299..5df15b787 100644 --- a/src/app/(main)/links/LinksTable.tsx +++ b/src/app/(main)/links/LinksTable.tsx @@ -1,23 +1,18 @@ 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 { Empty } from '@/components/common/Empty'; import { DateDistance } from '@/components/common/DateDistance'; import { ExternalLink } from '@/components/common/ExternalLink'; import { LinkEditButton } from './LinkEditButton'; import { LinkDeleteButton } from './LinkDeleteButton'; -export function LinksTable({ data = [] }) { +export function LinksTable(props: DataTableProps) { const { formatMessage, labels } = useMessages(); const { websiteId, renderUrl } = useNavigation(); const { getSlugUrl } = useSlug('link'); - if (data.length === 0) { - return ; - } - return ( - + {({ id, name }: any) => { return {name}; diff --git a/src/app/(main)/pixels/PixelsTable.tsx b/src/app/(main)/pixels/PixelsTable.tsx index dcb5307c1..4edbb1cf4 100644 --- a/src/app/(main)/pixels/PixelsTable.tsx +++ b/src/app/(main)/pixels/PixelsTable.tsx @@ -1,23 +1,18 @@ 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 { Empty } from '@/components/common/Empty'; import { DateDistance } from '@/components/common/DateDistance'; import { PixelEditButton } from './PixelEditButton'; import { PixelDeleteButton } from './PixelDeleteButton'; import { ExternalLink } from '@/components/common/ExternalLink'; -export function PixelsTable({ data = [] }) { +export function PixelsTable(props: DataTableProps) { const { formatMessage, labels } = useMessages(); const { renderUrl } = useNavigation(); const { getSlugUrl } = useSlug('pixel'); - if (data.length === 0) { - return ; - } - return ( - + {({ id, name }: any) => { return {name}; diff --git a/src/app/(main)/teams/TeamsTable.tsx b/src/app/(main)/teams/TeamsTable.tsx index 5a22bcda8..b5f112331 100644 --- a/src/app/(main)/teams/TeamsTable.tsx +++ b/src/app/(main)/teams/TeamsTable.tsx @@ -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 { ROLES } from '@/lib/constants'; import { ReactNode } from 'react'; -export function TeamsTable({ - data = [], - renderLink, -}: { - data: any[]; +export interface TeamsTableProps extends DataTableProps { renderLink?: (row: any) => ReactNode; -}) { +} + +export function TeamsTable({ renderLink, ...props }: TeamsTableProps) { const { formatMessage, labels } = useMessages(); return ( - + {renderLink} diff --git a/src/app/(main)/websites/WebsitesTable.tsx b/src/app/(main)/websites/WebsitesTable.tsx index d6d743906..c2f7e0d46 100644 --- a/src/app/(main)/websites/WebsitesTable.tsx +++ b/src/app/(main)/websites/WebsitesTable.tsx @@ -1,30 +1,22 @@ 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 { useMessages, useNavigation } from '@/components/hooks'; import { SquarePen } from '@/components/icons'; -import { Empty } from '@/components/common/Empty'; -export function WebsitesTable({ - data = [], - showActions, - renderLink, -}: { - data: Record[]; +export interface WebsitesTableProps extends DataTableProps { showActions?: boolean; allowEdit?: boolean; allowView?: boolean; renderLink?: (row: any) => ReactNode; -}) { +} + +export function WebsitesTable({ showActions, renderLink, ...props }: WebsitesTableProps) { const { formatMessage, labels } = useMessages(); const { renderUrl } = useNavigation(); - if (data.length === 0) { - return ; - } - return ( - + {renderLink} diff --git a/src/app/(main)/websites/[websiteId]/events/EventsTable.tsx b/src/app/(main)/websites/[websiteId]/events/EventsTable.tsx index 7cefdd274..e9e3e6a0c 100644 --- a/src/app/(main)/websites/[websiteId]/events/EventsTable.tsx +++ b/src/app/(main)/websites/[websiteId]/events/EventsTable.tsx @@ -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 { Empty } from '@/components/common/Empty'; import { Avatar } from '@/components/common/Avatar'; import Link from 'next/link'; import { Eye } from '@/components/icons'; @@ -8,35 +7,44 @@ import { Lightning } from '@/components/svg'; import { DateDistance } from '@/components/common/DateDistance'; import { TypeIcon } from '@/components/common/TypeIcon'; -export function EventsTable({ data = [] }) { +export function EventsTable(props: DataTableProps) { const { formatMessage, labels } = useMessages(); const { updateParams } = useNavigation(); const { formatValue } = useFormat(); - if (data.length === 0) { - return ; - } - return ( - + {(row: any) => { return ( - - - - - {row.eventName ? : } - - {formatMessage(row.eventName ? labels.triggeredEvent : labels.viewedPage)} - - + + + : } + label={formatMessage(row.eventName ? labels.triggeredEvent : labels.viewedPage)} + /> + + {row.eventName || row.urlPath} ); }} + + {(row: any) => { + return ( + + + + ); + }} + {(row: any) => ( diff --git a/src/app/(main)/websites/[websiteId]/sessions/SessionsDataTable.tsx b/src/app/(main)/websites/[websiteId]/sessions/SessionsDataTable.tsx index 4c459bdc1..53b35b214 100644 --- a/src/app/(main)/websites/[websiteId]/sessions/SessionsDataTable.tsx +++ b/src/app/(main)/websites/[websiteId]/sessions/SessionsDataTable.tsx @@ -8,7 +8,7 @@ export function SessionsDataTable({ websiteId }: { websiteId?: string; teamId?: return ( {({ data }) => { - return ; + return ; }} ); diff --git a/src/app/(main)/websites/[websiteId]/sessions/SessionsTable.tsx b/src/app/(main)/websites/[websiteId]/sessions/SessionsTable.tsx index 391c57f43..ab4977ea2 100644 --- a/src/app/(main)/websites/[websiteId]/sessions/SessionsTable.tsx +++ b/src/app/(main)/websites/[websiteId]/sessions/SessionsTable.tsx @@ -1,17 +1,17 @@ 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 { Avatar } from '@/components/common/Avatar'; import { TypeIcon } from '@/components/common/TypeIcon'; import { DateDistance } from '@/components/common/DateDistance'; -export function SessionsTable({ data = [] }: { data: any[]; showDomain?: boolean }) { +export function SessionsTable(props: DataTableProps) { const { formatMessage, labels } = useMessages(); const { formatValue } = useFormat(); const { updateParams } = useNavigation(); return ( - + {(row: any) => ( diff --git a/src/components/common/DataGrid.tsx b/src/components/common/DataGrid.tsx index d95ee5155..18cb763c4 100644 --- a/src/components/common/DataGrid.tsx +++ b/src/components/common/DataGrid.tsx @@ -1,5 +1,12 @@ -import { ReactNode, useState, useCallback } from 'react'; -import { SearchField, Row, Column } from '@umami/react-zen'; +import { + ReactNode, + useState, + useCallback, + ReactElement, + cloneElement, + isValidElement, +} from 'react'; +import { SearchField, Row, Column, useBreakpoint } from '@umami/react-zen'; import { UseQueryResult } from '@tanstack/react-query'; import { useMessages, useNavigation } from '@/components/hooks'; import { Pager } from '@/components/common/Pager'; @@ -35,6 +42,8 @@ export function DataGrid({ const { router, updateParams, query: queryParams } = useNavigation(); const [search, setSearch] = useState(queryParams?.search || data?.search || ''); 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) => { if (value !== search) { @@ -50,6 +59,8 @@ export function DataGrid({ [search], ); + const child = data ? (typeof children === 'function' ? children(data) : children) : null; + return ( {allowSearch && ( @@ -73,7 +84,11 @@ export function DataGrid({ > {data && ( <> - {typeof children === 'function' ? children(data) : children} + + {isValidElement(child) + ? cloneElement(child as ReactElement, { displayMode }) + : child} + {showPager && ( {children} diff --git a/src/components/input/NavButton.tsx b/src/components/input/NavButton.tsx index 257cd685a..8be136968 100644 --- a/src/components/input/NavButton.tsx +++ b/src/components/input/NavButton.tsx @@ -61,6 +61,7 @@ export function NavButton({ showText = true }: TeamsButtonProps) { borderRadius shadow="1" maxHeight="40px" + role="button" style={{ cursor: 'pointer', textWrap: 'nowrap', outline: 'none' }} > From be5f0494ccceab26369374ce80fe79dfa27b9760 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Tue, 14 Oct 2025 15:43:57 -0700 Subject: [PATCH 4/6] Added MobileMenuButton component. --- package.json | 2 +- pnpm-lock.yaml | 10 +++++----- src/app/(main)/App.tsx | 13 +++++++++++-- src/components/common/PageHeader.tsx | 2 +- src/components/input/MobileMenuButton.tsx | 18 ++++++++++++++++++ 5 files changed, 36 insertions(+), 9 deletions(-) create mode 100644 src/components/input/MobileMenuButton.tsx diff --git a/package.json b/package.json index db28ef615..78ee92685 100644 --- a/package.json +++ b/package.json @@ -78,7 +78,7 @@ "@react-spring/web": "^10.0.3", "@svgr/cli": "^8.1.0", "@tanstack/react-query": "^5.90.2", - "@umami/react-zen": "^0.195.0", + "@umami/react-zen": "^0.196.0", "@umami/redis-client": "^0.29.0", "bcryptjs": "^3.0.2", "chalk": "^5.6.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5b3d91033..f3bc7fb2e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -45,8 +45,8 @@ importers: specifier: ^5.90.2 version: 5.90.2(react@19.1.1) '@umami/react-zen': - specifier: ^0.195.0 - version: 0.195.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)) + specifier: ^0.196.0 + 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': specifier: ^0.29.0 version: 0.29.0 @@ -2756,8 +2756,8 @@ packages: resolution: {integrity: sha512-qsaFBA3e09MIDAGFUrTk+dzqtfv1XPVz8t8d1f0ybTzrCY7BKiMC5cjrl1O/P7UmHsNyW90EYSkU/ZWpmXelag==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@umami/react-zen@0.195.0': - resolution: {integrity: sha512-DI/o0AOwq6wfWEx+PgXFQ8xV0NJFP9xY1qd0+cv2Bme9Bho0U5+vxyFPkHawJyC7bfMDi3BgC7JHgTqeCqE99A==} + '@umami/react-zen@0.196.0': + resolution: {integrity: sha512-CLxrDAJOdo+0aJAclOq7naIDg+2I5wP9wXxAFhxhQVPXHV8yUHqH9Ula632cLMo51JYp0l+eEtOtuimpuKX3jg==} '@umami/redis-client@0.29.0': resolution: {integrity: sha512-Jaqh++jskqDB7ny75pfC02OvKp1JTS4asGDsFrRL3qy8sxL3PAl9+/mybCJe4/6vWrXDJKqpgkSfUDJq2bFjyw==} @@ -10387,7 +10387,7 @@ snapshots: '@typescript-eslint/types': 8.45.0 eslint-visitor-keys: 4.2.1 - '@umami/react-zen@0.195.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: '@fontsource/jetbrains-mono': 5.2.8 '@internationalized/date': 3.10.0 diff --git a/src/app/(main)/App.tsx b/src/app/(main)/App.tsx index b32b27852..ee95850b4 100644 --- a/src/app/(main)/App.tsx +++ b/src/app/(main)/App.tsx @@ -1,9 +1,10 @@ 'use client'; -import { Grid, Loading, Column, Row } from '@umami/react-zen'; +import { Grid, Loading, Column, Row, List, ListItem } from '@umami/react-zen'; import Script from 'next/script'; import { UpdateNotice } from './UpdateNotice'; import { SideNav } from '@/app/(main)/SideNav'; import { useLoginQuery, useConfig, useNavigation } from '@/components/hooks'; +import { MobileMenuButton } from '@/components/input/MobileMenuButton'; export function App({ children }) { const { user, isLoading, error } = useLoginQuery(); @@ -29,7 +30,15 @@ export function App({ children }) { return ( - + + + + Websites + Links + Pixels + + + diff --git a/src/components/common/PageHeader.tsx b/src/components/common/PageHeader.tsx index 27d33a72b..8905d0cda 100644 --- a/src/components/common/PageHeader.tsx +++ b/src/components/common/PageHeader.tsx @@ -37,7 +37,7 @@ export function PageHeader({ {icon} )} - {title && {title}} + {title && {title}} {description && ( diff --git a/src/components/input/MobileMenuButton.tsx b/src/components/input/MobileMenuButton.tsx new file mode 100644 index 000000000..8994922b1 --- /dev/null +++ b/src/components/input/MobileMenuButton.tsx @@ -0,0 +1,18 @@ +import { Button, Icon, DialogTrigger, Dialog, Modal } from '@umami/react-zen'; +import { Menu } from '@/components/icons'; +import { ReactNode } from 'react'; + +export function MobileMenuButton({ children }: { children: ReactNode }) { + return ( + + + + {children} + + + ); +} From 10bc2895eb372a0931e87270c74365e0ca0515b9 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Tue, 14 Oct 2025 21:57:31 -0700 Subject: [PATCH 5/6] New mobile menu. --- src/app/(main)/App.tsx | 16 ++-- src/app/(main)/MobileNav.tsx | 77 +++++++++++++++++++ .../websites/[websiteId]/WebsiteLayout.tsx | 1 + src/components/common/SideMenu.tsx | 1 - src/components/input/MobileMenuButton.tsx | 18 ----- src/components/input/WebsiteSelect.tsx | 9 ++- 6 files changed, 90 insertions(+), 32 deletions(-) create mode 100644 src/app/(main)/MobileNav.tsx delete mode 100644 src/components/input/MobileMenuButton.tsx diff --git a/src/app/(main)/App.tsx b/src/app/(main)/App.tsx index ee95850b4..16676523d 100644 --- a/src/app/(main)/App.tsx +++ b/src/app/(main)/App.tsx @@ -1,10 +1,10 @@ 'use client'; -import { Grid, Loading, Column, Row, List, ListItem } from '@umami/react-zen'; +import { Grid, Loading, Column, Row } from '@umami/react-zen'; import Script from 'next/script'; import { UpdateNotice } from './UpdateNotice'; import { SideNav } from '@/app/(main)/SideNav'; import { useLoginQuery, useConfig, useNavigation } from '@/components/hooks'; -import { MobileMenuButton } from '@/components/input/MobileMenuButton'; +import { MobileNav } from '@/app/(main)/MobileNav'; export function App({ children }) { const { user, isLoading, error } = useLoginQuery(); @@ -29,15 +29,9 @@ export function App({ children }) { } return ( - - - - - Websites - Links - Pixels - - + + + diff --git a/src/app/(main)/MobileNav.tsx b/src/app/(main)/MobileNav.tsx new file mode 100644 index 000000000..9e5565c01 --- /dev/null +++ b/src/app/(main)/MobileNav.tsx @@ -0,0 +1,77 @@ +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'; + +export function MobileNav() { + const { formatMessage, labels } = useMessages(); + const { websiteId } = useNavigation(); + + const links = [ + { + id: 'websites', + label: formatMessage(labels.websites), + path: '/websites', + icon: , + }, + { + id: 'links', + label: formatMessage(labels.links), + path: '/links', + icon: , + }, + { + id: 'pixels', + label: formatMessage(labels.pixels), + path: '/pixels', + icon: , + }, + ]; + + return ( + + + + + + + {links.map(link => { + return ( + + + + + + ); + })} + + {websiteId && } + + + + + } style={{ width: 'auto' }}> + umami + + + + ); +} diff --git a/src/app/(main)/websites/[websiteId]/WebsiteLayout.tsx b/src/app/(main)/websites/[websiteId]/WebsiteLayout.tsx index d357c6a3c..c92bbd9c8 100644 --- a/src/app/(main)/websites/[websiteId]/WebsiteLayout.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsiteLayout.tsx @@ -12,6 +12,7 @@ export function WebsiteLayout({ websiteId, children }: { websiteId: string; chil - - - {children} - - - ); -} diff --git a/src/components/input/WebsiteSelect.tsx b/src/components/input/WebsiteSelect.tsx index 01861fbd4..29ae040b0 100644 --- a/src/components/input/WebsiteSelect.tsx +++ b/src/components/input/WebsiteSelect.tsx @@ -1,6 +1,11 @@ import { useState } from 'react'; 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'; export function WebsiteSelect({ @@ -15,7 +20,7 @@ export function WebsiteSelect({ includeTeams?: boolean; } & SelectProps) { const { formatMessage, messages } = useMessages(); - const website = useWebsite(); + const { data: website } = useWebsiteQuery(websiteId); const [name, setName] = useState(website?.name); const [search, setSearch] = useState(''); const { user } = useLoginQuery(); From 9df012084d958fbd2d6cbe7d0a83cd22db55b68c Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Tue, 14 Oct 2025 23:10:27 -0700 Subject: [PATCH 6/6] Mobile fixes. --- package.components.json | 2 +- src/app/(main)/App.tsx | 7 ++++++- src/app/(main)/MobileNav.tsx | 2 ++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/package.components.json b/package.components.json index d231f5b9b..51214f892 100644 --- a/package.components.json +++ b/package.components.json @@ -1,6 +1,6 @@ { "name": "@umami/components", - "version": "0.128.0", + "version": "0.129.0", "description": "Umami React components.", "author": "Mike Cao ", "license": "MIT", diff --git a/src/app/(main)/App.tsx b/src/app/(main)/App.tsx index 16676523d..32218d115 100644 --- a/src/app/(main)/App.tsx +++ b/src/app/(main)/App.tsx @@ -29,7 +29,12 @@ export function App({ children }) { } return ( - + diff --git a/src/app/(main)/MobileNav.tsx b/src/app/(main)/MobileNav.tsx index 9e5565c01..32751f0e5 100644 --- a/src/app/(main)/MobileNav.tsx +++ b/src/app/(main)/MobileNav.tsx @@ -16,6 +16,7 @@ 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(); @@ -53,6 +54,7 @@ export function MobileNav() { + {links.map(link => { return (