diff --git a/apps/public/src/app/page.tsx b/apps/public/src/app/page.tsx index 1af52927..a927e976 100644 --- a/apps/public/src/app/page.tsx +++ b/apps/public/src/app/page.tsx @@ -10,7 +10,7 @@ import { JoinWaitlist } from './join-waitlist'; import { Sections } from './section'; export const dynamic = 'force-dynamic'; -export const revalidate = 60 * 60; +export const revalidate = 3600; export default async function Page() { const waitlistCount = await db.waitlist.count(); diff --git a/apps/web/src/app/(app)/[organizationId]/[projectId]/events/event-list-item.tsx b/apps/web/src/app/(app)/[organizationId]/[projectId]/events/event-list-item.tsx index e2b337ec..b2cff72e 100644 --- a/apps/web/src/app/(app)/[organizationId]/[projectId]/events/event-list-item.tsx +++ b/apps/web/src/app/(app)/[organizationId]/[projectId]/events/event-list-item.tsx @@ -173,7 +173,6 @@ export function EventListItem({ {profile && ( } value={getProfileName(profile)} href={`/${params.organizationId}/${params.projectId}/profiles/${profile.id}`} /> @@ -191,28 +190,36 @@ export function EventListItem({ } image={} > - {propertiesList.length > 0 && ( -
-
Your properties
-
- {propertiesList.map((item) => ( - - ))} +
+
+ {propertiesList.length > 0 && ( +
+
Your properties
+
+ {propertiesList.map((item) => ( + + ))} +
+
+ )} +
+
Properties
+
+ {keyValueList.map((item) => ( + item.onClick?.()} + key={item.name} + name={item.name} + value={item.value} + /> + ))} +
- )} -
-
Properties
-
- {keyValueList.map((item) => ( - - ))} -
); diff --git a/apps/web/src/app/(app)/[organizationId]/[projectId]/events/event-list.tsx b/apps/web/src/app/(app)/[organizationId]/[projectId]/events/event-list.tsx new file mode 100644 index 00000000..b45fadae --- /dev/null +++ b/apps/web/src/app/(app)/[organizationId]/[projectId]/events/event-list.tsx @@ -0,0 +1,53 @@ +'use client'; + +import { FullPageEmptyState } from '@/components/FullPageEmptyState'; +import { Pagination } from '@/components/Pagination'; +import { Button } from '@/components/ui/button'; +import { useCursor } from '@/hooks/useCursor'; +import { GanttChartIcon } from 'lucide-react'; +import { last } from 'ramda'; + +import { IServiceCreateEventPayload } from '@mixan/db'; + +import { EventListItem } from './event-list-item'; + +interface EventListProps { + data: IServiceCreateEventPayload[]; +} +export function EventList({ data }: EventListProps) { + const { cursor, setCursor } = useCursor(); + return ( + <> +
+ {data.length === 0 ? ( + + {/* {filterEvents.length ? ( +

Could not find any events with your filter

+ ) : ( +

We have not recieved any events yet

+ )} */} +

We have not recieved any events yet

+
+ ) : ( + <> +
+ {data.map((item) => ( + + ))} +
+ + + )} +
+ + ); +} diff --git a/apps/web/src/app/(app)/[organizationId]/[projectId]/events/list-events.tsx b/apps/web/src/app/(app)/[organizationId]/[projectId]/events/list-events.tsx deleted file mode 100644 index 866295cf..00000000 --- a/apps/web/src/app/(app)/[organizationId]/[projectId]/events/list-events.tsx +++ /dev/null @@ -1,75 +0,0 @@ -'use client'; - -import { useMemo, useState } from 'react'; -import { api } from '@/app/_trpc/client'; -import { StickyBelowHeader } from '@/app/(app)/[organizationId]/[projectId]/layout-sticky-below-header'; -import { FullPageEmptyState } from '@/components/FullPageEmptyState'; -import { Pagination, usePagination } from '@/components/Pagination'; -import { ComboboxAdvanced } from '@/components/ui/combobox-advanced'; -import { GanttChartIcon } from 'lucide-react'; -import { parseAsArrayOf, parseAsString, useQueryState } from 'nuqs'; - -import { EventListItem } from './event-list-item'; - -interface ListEventsProps { - projectId: string; -} -export function ListEvents({ projectId }: ListEventsProps) { - const pagination = usePagination(); - const [eventFilters, setEventFilters] = useQueryState( - 'events', - parseAsArrayOf(parseAsString).withDefault([]) - ); - const eventsQuery = api.event.list.useQuery({ - events: eventFilters, - projectId: projectId, - ...pagination, - }); - const events = useMemo(() => eventsQuery.data ?? [], [eventsQuery]); - - const filterEventsQuery = api.chart.events.useQuery({ - projectId: projectId, - }); - - const filterEvents = (filterEventsQuery.data ?? []).map((item) => ({ - value: item.name, - label: item.name, - })); - - return ( - <> - -
- -
-
-
- {events.length === 0 ? ( - - {eventFilters.length ? ( -

Could not find any events with your filter

- ) : ( -

We have not recieved any events yet

- )} -
- ) : ( - <> -
- {events.map((item) => ( - - ))} -
-
- -
- - )} -
- - ); -} diff --git a/apps/web/src/app/(app)/[organizationId]/[projectId]/events/page.tsx b/apps/web/src/app/(app)/[organizationId]/[projectId]/events/page.tsx index b29ef420..49fff6d8 100644 --- a/apps/web/src/app/(app)/[organizationId]/[projectId]/events/page.tsx +++ b/apps/web/src/app/(app)/[organizationId]/[projectId]/events/page.tsx @@ -1,22 +1,39 @@ +import { Suspense } from 'react'; import PageLayout from '@/app/(app)/[organizationId]/[projectId]/page-layout'; +import { OverviewFiltersDrawer } from '@/components/overview/filters/overview-filters-drawer'; import { getExists } from '@/server/pageExists'; -import { ListEvents } from './list-events'; +import { getEventList, getEvents } from '@mixan/db'; + +import { StickyBelowHeader } from '../layout-sticky-below-header'; +import { EventList } from './event-list'; interface PageProps { params: { projectId: string; organizationId: string; }; + searchParams: { + cursor?: string; + }; } export default async function Page({ params: { projectId, organizationId }, + searchParams: { cursor }, }: PageProps) { await getExists(organizationId, projectId); + const events = await getEventList({ + cursor, + projectId, + take: 50, + }); return ( - + + + + ); } diff --git a/apps/web/src/app/(app)/[organizationId]/[projectId]/overview-metrics.tsx b/apps/web/src/app/(app)/[organizationId]/[projectId]/overview-metrics.tsx index 916c1af6..0ba87f7d 100644 --- a/apps/web/src/app/(app)/[organizationId]/[projectId]/overview-metrics.tsx +++ b/apps/web/src/app/(app)/[organizationId]/[projectId]/overview-metrics.tsx @@ -1,12 +1,10 @@ 'use client'; -import { Suspense } from 'react'; import { WidgetHead } from '@/components/overview/overview-widget'; import { useOverviewOptions } from '@/components/overview/useOverviewOptions'; import { Chart } from '@/components/report/chart'; -import { ChartLoading } from '@/components/report/chart/ChartLoading'; -import { MetricCardLoading } from '@/components/report/chart/MetricCard'; import { Widget, WidgetBody } from '@/components/Widget'; +import { useEventFilters } from '@/hooks/useEventQueryFilters'; import type { IChartInput } from '@/types'; import { cn } from '@/utils/cn'; @@ -15,8 +13,8 @@ interface OverviewMetricsProps { } export default function OverviewMetrics({ projectId }: OverviewMetricsProps) { - const { previous, range, interval, metric, setMetric, filters } = - useOverviewOptions(); + const { previous, range, interval, metric, setMetric } = useOverviewOptions(); + const filters = useEventFilters(); const reports = [ { diff --git a/apps/web/src/app/(app)/[organizationId]/[projectId]/overview-sticky-header.tsx b/apps/web/src/app/(app)/[organizationId]/[projectId]/overview-sticky-header.tsx index 8f2771ae..66e67071 100644 --- a/apps/web/src/app/(app)/[organizationId]/[projectId]/overview-sticky-header.tsx +++ b/apps/web/src/app/(app)/[organizationId]/[projectId]/overview-sticky-header.tsx @@ -2,26 +2,8 @@ import { useOverviewOptions } from '@/components/overview/useOverviewOptions'; import { ReportRange } from '@/components/report/ReportRange'; -import { Button } from '@/components/ui/button'; -import { SheetTrigger } from '@/components/ui/sheet'; -import { FilterIcon } from 'lucide-react'; export function OverviewReportRange() { - const { previous, range, setRange, interval, metric, setMetric, filters } = - useOverviewOptions(); - + const { range, setRange } = useOverviewOptions(); return setRange(value)} />; } - -export function OverviewFilterSheetTrigger() { - const { previous, range, setRange, interval, metric, setMetric, filters } = - useOverviewOptions(); - - return ( - - - - ); -} diff --git a/apps/web/src/app/(app)/[organizationId]/[projectId]/page.tsx b/apps/web/src/app/(app)/[organizationId]/[projectId]/page.tsx index a35b92ce..1a905364 100644 --- a/apps/web/src/app/(app)/[organizationId]/[projectId]/page.tsx +++ b/apps/web/src/app/(app)/[organizationId]/[projectId]/page.tsx @@ -1,7 +1,7 @@ import PageLayout from '@/app/(app)/[organizationId]/[projectId]/page-layout'; +import { OverviewFiltersButtons } from '@/components/overview/filters/overview-filters-buttons'; +import { OverviewFiltersDrawer } from '@/components/overview/filters/overview-filters-drawer'; import ServerLiveCounter from '@/components/overview/live-counter'; -import { OverviewFilters } from '@/components/overview/overview-filters'; -import { OverviewFiltersButtons } from '@/components/overview/overview-filters-buttons'; import { OverviewLiveHistogram } from '@/components/overview/overview-live-histogram'; import { OverviewShare } from '@/components/overview/overview-share'; import OverviewTopDevices from '@/components/overview/overview-top-devices'; @@ -9,17 +9,13 @@ import OverviewTopEvents from '@/components/overview/overview-top-events'; import OverviewTopGeo from '@/components/overview/overview-top-geo'; import OverviewTopPages from '@/components/overview/overview-top-pages'; import OverviewTopSources from '@/components/overview/overview-top-sources'; -import { Sheet, SheetContent } from '@/components/ui/sheet'; import { getExists } from '@/server/pageExists'; import { db } from '@mixan/db'; import { StickyBelowHeader } from './layout-sticky-below-header'; import OverviewMetrics from './overview-metrics'; -import { - OverviewFilterSheetTrigger, - OverviewReportRange, -} from './overview-sticky-header'; +import { OverviewReportRange } from './overview-sticky-header'; interface PageProps { params: { @@ -42,37 +38,32 @@ export default async function Page({ return ( - - -
-
- - -
-
- - -
+ +
+
+ +
- - -
-
- -
- - - - - -
- +
+ +
- - - - + + +
+
+ +
+ + + + + +
+ +
+
); } diff --git a/apps/web/src/app/(public)/share/overview/[id]/page.tsx b/apps/web/src/app/(public)/share/overview/[id]/page.tsx index d25c841c..2641b3cb 100644 --- a/apps/web/src/app/(public)/share/overview/[id]/page.tsx +++ b/apps/web/src/app/(public)/share/overview/[id]/page.tsx @@ -1,20 +1,16 @@ import { StickyBelowHeader } from '@/app/(app)/[organizationId]/[projectId]/layout-sticky-below-header'; import OverviewMetrics from '@/app/(app)/[organizationId]/[projectId]/overview-metrics'; -import { - OverviewFilterSheetTrigger, - OverviewReportRange, -} from '@/app/(app)/[organizationId]/[projectId]/overview-sticky-header'; +import { OverviewReportRange } from '@/app/(app)/[organizationId]/[projectId]/overview-sticky-header'; import { Logo } from '@/components/Logo'; +import { OverviewFiltersButtons } from '@/components/overview/filters/overview-filters-buttons'; +import { OverviewFiltersDrawer } from '@/components/overview/filters/overview-filters-drawer'; import ServerLiveCounter from '@/components/overview/live-counter'; -import { OverviewFilters } from '@/components/overview/overview-filters'; -import { OverviewFiltersButtons } from '@/components/overview/overview-filters-buttons'; import { OverviewLiveHistogram } from '@/components/overview/overview-live-histogram'; import OverviewTopDevices from '@/components/overview/overview-top-devices'; import OverviewTopEvents from '@/components/overview/overview-top-events'; import OverviewTopGeo from '@/components/overview/overview-top-geo'; import OverviewTopPages from '@/components/overview/overview-top-pages'; import OverviewTopSources from '@/components/overview/overview-top-sources'; -import { Sheet, SheetContent } from '@/components/ui/sheet'; import { getOrganizationBySlug } from '@/server/services/organization.service'; import { notFound } from 'next/navigation'; @@ -49,36 +45,31 @@ export default async function Page({ params: { id } }: PageProps) {
- - -
-
- - -
-
- -
+ +
+
+ +
- - -
-
- -
- - - - - -
- +
+
- - - - + + +
+
+ +
+ + + + + +
+ +
+
diff --git a/apps/web/src/components/overview/filters/overview-filters-buttons.tsx b/apps/web/src/components/overview/filters/overview-filters-buttons.tsx new file mode 100644 index 00000000..25c8dc01 --- /dev/null +++ b/apps/web/src/components/overview/filters/overview-filters-buttons.tsx @@ -0,0 +1,34 @@ +'use client'; + +import { Button } from '@/components/ui/button'; +import { + useEventFilters, + useEventQueryFilters, +} from '@/hooks/useEventQueryFilters'; +import { cn } from '@/utils/cn'; +import { X } from 'lucide-react'; + +export function OverviewFiltersButtons() { + const eventQueryFilters = useEventQueryFilters(); + const filters = Object.entries(eventQueryFilters).filter( + ([, filter]) => filter.get !== null + ); + return ( +
0 && 'px-4 pb-4')} + > + {filters.map(([key, filter]) => ( + + ))} +
+ ); +} diff --git a/apps/web/src/components/overview/filters/overview-filters-drawer-content.tsx b/apps/web/src/components/overview/filters/overview-filters-drawer-content.tsx new file mode 100644 index 00000000..773fe39e --- /dev/null +++ b/apps/web/src/components/overview/filters/overview-filters-drawer-content.tsx @@ -0,0 +1,92 @@ +'use client'; + +import { api } from '@/app/_trpc/client'; +import { Button } from '@/components/ui/button'; +import { Combobox } from '@/components/ui/combobox'; +import { useEventQueryFilters } from '@/hooks/useEventQueryFilters'; +import { XIcon } from 'lucide-react'; + +interface OverviewFiltersProps { + projectId: string; +} + +export function OverviewFiltersDrawerContent({ + projectId, +}: OverviewFiltersProps) { + const eventQueryFilters = useEventQueryFilters(); + + return ( +
+

Overview filters

+ { + // @ts-expect-error + eventQueryFilters[value].set(''); + }} + value="" + placeholder="Filter by..." + label="What do you want to filter by?" + items={Object.entries(eventQueryFilters) + .filter(([, filter]) => filter.get === null) + .map(([name]) => ({ + label: name, + value: name, + }))} + searchable + /> + +
+ {Object.entries(eventQueryFilters) + .filter(([, filter]) => filter.get !== null) + .map(([name, filter]) => ( + + ))} +
+
+ ); +} + +export function FilterOption({ + name, + get, + set, + projectId, +}: { + name: string; + get: string | null; + set: (value: string | null) => void; + projectId: string; +}) { + const { data } = api.chart.values.useQuery({ + projectId, + event: name === 'path' ? 'screen_view' : 'session_start', + property: name, + }); + + return ( +
+
{name}
+ set(value)} + placeholder={'Select a value'} + items={ + data?.values.filter(Boolean).map((value) => ({ + value, + label: value, + })) ?? [] + } + value={get} + /> + +
+ ); +} diff --git a/apps/web/src/components/overview/filters/overview-filters-drawer.tsx b/apps/web/src/components/overview/filters/overview-filters-drawer.tsx new file mode 100644 index 00000000..7e7a4873 --- /dev/null +++ b/apps/web/src/components/overview/filters/overview-filters-drawer.tsx @@ -0,0 +1,28 @@ +'use client'; + +import { Button } from '@/components/ui/button'; +import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet'; +import { FilterIcon } from 'lucide-react'; + +import { OverviewFiltersDrawerContent } from './overview-filters-drawer-content'; + +interface OverviewFiltersDrawerProps { + projectId: string; +} + +export function OverviewFiltersDrawer({ + projectId, +}: OverviewFiltersDrawerProps) { + return ( + + + + + + + + + ); +} diff --git a/apps/web/src/components/overview/overview-filters-buttons.tsx b/apps/web/src/components/overview/overview-filters-buttons.tsx deleted file mode 100644 index d089258b..00000000 --- a/apps/web/src/components/overview/overview-filters-buttons.tsx +++ /dev/null @@ -1,204 +0,0 @@ -'use client'; - -import { cn } from '@/utils/cn'; -import { X } from 'lucide-react'; - -import { Button } from '../ui/button'; -import { useOverviewOptions } from './useOverviewOptions'; - -export function OverviewFiltersButtons() { - const options = useOverviewOptions(); - const activeFilter = options.filters.length > 0; - - return ( -
- {options.referrer && ( - - )} - {options.referrerName && ( - - )} - {options.referrerType && ( - - )} - {options.device && ( - - )} - {options.page && ( - - )} - {options.utmSource && ( - - )} - {options.utmMedium && ( - - )} - {options.utmCampaign && ( - - )} - {options.utmTerm && ( - - )} - {options.utmContent && ( - - )} - {options.country && ( - - )} - {options.region && ( - - )} - {options.city && ( - - )} - {options.browser && ( - - )} - {options.browserVersion && ( - - )} - {options.os && ( - - )} - {options.osVersion && ( - - )} -
- ); -} diff --git a/apps/web/src/components/overview/overview-filters.tsx b/apps/web/src/components/overview/overview-filters.tsx deleted file mode 100644 index 1601d5b0..00000000 --- a/apps/web/src/components/overview/overview-filters.tsx +++ /dev/null @@ -1,123 +0,0 @@ -'use client'; - -import { api } from '@/app/_trpc/client'; -import { useAppParams } from '@/hooks/useAppParams'; -import { cn } from '@/utils/cn'; - -import { Combobox } from '../ui/combobox'; -import { Label } from '../ui/label'; -import { useOverviewOptions } from './useOverviewOptions'; - -interface OverviewFiltersProps { - projectId: string; -} -export function OverviewFilters({ projectId }: OverviewFiltersProps) { - const options = useOverviewOptions(); - - const { data: referrers } = api.chart.values.useQuery({ - projectId, - property: 'referrer', - event: 'session_start', - }); - - const { data: devices } = api.chart.values.useQuery({ - projectId, - property: 'device', - event: 'session_start', - }); - - const { data: pages } = api.chart.values.useQuery({ - projectId, - property: 'path', - event: 'screen_view', - }); - - return ( -
-

Overview filters

-
-
- - options.setReferrer(value)} - label="Referrer" - placeholder="Referrer" - items={ - referrers?.values?.filter(Boolean)?.map((value) => ({ - value, - label: value, - })) ?? [] - } - value={options.referrer} - /> -
-
- - options.setDevice(value)} - label="Device" - placeholder="Device" - items={ - devices?.values?.filter(Boolean)?.map((value) => ({ - value, - label: value, - })) ?? [] - } - value={options.device} - /> -
-
- - options.setPage(value)} - label="Page" - placeholder="Page" - items={ - pages?.values?.filter(Boolean)?.map((value) => ({ - value, - label: value, - })) ?? [] - } - value={options.page} - /> -
-
-
- ); -} diff --git a/apps/web/src/components/overview/overview-top-devices.tsx b/apps/web/src/components/overview/overview-top-devices.tsx index 2629fe70..593b9f8e 100644 --- a/apps/web/src/components/overview/overview-top-devices.tsx +++ b/apps/web/src/components/overview/overview-top-devices.tsx @@ -1,6 +1,10 @@ 'use client'; import { Chart } from '@/components/report/chart'; +import { + useEventFilters, + useEventQueryFilters, +} from '@/hooks/useEventQueryFilters'; import { cn } from '@/utils/cn'; import { Widget, WidgetBody } from '../Widget'; @@ -14,17 +18,10 @@ interface OverviewTopDevicesProps { export default function OverviewTopDevices({ projectId, }: OverviewTopDevicesProps) { - const { - filters, - interval, - range, - previous, - setBrowser, - setBrowserVersion, - setOS, - setOSVersion, - setDevice, - } = useOverviewOptions(); + const { interval, range, previous } = useOverviewOptions(); + const filters = useEventFilters(); + const { device, browser, browserVersion, os, osVersion } = + useEventQueryFilters(); const [widget, setWidget, widgets] = useOverviewWidget('tech', { devices: { title: 'Top devices', @@ -193,21 +190,21 @@ export default function OverviewTopDevices({ onClick={(item) => { switch (widget.key) { case 'devices': - setDevice(item.name); + device.set(item.name); break; case 'browser': setWidget('browser_version'); - setBrowser(item.name); + browser.set(item.name); break; case 'browser_version': - setBrowserVersion(item.name); + browserVersion.set(item.name); break; case 'os': setWidget('os_version'); - setOS(item.name); + os.set(item.name); break; case 'os_version': - setOSVersion(item.name); + osVersion.set(item.name); break; } }} diff --git a/apps/web/src/components/overview/overview-top-events.tsx b/apps/web/src/components/overview/overview-top-events.tsx index 17e7dac5..ee2695ac 100644 --- a/apps/web/src/components/overview/overview-top-events.tsx +++ b/apps/web/src/components/overview/overview-top-events.tsx @@ -1,8 +1,7 @@ 'use client'; -import { Suspense } from 'react'; import { Chart } from '@/components/report/chart'; -import { ChartLoading } from '@/components/report/chart/ChartLoading'; +import { useEventFilters } from '@/hooks/useEventQueryFilters'; import { cn } from '@/utils/cn'; import { Widget, WidgetBody } from '../Widget'; @@ -16,7 +15,8 @@ interface OverviewTopEventsProps { export default function OverviewTopEvents({ projectId, }: OverviewTopEventsProps) { - const { filters, interval, range, previous } = useOverviewOptions(); + const { interval, range, previous } = useOverviewOptions(); + const filters = useEventFilters(); const [widget, setWidget, widgets] = useOverviewWidget('ev', { all: { title: 'Top events', diff --git a/apps/web/src/components/overview/overview-top-geo.tsx b/apps/web/src/components/overview/overview-top-geo.tsx index 8e73f5b2..fb78e79b 100644 --- a/apps/web/src/components/overview/overview-top-geo.tsx +++ b/apps/web/src/components/overview/overview-top-geo.tsx @@ -1,8 +1,10 @@ 'use client'; -import { Suspense } from 'react'; import { Chart } from '@/components/report/chart'; -import { ChartLoading } from '@/components/report/chart/ChartLoading'; +import { + useEventFilters, + useEventQueryFilters, +} from '@/hooks/useEventQueryFilters'; import { cn } from '@/utils/cn'; import { Widget, WidgetBody } from '../Widget'; @@ -14,8 +16,9 @@ interface OverviewTopGeoProps { projectId: string; } export default function OverviewTopGeo({ projectId }: OverviewTopGeoProps) { - const { filters, interval, range, previous, setCountry, setRegion, setCity } = - useOverviewOptions(); + const { interval, range, previous } = useOverviewOptions(); + const filters = useEventFilters(); + const { region, country, city } = useEventQueryFilters(); const [widget, setWidget, widgets] = useOverviewWidget('geo', { map: { title: 'Map', @@ -157,14 +160,14 @@ export default function OverviewTopGeo({ projectId }: OverviewTopGeoProps) { switch (widget.key) { case 'countries': setWidget('regions'); - setCountry(item.name); + country.set(item.name); break; case 'regions': setWidget('cities'); - setRegion(item.name); + region.set(item.name); break; case 'cities': - setCity(item.name); + city.set(item.name); break; } }} diff --git a/apps/web/src/components/overview/overview-top-pages.tsx b/apps/web/src/components/overview/overview-top-pages.tsx index 5599924f..0ead0ccc 100644 --- a/apps/web/src/components/overview/overview-top-pages.tsx +++ b/apps/web/src/components/overview/overview-top-pages.tsx @@ -1,8 +1,10 @@ 'use client'; -import { Suspense } from 'react'; import { Chart } from '@/components/report/chart'; -import { ChartLoading } from '@/components/report/chart/ChartLoading'; +import { + useEventFilters, + useEventQueryFilters, +} from '@/hooks/useEventQueryFilters'; import { cn } from '@/utils/cn'; import { Widget, WidgetBody } from '../Widget'; @@ -14,7 +16,9 @@ interface OverviewTopPagesProps { projectId: string; } export default function OverviewTopPages({ projectId }: OverviewTopPagesProps) { - const { filters, interval, range, previous, setPage } = useOverviewOptions(); + const { interval, range, previous } = useOverviewOptions(); + const filters = useEventFilters(); + const { path } = useEventQueryFilters(); const [widget, setWidget, widgets] = useOverviewWidget('pages', { top: { title: 'Top pages', @@ -125,7 +129,7 @@ export default function OverviewTopPages({ projectId }: OverviewTopPagesProps) { {...widget.chart} previous={false} onClick={(item) => { - setPage(item.name); + path.set(item.name); }} /> diff --git a/apps/web/src/components/overview/overview-top-sources.tsx b/apps/web/src/components/overview/overview-top-sources.tsx index b232b804..fa5f7cf2 100644 --- a/apps/web/src/components/overview/overview-top-sources.tsx +++ b/apps/web/src/components/overview/overview-top-sources.tsx @@ -1,8 +1,10 @@ 'use client'; -import { Suspense } from 'react'; import { Chart } from '@/components/report/chart'; -import { ChartLoading } from '@/components/report/chart/ChartLoading'; +import { + useEventFilters, + useEventQueryFilters, +} from '@/hooks/useEventQueryFilters'; import { cn } from '@/utils/cn'; import { Widget, WidgetBody } from '../Widget'; @@ -16,20 +18,18 @@ interface OverviewTopSourcesProps { export default function OverviewTopSources({ projectId, }: OverviewTopSourcesProps) { + const { interval, range, previous } = useOverviewOptions(); const { - filters, - interval, - range, - previous, - setReferrer, - setUtmSource, - setUtmMedium, - setUtmCampaign, - setUtmTerm, - setUtmContent, - setReferrerName, - setReferrerType, - } = useOverviewOptions(); + referrer, + referrerName, + referrerType, + utmCampaign, + utmContent, + utmMedium, + utmSource, + utmTerm, + } = useEventQueryFilters(); + const filters = useEventFilters(); const [widget, setWidget, widgets] = useOverviewWidget('sources', { all: { title: 'Top sources', @@ -282,30 +282,30 @@ export default function OverviewTopSources({ onClick={(item) => { switch (widget.key) { case 'all': - setReferrerName(item.name); + referrerName.set(item.name); setWidget('domain'); break; case 'domain': - setReferrer(item.name); + referrer.set(item.name); break; case 'type': - setReferrerType(item.name); + referrerType.set(item.name); setWidget('domain'); break; case 'utm_source': - setUtmSource(item.name); + utmSource.set(item.name); break; case 'utm_medium': - setUtmMedium(item.name); + utmMedium.set(item.name); break; case 'utm_campaign': - setUtmCampaign(item.name); + utmCampaign.set(item.name); break; case 'utm_term': - setUtmTerm(item.name); + utmTerm.set(item.name); break; case 'utm_content': - setUtmContent(item.name); + utmContent.set(item.name); break; } }} diff --git a/apps/web/src/components/overview/useOverviewOptions.ts b/apps/web/src/components/overview/useOverviewOptions.ts index a5e73df7..6cd076e9 100644 --- a/apps/web/src/components/overview/useOverviewOptions.ts +++ b/apps/web/src/components/overview/useOverviewOptions.ts @@ -1,11 +1,9 @@ -import { useMemo } from 'react'; -import type { IChartInput } from '@/types'; +import { useEventQueryFilters } from '@/hooks/useEventQueryFilters'; import { getDefaultIntervalByRange, timeRanges } from '@/utils/constants'; import { mapKeys } from '@/utils/validation'; import { parseAsBoolean, parseAsInteger, - parseAsString, parseAsStringEnum, useQueryState, } from 'nuqs'; @@ -29,267 +27,12 @@ export function useOverviewOptions() { parseAsInteger.withDefault(0).withOptions(nuqsOptions) ); - // Filters - const [page, setPage] = useQueryState( - 'page', - parseAsString.withOptions(nuqsOptions) - ); - - // Referrer - const [referrer, setReferrer] = useQueryState( - 'referrer', - parseAsString.withOptions(nuqsOptions) - ); - const [referrerName, setReferrerName] = useQueryState( - 'referrer_name', - parseAsString.withOptions(nuqsOptions) - ); - const [referrerType, setReferrerType] = useQueryState( - 'referrer_type', - parseAsString.withOptions(nuqsOptions) - ); - - // Sources - const [utmSource, setUtmSource] = useQueryState( - 'utm_source', - parseAsString.withOptions(nuqsOptions) - ); - const [utmMedium, setUtmMedium] = useQueryState( - 'utm_medium', - parseAsString.withOptions(nuqsOptions) - ); - const [utmCampaign, setUtmCampaign] = useQueryState( - 'utm_campaign', - parseAsString.withOptions(nuqsOptions) - ); - const [utmContent, setUtmContent] = useQueryState( - 'utm_content', - parseAsString.withOptions(nuqsOptions) - ); - const [utmTerm, setUtmTerm] = useQueryState( - 'utm_term', - parseAsString.withOptions(nuqsOptions) - ); - - // Geo - const [country, setCountry] = useQueryState( - 'country', - parseAsString.withOptions(nuqsOptions) - ); - const [region, setRegion] = useQueryState( - 'region', - parseAsString.withOptions(nuqsOptions) - ); - const [city, setCity] = useQueryState( - 'city', - parseAsString.withOptions(nuqsOptions) - ); - - // - const [device, setDevice] = useQueryState( - 'device', - parseAsString.withOptions(nuqsOptions) - ); - const [browser, setBrowser] = useQueryState( - 'browser', - parseAsString.withOptions(nuqsOptions) - ); - const [browserVersion, setBrowserVersion] = useQueryState( - 'browser_version', - parseAsString.withOptions(nuqsOptions) - ); - const [os, setOS] = useQueryState( - 'os', - parseAsString.withOptions(nuqsOptions) - ); - const [osVersion, setOSVersion] = useQueryState( - 'os_version', - parseAsString.withOptions(nuqsOptions) - ); - // Toggles const [liveHistogram, setLiveHistogram] = useQueryState( 'live', parseAsBoolean.withDefault(false).withOptions(nuqsOptions) ); - const filters = useMemo(() => { - const filters: IChartInput['events'][number]['filters'] = []; - - if (page) { - filters.push({ - id: 'path', - operator: 'is', - name: 'path', - value: [page], - }); - } - - if (device) { - filters.push({ - id: 'device', - operator: 'is', - name: 'device', - value: [device], - }); - } - - if (referrer) { - filters.push({ - id: 'referrer', - operator: 'is', - name: 'referrer', - value: [referrer], - }); - } - - if (referrerName) { - filters.push({ - id: 'referrer_name', - operator: 'is', - name: 'referrer_name', - value: [referrerName], - }); - } - - if (referrerType) { - filters.push({ - id: 'referrer_type', - operator: 'is', - name: 'referrer_type', - value: [referrerType], - }); - } - - if (utmSource) { - filters.push({ - id: 'utm_source', - operator: 'is', - name: 'properties.query.utm_source', - value: [utmSource], - }); - } - - if (utmMedium) { - filters.push({ - id: 'utm_medium', - operator: 'is', - name: 'properties.query.utm_medium', - value: [utmMedium], - }); - } - - if (utmCampaign) { - filters.push({ - id: 'utm_campaign', - operator: 'is', - name: 'properties.query.utm_campaign', - value: [utmCampaign], - }); - } - - if (utmContent) { - filters.push({ - id: 'utm_content', - operator: 'is', - name: 'properties.query.utm_content', - value: [utmContent], - }); - } - - if (utmTerm) { - filters.push({ - id: 'utm_term', - operator: 'is', - name: 'properties.query.utm_term', - value: [utmTerm], - }); - } - - if (country) { - filters.push({ - id: 'country', - operator: 'is', - name: 'country', - value: [country], - }); - } - - if (region) { - filters.push({ - id: 'region', - operator: 'is', - name: 'region', - value: [region], - }); - } - - if (city) { - filters.push({ - id: 'city', - operator: 'is', - name: 'city', - value: [city], - }); - } - - if (browser) { - filters.push({ - id: 'browser', - operator: 'is', - name: 'browser', - value: [browser], - }); - } - - if (browserVersion) { - filters.push({ - id: 'browser_version', - operator: 'is', - name: 'browser_version', - value: [browserVersion], - }); - } - - if (os) { - filters.push({ - id: 'os', - operator: 'is', - name: 'os', - value: [os], - }); - } - - if (osVersion) { - filters.push({ - id: 'os_version', - operator: 'is', - name: 'os_version', - value: [osVersion], - }); - } - - return filters; - }, [ - page, - device, - referrer, - referrerName, - referrerType, - utmSource, - utmMedium, - utmCampaign, - utmContent, - utmTerm, - country, - region, - city, - browser, - browserVersion, - os, - osVersion, - ]); - return { previous, setPrevious, @@ -297,52 +40,9 @@ export function useOverviewOptions() { setRange, metric, setMetric, - page, - setPage, // Computed interval, - filters, - - // Refs - referrer, - setReferrer, - referrerName, - setReferrerName, - referrerType, - setReferrerType, - - // UTM - utmSource, - setUtmSource, - utmMedium, - setUtmMedium, - utmCampaign, - setUtmCampaign, - utmContent, - setUtmContent, - utmTerm, - setUtmTerm, - - // GEO - country, - setCountry, - region, - setRegion, - city, - setCity, - - // Tech - device, - setDevice, - browser, - setBrowser, - browserVersion, - setBrowserVersion, - os, - setOS, - osVersion, - setOSVersion, // Toggles liveHistogram, diff --git a/apps/web/src/components/ui/key-value.tsx b/apps/web/src/components/ui/key-value.tsx index 34eb1c9a..99d37b06 100644 --- a/apps/web/src/components/ui/key-value.tsx +++ b/apps/web/src/components/ui/key-value.tsx @@ -13,13 +13,16 @@ export function KeyValue({ href, onClick, name, value }: KeyValueProps) { const Component = href ? (Link as any) : onClick ? 'button' : 'div'; return ( -
{name}
+
{name}
diff --git a/apps/web/src/hooks/useCursor.ts b/apps/web/src/hooks/useCursor.ts new file mode 100644 index 00000000..d0037913 --- /dev/null +++ b/apps/web/src/hooks/useCursor.ts @@ -0,0 +1,12 @@ +import { parseAsIsoDateTime, useQueryState } from 'nuqs'; + +export function useCursor() { + const [cursor, setCursor] = useQueryState( + 'cursor', + parseAsIsoDateTime.withOptions({ shallow: false }) + ); + return { + cursor, + setCursor, + }; +} diff --git a/apps/web/src/hooks/useEventQueryFilters copy.ts b/apps/web/src/hooks/useEventQueryFilters copy.ts new file mode 100644 index 00000000..ab33da44 --- /dev/null +++ b/apps/web/src/hooks/useEventQueryFilters copy.ts @@ -0,0 +1,311 @@ +import { useMemo } from 'react'; +import type { IChartInput } from '@/types'; +import { parseAsString, useQueryState } from 'nuqs'; + +const nuqsOptions = { history: 'push' } as const; + +export function useEventQueryFiltersqweqweqweqweqwe() { + // Path + const [path, setPath] = useQueryState( + 'path', + parseAsString.withOptions(nuqsOptions) + ); + + // Referrer + const [referrer, setReferrer] = useQueryState( + 'referrer', + parseAsString.withOptions(nuqsOptions) + ); + const [referrerName, setReferrerName] = useQueryState( + 'referrer_name', + parseAsString.withOptions(nuqsOptions) + ); + const [referrerType, setReferrerType] = useQueryState( + 'referrer_type', + parseAsString.withOptions(nuqsOptions) + ); + + // Sources + const [utmSource, setUtmSource] = useQueryState( + 'utm_source', + parseAsString.withOptions(nuqsOptions) + ); + const [utmMedium, setUtmMedium] = useQueryState( + 'utm_medium', + parseAsString.withOptions(nuqsOptions) + ); + const [utmCampaign, setUtmCampaign] = useQueryState( + 'utm_campaign', + parseAsString.withOptions(nuqsOptions) + ); + const [utmContent, setUtmContent] = useQueryState( + 'utm_content', + parseAsString.withOptions(nuqsOptions) + ); + const [utmTerm, setUtmTerm] = useQueryState( + 'utm_term', + parseAsString.withOptions(nuqsOptions) + ); + + // Geo + const [country, setCountry] = useQueryState( + 'country', + parseAsString.withOptions(nuqsOptions) + ); + const [region, setRegion] = useQueryState( + 'region', + parseAsString.withOptions(nuqsOptions) + ); + const [city, setCity] = useQueryState( + 'city', + parseAsString.withOptions(nuqsOptions) + ); + + // tech + const [device, setDevice] = useQueryState( + 'device', + parseAsString.withOptions(nuqsOptions) + ); + const [browser, setBrowser] = useQueryState( + 'browser', + parseAsString.withOptions(nuqsOptions) + ); + const [browserVersion, setBrowserVersion] = useQueryState( + 'browser_version', + parseAsString.withOptions(nuqsOptions) + ); + const [os, setOS] = useQueryState( + 'os', + parseAsString.withOptions(nuqsOptions) + ); + const [osVersion, setOSVersion] = useQueryState( + 'os_version', + parseAsString.withOptions(nuqsOptions) + ); + + const filters = useMemo(() => { + const filters: IChartInput['events'][number]['filters'] = []; + + if (path) { + filters.push({ + id: 'path', + operator: 'is', + name: 'path', + value: [path], + }); + } + + if (device) { + filters.push({ + id: 'device', + operator: 'is', + name: 'device', + value: [device], + }); + } + + if (referrer) { + filters.push({ + id: 'referrer', + operator: 'is', + name: 'referrer', + value: [referrer], + }); + } + + if (referrerName) { + filters.push({ + id: 'referrer_name', + operator: 'is', + name: 'referrer_name', + value: [referrerName], + }); + } + + if (referrerType) { + filters.push({ + id: 'referrer_type', + operator: 'is', + name: 'referrer_type', + value: [referrerType], + }); + } + + if (utmSource) { + filters.push({ + id: 'utm_source', + operator: 'is', + name: 'properties.query.utm_source', + value: [utmSource], + }); + } + + if (utmMedium) { + filters.push({ + id: 'utm_medium', + operator: 'is', + name: 'properties.query.utm_medium', + value: [utmMedium], + }); + } + + if (utmCampaign) { + filters.push({ + id: 'utm_campaign', + operator: 'is', + name: 'properties.query.utm_campaign', + value: [utmCampaign], + }); + } + + if (utmContent) { + filters.push({ + id: 'utm_content', + operator: 'is', + name: 'properties.query.utm_content', + value: [utmContent], + }); + } + + if (utmTerm) { + filters.push({ + id: 'utm_term', + operator: 'is', + name: 'properties.query.utm_term', + value: [utmTerm], + }); + } + + if (country) { + filters.push({ + id: 'country', + operator: 'is', + name: 'country', + value: [country], + }); + } + + if (region) { + filters.push({ + id: 'region', + operator: 'is', + name: 'region', + value: [region], + }); + } + + if (city) { + filters.push({ + id: 'city', + operator: 'is', + name: 'city', + value: [city], + }); + } + + if (browser) { + filters.push({ + id: 'browser', + operator: 'is', + name: 'browser', + value: [browser], + }); + } + + if (browserVersion) { + filters.push({ + id: 'browser_version', + operator: 'is', + name: 'browser_version', + value: [browserVersion], + }); + } + + if (os) { + filters.push({ + id: 'os', + operator: 'is', + name: 'os', + value: [os], + }); + } + + if (osVersion) { + filters.push({ + id: 'os_version', + operator: 'is', + name: 'os_version', + value: [osVersion], + }); + } + + return filters; + }, [ + path, + device, + referrer, + referrerName, + referrerType, + utmSource, + utmMedium, + utmCampaign, + utmContent, + utmTerm, + country, + region, + city, + browser, + browserVersion, + os, + osVersion, + ]); + + return { + // Computed + filters, + + // Path + path, + setPath, + + // Refs + referrer, + setReferrer, + referrerName, + setReferrerName, + referrerType, + setReferrerType, + + // UTM + utmSource, + setUtmSource, + utmMedium, + setUtmMedium, + utmCampaign, + setUtmCampaign, + utmContent, + setUtmContent, + utmTerm, + setUtmTerm, + + // GEO + country, + setCountry, + region, + setRegion, + city, + setCity, + + // Tech + device, + setDevice, + browser, + setBrowser, + browserVersion, + setBrowserVersion, + os, + setOS, + osVersion, + setOSVersion, + }; +} diff --git a/apps/web/src/hooks/useEventQueryFilters.ts b/apps/web/src/hooks/useEventQueryFilters.ts new file mode 100644 index 00000000..9a0e0add --- /dev/null +++ b/apps/web/src/hooks/useEventQueryFilters.ts @@ -0,0 +1,227 @@ +import { useMemo } from 'react'; +import type { IChartInput } from '@/types'; + +// prettier-ignore +import type { UseQueryStateReturn } from 'nuqs'; + +import { parseAsString, useQueryState } from 'nuqs'; + +const nuqsOptions = { history: 'push' } as const; + +function useFix(hook: UseQueryStateReturn) { + return useMemo( + () => ({ + get: hook[0], + set: hook[1], + }), + [hook] + ); +} + +export function useEventQueryFilters() { + // Ignore prettier so that we have all one same line + // prettier-ignore + return { + path: useFix(useQueryState('path', parseAsString.withOptions(nuqsOptions))), + referrer: useFix(useQueryState('referrer', parseAsString.withOptions(nuqsOptions))), + referrerName: useFix(useQueryState('referrerName',parseAsString.withOptions(nuqsOptions))), + referrerType: useFix(useQueryState('referrerType',parseAsString.withOptions(nuqsOptions))), + utmSource: useFix(useQueryState('utmSource',parseAsString.withOptions(nuqsOptions))), + utmMedium: useFix(useQueryState('utmMedium',parseAsString.withOptions(nuqsOptions))), + utmCampaign: useFix(useQueryState('utmCampaign',parseAsString.withOptions(nuqsOptions))), + utmContent: useFix(useQueryState('utmContent',parseAsString.withOptions(nuqsOptions))), + utmTerm: useFix(useQueryState('utmTerm', parseAsString.withOptions(nuqsOptions))), + country: useFix(useQueryState('country', parseAsString.withOptions(nuqsOptions))), + region: useFix(useQueryState('region', parseAsString.withOptions(nuqsOptions))), + city: useFix(useQueryState('city', parseAsString.withOptions(nuqsOptions))), + device: useFix(useQueryState('device', parseAsString.withOptions(nuqsOptions))), + browser: useFix(useQueryState('browser', parseAsString.withOptions(nuqsOptions))), + browserVersion: useFix(useQueryState('browserVersion',parseAsString.withOptions(nuqsOptions))), + os: useFix(useQueryState('os', parseAsString.withOptions(nuqsOptions))), + osVersion: useFix(useQueryState('osVersion',parseAsString.withOptions(nuqsOptions))), + } as const; +} + +export function useEventFilters() { + const hej = useEventQueryFilters(); + + const filters = useMemo(() => { + const filters: IChartInput['events'][number]['filters'] = []; + + if (hej.path.get) { + filters.push({ + id: 'path', + operator: 'is', + name: 'path' as const, + value: [hej.path.get], + }); + } + + if (hej.device.get) { + filters.push({ + id: 'device', + operator: 'is', + name: 'device' as const, + value: [hej.device.get], + }); + } + + if (hej.referrer.get) { + filters.push({ + id: 'referrer', + operator: 'is', + name: 'referrer' as const, + value: [hej.referrer.get], + }); + } + console.log('hej.referrerName.get', hej.referrerName.get); + + if (hej.referrerName.get) { + filters.push({ + id: 'referrerName', + operator: 'is', + name: 'referrer_name' as const, + value: [hej.referrerName.get], + }); + } + + if (hej.referrerType.get) { + filters.push({ + id: 'referrerType', + operator: 'is', + name: 'referrer_type' as const, + value: [hej.referrerType.get], + }); + } + + if (hej.utmSource.get) { + filters.push({ + id: 'utmSource', + operator: 'is', + name: 'properties.query.utm_source' as const, + value: [hej.utmSource.get], + }); + } + + if (hej.utmMedium.get) { + filters.push({ + id: 'utmMedium', + operator: 'is', + name: 'properties.query.utm_medium' as const, + value: [hej.utmMedium.get], + }); + } + + if (hej.utmCampaign.get) { + filters.push({ + id: 'utmCampaign', + operator: 'is', + name: 'properties.query.utm_campaign' as const, + value: [hej.utmCampaign.get], + }); + } + + if (hej.utmContent.get) { + filters.push({ + id: 'utmContent', + operator: 'is', + name: 'properties.query.utm_content' as const, + value: [hej.utmContent.get], + }); + } + + if (hej.utmTerm.get) { + filters.push({ + id: 'utmTerm', + operator: 'is', + name: 'properties.query.utm_term' as const, + value: [hej.utmTerm.get], + }); + } + + if (hej.country.get) { + filters.push({ + id: 'country', + operator: 'is', + name: 'country' as const, + value: [hej.country.get], + }); + } + + if (hej.region.get) { + filters.push({ + id: 'region', + operator: 'is', + name: 'region' as const, + value: [hej.region.get], + }); + } + + if (hej.city.get) { + filters.push({ + id: 'city', + operator: 'is', + name: 'city' as const, + value: [hej.city.get], + }); + } + + if (hej.browser.get) { + filters.push({ + id: 'browser', + operator: 'is', + name: 'browser' as const, + value: [hej.browser.get], + }); + } + + if (hej.browserVersion.get) { + filters.push({ + id: 'browserVersion', + operator: 'is', + name: 'browser_version' as const, + value: [hej.browserVersion.get], + }); + } + + if (hej.os.get) { + filters.push({ + id: 'os', + operator: 'is', + name: 'os' as const, + value: [hej.os.get], + }); + } + + if (hej.osVersion.get) { + filters.push({ + id: 'osVersion', + operator: 'is', + name: 'os_version' as const, + value: [hej.osVersion.get], + }); + } + + return filters; + }, [ + hej.path, + hej.device, + hej.referrer, + hej.referrerName, + hej.referrerType, + hej.utmSource, + hej.utmMedium, + hej.utmCampaign, + hej.utmContent, + hej.utmTerm, + hej.country, + hej.region, + hej.city, + hej.browser, + hej.browserVersion, + hej.os, + hej.osVersion, + ]); + + return filters; +} diff --git a/apps/web/src/utils/getters.ts b/apps/web/src/utils/getters.ts index 53d3e784..838c5135 100644 --- a/apps/web/src/utils/getters.ts +++ b/apps/web/src/utils/getters.ts @@ -1,6 +1,6 @@ -import type { Profile } from '@mixan/db'; +import type { IDBProfile } from '@mixan/db'; -export function getProfileName(profile: Profile | undefined | null) { +export function getProfileName(profile: IDBProfile | undefined | null) { if (!profile) return 'No profile'; return [profile.first_name, profile.last_name].filter(Boolean).join(' '); } diff --git a/packages/db/clickhouse_tables.sql b/packages/db/clickhouse_tables.sql index d9f8162c..dc765520 100644 --- a/packages/db/clickhouse_tables.sql +++ b/packages/db/clickhouse_tables.sql @@ -1,4 +1,10 @@ +ALTER TABLE + events +ADD + COLUMN id UUID; + CREATE TABLE openpanel.events ( + `id` UUID, `name` String, `profile_id` String, `project_id` String, diff --git a/packages/db/package.json b/packages/db/package.json index 3296fdac..b98c3f1c 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -12,11 +12,12 @@ "with-env": "dotenv -e ../../.env -c --" }, "dependencies": { + "@clickhouse/client": "^0.2.9", "@mixan/common": "workspace:*", "@mixan/redis": "workspace:*", - "@clickhouse/client": "^0.2.9", "@prisma/client": "^5.1.1", - "ramda": "^0.29.1" + "ramda": "^0.29.1", + "uuid": "^9.0.1" }, "devDependencies": { "@mixan/eslint-config": "workspace:*", @@ -25,6 +26,7 @@ "@mixan/types": "workspace:*", "@types/node": "^18.16.0", "@types/ramda": "^0.29.6", + "@types/uuid": "^9.0.8", "eslint": "^8.48.0", "prettier": "^3.0.3", "prisma": "^5.1.1", diff --git a/packages/db/src/services/event.service.ts b/packages/db/src/services/event.service.ts index 1bd5b1f5..90c7b256 100644 --- a/packages/db/src/services/event.service.ts +++ b/packages/db/src/services/event.service.ts @@ -1,5 +1,5 @@ -import type { IDBProfile } from '@/prisma-types'; import { omit } from 'ramda'; +import { v4 as uuid } from 'uuid'; import { randomSplitName, toDots } from '@mixan/common'; import { redis, redisPub } from '@mixan/redis'; @@ -12,8 +12,11 @@ import { } from '../clickhouse-client'; import type { Prisma } from '../prisma-client'; import { db } from '../prisma-client'; +import type { IDBProfile } from '../prisma-types'; +import { createSqlBuilder } from '../sql-builder'; export interface IClickhouseEvent { + id: string; name: string; profile_id: string; project_id: string; @@ -41,6 +44,7 @@ export function transformEvent( event: IClickhouseEvent ): IServiceCreateEventPayload { return { + id: event.id, name: event.name, profileId: event.profile_id, projectId: event.project_id, @@ -66,6 +70,7 @@ export function transformEvent( } export interface IServiceCreateEventPayload { + id: string; name: string; profileId: string; projectId: string; @@ -102,7 +107,10 @@ export async function getLiveVisitors(projectId: string) { return keys.length; } -export async function getEvents(sql: string, options: GetEventsOptions = {}) { +export async function getEvents( + sql: string, + options: GetEventsOptions = {} +): Promise { const events = await chQuery(sql); if (options.profile) { const profileIds = events.map((e) => e.profile_id); @@ -124,7 +132,9 @@ export async function getEvents(sql: string, options: GetEventsOptions = {}) { return events.map(transformEvent); } -export async function createEvent(payload: IServiceCreateEventPayload) { +export async function createEvent( + payload: Omit +) { console.log(`create event ${payload.name} for ${payload.profileId}`); if (payload.name === 'session_start') { @@ -167,6 +177,7 @@ export async function createEvent(payload: IServiceCreateEventPayload) { } const event: IClickhouseEvent = { + id: uuid(), name: payload.name, profile_id: payload.profileId, project_id: payload.projectId, @@ -193,6 +204,9 @@ export async function createEvent(payload: IServiceCreateEventPayload) { table: 'events', values: [event], format: 'JSONEachRow', + clickhouse_settings: { + date_time_input_format: 'best_effort', + }, }); redisPub.publish('event', JSON.stringify(transformEvent(event))); @@ -208,3 +222,35 @@ export async function createEvent(payload: IServiceCreateEventPayload) { document: event, }; } + +interface GetEventListOptions { + projectId: string; + profileId?: string; + take: number; + cursor?: string; +} + +export async function getEventList({ + cursor, + take, + projectId, + profileId, +}: GetEventListOptions) { + const { sb, getSql } = createSqlBuilder(); + + sb.limit = take; + sb.where.projectId = `project_id = '${projectId}'`; + if (profileId) { + sb.where.profileId = `profile_id = '${profileId}'`; + } + + if (cursor) { + sb.where.cursor = `created_at <= '${formatClickhouseDate(cursor)}'`; + } + + sb.orderBy.created_at = 'created_at DESC'; + + const res = await getEvents(getSql(), { profile: true }); + + return res; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1a53578a..60182cdc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -713,6 +713,9 @@ importers: ramda: specifier: ^0.29.1 version: 0.29.1 + uuid: + specifier: ^9.0.1 + version: 9.0.1 devDependencies: '@mixan/eslint-config': specifier: workspace:* @@ -732,6 +735,9 @@ importers: '@types/ramda': specifier: ^0.29.6 version: 0.29.7 + '@types/uuid': + specifier: ^9.0.8 + version: 9.0.8 eslint: specifier: ^8.48.0 version: 8.52.0 @@ -7645,6 +7651,10 @@ packages: resolution: {integrity: sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==} dev: false + /@types/uuid@9.0.8: + resolution: {integrity: sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==} + dev: true + /@types/ws@8.5.10: resolution: {integrity: sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==} dependencies: