wip event list
This commit is contained in:
@@ -10,7 +10,7 @@ import { JoinWaitlist } from './join-waitlist';
|
|||||||
import { Sections } from './section';
|
import { Sections } from './section';
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
export const revalidate = 60 * 60;
|
export const revalidate = 3600;
|
||||||
|
|
||||||
export default async function Page() {
|
export default async function Page() {
|
||||||
const waitlistCount = await db.waitlist.count();
|
const waitlistCount = await db.waitlist.count();
|
||||||
|
|||||||
@@ -173,7 +173,6 @@ export function EventListItem({
|
|||||||
{profile && (
|
{profile && (
|
||||||
<KeyValueSubtle
|
<KeyValueSubtle
|
||||||
name="Profile"
|
name="Profile"
|
||||||
// icon={<ProfileAvatar size="xs" {...(profile ?? {})} />}
|
|
||||||
value={getProfileName(profile)}
|
value={getProfileName(profile)}
|
||||||
href={`/${params.organizationId}/${params.projectId}/profiles/${profile.id}`}
|
href={`/${params.organizationId}/${params.projectId}/profiles/${profile.id}`}
|
||||||
/>
|
/>
|
||||||
@@ -191,12 +190,18 @@ export function EventListItem({
|
|||||||
}
|
}
|
||||||
image={<EventIcon name={name} />}
|
image={<EventIcon name={name} />}
|
||||||
>
|
>
|
||||||
|
<div className="p-2">
|
||||||
|
<div className="bg-gradient-to-tr from-slate-100 to-white rounded-md">
|
||||||
{propertiesList.length > 0 && (
|
{propertiesList.length > 0 && (
|
||||||
<div className="p-4 flex flex-col gap-4">
|
<div className="p-4 flex flex-col gap-4">
|
||||||
<div className="font-medium">Your properties</div>
|
<div className="font-medium">Your properties</div>
|
||||||
<div className="flex flex-wrap gap-x-4 gap-y-2">
|
<div className="flex flex-wrap gap-x-4 gap-y-2">
|
||||||
{propertiesList.map((item) => (
|
{propertiesList.map((item) => (
|
||||||
<KeyValue key={item.name} name={item.name} value={item.value} />
|
<KeyValue
|
||||||
|
key={item.name}
|
||||||
|
name={item.name}
|
||||||
|
value={item.value}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -206,7 +211,7 @@ export function EventListItem({
|
|||||||
<div className="flex flex-wrap gap-x-4 gap-y-2">
|
<div className="flex flex-wrap gap-x-4 gap-y-2">
|
||||||
{keyValueList.map((item) => (
|
{keyValueList.map((item) => (
|
||||||
<KeyValue
|
<KeyValue
|
||||||
onClick={item.onClick}
|
onClick={() => item.onClick?.()}
|
||||||
key={item.name}
|
key={item.name}
|
||||||
name={item.name}
|
name={item.name}
|
||||||
value={item.value}
|
value={item.value}
|
||||||
@@ -214,6 +219,8 @@ export function EventListItem({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</ExpandableListItem>
|
</ExpandableListItem>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<>
|
||||||
|
<div className="p-4">
|
||||||
|
{data.length === 0 ? (
|
||||||
|
<FullPageEmptyState title="No events here" icon={GanttChartIcon}>
|
||||||
|
{/* {filterEvents.length ? (
|
||||||
|
<p>Could not find any events with your filter</p>
|
||||||
|
) : (
|
||||||
|
<p>We have not recieved any events yet</p>
|
||||||
|
)} */}
|
||||||
|
<p>We have not recieved any events yet</p>
|
||||||
|
</FullPageEmptyState>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
{data.map((item) => (
|
||||||
|
<EventListItem
|
||||||
|
key={item.createdAt.toString() + item.name + item.profileId}
|
||||||
|
{...item}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setCursor(last(data)?.createdAt ?? null)}
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 (
|
|
||||||
<>
|
|
||||||
<StickyBelowHeader className="p-4 flex justify-between">
|
|
||||||
<div>
|
|
||||||
<ComboboxAdvanced
|
|
||||||
items={filterEvents}
|
|
||||||
value={eventFilters}
|
|
||||||
onChange={setEventFilters}
|
|
||||||
placeholder="Filter by event"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</StickyBelowHeader>
|
|
||||||
<div className="p-4">
|
|
||||||
{events.length === 0 ? (
|
|
||||||
<FullPageEmptyState title="No events here" icon={GanttChartIcon}>
|
|
||||||
{eventFilters.length ? (
|
|
||||||
<p>Could not find any events with your filter</p>
|
|
||||||
) : (
|
|
||||||
<p>We have not recieved any events yet</p>
|
|
||||||
)}
|
|
||||||
</FullPageEmptyState>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
{events.map((item) => (
|
|
||||||
<EventListItem key={item.createdAt.toString()} {...item} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="mt-2">
|
|
||||||
<Pagination {...pagination} />
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,22 +1,39 @@
|
|||||||
|
import { Suspense } from 'react';
|
||||||
import PageLayout from '@/app/(app)/[organizationId]/[projectId]/page-layout';
|
import PageLayout from '@/app/(app)/[organizationId]/[projectId]/page-layout';
|
||||||
|
import { OverviewFiltersDrawer } from '@/components/overview/filters/overview-filters-drawer';
|
||||||
import { getExists } from '@/server/pageExists';
|
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 {
|
interface PageProps {
|
||||||
params: {
|
params: {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
};
|
};
|
||||||
|
searchParams: {
|
||||||
|
cursor?: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
export default async function Page({
|
export default async function Page({
|
||||||
params: { projectId, organizationId },
|
params: { projectId, organizationId },
|
||||||
|
searchParams: { cursor },
|
||||||
}: PageProps) {
|
}: PageProps) {
|
||||||
await getExists(organizationId, projectId);
|
await getExists(organizationId, projectId);
|
||||||
|
const events = await getEventList({
|
||||||
|
cursor,
|
||||||
|
projectId,
|
||||||
|
take: 50,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageLayout title="Events" organizationSlug={organizationId}>
|
<PageLayout title="Events" organizationSlug={organizationId}>
|
||||||
<ListEvents projectId={projectId} />
|
<StickyBelowHeader className="p-4 flex justify-between">
|
||||||
|
<OverviewFiltersDrawer projectId={projectId} />
|
||||||
|
</StickyBelowHeader>
|
||||||
|
<EventList data={events} />
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Suspense } from 'react';
|
|
||||||
import { WidgetHead } from '@/components/overview/overview-widget';
|
import { WidgetHead } from '@/components/overview/overview-widget';
|
||||||
import { useOverviewOptions } from '@/components/overview/useOverviewOptions';
|
import { useOverviewOptions } from '@/components/overview/useOverviewOptions';
|
||||||
import { Chart } from '@/components/report/chart';
|
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 { Widget, WidgetBody } from '@/components/Widget';
|
||||||
|
import { useEventFilters } from '@/hooks/useEventQueryFilters';
|
||||||
import type { IChartInput } from '@/types';
|
import type { IChartInput } from '@/types';
|
||||||
import { cn } from '@/utils/cn';
|
import { cn } from '@/utils/cn';
|
||||||
|
|
||||||
@@ -15,8 +13,8 @@ interface OverviewMetricsProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function OverviewMetrics({ projectId }: OverviewMetricsProps) {
|
export default function OverviewMetrics({ projectId }: OverviewMetricsProps) {
|
||||||
const { previous, range, interval, metric, setMetric, filters } =
|
const { previous, range, interval, metric, setMetric } = useOverviewOptions();
|
||||||
useOverviewOptions();
|
const filters = useEventFilters();
|
||||||
|
|
||||||
const reports = [
|
const reports = [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,26 +2,8 @@
|
|||||||
|
|
||||||
import { useOverviewOptions } from '@/components/overview/useOverviewOptions';
|
import { useOverviewOptions } from '@/components/overview/useOverviewOptions';
|
||||||
import { ReportRange } from '@/components/report/ReportRange';
|
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() {
|
export function OverviewReportRange() {
|
||||||
const { previous, range, setRange, interval, metric, setMetric, filters } =
|
const { range, setRange } = useOverviewOptions();
|
||||||
useOverviewOptions();
|
|
||||||
|
|
||||||
return <ReportRange value={range} onChange={(value) => setRange(value)} />;
|
return <ReportRange value={range} onChange={(value) => setRange(value)} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function OverviewFilterSheetTrigger() {
|
|
||||||
const { previous, range, setRange, interval, metric, setMetric, filters } =
|
|
||||||
useOverviewOptions();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SheetTrigger asChild>
|
|
||||||
<Button variant="outline" responsive icon={FilterIcon}>
|
|
||||||
Filters
|
|
||||||
</Button>
|
|
||||||
</SheetTrigger>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import PageLayout from '@/app/(app)/[organizationId]/[projectId]/page-layout';
|
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 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 { OverviewLiveHistogram } from '@/components/overview/overview-live-histogram';
|
||||||
import { OverviewShare } from '@/components/overview/overview-share';
|
import { OverviewShare } from '@/components/overview/overview-share';
|
||||||
import OverviewTopDevices from '@/components/overview/overview-top-devices';
|
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 OverviewTopGeo from '@/components/overview/overview-top-geo';
|
||||||
import OverviewTopPages from '@/components/overview/overview-top-pages';
|
import OverviewTopPages from '@/components/overview/overview-top-pages';
|
||||||
import OverviewTopSources from '@/components/overview/overview-top-sources';
|
import OverviewTopSources from '@/components/overview/overview-top-sources';
|
||||||
import { Sheet, SheetContent } from '@/components/ui/sheet';
|
|
||||||
import { getExists } from '@/server/pageExists';
|
import { getExists } from '@/server/pageExists';
|
||||||
|
|
||||||
import { db } from '@mixan/db';
|
import { db } from '@mixan/db';
|
||||||
|
|
||||||
import { StickyBelowHeader } from './layout-sticky-below-header';
|
import { StickyBelowHeader } from './layout-sticky-below-header';
|
||||||
import OverviewMetrics from './overview-metrics';
|
import OverviewMetrics from './overview-metrics';
|
||||||
import {
|
import { OverviewReportRange } from './overview-sticky-header';
|
||||||
OverviewFilterSheetTrigger,
|
|
||||||
OverviewReportRange,
|
|
||||||
} from './overview-sticky-header';
|
|
||||||
|
|
||||||
interface PageProps {
|
interface PageProps {
|
||||||
params: {
|
params: {
|
||||||
@@ -42,12 +38,11 @@ export default async function Page({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PageLayout title="Overview" organizationSlug={organizationId}>
|
<PageLayout title="Overview" organizationSlug={organizationId}>
|
||||||
<Sheet>
|
|
||||||
<StickyBelowHeader>
|
<StickyBelowHeader>
|
||||||
<div className="p-4 flex gap-2 justify-between">
|
<div className="p-4 flex gap-2 justify-between">
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<OverviewReportRange />
|
<OverviewReportRange />
|
||||||
<OverviewFilterSheetTrigger />
|
<OverviewFiltersDrawer projectId={projectId} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<ServerLiveCounter projectId={projectId} />
|
<ServerLiveCounter projectId={projectId} />
|
||||||
@@ -69,10 +64,6 @@ export default async function Page({
|
|||||||
<OverviewTopGeo projectId={projectId} />
|
<OverviewTopGeo projectId={projectId} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<SheetContent className="!max-w-lg w-full" side="right">
|
|
||||||
<OverviewFilters projectId={projectId} />
|
|
||||||
</SheetContent>
|
|
||||||
</Sheet>
|
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,16 @@
|
|||||||
import { StickyBelowHeader } from '@/app/(app)/[organizationId]/[projectId]/layout-sticky-below-header';
|
import { StickyBelowHeader } from '@/app/(app)/[organizationId]/[projectId]/layout-sticky-below-header';
|
||||||
import OverviewMetrics from '@/app/(app)/[organizationId]/[projectId]/overview-metrics';
|
import OverviewMetrics from '@/app/(app)/[organizationId]/[projectId]/overview-metrics';
|
||||||
import {
|
import { OverviewReportRange } from '@/app/(app)/[organizationId]/[projectId]/overview-sticky-header';
|
||||||
OverviewFilterSheetTrigger,
|
|
||||||
OverviewReportRange,
|
|
||||||
} from '@/app/(app)/[organizationId]/[projectId]/overview-sticky-header';
|
|
||||||
import { Logo } from '@/components/Logo';
|
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 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 { OverviewLiveHistogram } from '@/components/overview/overview-live-histogram';
|
||||||
import OverviewTopDevices from '@/components/overview/overview-top-devices';
|
import OverviewTopDevices from '@/components/overview/overview-top-devices';
|
||||||
import OverviewTopEvents from '@/components/overview/overview-top-events';
|
import OverviewTopEvents from '@/components/overview/overview-top-events';
|
||||||
import OverviewTopGeo from '@/components/overview/overview-top-geo';
|
import OverviewTopGeo from '@/components/overview/overview-top-geo';
|
||||||
import OverviewTopPages from '@/components/overview/overview-top-pages';
|
import OverviewTopPages from '@/components/overview/overview-top-pages';
|
||||||
import OverviewTopSources from '@/components/overview/overview-top-sources';
|
import OverviewTopSources from '@/components/overview/overview-top-sources';
|
||||||
import { Sheet, SheetContent } from '@/components/ui/sheet';
|
|
||||||
import { getOrganizationBySlug } from '@/server/services/organization.service';
|
import { getOrganizationBySlug } from '@/server/services/organization.service';
|
||||||
import { notFound } from 'next/navigation';
|
import { notFound } from 'next/navigation';
|
||||||
|
|
||||||
@@ -49,12 +45,11 @@ export default async function Page({ params: { id } }: PageProps) {
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-white rounded-lg shadow ring-8 ring-blue-600/50">
|
<div className="bg-white rounded-lg shadow ring-8 ring-blue-600/50">
|
||||||
<Sheet>
|
|
||||||
<StickyBelowHeader>
|
<StickyBelowHeader>
|
||||||
<div className="p-4 flex gap-2 justify-between">
|
<div className="p-4 flex gap-2 justify-between">
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<OverviewReportRange />
|
<OverviewReportRange />
|
||||||
<OverviewFilterSheetTrigger />
|
<OverviewFiltersDrawer projectId={projectId} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<ServerLiveCounter projectId={projectId} />
|
<ServerLiveCounter projectId={projectId} />
|
||||||
@@ -75,10 +70,6 @@ export default async function Page({ params: { id } }: PageProps) {
|
|||||||
<OverviewTopGeo projectId={projectId} />
|
<OverviewTopGeo projectId={projectId} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<SheetContent className="!max-w-lg w-full" side="right">
|
|
||||||
<OverviewFilters projectId={projectId} />
|
|
||||||
</SheetContent>
|
|
||||||
</Sheet>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<div
|
||||||
|
className={cn('flex flex-wrap gap-2', filters.length > 0 && 'px-4 pb-4')}
|
||||||
|
>
|
||||||
|
{filters.map(([key, filter]) => (
|
||||||
|
<Button
|
||||||
|
key={key}
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
icon={X}
|
||||||
|
onClick={() => filter.set(null)}
|
||||||
|
>
|
||||||
|
<span className="mr-1">{key} is</span>
|
||||||
|
<strong>{filter.get}</strong>
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 (
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xl font-medium mb-8">Overview filters</h2>
|
||||||
|
<Combobox
|
||||||
|
className="w-full"
|
||||||
|
onChange={(value) => {
|
||||||
|
// @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
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-4 mt-8">
|
||||||
|
{Object.entries(eventQueryFilters)
|
||||||
|
.filter(([, filter]) => filter.get !== null)
|
||||||
|
.map(([name, filter]) => (
|
||||||
|
<FilterOption
|
||||||
|
key={name}
|
||||||
|
projectId={projectId}
|
||||||
|
name={name}
|
||||||
|
{...filter}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div className="flex gap-2 items-center">
|
||||||
|
<div>{name}</div>
|
||||||
|
<Combobox
|
||||||
|
className="flex-1"
|
||||||
|
onChange={(value) => set(value)}
|
||||||
|
placeholder={'Select a value'}
|
||||||
|
items={
|
||||||
|
data?.values.filter(Boolean).map((value) => ({
|
||||||
|
value,
|
||||||
|
label: value,
|
||||||
|
})) ?? []
|
||||||
|
}
|
||||||
|
value={get}
|
||||||
|
/>
|
||||||
|
<Button size="icon" variant="ghost" onClick={() => set(null)}>
|
||||||
|
<XIcon />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 (
|
||||||
|
<Sheet>
|
||||||
|
<SheetTrigger asChild>
|
||||||
|
<Button variant="outline" responsive icon={FilterIcon}>
|
||||||
|
Filters
|
||||||
|
</Button>
|
||||||
|
</SheetTrigger>
|
||||||
|
<SheetContent className="!max-w-lg w-full" side="right">
|
||||||
|
<OverviewFiltersDrawerContent projectId={projectId} />
|
||||||
|
</SheetContent>
|
||||||
|
</Sheet>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 (
|
|
||||||
<div className={cn('flex flex-wrap gap-2', activeFilter && 'px-4 pb-4')}>
|
|
||||||
{options.referrer && (
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
icon={X}
|
|
||||||
onClick={() => options.setReferrer(null)}
|
|
||||||
>
|
|
||||||
<span className="mr-1">Referrer is</span>
|
|
||||||
<strong>{options.referrer}</strong>
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{options.referrerName && (
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
icon={X}
|
|
||||||
onClick={() => options.setReferrerName(null)}
|
|
||||||
>
|
|
||||||
<span className="mr-1">Referrer name is</span>
|
|
||||||
<strong>{options.referrerName}</strong>
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{options.referrerType && (
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
icon={X}
|
|
||||||
onClick={() => options.setReferrerType(null)}
|
|
||||||
>
|
|
||||||
<span className="mr-1">Referrer type is</span>
|
|
||||||
<strong>{options.referrerType}</strong>
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{options.device && (
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
icon={X}
|
|
||||||
onClick={() => options.setDevice(null)}
|
|
||||||
>
|
|
||||||
<span className="mr-1">Device is</span>
|
|
||||||
<strong>{options.device}</strong>
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{options.page && (
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
icon={X}
|
|
||||||
onClick={() => options.setPage(null)}
|
|
||||||
>
|
|
||||||
<span className="mr-1">Page is</span>
|
|
||||||
<strong>{options.page}</strong>
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{options.utmSource && (
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
icon={X}
|
|
||||||
onClick={() => options.setUtmSource(null)}
|
|
||||||
>
|
|
||||||
<span className="mr-1">Utm Source is</span>
|
|
||||||
<strong>{options.utmSource}</strong>
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{options.utmMedium && (
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
icon={X}
|
|
||||||
onClick={() => options.setUtmMedium(null)}
|
|
||||||
>
|
|
||||||
<span className="mr-1">Utm Medium is</span>
|
|
||||||
<strong>{options.utmMedium}</strong>
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{options.utmCampaign && (
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
icon={X}
|
|
||||||
onClick={() => options.setUtmCampaign(null)}
|
|
||||||
>
|
|
||||||
<span className="mr-1">Utm Campaign is</span>
|
|
||||||
<strong>{options.utmCampaign}</strong>
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{options.utmTerm && (
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
icon={X}
|
|
||||||
onClick={() => options.setUtmTerm(null)}
|
|
||||||
>
|
|
||||||
<span className="mr-1">Utm Term is</span>
|
|
||||||
<strong>{options.utmTerm}</strong>
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{options.utmContent && (
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
icon={X}
|
|
||||||
onClick={() => options.setUtmContent(null)}
|
|
||||||
>
|
|
||||||
<span className="mr-1">Utm Content is</span>
|
|
||||||
<strong>{options.utmContent}</strong>
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{options.country && (
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
icon={X}
|
|
||||||
onClick={() => options.setCountry(null)}
|
|
||||||
>
|
|
||||||
<span className="mr-1">Country is</span>
|
|
||||||
<strong>{options.country}</strong>
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{options.region && (
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
icon={X}
|
|
||||||
onClick={() => options.setRegion(null)}
|
|
||||||
>
|
|
||||||
<span className="mr-1">Region is</span>
|
|
||||||
<strong>{options.region}</strong>
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{options.city && (
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
icon={X}
|
|
||||||
onClick={() => options.setCity(null)}
|
|
||||||
>
|
|
||||||
<span className="mr-1">City is</span>
|
|
||||||
<strong>{options.city}</strong>
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{options.browser && (
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
icon={X}
|
|
||||||
onClick={() => options.setBrowser(null)}
|
|
||||||
>
|
|
||||||
<span className="mr-1">Browser is</span>
|
|
||||||
<strong>{options.browser}</strong>
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{options.browserVersion && (
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
icon={X}
|
|
||||||
onClick={() => options.setBrowserVersion(null)}
|
|
||||||
>
|
|
||||||
<span className="mr-1">Browser Version is</span>
|
|
||||||
<strong>{options.browserVersion}</strong>
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{options.os && (
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
icon={X}
|
|
||||||
onClick={() => options.setOS(null)}
|
|
||||||
>
|
|
||||||
<span className="mr-1">OS is</span>
|
|
||||||
<strong>{options.os}</strong>
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{options.osVersion && (
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
icon={X}
|
|
||||||
onClick={() => options.setOSVersion(null)}
|
|
||||||
>
|
|
||||||
<span className="mr-1">OS Version is</span>
|
|
||||||
<strong>{options.osVersion}</strong>
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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 (
|
|
||||||
<div>
|
|
||||||
<h2 className="text-xl font-medium mb-8">Overview filters</h2>
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
<div>
|
|
||||||
<Label className="flex justify-between">
|
|
||||||
Referrer
|
|
||||||
<button
|
|
||||||
className={cn(
|
|
||||||
'text-slate-500 transition-opacity opacity-100',
|
|
||||||
options.referrer === null && 'opacity-0'
|
|
||||||
)}
|
|
||||||
onClick={() => options.setReferrer(null)}
|
|
||||||
>
|
|
||||||
Reset
|
|
||||||
</button>
|
|
||||||
</Label>
|
|
||||||
<Combobox
|
|
||||||
className="w-full"
|
|
||||||
onChange={(value) => options.setReferrer(value)}
|
|
||||||
label="Referrer"
|
|
||||||
placeholder="Referrer"
|
|
||||||
items={
|
|
||||||
referrers?.values?.filter(Boolean)?.map((value) => ({
|
|
||||||
value,
|
|
||||||
label: value,
|
|
||||||
})) ?? []
|
|
||||||
}
|
|
||||||
value={options.referrer}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label className="flex justify-between">
|
|
||||||
Device
|
|
||||||
<button
|
|
||||||
className={cn(
|
|
||||||
'opacity-100 text-slate-500 transition-opacity',
|
|
||||||
options.device === null && 'opacity-0'
|
|
||||||
)}
|
|
||||||
onClick={() => options.setDevice(null)}
|
|
||||||
>
|
|
||||||
Reset
|
|
||||||
</button>
|
|
||||||
</Label>
|
|
||||||
<Combobox
|
|
||||||
className="w-full"
|
|
||||||
onChange={(value) => options.setDevice(value)}
|
|
||||||
label="Device"
|
|
||||||
placeholder="Device"
|
|
||||||
items={
|
|
||||||
devices?.values?.filter(Boolean)?.map((value) => ({
|
|
||||||
value,
|
|
||||||
label: value,
|
|
||||||
})) ?? []
|
|
||||||
}
|
|
||||||
value={options.device}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label className="flex justify-between">
|
|
||||||
Page
|
|
||||||
<button
|
|
||||||
className={cn(
|
|
||||||
'opacity-100 text-slate-500 transition-opacity',
|
|
||||||
options.page === null && 'opacity-0'
|
|
||||||
)}
|
|
||||||
onClick={() => options.setPage(null)}
|
|
||||||
>
|
|
||||||
Reset
|
|
||||||
</button>
|
|
||||||
</Label>
|
|
||||||
<Combobox
|
|
||||||
className="w-full"
|
|
||||||
onChange={(value) => options.setPage(value)}
|
|
||||||
label="Page"
|
|
||||||
placeholder="Page"
|
|
||||||
items={
|
|
||||||
pages?.values?.filter(Boolean)?.map((value) => ({
|
|
||||||
value,
|
|
||||||
label: value,
|
|
||||||
})) ?? []
|
|
||||||
}
|
|
||||||
value={options.page}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,10 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Chart } from '@/components/report/chart';
|
import { Chart } from '@/components/report/chart';
|
||||||
|
import {
|
||||||
|
useEventFilters,
|
||||||
|
useEventQueryFilters,
|
||||||
|
} from '@/hooks/useEventQueryFilters';
|
||||||
import { cn } from '@/utils/cn';
|
import { cn } from '@/utils/cn';
|
||||||
|
|
||||||
import { Widget, WidgetBody } from '../Widget';
|
import { Widget, WidgetBody } from '../Widget';
|
||||||
@@ -14,17 +18,10 @@ interface OverviewTopDevicesProps {
|
|||||||
export default function OverviewTopDevices({
|
export default function OverviewTopDevices({
|
||||||
projectId,
|
projectId,
|
||||||
}: OverviewTopDevicesProps) {
|
}: OverviewTopDevicesProps) {
|
||||||
const {
|
const { interval, range, previous } = useOverviewOptions();
|
||||||
filters,
|
const filters = useEventFilters();
|
||||||
interval,
|
const { device, browser, browserVersion, os, osVersion } =
|
||||||
range,
|
useEventQueryFilters();
|
||||||
previous,
|
|
||||||
setBrowser,
|
|
||||||
setBrowserVersion,
|
|
||||||
setOS,
|
|
||||||
setOSVersion,
|
|
||||||
setDevice,
|
|
||||||
} = useOverviewOptions();
|
|
||||||
const [widget, setWidget, widgets] = useOverviewWidget('tech', {
|
const [widget, setWidget, widgets] = useOverviewWidget('tech', {
|
||||||
devices: {
|
devices: {
|
||||||
title: 'Top devices',
|
title: 'Top devices',
|
||||||
@@ -193,21 +190,21 @@ export default function OverviewTopDevices({
|
|||||||
onClick={(item) => {
|
onClick={(item) => {
|
||||||
switch (widget.key) {
|
switch (widget.key) {
|
||||||
case 'devices':
|
case 'devices':
|
||||||
setDevice(item.name);
|
device.set(item.name);
|
||||||
break;
|
break;
|
||||||
case 'browser':
|
case 'browser':
|
||||||
setWidget('browser_version');
|
setWidget('browser_version');
|
||||||
setBrowser(item.name);
|
browser.set(item.name);
|
||||||
break;
|
break;
|
||||||
case 'browser_version':
|
case 'browser_version':
|
||||||
setBrowserVersion(item.name);
|
browserVersion.set(item.name);
|
||||||
break;
|
break;
|
||||||
case 'os':
|
case 'os':
|
||||||
setWidget('os_version');
|
setWidget('os_version');
|
||||||
setOS(item.name);
|
os.set(item.name);
|
||||||
break;
|
break;
|
||||||
case 'os_version':
|
case 'os_version':
|
||||||
setOSVersion(item.name);
|
osVersion.set(item.name);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Suspense } from 'react';
|
|
||||||
import { Chart } from '@/components/report/chart';
|
import { Chart } from '@/components/report/chart';
|
||||||
import { ChartLoading } from '@/components/report/chart/ChartLoading';
|
import { useEventFilters } from '@/hooks/useEventQueryFilters';
|
||||||
import { cn } from '@/utils/cn';
|
import { cn } from '@/utils/cn';
|
||||||
|
|
||||||
import { Widget, WidgetBody } from '../Widget';
|
import { Widget, WidgetBody } from '../Widget';
|
||||||
@@ -16,7 +15,8 @@ interface OverviewTopEventsProps {
|
|||||||
export default function OverviewTopEvents({
|
export default function OverviewTopEvents({
|
||||||
projectId,
|
projectId,
|
||||||
}: OverviewTopEventsProps) {
|
}: OverviewTopEventsProps) {
|
||||||
const { filters, interval, range, previous } = useOverviewOptions();
|
const { interval, range, previous } = useOverviewOptions();
|
||||||
|
const filters = useEventFilters();
|
||||||
const [widget, setWidget, widgets] = useOverviewWidget('ev', {
|
const [widget, setWidget, widgets] = useOverviewWidget('ev', {
|
||||||
all: {
|
all: {
|
||||||
title: 'Top events',
|
title: 'Top events',
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Suspense } from 'react';
|
|
||||||
import { Chart } from '@/components/report/chart';
|
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 { cn } from '@/utils/cn';
|
||||||
|
|
||||||
import { Widget, WidgetBody } from '../Widget';
|
import { Widget, WidgetBody } from '../Widget';
|
||||||
@@ -14,8 +16,9 @@ interface OverviewTopGeoProps {
|
|||||||
projectId: string;
|
projectId: string;
|
||||||
}
|
}
|
||||||
export default function OverviewTopGeo({ projectId }: OverviewTopGeoProps) {
|
export default function OverviewTopGeo({ projectId }: OverviewTopGeoProps) {
|
||||||
const { filters, interval, range, previous, setCountry, setRegion, setCity } =
|
const { interval, range, previous } = useOverviewOptions();
|
||||||
useOverviewOptions();
|
const filters = useEventFilters();
|
||||||
|
const { region, country, city } = useEventQueryFilters();
|
||||||
const [widget, setWidget, widgets] = useOverviewWidget('geo', {
|
const [widget, setWidget, widgets] = useOverviewWidget('geo', {
|
||||||
map: {
|
map: {
|
||||||
title: 'Map',
|
title: 'Map',
|
||||||
@@ -157,14 +160,14 @@ export default function OverviewTopGeo({ projectId }: OverviewTopGeoProps) {
|
|||||||
switch (widget.key) {
|
switch (widget.key) {
|
||||||
case 'countries':
|
case 'countries':
|
||||||
setWidget('regions');
|
setWidget('regions');
|
||||||
setCountry(item.name);
|
country.set(item.name);
|
||||||
break;
|
break;
|
||||||
case 'regions':
|
case 'regions':
|
||||||
setWidget('cities');
|
setWidget('cities');
|
||||||
setRegion(item.name);
|
region.set(item.name);
|
||||||
break;
|
break;
|
||||||
case 'cities':
|
case 'cities':
|
||||||
setCity(item.name);
|
city.set(item.name);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Suspense } from 'react';
|
|
||||||
import { Chart } from '@/components/report/chart';
|
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 { cn } from '@/utils/cn';
|
||||||
|
|
||||||
import { Widget, WidgetBody } from '../Widget';
|
import { Widget, WidgetBody } from '../Widget';
|
||||||
@@ -14,7 +16,9 @@ interface OverviewTopPagesProps {
|
|||||||
projectId: string;
|
projectId: string;
|
||||||
}
|
}
|
||||||
export default function OverviewTopPages({ projectId }: OverviewTopPagesProps) {
|
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', {
|
const [widget, setWidget, widgets] = useOverviewWidget('pages', {
|
||||||
top: {
|
top: {
|
||||||
title: 'Top pages',
|
title: 'Top pages',
|
||||||
@@ -125,7 +129,7 @@ export default function OverviewTopPages({ projectId }: OverviewTopPagesProps) {
|
|||||||
{...widget.chart}
|
{...widget.chart}
|
||||||
previous={false}
|
previous={false}
|
||||||
onClick={(item) => {
|
onClick={(item) => {
|
||||||
setPage(item.name);
|
path.set(item.name);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</WidgetBody>
|
</WidgetBody>
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Suspense } from 'react';
|
|
||||||
import { Chart } from '@/components/report/chart';
|
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 { cn } from '@/utils/cn';
|
||||||
|
|
||||||
import { Widget, WidgetBody } from '../Widget';
|
import { Widget, WidgetBody } from '../Widget';
|
||||||
@@ -16,20 +18,18 @@ interface OverviewTopSourcesProps {
|
|||||||
export default function OverviewTopSources({
|
export default function OverviewTopSources({
|
||||||
projectId,
|
projectId,
|
||||||
}: OverviewTopSourcesProps) {
|
}: OverviewTopSourcesProps) {
|
||||||
|
const { interval, range, previous } = useOverviewOptions();
|
||||||
const {
|
const {
|
||||||
filters,
|
referrer,
|
||||||
interval,
|
referrerName,
|
||||||
range,
|
referrerType,
|
||||||
previous,
|
utmCampaign,
|
||||||
setReferrer,
|
utmContent,
|
||||||
setUtmSource,
|
utmMedium,
|
||||||
setUtmMedium,
|
utmSource,
|
||||||
setUtmCampaign,
|
utmTerm,
|
||||||
setUtmTerm,
|
} = useEventQueryFilters();
|
||||||
setUtmContent,
|
const filters = useEventFilters();
|
||||||
setReferrerName,
|
|
||||||
setReferrerType,
|
|
||||||
} = useOverviewOptions();
|
|
||||||
const [widget, setWidget, widgets] = useOverviewWidget('sources', {
|
const [widget, setWidget, widgets] = useOverviewWidget('sources', {
|
||||||
all: {
|
all: {
|
||||||
title: 'Top sources',
|
title: 'Top sources',
|
||||||
@@ -282,30 +282,30 @@ export default function OverviewTopSources({
|
|||||||
onClick={(item) => {
|
onClick={(item) => {
|
||||||
switch (widget.key) {
|
switch (widget.key) {
|
||||||
case 'all':
|
case 'all':
|
||||||
setReferrerName(item.name);
|
referrerName.set(item.name);
|
||||||
setWidget('domain');
|
setWidget('domain');
|
||||||
break;
|
break;
|
||||||
case 'domain':
|
case 'domain':
|
||||||
setReferrer(item.name);
|
referrer.set(item.name);
|
||||||
break;
|
break;
|
||||||
case 'type':
|
case 'type':
|
||||||
setReferrerType(item.name);
|
referrerType.set(item.name);
|
||||||
setWidget('domain');
|
setWidget('domain');
|
||||||
break;
|
break;
|
||||||
case 'utm_source':
|
case 'utm_source':
|
||||||
setUtmSource(item.name);
|
utmSource.set(item.name);
|
||||||
break;
|
break;
|
||||||
case 'utm_medium':
|
case 'utm_medium':
|
||||||
setUtmMedium(item.name);
|
utmMedium.set(item.name);
|
||||||
break;
|
break;
|
||||||
case 'utm_campaign':
|
case 'utm_campaign':
|
||||||
setUtmCampaign(item.name);
|
utmCampaign.set(item.name);
|
||||||
break;
|
break;
|
||||||
case 'utm_term':
|
case 'utm_term':
|
||||||
setUtmTerm(item.name);
|
utmTerm.set(item.name);
|
||||||
break;
|
break;
|
||||||
case 'utm_content':
|
case 'utm_content':
|
||||||
setUtmContent(item.name);
|
utmContent.set(item.name);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import { useMemo } from 'react';
|
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
||||||
import type { IChartInput } from '@/types';
|
|
||||||
import { getDefaultIntervalByRange, timeRanges } from '@/utils/constants';
|
import { getDefaultIntervalByRange, timeRanges } from '@/utils/constants';
|
||||||
import { mapKeys } from '@/utils/validation';
|
import { mapKeys } from '@/utils/validation';
|
||||||
import {
|
import {
|
||||||
parseAsBoolean,
|
parseAsBoolean,
|
||||||
parseAsInteger,
|
parseAsInteger,
|
||||||
parseAsString,
|
|
||||||
parseAsStringEnum,
|
parseAsStringEnum,
|
||||||
useQueryState,
|
useQueryState,
|
||||||
} from 'nuqs';
|
} from 'nuqs';
|
||||||
@@ -29,267 +27,12 @@ export function useOverviewOptions() {
|
|||||||
parseAsInteger.withDefault(0).withOptions(nuqsOptions)
|
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
|
// Toggles
|
||||||
const [liveHistogram, setLiveHistogram] = useQueryState(
|
const [liveHistogram, setLiveHistogram] = useQueryState(
|
||||||
'live',
|
'live',
|
||||||
parseAsBoolean.withDefault(false).withOptions(nuqsOptions)
|
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 {
|
return {
|
||||||
previous,
|
previous,
|
||||||
setPrevious,
|
setPrevious,
|
||||||
@@ -297,52 +40,9 @@ export function useOverviewOptions() {
|
|||||||
setRange,
|
setRange,
|
||||||
metric,
|
metric,
|
||||||
setMetric,
|
setMetric,
|
||||||
page,
|
|
||||||
setPage,
|
|
||||||
|
|
||||||
// Computed
|
// Computed
|
||||||
interval,
|
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
|
// Toggles
|
||||||
liveHistogram,
|
liveHistogram,
|
||||||
|
|||||||
@@ -13,13 +13,16 @@ export function KeyValue({ href, onClick, name, value }: KeyValueProps) {
|
|||||||
const Component = href ? (Link as any) : onClick ? 'button' : 'div';
|
const Component = href ? (Link as any) : onClick ? 'button' : 'div';
|
||||||
return (
|
return (
|
||||||
<Component
|
<Component
|
||||||
className="group flex border border-border rounded-md text-xs divide-x font-medium self-start min-w-0 max-w-full"
|
className={cn(
|
||||||
|
'group overflow-hidden flex border border-border rounded-md text-xs divide-x font-medium self-start min-w-0 max-w-full transition-transform',
|
||||||
|
clickable && 'hover:-translate-y-0.5'
|
||||||
|
)}
|
||||||
{...{ href, onClick }}
|
{...{ href, onClick }}
|
||||||
>
|
>
|
||||||
<div className="p-1 px-2">{name}</div>
|
<div className="p-1 px-2 bg-slate-50">{name}</div>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'p-1 px-2 font-mono text-blue-700 bg-slate-50 whitespace-nowrap overflow-hidden text-ellipsis',
|
'p-1 px-2 font-mono text-blue-700 bg-white whitespace-nowrap overflow-hidden text-ellipsis shadow-[inset_0_0_0_1px_#fff]',
|
||||||
clickable && 'group-hover:underline'
|
clickable && 'group-hover:underline'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
12
apps/web/src/hooks/useCursor.ts
Normal file
12
apps/web/src/hooks/useCursor.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { parseAsIsoDateTime, useQueryState } from 'nuqs';
|
||||||
|
|
||||||
|
export function useCursor() {
|
||||||
|
const [cursor, setCursor] = useQueryState(
|
||||||
|
'cursor',
|
||||||
|
parseAsIsoDateTime.withOptions({ shallow: false })
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
cursor,
|
||||||
|
setCursor,
|
||||||
|
};
|
||||||
|
}
|
||||||
311
apps/web/src/hooks/useEventQueryFilters copy.ts
Normal file
311
apps/web/src/hooks/useEventQueryFilters copy.ts
Normal file
@@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
227
apps/web/src/hooks/useEventQueryFilters.ts
Normal file
227
apps/web/src/hooks/useEventQueryFilters.ts
Normal file
@@ -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<T>(hook: UseQueryStateReturn<T, undefined>) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
@@ -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';
|
if (!profile) return 'No profile';
|
||||||
return [profile.first_name, profile.last_name].filter(Boolean).join(' ');
|
return [profile.first_name, profile.last_name].filter(Boolean).join(' ');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
|
ALTER TABLE
|
||||||
|
events
|
||||||
|
ADD
|
||||||
|
COLUMN id UUID;
|
||||||
|
|
||||||
CREATE TABLE openpanel.events (
|
CREATE TABLE openpanel.events (
|
||||||
|
`id` UUID,
|
||||||
`name` String,
|
`name` String,
|
||||||
`profile_id` String,
|
`profile_id` String,
|
||||||
`project_id` String,
|
`project_id` String,
|
||||||
|
|||||||
@@ -12,11 +12,12 @@
|
|||||||
"with-env": "dotenv -e ../../.env -c --"
|
"with-env": "dotenv -e ../../.env -c --"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@clickhouse/client": "^0.2.9",
|
||||||
"@mixan/common": "workspace:*",
|
"@mixan/common": "workspace:*",
|
||||||
"@mixan/redis": "workspace:*",
|
"@mixan/redis": "workspace:*",
|
||||||
"@clickhouse/client": "^0.2.9",
|
|
||||||
"@prisma/client": "^5.1.1",
|
"@prisma/client": "^5.1.1",
|
||||||
"ramda": "^0.29.1"
|
"ramda": "^0.29.1",
|
||||||
|
"uuid": "^9.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@mixan/eslint-config": "workspace:*",
|
"@mixan/eslint-config": "workspace:*",
|
||||||
@@ -25,6 +26,7 @@
|
|||||||
"@mixan/types": "workspace:*",
|
"@mixan/types": "workspace:*",
|
||||||
"@types/node": "^18.16.0",
|
"@types/node": "^18.16.0",
|
||||||
"@types/ramda": "^0.29.6",
|
"@types/ramda": "^0.29.6",
|
||||||
|
"@types/uuid": "^9.0.8",
|
||||||
"eslint": "^8.48.0",
|
"eslint": "^8.48.0",
|
||||||
"prettier": "^3.0.3",
|
"prettier": "^3.0.3",
|
||||||
"prisma": "^5.1.1",
|
"prisma": "^5.1.1",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { IDBProfile } from '@/prisma-types';
|
|
||||||
import { omit } from 'ramda';
|
import { omit } from 'ramda';
|
||||||
|
import { v4 as uuid } from 'uuid';
|
||||||
|
|
||||||
import { randomSplitName, toDots } from '@mixan/common';
|
import { randomSplitName, toDots } from '@mixan/common';
|
||||||
import { redis, redisPub } from '@mixan/redis';
|
import { redis, redisPub } from '@mixan/redis';
|
||||||
@@ -12,8 +12,11 @@ import {
|
|||||||
} from '../clickhouse-client';
|
} from '../clickhouse-client';
|
||||||
import type { Prisma } from '../prisma-client';
|
import type { Prisma } from '../prisma-client';
|
||||||
import { db } from '../prisma-client';
|
import { db } from '../prisma-client';
|
||||||
|
import type { IDBProfile } from '../prisma-types';
|
||||||
|
import { createSqlBuilder } from '../sql-builder';
|
||||||
|
|
||||||
export interface IClickhouseEvent {
|
export interface IClickhouseEvent {
|
||||||
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
profile_id: string;
|
profile_id: string;
|
||||||
project_id: string;
|
project_id: string;
|
||||||
@@ -41,6 +44,7 @@ export function transformEvent(
|
|||||||
event: IClickhouseEvent
|
event: IClickhouseEvent
|
||||||
): IServiceCreateEventPayload {
|
): IServiceCreateEventPayload {
|
||||||
return {
|
return {
|
||||||
|
id: event.id,
|
||||||
name: event.name,
|
name: event.name,
|
||||||
profileId: event.profile_id,
|
profileId: event.profile_id,
|
||||||
projectId: event.project_id,
|
projectId: event.project_id,
|
||||||
@@ -66,6 +70,7 @@ export function transformEvent(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface IServiceCreateEventPayload {
|
export interface IServiceCreateEventPayload {
|
||||||
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
profileId: string;
|
profileId: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
@@ -102,7 +107,10 @@ export async function getLiveVisitors(projectId: string) {
|
|||||||
return keys.length;
|
return keys.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getEvents(sql: string, options: GetEventsOptions = {}) {
|
export async function getEvents(
|
||||||
|
sql: string,
|
||||||
|
options: GetEventsOptions = {}
|
||||||
|
): Promise<IServiceCreateEventPayload[]> {
|
||||||
const events = await chQuery<IClickhouseEvent>(sql);
|
const events = await chQuery<IClickhouseEvent>(sql);
|
||||||
if (options.profile) {
|
if (options.profile) {
|
||||||
const profileIds = events.map((e) => e.profile_id);
|
const profileIds = events.map((e) => e.profile_id);
|
||||||
@@ -124,7 +132,9 @@ export async function getEvents(sql: string, options: GetEventsOptions = {}) {
|
|||||||
return events.map(transformEvent);
|
return events.map(transformEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createEvent(payload: IServiceCreateEventPayload) {
|
export async function createEvent(
|
||||||
|
payload: Omit<IServiceCreateEventPayload, 'id'>
|
||||||
|
) {
|
||||||
console.log(`create event ${payload.name} for ${payload.profileId}`);
|
console.log(`create event ${payload.name} for ${payload.profileId}`);
|
||||||
|
|
||||||
if (payload.name === 'session_start') {
|
if (payload.name === 'session_start') {
|
||||||
@@ -167,6 +177,7 @@ export async function createEvent(payload: IServiceCreateEventPayload) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const event: IClickhouseEvent = {
|
const event: IClickhouseEvent = {
|
||||||
|
id: uuid(),
|
||||||
name: payload.name,
|
name: payload.name,
|
||||||
profile_id: payload.profileId,
|
profile_id: payload.profileId,
|
||||||
project_id: payload.projectId,
|
project_id: payload.projectId,
|
||||||
@@ -193,6 +204,9 @@ export async function createEvent(payload: IServiceCreateEventPayload) {
|
|||||||
table: 'events',
|
table: 'events',
|
||||||
values: [event],
|
values: [event],
|
||||||
format: 'JSONEachRow',
|
format: 'JSONEachRow',
|
||||||
|
clickhouse_settings: {
|
||||||
|
date_time_input_format: 'best_effort',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
redisPub.publish('event', JSON.stringify(transformEvent(event)));
|
redisPub.publish('event', JSON.stringify(transformEvent(event)));
|
||||||
@@ -208,3 +222,35 @@ export async function createEvent(payload: IServiceCreateEventPayload) {
|
|||||||
document: event,
|
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;
|
||||||
|
}
|
||||||
|
|||||||
10
pnpm-lock.yaml
generated
10
pnpm-lock.yaml
generated
@@ -713,6 +713,9 @@ importers:
|
|||||||
ramda:
|
ramda:
|
||||||
specifier: ^0.29.1
|
specifier: ^0.29.1
|
||||||
version: 0.29.1
|
version: 0.29.1
|
||||||
|
uuid:
|
||||||
|
specifier: ^9.0.1
|
||||||
|
version: 9.0.1
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@mixan/eslint-config':
|
'@mixan/eslint-config':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
@@ -732,6 +735,9 @@ importers:
|
|||||||
'@types/ramda':
|
'@types/ramda':
|
||||||
specifier: ^0.29.6
|
specifier: ^0.29.6
|
||||||
version: 0.29.7
|
version: 0.29.7
|
||||||
|
'@types/uuid':
|
||||||
|
specifier: ^9.0.8
|
||||||
|
version: 9.0.8
|
||||||
eslint:
|
eslint:
|
||||||
specifier: ^8.48.0
|
specifier: ^8.48.0
|
||||||
version: 8.52.0
|
version: 8.52.0
|
||||||
@@ -7645,6 +7651,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==}
|
resolution: {integrity: sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@types/uuid@9.0.8:
|
||||||
|
resolution: {integrity: sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@types/ws@8.5.10:
|
/@types/ws@8.5.10:
|
||||||
resolution: {integrity: sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==}
|
resolution: {integrity: sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|||||||
Reference in New Issue
Block a user