refactor packages
This commit is contained in:
@@ -5,7 +5,6 @@ import { StickyBelowHeader } from '@/app/(app)/[organizationId]/[projectId]/layo
|
||||
import { LazyChart } from '@/components/report/chart/LazyChart';
|
||||
import { ReportRange } from '@/components/report/ReportRange';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Combobox } from '@/components/ui/combobox';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -14,14 +13,15 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { useAppParams } from '@/hooks/useAppParams';
|
||||
import type { getReportsByDashboardId } from '@/server/services/reports.service';
|
||||
import type { IChartRange } from '@/types';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { getDefaultIntervalByRange, timeRanges } from '@/utils/constants';
|
||||
import { ChevronRight, MoreHorizontal, PlusIcon, Trash } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { getDefaultIntervalByRange } from '@mixan/constants';
|
||||
import type { getReportsByDashboardId } from '@mixan/db';
|
||||
import type { IChartRange } from '@mixan/validation';
|
||||
|
||||
interface ListReportsProps {
|
||||
reports: Awaited<ReturnType<typeof getReportsByDashboardId>>;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import PageLayout from '@/app/(app)/[organizationId]/[projectId]/page-layout';
|
||||
import { getExists } from '@/server/pageExists';
|
||||
import { getDashboardById } from '@/server/services/dashboard.service';
|
||||
import { getReportsByDashboardId } from '@/server/services/reports.service';
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
import { getDashboardById, getReportsByDashboardId } from '@mixan/db';
|
||||
|
||||
import { ListReports } from './list-reports';
|
||||
|
||||
interface PageProps {
|
||||
|
||||
@@ -7,12 +7,13 @@ import { Button } from '@/components/ui/button';
|
||||
import { ToastAction } from '@/components/ui/toast';
|
||||
import { useAppParams } from '@/hooks/useAppParams';
|
||||
import { pushModal } from '@/modals';
|
||||
import type { IServiceDashboards } from '@/server/services/dashboard.service';
|
||||
import { LayoutPanelTopIcon, Pencil, PlusIcon, Trash } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import type { IServiceDashboards } from '@mixan/db';
|
||||
|
||||
interface ListDashboardsProps {
|
||||
dashboards: IServiceDashboards;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import PageLayout from '@/app/(app)/[organizationId]/[projectId]/page-layout';
|
||||
import { getExists } from '@/server/pageExists';
|
||||
import { getDashboardsByProjectId } from '@/server/services/dashboard.service';
|
||||
|
||||
import { getDashboardsByProjectId } from '@mixan/db';
|
||||
|
||||
import { HeaderDashboards } from './header-dashboards';
|
||||
import { ListDashboards } from './list-dashboards';
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
'use client';
|
||||
|
||||
import type { RouterOutputs } from '@/app/_trpc/client';
|
||||
import { ExpandableListItem } from '@/components/general/ExpandableListItem';
|
||||
import { KeyValue, KeyValueSubtle } from '@/components/ui/key-value';
|
||||
import { useAppParams } from '@/hooks/useAppParams';
|
||||
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
||||
import {
|
||||
useEventQueryFilters,
|
||||
useEventQueryNamesFilter,
|
||||
} from '@/hooks/useEventQueryFilters';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { getProfileName } from '@/utils/getters';
|
||||
import { round } from '@/utils/math';
|
||||
import { uniq } from 'ramda';
|
||||
|
||||
import type { IServiceCreateEventPayload } from '@mixan/db';
|
||||
|
||||
@@ -40,7 +43,8 @@ export function EventListItem({
|
||||
meta,
|
||||
}: EventListItemProps) {
|
||||
const params = useAppParams();
|
||||
const eventQueryFilters = useEventQueryFilters({ shallow: false });
|
||||
const [, setEvents] = useEventQueryNamesFilter({ shallow: false });
|
||||
const [, setFilter] = useEventQueryFilters({ shallow: false });
|
||||
const keyValueList = [
|
||||
{
|
||||
name: 'Duration',
|
||||
@@ -50,98 +54,98 @@ export function EventListItem({
|
||||
name: 'Referrer',
|
||||
value: referrer,
|
||||
onClick() {
|
||||
eventQueryFilters.referrer.set(referrer ?? null);
|
||||
setFilter('referrer', referrer ?? '');
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Referrer name',
|
||||
value: referrerName,
|
||||
onClick() {
|
||||
eventQueryFilters.referrerName.set(referrerName ?? null);
|
||||
setFilter('referrer_name', referrerName ?? '');
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Referrer type',
|
||||
value: referrerType,
|
||||
onClick() {
|
||||
eventQueryFilters.referrerType.set(referrerType ?? null);
|
||||
setFilter('referrer_type', referrerType ?? '');
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Brand',
|
||||
value: brand,
|
||||
onClick() {
|
||||
eventQueryFilters.brand.set(brand ?? null);
|
||||
setFilter('brand', brand ?? '');
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Model',
|
||||
value: model,
|
||||
onClick() {
|
||||
eventQueryFilters.model.set(model ?? null);
|
||||
setFilter('model', model ?? '');
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Browser',
|
||||
value: browser,
|
||||
onClick() {
|
||||
eventQueryFilters.browser.set(browser ?? null);
|
||||
setFilter('browser', browser ?? '');
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Browser version',
|
||||
value: browserVersion,
|
||||
onClick() {
|
||||
eventQueryFilters.browserVersion.set(browserVersion ?? null);
|
||||
setFilter('browser_version', browserVersion ?? '');
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'OS',
|
||||
value: os,
|
||||
onClick() {
|
||||
eventQueryFilters.os.set(os ?? null);
|
||||
setFilter('os', os ?? '');
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'OS cersion',
|
||||
value: osVersion,
|
||||
onClick() {
|
||||
eventQueryFilters.osVersion.set(osVersion ?? null);
|
||||
setFilter('os_version', osVersion ?? '');
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'City',
|
||||
value: city,
|
||||
onClick() {
|
||||
eventQueryFilters.city.set(city ?? null);
|
||||
setFilter('city', city ?? '');
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Region',
|
||||
value: region,
|
||||
onClick() {
|
||||
eventQueryFilters.region.set(region ?? null);
|
||||
setFilter('region', region ?? '');
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Country',
|
||||
value: country,
|
||||
onClick() {
|
||||
eventQueryFilters.country.set(country ?? null);
|
||||
setFilter('country', country ?? '');
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Continent',
|
||||
value: continent,
|
||||
onClick() {
|
||||
eventQueryFilters.continent.set(continent ?? null);
|
||||
setFilter('continent', continent ?? '');
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Device',
|
||||
value: device,
|
||||
onClick() {
|
||||
eventQueryFilters.device.set(device ?? null);
|
||||
setFilter('device', device ?? '');
|
||||
},
|
||||
},
|
||||
].filter((item) => typeof item.value === 'string' && item.value);
|
||||
@@ -156,7 +160,11 @@ export function EventListItem({
|
||||
return (
|
||||
<ExpandableListItem
|
||||
className={cn(meta?.conversion && 'ring-2 ring-primary-500')}
|
||||
title={name.split('_').join(' ')}
|
||||
title={
|
||||
<button onClick={() => setEvents((p) => uniq([...p, name]))}>
|
||||
{name.split('_').join(' ')}
|
||||
</button>
|
||||
}
|
||||
content={
|
||||
<>
|
||||
<KeyValueSubtle name="Time" value={createdAt.toLocaleString()} />
|
||||
@@ -172,7 +180,7 @@ export function EventListItem({
|
||||
name="Path"
|
||||
value={path}
|
||||
onClick={() => {
|
||||
eventQueryFilters.path.set(path);
|
||||
setFilter('path', path);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
@@ -191,6 +199,13 @@ export function EventListItem({
|
||||
key={item.name}
|
||||
name={item.name}
|
||||
value={item.value}
|
||||
onClick={() => {
|
||||
setFilter(
|
||||
`properties.${item.name}`,
|
||||
item.value ? String(item.value) : '',
|
||||
'is'
|
||||
);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { FullPageEmptyState } from '@/components/FullPageEmptyState';
|
||||
import { Pagination } from '@/components/Pagination';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useCursor } from '@/hooks/useCursor';
|
||||
import { useEventFilters } from '@/hooks/useEventQueryFilters';
|
||||
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
||||
import { GanttChartIcon } from 'lucide-react';
|
||||
|
||||
import type { IServiceCreateEventPayload } from '@mixan/db';
|
||||
@@ -18,8 +18,7 @@ interface EventListProps {
|
||||
}
|
||||
export function EventList({ data, count }: EventListProps) {
|
||||
const { cursor, setCursor } = useCursor();
|
||||
const filters = useEventFilters();
|
||||
|
||||
const [filters] = useEventQueryFilters();
|
||||
return (
|
||||
<Suspense>
|
||||
<div className="p-4">
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
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 { getEventFilters } from '@/hooks/useEventQueryFilters';
|
||||
import {
|
||||
eventQueryFiltersParser,
|
||||
eventQueryNamesFilter,
|
||||
} from '@/hooks/useEventQueryFilters';
|
||||
import { getExists } from '@/server/pageExists';
|
||||
|
||||
import { getEventList, getEventsCount } from '@mixan/db';
|
||||
@@ -15,27 +18,9 @@ interface PageProps {
|
||||
organizationId: string;
|
||||
};
|
||||
searchParams: {
|
||||
events?: string;
|
||||
cursor?: string;
|
||||
path?: string;
|
||||
device?: string;
|
||||
referrer?: string;
|
||||
referrerName?: string;
|
||||
referrerType?: string;
|
||||
utmSource?: string;
|
||||
utmMedium?: string;
|
||||
utmCampaign?: string;
|
||||
utmContent?: string;
|
||||
utmTerm?: string;
|
||||
continent?: string;
|
||||
country?: string;
|
||||
region?: string;
|
||||
city?: string;
|
||||
browser?: string;
|
||||
browserVersion?: string;
|
||||
os?: string;
|
||||
osVersion?: string;
|
||||
brand?: string;
|
||||
model?: string;
|
||||
f?: string;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -59,53 +44,13 @@ export default async function Page({
|
||||
cursor: parseQueryAsNumber(searchParams.cursor),
|
||||
projectId,
|
||||
take: 50,
|
||||
filters: getEventFilters({
|
||||
path: searchParams.path ?? null,
|
||||
device: searchParams.device ?? null,
|
||||
referrer: searchParams.referrer ?? null,
|
||||
referrerName: searchParams.referrerName ?? null,
|
||||
referrerType: searchParams.referrerType ?? null,
|
||||
utmSource: searchParams.utmSource ?? null,
|
||||
utmMedium: searchParams.utmMedium ?? null,
|
||||
utmCampaign: searchParams.utmCampaign ?? null,
|
||||
utmContent: searchParams.utmContent ?? null,
|
||||
utmTerm: searchParams.utmTerm ?? null,
|
||||
continent: searchParams.continent ?? null,
|
||||
country: searchParams.country ?? null,
|
||||
region: searchParams.region ?? null,
|
||||
city: searchParams.city ?? null,
|
||||
browser: searchParams.browser ?? null,
|
||||
browserVersion: searchParams.browserVersion ?? null,
|
||||
os: searchParams.os ?? null,
|
||||
osVersion: searchParams.osVersion ?? null,
|
||||
brand: searchParams.brand ?? null,
|
||||
model: searchParams.model ?? null,
|
||||
}),
|
||||
events: eventQueryNamesFilter.parse(searchParams.events ?? ''),
|
||||
filters: eventQueryFiltersParser.parse(searchParams.f ?? '') ?? undefined,
|
||||
}),
|
||||
getEventsCount({
|
||||
projectId,
|
||||
filters: getEventFilters({
|
||||
path: searchParams.path ?? null,
|
||||
device: searchParams.device ?? null,
|
||||
referrer: searchParams.referrer ?? null,
|
||||
referrerName: searchParams.referrerName ?? null,
|
||||
referrerType: searchParams.referrerType ?? null,
|
||||
utmSource: searchParams.utmSource ?? null,
|
||||
utmMedium: searchParams.utmMedium ?? null,
|
||||
utmCampaign: searchParams.utmCampaign ?? null,
|
||||
utmContent: searchParams.utmContent ?? null,
|
||||
utmTerm: searchParams.utmTerm ?? null,
|
||||
continent: searchParams.continent ?? null,
|
||||
country: searchParams.country ?? null,
|
||||
region: searchParams.region ?? null,
|
||||
city: searchParams.city ?? null,
|
||||
browser: searchParams.browser ?? null,
|
||||
browserVersion: searchParams.browserVersion ?? null,
|
||||
os: searchParams.os ?? null,
|
||||
osVersion: searchParams.osVersion ?? null,
|
||||
brand: searchParams.brand ?? null,
|
||||
model: searchParams.model ?? null,
|
||||
}),
|
||||
events: eventQueryNamesFilter.parse(searchParams.events ?? ''),
|
||||
filters: eventQueryFiltersParser.parse(searchParams.f ?? '') ?? undefined,
|
||||
}),
|
||||
getExists(organizationId, projectId),
|
||||
]);
|
||||
@@ -116,6 +61,7 @@ export default async function Page({
|
||||
<OverviewFiltersDrawer
|
||||
projectId={projectId}
|
||||
nuqsOptions={nuqsOptions}
|
||||
enableEventsFilter
|
||||
/>
|
||||
<OverviewFiltersButtons
|
||||
className="p-0 justify-end"
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { useAppParams } from '@/hooks/useAppParams';
|
||||
import type { IServiceDashboards } from '@/server/services/dashboard.service';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { useUser } from '@clerk/nextjs';
|
||||
import {
|
||||
@@ -21,6 +20,8 @@ import type { LucideProps } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
import type { IServiceDashboards } from '@mixan/db';
|
||||
|
||||
function LinkWithIcon({
|
||||
href,
|
||||
icon: Icon,
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
|
||||
import { Combobox } from '@/components/ui/combobox';
|
||||
import { useAppParams } from '@/hooks/useAppParams';
|
||||
import type { IServiceOrganization } from '@/server/services/organization.service';
|
||||
import { Building } from 'lucide-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import type { IServiceOrganization } from '@mixan/db';
|
||||
|
||||
interface LayoutOrganizationSelectorProps {
|
||||
organizations: IServiceOrganization[];
|
||||
}
|
||||
|
||||
@@ -2,11 +2,12 @@
|
||||
|
||||
import { Combobox } from '@/components/ui/combobox';
|
||||
import { useAppParams } from '@/hooks/useAppParams';
|
||||
import type { getCurrentProjects } from '@/server/services/project.service';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
|
||||
import type { getProjectsByOrganizationSlug } from '@mixan/db';
|
||||
|
||||
interface LayoutProjectSelectorProps {
|
||||
projects: Awaited<ReturnType<typeof getCurrentProjects>>;
|
||||
projects: Awaited<ReturnType<typeof getProjectsByOrganizationSlug>>;
|
||||
}
|
||||
export default function LayoutProjectSelector({
|
||||
projects,
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Logo } from '@/components/Logo';
|
||||
import type { IServiceDashboards } from '@/server/services/dashboard.service';
|
||||
import type { IServiceOrganization } from '@/server/services/organization.service';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { Rotate as Hamburger } from 'hamburger-react';
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
import type { IServiceDashboards, IServiceOrganization } from '@mixan/db';
|
||||
|
||||
import LayoutMenu from './layout-menu';
|
||||
import LayoutOrganizationSelector from './layout-organization-selector';
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { getDashboardsByOrganization } from '@/server/services/dashboard.service';
|
||||
import { getCurrentOrganizations } from '@/server/services/organization.service';
|
||||
import {
|
||||
getCurrentOrganizations,
|
||||
getDashboardsByOrganization,
|
||||
} from '@mixan/db';
|
||||
|
||||
import { LayoutSidebar } from './layout-sidebar';
|
||||
|
||||
|
||||
@@ -4,17 +4,18 @@ import { WidgetHead } from '@/components/overview/overview-widget';
|
||||
import { useOverviewOptions } from '@/components/overview/useOverviewOptions';
|
||||
import { Chart } from '@/components/report/chart';
|
||||
import { Widget, WidgetBody } from '@/components/Widget';
|
||||
import { useEventFilters } from '@/hooks/useEventQueryFilters';
|
||||
import type { IChartInput } from '@/types';
|
||||
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
||||
import { cn } from '@/utils/cn';
|
||||
|
||||
import type { IChartInput } from '@mixan/validation';
|
||||
|
||||
interface OverviewMetricsProps {
|
||||
projectId: string;
|
||||
}
|
||||
|
||||
export default function OverviewMetrics({ projectId }: OverviewMetricsProps) {
|
||||
const { previous, range, interval, metric, setMetric } = useOverviewOptions();
|
||||
const filters = useEventFilters();
|
||||
const [filters] = useEventQueryFilters();
|
||||
|
||||
const reports = [
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getCurrentProjects } from '@/server/services/project.service';
|
||||
import { getProjectsByOrganizationSlug } from '@mixan/db';
|
||||
|
||||
import LayoutProjectSelector from './layout-project-selector';
|
||||
|
||||
@@ -13,7 +13,7 @@ export default async function PageLayout({
|
||||
title,
|
||||
organizationSlug,
|
||||
}: PageLayoutProps) {
|
||||
const projects = await getCurrentProjects(organizationSlug);
|
||||
const projects = await getProjectsByOrganizationSlug(organizationSlug);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -18,7 +18,7 @@ export default function ListProfileEvents({
|
||||
projectId,
|
||||
profileId,
|
||||
}: ListProfileEvents) {
|
||||
const pagination = usePagination();
|
||||
const pagination = usePagination(50);
|
||||
const [eventFilters, setEventFilters] = useQueryState(
|
||||
'events',
|
||||
parseAsJson<string[]>().withDefault([])
|
||||
|
||||
@@ -3,13 +3,11 @@ import { ListProperties } from '@/components/events/ListProperties';
|
||||
import { ProfileAvatar } from '@/components/profiles/ProfileAvatar';
|
||||
import { Widget, WidgetBody, WidgetHead } from '@/components/Widget';
|
||||
import { getExists } from '@/server/pageExists';
|
||||
import {
|
||||
getProfileById,
|
||||
getProfilesByExternalId,
|
||||
} from '@/server/services/profile.service';
|
||||
import { formatDateTime } from '@/utils/date';
|
||||
import { getProfileName } from '@/utils/getters';
|
||||
|
||||
import { getProfileById, getProfilesByExternalId } from '@mixan/db';
|
||||
|
||||
import ListProfileEvents from './list-profile-events';
|
||||
|
||||
interface PageProps {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
'use client';
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import type { RouterOutputs } from '@/app/_trpc/client';
|
||||
import { ListProperties } from '@/components/events/ListProperties';
|
||||
import { ExpandableListItem } from '@/components/general/ExpandableListItem';
|
||||
@@ -16,24 +15,24 @@ export function ProfileListItem(props: ProfileListItemProps) {
|
||||
const { id, properties, createdAt } = props;
|
||||
const params = useAppParams();
|
||||
|
||||
const bullets = useMemo(() => {
|
||||
const bullets: React.ReactNode[] = [
|
||||
<span>{formatDateTime(createdAt)}</span>,
|
||||
<Link
|
||||
href={`/${params.organizationId}/${params.projectId}/profiles/${id}`}
|
||||
className="text-black font-medium hover:underline"
|
||||
>
|
||||
See profile
|
||||
</Link>,
|
||||
];
|
||||
|
||||
return bullets;
|
||||
}, [createdAt, id, params]);
|
||||
const renderContent = () => {
|
||||
return (
|
||||
<>
|
||||
<span>{formatDateTime(createdAt)}</span>
|
||||
<Link
|
||||
href={`/${params.organizationId}/${params.projectId}/profiles/${id}`}
|
||||
className="text-black font-medium hover:underline"
|
||||
>
|
||||
See profile
|
||||
</Link>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<ExpandableListItem
|
||||
title={getProfileName(props)}
|
||||
bullets={bullets}
|
||||
content={renderContent()}
|
||||
image={<ProfileAvatar {...props} />}
|
||||
>
|
||||
<ListProperties data={properties} className="rounded-none border-none" />
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import PageLayout from '@/app/(app)/[organizationId]/[projectId]/page-layout';
|
||||
import { getExists } from '@/server/pageExists';
|
||||
import { getOrganizationBySlug } from '@/server/services/organization.service';
|
||||
import { getReportById } from '@/server/services/reports.service';
|
||||
import { Pencil } from 'lucide-react';
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
import { getOrganizationBySlug, getReportById } from '@mixan/db';
|
||||
|
||||
import ReportEditor from '../report-editor';
|
||||
|
||||
interface PageProps {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import PageLayout from '@/app/(app)/[organizationId]/[projectId]/page-layout';
|
||||
import { getExists } from '@/server/pageExists';
|
||||
import { getOrganizationBySlug } from '@/server/services/organization.service';
|
||||
import { Pencil } from 'lucide-react';
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
import { getOrganizationBySlug } from '@mixan/db';
|
||||
|
||||
import ReportEditor from './report-editor';
|
||||
|
||||
interface PageProps {
|
||||
|
||||
@@ -19,9 +19,10 @@ import { Button } from '@/components/ui/button';
|
||||
import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet';
|
||||
import { useAppParams } from '@/hooks/useAppParams';
|
||||
import { useDispatch, useSelector } from '@/redux';
|
||||
import type { IServiceReport } from '@/server/services/reports.service';
|
||||
import { GanttChartSquareIcon } from 'lucide-react';
|
||||
|
||||
import type { IServiceReport } from '@mixan/db';
|
||||
|
||||
interface ReportEditorProps {
|
||||
report: IServiceReport | null;
|
||||
}
|
||||
|
||||
@@ -6,9 +6,10 @@ import { DataTable } from '@/components/DataTable';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useAppParams } from '@/hooks/useAppParams';
|
||||
import { pushModal } from '@/modals';
|
||||
import type { getClientsByOrganizationId } from '@/server/services/clients.service';
|
||||
import { PlusIcon } from 'lucide-react';
|
||||
|
||||
import type { getClientsByOrganizationId } from '@mixan/db';
|
||||
|
||||
interface ListClientsProps {
|
||||
clients: Awaited<ReturnType<typeof getClientsByOrganizationId>>;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import PageLayout from '@/app/(app)/[organizationId]/[projectId]/page-layout';
|
||||
import { getExists } from '@/server/pageExists';
|
||||
import { getClientsByOrganizationId } from '@/server/services/clients.service';
|
||||
|
||||
import { getClientsByOrganizationId } from '@mixan/db';
|
||||
|
||||
import ListClients from './list-clients';
|
||||
|
||||
|
||||
@@ -4,12 +4,13 @@ import { api, handleError } from '@/app/_trpc/client';
|
||||
import { InputWithLabel } from '@/components/forms/InputWithLabel';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Widget, WidgetBody, WidgetHead } from '@/components/Widget';
|
||||
import type { getOrganizationBySlug } from '@/server/services/organization.service';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { toast } from 'sonner';
|
||||
import { z } from 'zod';
|
||||
|
||||
import type { getOrganizationBySlug } from '@mixan/db';
|
||||
|
||||
const validator = z.object({
|
||||
id: z.string().min(2),
|
||||
name: z.string().min(2),
|
||||
|
||||
@@ -2,7 +2,6 @@ import { api } from '@/app/_trpc/client';
|
||||
import { InputWithLabel } from '@/components/forms/InputWithLabel';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useAppParams } from '@/hooks/useAppParams';
|
||||
import { zInviteUser } from '@/utils/validation';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { SendIcon } from 'lucide-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
@@ -10,6 +9,8 @@ import { useForm } from 'react-hook-form';
|
||||
import { toast } from 'sonner';
|
||||
import type { z } from 'zod';
|
||||
|
||||
import { zInviteUser } from '@mixan/validation';
|
||||
|
||||
type IForm = z.infer<typeof zInviteUser>;
|
||||
|
||||
export function InviteUser() {
|
||||
|
||||
@@ -9,7 +9,8 @@ import {
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import { Widget, WidgetBody, WidgetHead } from '@/components/Widget';
|
||||
import type { IServiceInvites } from '@/server/services/organization.service';
|
||||
|
||||
import type { IServiceInvites } from '@mixan/db';
|
||||
|
||||
import { InviteUser } from './invite-user';
|
||||
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import PageLayout from '@/app/(app)/[organizationId]/[projectId]/page-layout';
|
||||
import {
|
||||
getInvites,
|
||||
getOrganizationBySlug,
|
||||
} from '@/server/services/organization.service';
|
||||
import { clerkClient } from '@clerk/nextjs';
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
import { getInvites, getOrganizationBySlug } from '@mixan/db';
|
||||
|
||||
import EditOrganization from './edit-organization';
|
||||
import InvitedUsers from './invited-users';
|
||||
|
||||
|
||||
@@ -4,13 +4,14 @@ import { api, handleError } from '@/app/_trpc/client';
|
||||
import { InputWithLabel } from '@/components/forms/InputWithLabel';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Widget, WidgetBody, WidgetHead } from '@/components/Widget';
|
||||
import type { getUserById } from '@/server/services/user.service';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { toast } from 'sonner';
|
||||
import { z } from 'zod';
|
||||
|
||||
import type { getUserById } from '@mixan/db';
|
||||
|
||||
const validator = z.object({
|
||||
firstName: z.string().min(2),
|
||||
lastName: z.string().min(2),
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import PageLayout from '@/app/(app)/[organizationId]/[projectId]/page-layout';
|
||||
import { getExists } from '@/server/pageExists';
|
||||
import { getUserById } from '@/server/services/user.service';
|
||||
import { auth } from '@clerk/nextjs';
|
||||
|
||||
import { getUserById } from '@mixan/db';
|
||||
|
||||
import EditProfile from './edit-profile';
|
||||
import { Logout } from './logout';
|
||||
|
||||
|
||||
@@ -6,11 +6,12 @@ import { columns } from '@/components/projects/table';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useAppParams } from '@/hooks/useAppParams';
|
||||
import { pushModal } from '@/modals';
|
||||
import type { getProjectsByOrganizationId } from '@/server/services/project.service';
|
||||
import { PlusIcon } from 'lucide-react';
|
||||
|
||||
import type { getProjectsByOrganizationSlug } from '@mixan/db';
|
||||
|
||||
interface ListProjectsProps {
|
||||
projects: Awaited<ReturnType<typeof getProjectsByOrganizationId>>;
|
||||
projects: Awaited<ReturnType<typeof getProjectsByOrganizationSlug>>;
|
||||
}
|
||||
export default function ListProjects({ projects }: ListProjectsProps) {
|
||||
const organizationId = useAppParams().organizationId;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import PageLayout from '@/app/(app)/[organizationId]/[projectId]/page-layout';
|
||||
import { getExists } from '@/server/pageExists';
|
||||
import { getProjectsByOrganizationSlug } from '@/server/services/project.service';
|
||||
|
||||
import { getProjectsByOrganizationSlug } from '@mixan/db';
|
||||
|
||||
import ListProjects from './list-projects';
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { getOrganizationBySlug } from '@/server/services/organization.service';
|
||||
import { getProjectWithMostEvents } from '@/server/services/project.service';
|
||||
import { getOrganizationBySlug } from '@mixan/db';
|
||||
import { getProjectWithMostEvents } from '@mixan/db';
|
||||
import { notFound, redirect } from 'next/navigation';
|
||||
|
||||
import PageLayout from './[projectId]/page-layout';
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { getCurrentOrganizations } from '@/server/services/organization.service';
|
||||
import { CreateOrganization } from '@clerk/nextjs';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
import { getCurrentOrganizations } from '@mixan/db';
|
||||
|
||||
export default async function Page() {
|
||||
const organizations = await getCurrentOrganizations();
|
||||
|
||||
|
||||
@@ -11,10 +11,9 @@ 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 { getOrganizationBySlug } from '@/server/services/organization.service';
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
import { getShareOverviewById } from '@mixan/db';
|
||||
import { getOrganizationBySlug, getShareOverviewById } from '@mixan/db';
|
||||
|
||||
interface PageProps {
|
||||
params: {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { appRouter } from '@/server/api/root';
|
||||
import { getSession } from '@/server/auth';
|
||||
import { getAuth } from '@clerk/nextjs/server';
|
||||
import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
|
||||
|
||||
@@ -9,9 +8,7 @@ const handler = (req: Request) =>
|
||||
req,
|
||||
router: appRouter,
|
||||
async createContext({ req }) {
|
||||
console.log('------- createContext --------');
|
||||
const session = getAuth(req as any);
|
||||
console.log('session', JSON.stringify(session, null, 2));
|
||||
return {
|
||||
session,
|
||||
};
|
||||
|
||||
20
apps/web/src/app/manifest.ts
Normal file
20
apps/web/src/app/manifest.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import type { MetadataRoute } from 'next';
|
||||
|
||||
export default function manifest(): MetadataRoute.Manifest {
|
||||
return {
|
||||
name: 'Openpanel.dev',
|
||||
short_name: 'Openpanel.dev',
|
||||
description: '',
|
||||
start_url: '/',
|
||||
display: 'standalone',
|
||||
background_color: '#fff',
|
||||
theme_color: '#fff',
|
||||
icons: [
|
||||
{
|
||||
src: 'https://openpanel.dev/favicon.ico',
|
||||
sizes: 'any',
|
||||
type: 'image/x-icon',
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -2,12 +2,13 @@
|
||||
|
||||
import { api } from '@/app/_trpc/client';
|
||||
import { pushModal, showConfirm } from '@/modals';
|
||||
import type { IClientWithProject } from '@/types';
|
||||
import { clipboard } from '@/utils/clipboard';
|
||||
import { MoreHorizontal } from 'lucide-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import type { IServiceClientWithProject } from '@mixan/db';
|
||||
|
||||
import { Button } from '../ui/button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -18,7 +19,7 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from '../ui/dropdown-menu';
|
||||
|
||||
export function ClientActions(client: IClientWithProject) {
|
||||
export function ClientActions(client: IServiceClientWithProject) {
|
||||
const { id } = client;
|
||||
const router = useRouter();
|
||||
const deletion = api.client.remove.useMutation({
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import type { IClientWithProject } from '@/types';
|
||||
import { formatDate } from '@/utils/date';
|
||||
import type { ColumnDef } from '@tanstack/react-table';
|
||||
|
||||
import type { IServiceClientWithProject } from '@mixan/db';
|
||||
|
||||
import { ClientActions } from './ClientActions';
|
||||
|
||||
export const columns: ColumnDef<IClientWithProject>[] = [
|
||||
export const columns: ColumnDef<IServiceClientWithProject>[] = [
|
||||
{
|
||||
accessorKey: 'name',
|
||||
header: 'Name',
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Button } from '../ui/button';
|
||||
interface ExpandableListItemProps {
|
||||
children: React.ReactNode;
|
||||
content: React.ReactNode;
|
||||
title: string;
|
||||
title: React.ReactNode;
|
||||
image?: React.ReactNode;
|
||||
initialOpen?: boolean;
|
||||
className?: string;
|
||||
@@ -29,7 +29,7 @@ export function ExpandableListItem({
|
||||
<div className="p-2 sm:p-4 flex gap-4">
|
||||
<div className="flex gap-1">{image}</div>
|
||||
<div className="flex flex-col flex-1 gap-1 min-w-0">
|
||||
<span className="text-md font-medium leading-none mb-1">{title}</span>
|
||||
<div className="text-md font-medium leading-none mb-1">{title}</div>
|
||||
{!!content && (
|
||||
<div className="flex flex-col sm:flex-row sm:items-center gap-1 sm:gap-4">
|
||||
{content}
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
import { api } from '@/app/_trpc/client';
|
||||
import { useOrganizationParams } from '@/hooks/useOrganizationParams';
|
||||
import { ChevronRight, HomeIcon } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
import { Container } from '../Container';
|
||||
|
||||
export function Breadcrumbs() {
|
||||
const params = useOrganizationParams();
|
||||
|
||||
const org = api.organization.get.useQuery(
|
||||
{
|
||||
id: params.organizationId,
|
||||
},
|
||||
{
|
||||
enabled: !!params.organizationId,
|
||||
staleTime: Infinity,
|
||||
}
|
||||
);
|
||||
|
||||
const pro = api.project.get.useQuery(
|
||||
{
|
||||
id: params.projectId,
|
||||
},
|
||||
{
|
||||
enabled: !!params.projectId,
|
||||
staleTime: Infinity,
|
||||
}
|
||||
);
|
||||
|
||||
const dashboard = api.dashboard.get.useQuery(
|
||||
{
|
||||
id: params.dashboardId,
|
||||
},
|
||||
{
|
||||
enabled: !!params.dashboardId,
|
||||
staleTime: Infinity,
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="border-b border-border text-xs">
|
||||
<Container className="flex items-center gap-2 h-8">
|
||||
{org.isLoading && pro.isLoading && (
|
||||
<div className="animate-pulse bg-slate-200 h-4 w-24 rounded"></div>
|
||||
)}
|
||||
{org.data && (
|
||||
<>
|
||||
<HomeIcon size={14} />
|
||||
<Link shallow href={`/${org.data.id}`}>
|
||||
{org.data.name}
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
|
||||
{org.data && pro.data && (
|
||||
<>
|
||||
<ChevronRight size={10} />
|
||||
<Link shallow href={`/${org.data.id}/${pro.data.id}`}>
|
||||
{pro.data.name}
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
|
||||
{org.data && pro.data && dashboard.data && (
|
||||
<>
|
||||
<ChevronRight size={10} />
|
||||
<Link
|
||||
shallow
|
||||
href={`/${org.data.id}/${pro.data.id}/${dashboard.data.id}`}
|
||||
>
|
||||
{dashboard.data.name}
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</Container>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { useOrganizationParams } from '@/hooks/useOrganizationParams';
|
||||
import { LineChart } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
|
||||
export function NavbarCreate() {
|
||||
const params = useOrganizationParams();
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button>Create</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-56" align="end">
|
||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link
|
||||
shallow
|
||||
href={`/${params.organizationId}/${params.projectId}/reports`}
|
||||
>
|
||||
<LineChart className="mr-2 h-4 w-4" />
|
||||
<span>Create a report</span>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
import { useOrganizationParams } from '@/hooks/useOrganizationParams';
|
||||
import { cn } from '@/utils/cn';
|
||||
import type { LinkProps } from 'next/link';
|
||||
import Link from 'next/link';
|
||||
|
||||
import { strip } from '@mixan/common';
|
||||
|
||||
import { NavbarUserDropdown } from './NavbarUserDropdown';
|
||||
|
||||
function Item({
|
||||
children,
|
||||
...props
|
||||
}: LinkProps & { children: React.ReactNode }) {
|
||||
return (
|
||||
<Link
|
||||
{...props}
|
||||
className="h-9 items-center flex px-3 leading-none relative [&>div]:hover:opacity-100 [&>div]:hover:ring-1"
|
||||
shallow
|
||||
>
|
||||
<div className="opacity-0 absolute inset-0 transition-all bg-gradient-to-r from-blue-50 to-purple-50 rounded ring-0 ring-purple-900" />
|
||||
<span className="relative">{children}</span>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
export function NavbarMenu() {
|
||||
const params = useOrganizationParams();
|
||||
return (
|
||||
<div className={cn('flex gap-1 items-center text-sm', 'max-sm:flex-col')}>
|
||||
{params.projectId && (
|
||||
<Item href={`/${params.organizationId}/${params.projectId}`}>
|
||||
Dashboards
|
||||
</Item>
|
||||
)}
|
||||
{params.projectId && (
|
||||
<Item href={`/${params.organizationId}/${params.projectId}/events`}>
|
||||
Events
|
||||
</Item>
|
||||
)}
|
||||
{params.projectId && (
|
||||
<Item href={`/${params.organizationId}/${params.projectId}/profiles`}>
|
||||
Profiles
|
||||
</Item>
|
||||
)}
|
||||
{params.projectId && (
|
||||
<Item
|
||||
href={{
|
||||
pathname: `/${params.organizationId}/${params.projectId}/reports`,
|
||||
query: strip({
|
||||
dashboardId: params.dashboardId,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
Create report
|
||||
</Item>
|
||||
)}
|
||||
<NavbarUserDropdown />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { useOrganizationParams } from '@/hooks/useOrganizationParams';
|
||||
import { User } from 'lucide-react';
|
||||
import { signOut, useSession } from 'next-auth/react';
|
||||
import Link from 'next/link';
|
||||
|
||||
export function NavbarUserDropdown() {
|
||||
const params = useOrganizationParams();
|
||||
const session = useSession();
|
||||
const user = session.data?.user;
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<Avatar>
|
||||
<AvatarFallback>{user?.name?.charAt(0) ?? '🤠'}</AvatarFallback>
|
||||
</Avatar>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-[200px]">
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem asChild className="cursor-pointer">
|
||||
<Link
|
||||
href={`/${params.organizationId}/settings/organization`}
|
||||
shallow
|
||||
>
|
||||
<User className="mr-2 h-4 w-4" />
|
||||
Organization
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild className="cursor-pointer">
|
||||
<Link href={`/${params.organizationId}/settings/projects`} shallow>
|
||||
<User className="mr-2 h-4 w-4" />
|
||||
Projects
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild className="cursor-pointer">
|
||||
<Link href={`/${params.organizationId}/settings/clients`} shallow>
|
||||
<User className="mr-2 h-4 w-4" />
|
||||
Clients
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild className="cursor-pointer">
|
||||
<Link href={`/${params.organizationId}/settings/profile`} shallow>
|
||||
<User className="mr-2 h-4 w-4" />
|
||||
Profile
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
className="text-red-600 cursor-pointer"
|
||||
onClick={() => {
|
||||
signOut().catch(console.error);
|
||||
}}
|
||||
>
|
||||
<User className="mr-2 h-4 w-4" />
|
||||
Logout
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
@@ -1,10 +1,13 @@
|
||||
'use client';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
||||
import {
|
||||
useEventQueryFilters,
|
||||
useEventQueryNamesFilter,
|
||||
} from '@/hooks/useEventQueryFilters';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { X } from 'lucide-react';
|
||||
import { Options as NuqsOptions } from 'nuqs';
|
||||
import type { Options as NuqsOptions } from 'nuqs';
|
||||
|
||||
interface OverviewFiltersButtonsProps {
|
||||
className?: string;
|
||||
@@ -15,25 +18,40 @@ export function OverviewFiltersButtons({
|
||||
className,
|
||||
nuqsOptions,
|
||||
}: OverviewFiltersButtonsProps) {
|
||||
const eventQueryFilters = useEventQueryFilters(nuqsOptions);
|
||||
const filters = Object.entries(eventQueryFilters).filter(
|
||||
([, filter]) => filter.get !== null
|
||||
);
|
||||
if (filters.length === 0) return null;
|
||||
const [events, setEvents] = useEventQueryNamesFilter(nuqsOptions);
|
||||
const [filters, setFilter] = useEventQueryFilters(nuqsOptions);
|
||||
if (filters.length === 0 && events.length === 0) return null;
|
||||
return (
|
||||
<div className={cn('flex flex-wrap gap-2 px-4 pb-4', className)}>
|
||||
{filters.map(([key, filter]) => (
|
||||
{events.map((event) => (
|
||||
<Button
|
||||
key={key}
|
||||
key={event}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
icon={X}
|
||||
onClick={() => filter.set(null)}
|
||||
onClick={() => setEvents((p) => p.filter((e) => e !== event))}
|
||||
>
|
||||
<span className="mr-1">{key} is</span>
|
||||
<strong>{filter.get}</strong>
|
||||
<strong>{event}</strong>
|
||||
</Button>
|
||||
))}
|
||||
{filters.map((filter) => {
|
||||
if (!filter.value[0]) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
key={filter.name}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
icon={X}
|
||||
onClick={() => setFilter(filter.name, filter.value[0], 'is')}
|
||||
>
|
||||
<span className="mr-1">{filter.name} is</span>
|
||||
<strong>{filter.value[0]}</strong>
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,93 +1,131 @@
|
||||
'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 { ComboboxAdvanced } from '@/components/ui/combobox-advanced';
|
||||
import { SheetHeader, SheetTitle } from '@/components/ui/sheet';
|
||||
import { useEventNames } from '@/hooks/useEventNames';
|
||||
import { useEventProperties } from '@/hooks/useEventProperties';
|
||||
import {
|
||||
useEventQueryFilters,
|
||||
useEventQueryNamesFilter,
|
||||
} from '@/hooks/useEventQueryFilters';
|
||||
import { useEventValues } from '@/hooks/useEventValues';
|
||||
import { XIcon } from 'lucide-react';
|
||||
import { Options as NuqsOptions } from 'nuqs';
|
||||
import type { Options as NuqsOptions } from 'nuqs';
|
||||
|
||||
import type {
|
||||
IChartEventFilter,
|
||||
IChartEventFilterOperator,
|
||||
IChartEventFilterValue,
|
||||
} from '@mixan/validation';
|
||||
|
||||
interface OverviewFiltersProps {
|
||||
projectId: string;
|
||||
nuqsOptions?: NuqsOptions;
|
||||
enableEventsFilter?: boolean;
|
||||
}
|
||||
|
||||
export function OverviewFiltersDrawerContent({
|
||||
projectId,
|
||||
nuqsOptions,
|
||||
enableEventsFilter,
|
||||
}: OverviewFiltersProps) {
|
||||
const eventQueryFilters = useEventQueryFilters(nuqsOptions);
|
||||
const [filters, setFilter] = useEventQueryFilters(nuqsOptions);
|
||||
const [event, setEvent] = useEventQueryNamesFilter(nuqsOptions);
|
||||
const eventNames = useEventNames(projectId);
|
||||
const eventProperties = useEventProperties(projectId);
|
||||
|
||||
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,
|
||||
<SheetHeader className="mb-8">
|
||||
<SheetTitle>Overview filters</SheetTitle>
|
||||
</SheetHeader>
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
{enableEventsFilter && (
|
||||
<ComboboxAdvanced
|
||||
className="w-full"
|
||||
value={event}
|
||||
onChange={setEvent}
|
||||
// First items is * which is only used for report editing
|
||||
items={eventNames.slice(1).map((item) => ({
|
||||
label: item.name,
|
||||
value: item.name,
|
||||
}))}
|
||||
placeholder="Select event"
|
||||
/>
|
||||
)}
|
||||
<Combobox
|
||||
className="w-full"
|
||||
onChange={(value) => {
|
||||
setFilter(value, '');
|
||||
}}
|
||||
value=""
|
||||
placeholder="Filter by property"
|
||||
label="What do you want to filter by?"
|
||||
items={eventProperties.map((item) => ({
|
||||
label: item,
|
||||
value: item,
|
||||
}))}
|
||||
searchable
|
||||
/>
|
||||
searchable
|
||||
/>
|
||||
</div>
|
||||
|
||||
<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}
|
||||
/>
|
||||
))}
|
||||
{filters
|
||||
.filter((filter) => filter.value[0] !== null)
|
||||
.map((filter) => {
|
||||
return (
|
||||
<FilterOption
|
||||
key={filter.name}
|
||||
projectId={projectId}
|
||||
setFilter={setFilter}
|
||||
{...filter}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function FilterOption({
|
||||
name,
|
||||
get,
|
||||
set,
|
||||
setFilter,
|
||||
projectId,
|
||||
}: {
|
||||
name: string;
|
||||
get: string | null;
|
||||
set: (value: string | null) => void;
|
||||
...filter
|
||||
}: IChartEventFilter & {
|
||||
projectId: string;
|
||||
setFilter: (
|
||||
name: string,
|
||||
value: IChartEventFilterValue,
|
||||
operator: IChartEventFilterOperator
|
||||
) => void;
|
||||
}) {
|
||||
const { data } = api.chart.values.useQuery({
|
||||
const values = useEventValues(
|
||||
projectId,
|
||||
event: name === 'path' ? 'screen_view' : 'session_start',
|
||||
property: name,
|
||||
});
|
||||
filter.name === 'path' ? 'screen_view' : 'session_start',
|
||||
filter.name
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex gap-2 items-center">
|
||||
<div>{name}</div>
|
||||
<div>{filter.name}</div>
|
||||
<Combobox
|
||||
className="flex-1"
|
||||
onChange={(value) => set(value)}
|
||||
onChange={(value) => setFilter(filter.name, value, filter.operator)}
|
||||
placeholder={'Select a value'}
|
||||
items={
|
||||
data?.values.filter(Boolean).map((value) => ({
|
||||
value,
|
||||
label: value,
|
||||
})) ?? []
|
||||
}
|
||||
value={get}
|
||||
items={values.map((value) => ({
|
||||
value,
|
||||
label: value,
|
||||
}))}
|
||||
value={String(filter.value[0] ?? '')}
|
||||
/>
|
||||
<Button size="icon" variant="ghost" onClick={() => set(null)}>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
onClick={() =>
|
||||
setFilter(filter.name, filter.value[0] ?? '', filter.operator)
|
||||
}
|
||||
>
|
||||
<XIcon />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -3,18 +3,20 @@
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet';
|
||||
import { FilterIcon } from 'lucide-react';
|
||||
import { Options as NuqsOptions } from 'nuqs';
|
||||
import type { Options as NuqsOptions } from 'nuqs';
|
||||
|
||||
import { OverviewFiltersDrawerContent } from './overview-filters-drawer-content';
|
||||
|
||||
interface OverviewFiltersDrawerProps {
|
||||
projectId: string;
|
||||
nuqsOptions?: NuqsOptions;
|
||||
enableEventsFilter?: boolean;
|
||||
}
|
||||
|
||||
export function OverviewFiltersDrawer({
|
||||
projectId,
|
||||
nuqsOptions,
|
||||
enableEventsFilter,
|
||||
}: OverviewFiltersDrawerProps) {
|
||||
return (
|
||||
<Sheet>
|
||||
@@ -27,6 +29,7 @@ export function OverviewFiltersDrawer({
|
||||
<OverviewFiltersDrawerContent
|
||||
projectId={projectId}
|
||||
nuqsOptions={nuqsOptions}
|
||||
enableEventsFilter={enableEventsFilter}
|
||||
/>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import type { IChartInput } from '@/types';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { ChevronsUpDownIcon } from 'lucide-react';
|
||||
import AnimateHeight from 'react-animate-height';
|
||||
|
||||
import type { IChartInput } from '@mixan/validation';
|
||||
|
||||
import { Chart } from '../report/chart';
|
||||
import { Widget, WidgetBody, WidgetHead } from '../Widget';
|
||||
import { useOverviewOptions } from './useOverviewOptions';
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { Chart } from '@/components/report/chart';
|
||||
import {
|
||||
useEventFilters,
|
||||
useEventQueryFilters,
|
||||
} from '@/hooks/useEventQueryFilters';
|
||||
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
||||
import { cn } from '@/utils/cn';
|
||||
|
||||
import { Widget, WidgetBody } from '../Widget';
|
||||
@@ -19,9 +16,7 @@ export default function OverviewTopDevices({
|
||||
projectId,
|
||||
}: OverviewTopDevicesProps) {
|
||||
const { interval, range, previous } = useOverviewOptions();
|
||||
const filters = useEventFilters();
|
||||
const { device, browser, browserVersion, os, osVersion } =
|
||||
useEventQueryFilters();
|
||||
const [filters, setFilter] = useEventQueryFilters();
|
||||
const [widget, setWidget, widgets] = useOverviewWidget('tech', {
|
||||
devices: {
|
||||
title: 'Top devices',
|
||||
@@ -190,21 +185,21 @@ export default function OverviewTopDevices({
|
||||
onClick={(item) => {
|
||||
switch (widget.key) {
|
||||
case 'devices':
|
||||
device.set(item.name);
|
||||
setFilter('device', item.name);
|
||||
break;
|
||||
case 'browser':
|
||||
setWidget('browser_version');
|
||||
browser.set(item.name);
|
||||
setFilter('browser', item.name);
|
||||
break;
|
||||
case 'browser_version':
|
||||
browserVersion.set(item.name);
|
||||
setFilter('browser_version', item.name);
|
||||
break;
|
||||
case 'os':
|
||||
setWidget('os_version');
|
||||
os.set(item.name);
|
||||
setFilter('os', item.name);
|
||||
break;
|
||||
case 'os_version':
|
||||
osVersion.set(item.name);
|
||||
setFilter('os_version', item.name);
|
||||
break;
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { Chart } from '@/components/report/chart';
|
||||
import { useEventFilters } from '@/hooks/useEventQueryFilters';
|
||||
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
||||
import { cn } from '@/utils/cn';
|
||||
|
||||
import { Widget, WidgetBody } from '../Widget';
|
||||
@@ -16,7 +16,7 @@ export default function OverviewTopEvents({
|
||||
projectId,
|
||||
}: OverviewTopEventsProps) {
|
||||
const { interval, range, previous } = useOverviewOptions();
|
||||
const filters = useEventFilters();
|
||||
const [filters] = useEventQueryFilters();
|
||||
const [widget, setWidget, widgets] = useOverviewWidget('ev', {
|
||||
all: {
|
||||
title: 'Top events',
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { Chart } from '@/components/report/chart';
|
||||
import {
|
||||
useEventFilters,
|
||||
useEventQueryFilters,
|
||||
} from '@/hooks/useEventQueryFilters';
|
||||
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
||||
import { cn } from '@/utils/cn';
|
||||
|
||||
import { Widget, WidgetBody } from '../Widget';
|
||||
@@ -17,8 +14,7 @@ interface OverviewTopGeoProps {
|
||||
}
|
||||
export default function OverviewTopGeo({ projectId }: OverviewTopGeoProps) {
|
||||
const { interval, range, previous } = useOverviewOptions();
|
||||
const filters = useEventFilters();
|
||||
const { region, country, city } = useEventQueryFilters();
|
||||
const [filters, setFilter] = useEventQueryFilters();
|
||||
const [widget, setWidget, widgets] = useOverviewWidget('geo', {
|
||||
map: {
|
||||
title: 'Map',
|
||||
@@ -160,14 +156,14 @@ export default function OverviewTopGeo({ projectId }: OverviewTopGeoProps) {
|
||||
switch (widget.key) {
|
||||
case 'countries':
|
||||
setWidget('regions');
|
||||
country.set(item.name);
|
||||
setFilter('country', item.name);
|
||||
break;
|
||||
case 'regions':
|
||||
setWidget('cities');
|
||||
region.set(item.name);
|
||||
setFilter('region', item.name);
|
||||
break;
|
||||
case 'cities':
|
||||
city.set(item.name);
|
||||
setFilter('city', item.name);
|
||||
break;
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { Chart } from '@/components/report/chart';
|
||||
import {
|
||||
useEventFilters,
|
||||
useEventQueryFilters,
|
||||
} from '@/hooks/useEventQueryFilters';
|
||||
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
||||
import { cn } from '@/utils/cn';
|
||||
|
||||
import { Widget, WidgetBody } from '../Widget';
|
||||
@@ -17,8 +14,7 @@ interface OverviewTopPagesProps {
|
||||
}
|
||||
export default function OverviewTopPages({ projectId }: OverviewTopPagesProps) {
|
||||
const { interval, range, previous } = useOverviewOptions();
|
||||
const filters = useEventFilters();
|
||||
const { path } = useEventQueryFilters();
|
||||
const [filters, setFilter] = useEventQueryFilters();
|
||||
const [widget, setWidget, widgets] = useOverviewWidget('pages', {
|
||||
top: {
|
||||
title: 'Top pages',
|
||||
@@ -129,7 +125,7 @@ export default function OverviewTopPages({ projectId }: OverviewTopPagesProps) {
|
||||
{...widget.chart}
|
||||
previous={false}
|
||||
onClick={(item) => {
|
||||
path.set(item.name);
|
||||
setFilter('path', item.name);
|
||||
}}
|
||||
/>
|
||||
</WidgetBody>
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { Chart } from '@/components/report/chart';
|
||||
import {
|
||||
useEventFilters,
|
||||
useEventQueryFilters,
|
||||
} from '@/hooks/useEventQueryFilters';
|
||||
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
||||
import { cn } from '@/utils/cn';
|
||||
|
||||
import { Widget, WidgetBody } from '../Widget';
|
||||
@@ -19,17 +16,7 @@ export default function OverviewTopSources({
|
||||
projectId,
|
||||
}: OverviewTopSourcesProps) {
|
||||
const { interval, range, previous } = useOverviewOptions();
|
||||
const {
|
||||
referrer,
|
||||
referrerName,
|
||||
referrerType,
|
||||
utmCampaign,
|
||||
utmContent,
|
||||
utmMedium,
|
||||
utmSource,
|
||||
utmTerm,
|
||||
} = useEventQueryFilters();
|
||||
const filters = useEventFilters();
|
||||
const [filters, setFilter] = useEventQueryFilters();
|
||||
const [widget, setWidget, widgets] = useOverviewWidget('sources', {
|
||||
all: {
|
||||
title: 'Top sources',
|
||||
@@ -282,30 +269,30 @@ export default function OverviewTopSources({
|
||||
onClick={(item) => {
|
||||
switch (widget.key) {
|
||||
case 'all':
|
||||
referrerName.set(item.name);
|
||||
setFilter('referrer_name', item.name);
|
||||
setWidget('domain');
|
||||
break;
|
||||
case 'domain':
|
||||
referrer.set(item.name);
|
||||
setFilter('referrer', item.name);
|
||||
break;
|
||||
case 'type':
|
||||
referrerType.set(item.name);
|
||||
setFilter('referrer_type', item.name);
|
||||
setWidget('domain');
|
||||
break;
|
||||
case 'utm_source':
|
||||
utmSource.set(item.name);
|
||||
setFilter('utm_source', item.name);
|
||||
break;
|
||||
case 'utm_medium':
|
||||
utmMedium.set(item.name);
|
||||
setFilter('utm_medium', item.name);
|
||||
break;
|
||||
case 'utm_campaign':
|
||||
utmCampaign.set(item.name);
|
||||
setFilter('utm_campaign', item.name);
|
||||
break;
|
||||
case 'utm_term':
|
||||
utmTerm.set(item.name);
|
||||
setFilter('utm_term', item.name);
|
||||
break;
|
||||
case 'utm_content':
|
||||
utmContent.set(item.name);
|
||||
setFilter('utm_content', item.name);
|
||||
break;
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
||||
import { getDefaultIntervalByRange, timeRanges } from '@/utils/constants';
|
||||
import { mapKeys } from '@/utils/validation';
|
||||
import {
|
||||
parseAsBoolean,
|
||||
parseAsInteger,
|
||||
@@ -8,6 +5,9 @@ import {
|
||||
useQueryState,
|
||||
} from 'nuqs';
|
||||
|
||||
import { getDefaultIntervalByRange, timeRanges } from '@mixan/constants';
|
||||
import { mapKeys } from '@mixan/validation';
|
||||
|
||||
const nuqsOptions = { history: 'push' } as const;
|
||||
|
||||
export function useOverviewOptions() {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { IChartInput } from '@/types';
|
||||
import { mapKeys } from '@/utils/validation';
|
||||
import { parseAsStringEnum, useQueryState } from 'nuqs';
|
||||
|
||||
import { mapKeys } from '@mixan/validation';
|
||||
import type { IChartInput } from '@mixan/validation';
|
||||
|
||||
export function useOverviewWidget<T extends string>(
|
||||
key: string,
|
||||
widgets: Record<T, { title: string; btn: string; chart: IChartInput }>
|
||||
@@ -15,7 +16,7 @@ export function useOverviewWidget<T extends string>(
|
||||
);
|
||||
return [
|
||||
{
|
||||
...widgets[widget]!,
|
||||
...widgets[widget],
|
||||
key: widget,
|
||||
},
|
||||
setWidget,
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
'use client';
|
||||
|
||||
import type { IServiceProfile } from '@/server/services/profile.service';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { AvatarImage } from '@radix-ui/react-avatar';
|
||||
import type { VariantProps } from 'class-variance-authority';
|
||||
import { cva } from 'class-variance-authority';
|
||||
|
||||
import type { IServiceProfile } from '@mixan/db';
|
||||
|
||||
import { Avatar, AvatarFallback } from '../ui/avatar';
|
||||
|
||||
interface ProfileAvatarProps
|
||||
@@ -41,8 +42,8 @@ export function ProfileAvatar({
|
||||
size === 'sm'
|
||||
? 'text-xs'
|
||||
: size === 'xs'
|
||||
? 'text-[8px]'
|
||||
: 'text-base',
|
||||
? 'text-[8px]'
|
||||
: 'text-base',
|
||||
'bg-slate-200 text-slate-800'
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -2,12 +2,13 @@
|
||||
|
||||
import { api } from '@/app/_trpc/client';
|
||||
import { pushModal, showConfirm } from '@/modals';
|
||||
import type { IProject } from '@/types';
|
||||
import { clipboard } from '@/utils/clipboard';
|
||||
import { MoreHorizontal } from 'lucide-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import type { IServiceProject } from '@mixan/db';
|
||||
|
||||
import { Button } from '../ui/button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -18,7 +19,7 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from '../ui/dropdown-menu';
|
||||
|
||||
export function ProjectActions(project: IProject) {
|
||||
export function ProjectActions(project: Exclude<IServiceProject, null>) {
|
||||
const { id } = project;
|
||||
const router = useRouter();
|
||||
const deletion = api.project.remove.useMutation({
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { IServiceProject } from '@/server/services/project.service';
|
||||
import { formatDate } from '@/utils/date';
|
||||
import type { ColumnDef } from '@tanstack/react-table';
|
||||
|
||||
import { IServiceProject } from '@mixan/db';
|
||||
import type { Project as IProject } from '@mixan/db';
|
||||
|
||||
import { ProjectActions } from './ProjectActions';
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { useDispatch, useSelector } from '@/redux';
|
||||
import { chartTypes } from '@/utils/constants';
|
||||
import { objectToZodEnums } from '@/utils/validation';
|
||||
import { LineChartIcon } from 'lucide-react';
|
||||
|
||||
import { chartTypes } from '@mixan/constants';
|
||||
import { objectToZodEnums } from '@mixan/validation';
|
||||
|
||||
import { Combobox } from '../ui/combobox';
|
||||
import { changeChartType } from './reportSlice';
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { useDispatch, useSelector } from '@/redux';
|
||||
import type { IInterval } from '@/types';
|
||||
import { ClockIcon } from 'lucide-react';
|
||||
|
||||
import {
|
||||
isHourIntervalEnabledByRange,
|
||||
isMinuteIntervalEnabledByRange,
|
||||
} from '@/utils/constants';
|
||||
import { ClockIcon } from 'lucide-react';
|
||||
} from '@mixan/constants';
|
||||
import type { IInterval } from '@mixan/validation';
|
||||
|
||||
import { Combobox } from '../ui/combobox';
|
||||
import { changeInterval } from './reportSlice';
|
||||
@@ -32,7 +33,7 @@ export function ReportInterval({ className }: ReportIntervalProps) {
|
||||
className={className}
|
||||
placeholder="Interval"
|
||||
onChange={(value) => {
|
||||
dispatch(changeInterval(value as IInterval));
|
||||
dispatch(changeInterval(value));
|
||||
}}
|
||||
value={interval}
|
||||
items={[
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { useDispatch, useSelector } from '@/redux';
|
||||
import { lineTypes } from '@/utils/constants';
|
||||
import { objectToZodEnums } from '@/utils/validation';
|
||||
import { Tv2Icon } from 'lucide-react';
|
||||
|
||||
import { lineTypes } from '@mixan/constants';
|
||||
import { objectToZodEnums } from '@mixan/validation';
|
||||
|
||||
import { Combobox } from '../ui/combobox';
|
||||
import { changeLineType } from './reportSlice';
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { IChartRange } from '@/types';
|
||||
import { timeRanges } from '@/utils/constants';
|
||||
import { CalendarIcon } from 'lucide-react';
|
||||
|
||||
import { timeRanges } from '@mixan/constants';
|
||||
import type { IChartRange } from '@mixan/validation';
|
||||
|
||||
import type { ExtendedComboboxProps } from '../ui/combobox';
|
||||
import { Combobox } from '../ui/combobox';
|
||||
|
||||
|
||||
@@ -10,7 +10,8 @@ import {
|
||||
useState,
|
||||
} from 'react';
|
||||
import type { IChartSerie } from '@/server/api/routers/chart';
|
||||
import type { IChartInput } from '@/types';
|
||||
|
||||
import type { IChartInput } from '@mixan/validation';
|
||||
|
||||
import { ChartLoading } from './ChartLoading';
|
||||
import { MetricCardLoading } from './MetricCard';
|
||||
|
||||
@@ -3,11 +3,12 @@
|
||||
import type { IChartData } from '@/app/_trpc/client';
|
||||
import { ColorSquare } from '@/components/ColorSquare';
|
||||
import { useNumber } from '@/hooks/useNumerFormatter';
|
||||
import type { IChartMetric } from '@/types';
|
||||
import { theme } from '@/utils/theme';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
import { Area, AreaChart } from 'recharts';
|
||||
|
||||
import type { IChartMetric } from '@mixan/validation';
|
||||
|
||||
import { PreviousDiffIndicator } from '../PreviousDiffIndicator';
|
||||
|
||||
interface MetricCardProps {
|
||||
|
||||
@@ -4,7 +4,6 @@ import { AutoSizer } from '@/components/AutoSizer';
|
||||
import { useFormatDateInterval } from '@/hooks/useFormatDateInterval';
|
||||
import { useRechartDataModel } from '@/hooks/useRechartDataModel';
|
||||
import { useVisibleSeries } from '@/hooks/useVisibleSeries';
|
||||
import type { IChartLineType, IInterval } from '@/types';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { getChartColor } from '@/utils/theme';
|
||||
import {
|
||||
@@ -16,6 +15,8 @@ import {
|
||||
YAxis,
|
||||
} from 'recharts';
|
||||
|
||||
import type { IChartLineType, IInterval } from '@mixan/validation';
|
||||
|
||||
import { getYAxisWidth } from './chart-utils';
|
||||
import { useChartContext } from './ChartProvider';
|
||||
import { ReportChartTooltip } from './ReportChartTooltip';
|
||||
|
||||
@@ -5,9 +5,10 @@ import type { IChartData } from '@/app/_trpc/client';
|
||||
import { Progress } from '@/components/ui/progress';
|
||||
import { useNumber } from '@/hooks/useNumerFormatter';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { NOT_SET_VALUE } from '@/utils/constants';
|
||||
import { getChartColor } from '@/utils/theme';
|
||||
|
||||
import { NOT_SET_VALUE } from '@mixan/constants';
|
||||
|
||||
import { PreviousDiffIndicator } from '../PreviousDiffIndicator';
|
||||
import { useChartContext } from './ChartProvider';
|
||||
import { SerieIcon } from './SerieIcon';
|
||||
|
||||
@@ -3,7 +3,6 @@ import { useFormatDateInterval } from '@/hooks/useFormatDateInterval';
|
||||
import { useMappings } from '@/hooks/useMappings';
|
||||
import { useNumber } from '@/hooks/useNumerFormatter';
|
||||
import type { IRechartPayloadItem } from '@/hooks/useRechartDataModel';
|
||||
import { useSelector } from '@/redux';
|
||||
import type { IToolTipProps } from '@/types';
|
||||
|
||||
import { PreviousDiffIndicator } from '../PreviousDiffIndicator';
|
||||
|
||||
@@ -4,11 +4,12 @@ import { AutoSizer } from '@/components/AutoSizer';
|
||||
import { useFormatDateInterval } from '@/hooks/useFormatDateInterval';
|
||||
import { useRechartDataModel } from '@/hooks/useRechartDataModel';
|
||||
import { useVisibleSeries } from '@/hooks/useVisibleSeries';
|
||||
import type { IInterval } from '@/types';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { getChartColor, theme } from '@/utils/theme';
|
||||
import { Bar, BarChart, CartesianGrid, Tooltip, XAxis, YAxis } from 'recharts';
|
||||
|
||||
import type { IInterval } from '@mixan/validation';
|
||||
|
||||
import { getYAxisWidth } from './chart-utils';
|
||||
import { useChartContext } from './ChartProvider';
|
||||
import { ReportChartTooltip } from './ReportChartTooltip';
|
||||
|
||||
@@ -6,7 +6,6 @@ import { AutoSizer } from '@/components/AutoSizer';
|
||||
import { useFormatDateInterval } from '@/hooks/useFormatDateInterval';
|
||||
import { useRechartDataModel } from '@/hooks/useRechartDataModel';
|
||||
import { useVisibleSeries } from '@/hooks/useVisibleSeries';
|
||||
import type { IChartLineType, IInterval } from '@/types';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { getChartColor } from '@/utils/theme';
|
||||
import {
|
||||
@@ -18,6 +17,8 @@ import {
|
||||
YAxis,
|
||||
} from 'recharts';
|
||||
|
||||
import type { IChartLineType, IInterval } from '@mixan/validation';
|
||||
|
||||
import { getYAxisWidth } from './chart-utils';
|
||||
import { useChartContext } from './ChartProvider';
|
||||
import { ReportChartTooltip } from './ReportChartTooltip';
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useMemo } from 'react';
|
||||
import { NOT_SET_VALUE } from '@/utils/constants';
|
||||
import type { LucideIcon, LucideProps } from 'lucide-react';
|
||||
import {
|
||||
ActivityIcon,
|
||||
@@ -15,6 +14,8 @@ import {
|
||||
TabletIcon,
|
||||
} from 'lucide-react';
|
||||
|
||||
import { NOT_SET_VALUE } from '@mixan/constants';
|
||||
|
||||
interface SerieIconProps extends LucideProps {
|
||||
name: string;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
import { memo, useEffect, useState } from 'react';
|
||||
import type { RouterOutputs } from '@/app/_trpc/client';
|
||||
import { api } from '@/app/_trpc/client';
|
||||
import type { IChartInput } from '@/types';
|
||||
|
||||
import type { IChartInput } from '@mixan/validation';
|
||||
|
||||
import { ChartEmpty } from './ChartEmpty';
|
||||
import { ChartLoading } from './ChartLoading';
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||
|
||||
import {
|
||||
alphabetIds,
|
||||
getDefaultIntervalByRange,
|
||||
isHourIntervalEnabledByRange,
|
||||
isMinuteIntervalEnabledByRange,
|
||||
} from '@mixan/constants';
|
||||
import type {
|
||||
IChartBreakdown,
|
||||
IChartEvent,
|
||||
@@ -6,15 +15,7 @@ import type {
|
||||
IChartRange,
|
||||
IChartType,
|
||||
IInterval,
|
||||
} from '@/types';
|
||||
import {
|
||||
alphabetIds,
|
||||
getDefaultIntervalByRange,
|
||||
isHourIntervalEnabledByRange,
|
||||
isMinuteIntervalEnabledByRange,
|
||||
} from '@/utils/constants';
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||
} from '@mixan/validation';
|
||||
|
||||
type InitialState = IChartInput & {
|
||||
dirty: boolean;
|
||||
|
||||
@@ -2,10 +2,11 @@ import { api } from '@/app/_trpc/client';
|
||||
import { Combobox } from '@/components/ui/combobox';
|
||||
import { useAppParams } from '@/hooks/useAppParams';
|
||||
import { useDispatch } from '@/redux';
|
||||
import type { IChartEvent } from '@/types';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { DatabaseIcon } from 'lucide-react';
|
||||
|
||||
import type { IChartEvent } from '@mixan/validation';
|
||||
|
||||
import { changeEvent } from '../reportSlice';
|
||||
|
||||
interface EventPropertiesComboboxProps {
|
||||
|
||||
@@ -5,9 +5,10 @@ import { ColorSquare } from '@/components/ColorSquare';
|
||||
import { Combobox } from '@/components/ui/combobox';
|
||||
import { useAppParams } from '@/hooks/useAppParams';
|
||||
import { useDispatch, useSelector } from '@/redux';
|
||||
import type { IChartBreakdown } from '@/types';
|
||||
import { SplitIcon } from 'lucide-react';
|
||||
|
||||
import type { IChartBreakdown } from '@mixan/validation';
|
||||
|
||||
import { addBreakdown, changeBreakdown, removeBreakdown } from '../reportSlice';
|
||||
import { ReportBreakdownMore } from './ReportBreakdownMore';
|
||||
import type { ReportEventMoreProps } from './ReportEventMore';
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
'use client';
|
||||
|
||||
import { api } from '@/app/_trpc/client';
|
||||
import { ColorSquare } from '@/components/ColorSquare';
|
||||
import { Dropdown } from '@/components/Dropdown';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
@@ -8,10 +7,12 @@ import { Combobox } from '@/components/ui/combobox';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { useAppParams } from '@/hooks/useAppParams';
|
||||
import { useDebounceFn } from '@/hooks/useDebounceFn';
|
||||
import { useEventNames } from '@/hooks/useEventNames';
|
||||
import { useDispatch, useSelector } from '@/redux';
|
||||
import type { IChartEvent } from '@/types';
|
||||
import { GanttChart, GanttChartIcon, Users } from 'lucide-react';
|
||||
|
||||
import type { IChartEvent } from '@mixan/validation';
|
||||
|
||||
import {
|
||||
addEvent,
|
||||
changeEvent,
|
||||
@@ -29,14 +30,8 @@ export function ReportEvents() {
|
||||
const selectedEvents = useSelector((state) => state.report.events);
|
||||
const dispatch = useDispatch();
|
||||
const { projectId } = useAppParams();
|
||||
const eventNames = useEventNames(projectId);
|
||||
|
||||
const eventsQuery = api.chart.events.useQuery({
|
||||
projectId,
|
||||
});
|
||||
const eventsCombobox = (eventsQuery.data ?? []).map((item) => ({
|
||||
value: item.name,
|
||||
label: item.name,
|
||||
}));
|
||||
const dispatchChangeEvent = useDebounceFn((event: IChartEvent) => {
|
||||
dispatch(changeEvent(event));
|
||||
});
|
||||
@@ -76,7 +71,10 @@ export function ReportEvents() {
|
||||
})
|
||||
);
|
||||
}}
|
||||
items={eventsCombobox}
|
||||
items={eventNames.map((item) => ({
|
||||
label: item.name,
|
||||
value: item.name,
|
||||
}))}
|
||||
placeholder="Select event"
|
||||
/>
|
||||
<Input
|
||||
@@ -189,7 +187,10 @@ export function ReportEvents() {
|
||||
})
|
||||
);
|
||||
}}
|
||||
items={eventsCombobox}
|
||||
items={eventNames.map((item) => ({
|
||||
label: item.name,
|
||||
value: item.name,
|
||||
}))}
|
||||
placeholder="Select event"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -7,13 +7,15 @@ import { RenderDots } from '@/components/ui/RenderDots';
|
||||
import { useAppParams } from '@/hooks/useAppParams';
|
||||
import { useMappings } from '@/hooks/useMappings';
|
||||
import { useDispatch } from '@/redux';
|
||||
import { SlidersHorizontal, Trash } from 'lucide-react';
|
||||
|
||||
import { operators } from '@mixan/constants';
|
||||
import type {
|
||||
IChartEvent,
|
||||
IChartEventFilter,
|
||||
IChartEventFilterOperator,
|
||||
IChartEventFilterValue,
|
||||
} from '@/types';
|
||||
import { operators } from '@/utils/constants';
|
||||
import { SlidersHorizontal, Trash } from 'lucide-react';
|
||||
} from '@mixan/validation';
|
||||
import { mapKeys } from '@mixan/validation';
|
||||
|
||||
import { changeEvent } from '../../reportSlice';
|
||||
|
||||
@@ -67,7 +69,7 @@ export function FilterItem({ filter, event }: FilterProps) {
|
||||
);
|
||||
};
|
||||
|
||||
const changeFilterOperator = (operator: IChartEventFilter['operator']) => {
|
||||
const changeFilterOperator = (operator: IChartEventFilterOperator) => {
|
||||
dispatch(
|
||||
changeEvent({
|
||||
...event,
|
||||
@@ -104,9 +106,9 @@ export function FilterItem({ filter, event }: FilterProps) {
|
||||
<div className="flex gap-1">
|
||||
<Dropdown
|
||||
onChange={changeFilterOperator}
|
||||
items={Object.entries(operators).map(([key, value]) => ({
|
||||
value: key as IChartEventFilter['operator'],
|
||||
label: value,
|
||||
items={mapKeys(operators).map((key) => ({
|
||||
value: key,
|
||||
label: operators[key],
|
||||
}))}
|
||||
label="Operator"
|
||||
>
|
||||
|
||||
@@ -2,9 +2,10 @@ import { api } from '@/app/_trpc/client';
|
||||
import { Combobox } from '@/components/ui/combobox';
|
||||
import { useAppParams } from '@/hooks/useAppParams';
|
||||
import { useDispatch } from '@/redux';
|
||||
import type { IChartEvent } from '@/types';
|
||||
import { FilterIcon } from 'lucide-react';
|
||||
|
||||
import type { IChartEvent } from '@mixan/validation';
|
||||
|
||||
import { changeEvent } from '../../reportSlice';
|
||||
|
||||
interface FiltersComboboxProps {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { IChartEvent } from '@/types';
|
||||
import type { IChartEvent } from '@mixan/validation';
|
||||
|
||||
import { FilterItem } from './FilterItem';
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ interface ComboboxAdvancedProps {
|
||||
onChange: React.Dispatch<React.SetStateAction<IValue[]>>;
|
||||
items: IItem[];
|
||||
placeholder: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function ComboboxAdvanced({
|
||||
@@ -32,6 +33,7 @@ export function ComboboxAdvanced({
|
||||
value,
|
||||
onChange,
|
||||
placeholder,
|
||||
className,
|
||||
}: ComboboxAdvancedProps) {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [inputValue, setInputValue] = React.useState('');
|
||||
@@ -81,8 +83,12 @@ export function ComboboxAdvanced({
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant={'outline'} onClick={() => setOpen((prev) => !prev)}>
|
||||
<div className="flex gap-1 flex-wrap">
|
||||
<Button
|
||||
variant={'outline'}
|
||||
onClick={() => setOpen((prev) => !prev)}
|
||||
className={className}
|
||||
>
|
||||
<div className="flex gap-1 flex-wrap w-full">
|
||||
{value.length === 0 && placeholder}
|
||||
{value.slice(0, 2).map((value) => {
|
||||
const item = items.find((item) => item.value === value) ?? {
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import { api } from '@/app/_trpc/client';
|
||||
|
||||
export function useEventNames(projectId: string) {
|
||||
const filterEventsQuery = api.chart.events.useQuery({
|
||||
const query = api.chart.events.useQuery({
|
||||
projectId: projectId,
|
||||
});
|
||||
|
||||
return (filterEventsQuery.data ?? []).map((item) => ({
|
||||
value: item.name,
|
||||
label: item.name,
|
||||
}));
|
||||
return query.data ?? [];
|
||||
}
|
||||
|
||||
10
apps/web/src/hooks/useEventProperties.ts
Normal file
10
apps/web/src/hooks/useEventProperties.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { api } from '@/app/_trpc/client';
|
||||
|
||||
export function useEventProperties(projectId: string, event?: string) {
|
||||
const query = api.chart.properties.useQuery({
|
||||
projectId: projectId,
|
||||
event,
|
||||
});
|
||||
|
||||
return query.data ?? [];
|
||||
}
|
||||
@@ -1,311 +0,0 @@
|
||||
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,
|
||||
};
|
||||
}
|
||||
@@ -1,326 +1,102 @@
|
||||
import { useMemo } from 'react';
|
||||
import type { IChartInput } from '@/types';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
// prettier-ignore
|
||||
import type { Options as NuqsOptions, UseQueryStateReturn } from 'nuqs';
|
||||
import type { Options as NuqsOptions } from 'nuqs';
|
||||
|
||||
import { parseAsString, useQueryState } from 'nuqs';
|
||||
import {
|
||||
createParser,
|
||||
parseAsArrayOf,
|
||||
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]
|
||||
);
|
||||
}
|
||||
type Operator = 'is' | 'isNot' | 'contains' | 'doesNotContain';
|
||||
|
||||
export const eventQueryFiltersParser = createParser({
|
||||
parse: (query: string) => {
|
||||
if (query === '') return [];
|
||||
const filters = query.split(';');
|
||||
|
||||
return (
|
||||
filters.map((filter) => {
|
||||
const [key, operator, value] = filter.split(',');
|
||||
return {
|
||||
id: key!,
|
||||
name: key!,
|
||||
operator: (operator ?? 'is') as Operator,
|
||||
value: [decodeURIComponent(value!)],
|
||||
};
|
||||
}) ?? []
|
||||
);
|
||||
},
|
||||
serialize: (value) => {
|
||||
return value
|
||||
.map(
|
||||
(filter) =>
|
||||
`${filter.id},${filter.operator},${encodeURIComponent(filter.value[0] ?? '')}`
|
||||
)
|
||||
.join(';');
|
||||
},
|
||||
});
|
||||
|
||||
export function useEventQueryFilters(options: NuqsOptions = {}) {
|
||||
// Ignore prettier so that we have all one same line
|
||||
// prettier-ignore
|
||||
return {
|
||||
path: useFix(useQueryState('path', parseAsString.withOptions({...nuqsOptions, ...options}))),
|
||||
referrer: useFix(useQueryState('referrer', parseAsString.withOptions({...nuqsOptions, ...options}))),
|
||||
referrerName: useFix(useQueryState('referrerName',parseAsString.withOptions({...nuqsOptions, ...options}))),
|
||||
referrerType: useFix(useQueryState('referrerType',parseAsString.withOptions({...nuqsOptions, ...options}))),
|
||||
utmSource: useFix(useQueryState('utmSource',parseAsString.withOptions({...nuqsOptions, ...options}))),
|
||||
utmMedium: useFix(useQueryState('utmMedium',parseAsString.withOptions({...nuqsOptions, ...options}))),
|
||||
utmCampaign: useFix(useQueryState('utmCampaign',parseAsString.withOptions({...nuqsOptions, ...options}))),
|
||||
utmContent: useFix(useQueryState('utmContent',parseAsString.withOptions({...nuqsOptions, ...options}))),
|
||||
utmTerm: useFix(useQueryState('utmTerm', parseAsString.withOptions({...nuqsOptions, ...options}))),
|
||||
continent: useFix(useQueryState('continent', parseAsString.withOptions({...nuqsOptions, ...options}))),
|
||||
country: useFix(useQueryState('country', parseAsString.withOptions({...nuqsOptions, ...options}))),
|
||||
region: useFix(useQueryState('region', parseAsString.withOptions({...nuqsOptions, ...options}))),
|
||||
city: useFix(useQueryState('city', parseAsString.withOptions({...nuqsOptions, ...options}))),
|
||||
device: useFix(useQueryState('device', parseAsString.withOptions({...nuqsOptions, ...options}))),
|
||||
browser: useFix(useQueryState('browser', parseAsString.withOptions({...nuqsOptions, ...options}))),
|
||||
browserVersion: useFix(useQueryState('browserVersion',parseAsString.withOptions({...nuqsOptions, ...options}))),
|
||||
os: useFix(useQueryState('os', parseAsString.withOptions({...nuqsOptions, ...options}))),
|
||||
osVersion: useFix(useQueryState('osVersion',parseAsString.withOptions({...nuqsOptions, ...options}))),
|
||||
brand: useFix(useQueryState('brand',parseAsString.withOptions({...nuqsOptions, ...options}))),
|
||||
model: useFix(useQueryState('model',parseAsString.withOptions({...nuqsOptions, ...options}))),
|
||||
} as const;
|
||||
const [filters, setFilters] = useQueryState(
|
||||
'f',
|
||||
eventQueryFiltersParser.withDefault([]).withOptions({
|
||||
...nuqsOptions,
|
||||
...options,
|
||||
})
|
||||
);
|
||||
|
||||
const setFilter = useCallback(
|
||||
(
|
||||
name: string,
|
||||
value: string | number | boolean | undefined | null,
|
||||
operator: Operator = 'is'
|
||||
) => {
|
||||
setFilters((prev) => {
|
||||
const exists = prev.find((filter) => filter.name === name);
|
||||
if (exists) {
|
||||
// If same value is already set, remove the filter
|
||||
if (exists.value[0] === value) {
|
||||
return prev.filter((filter) => filter.name !== name);
|
||||
}
|
||||
|
||||
return prev.map((filter) => {
|
||||
if (filter.name === name) {
|
||||
return {
|
||||
...filter,
|
||||
operator,
|
||||
value: [String(value)],
|
||||
};
|
||||
}
|
||||
return filter;
|
||||
});
|
||||
}
|
||||
|
||||
return [
|
||||
...prev,
|
||||
{
|
||||
id: name,
|
||||
name,
|
||||
operator,
|
||||
value: [String(value)],
|
||||
},
|
||||
];
|
||||
});
|
||||
},
|
||||
[setFilters]
|
||||
);
|
||||
|
||||
return [filters, setFilter, setFilters] as const;
|
||||
}
|
||||
|
||||
export function useEventFilters() {
|
||||
const eventQueryFilters = useEventQueryFilters();
|
||||
export const eventQueryNamesFilter = parseAsArrayOf(parseAsString).withDefault(
|
||||
[]
|
||||
);
|
||||
|
||||
const filters = useMemo(() => {
|
||||
return getEventFilters({
|
||||
path: eventQueryFilters.path.get,
|
||||
device: eventQueryFilters.device.get,
|
||||
referrer: eventQueryFilters.referrer.get,
|
||||
referrerName: eventQueryFilters.referrerName.get,
|
||||
referrerType: eventQueryFilters.referrerType.get,
|
||||
utmSource: eventQueryFilters.utmSource.get,
|
||||
utmMedium: eventQueryFilters.utmMedium.get,
|
||||
utmCampaign: eventQueryFilters.utmCampaign.get,
|
||||
utmContent: eventQueryFilters.utmContent.get,
|
||||
utmTerm: eventQueryFilters.utmTerm.get,
|
||||
continent: eventQueryFilters.continent.get,
|
||||
country: eventQueryFilters.country.get,
|
||||
region: eventQueryFilters.region.get,
|
||||
city: eventQueryFilters.city.get,
|
||||
browser: eventQueryFilters.browser.get,
|
||||
browserVersion: eventQueryFilters.browserVersion.get,
|
||||
os: eventQueryFilters.os.get,
|
||||
osVersion: eventQueryFilters.osVersion.get,
|
||||
brand: eventQueryFilters.brand.get,
|
||||
model: eventQueryFilters.model.get,
|
||||
});
|
||||
}, [
|
||||
eventQueryFilters.path.get,
|
||||
eventQueryFilters.device.get,
|
||||
eventQueryFilters.referrer.get,
|
||||
eventQueryFilters.referrerName.get,
|
||||
eventQueryFilters.referrerType.get,
|
||||
eventQueryFilters.utmSource.get,
|
||||
eventQueryFilters.utmMedium.get,
|
||||
eventQueryFilters.utmCampaign.get,
|
||||
eventQueryFilters.utmContent.get,
|
||||
eventQueryFilters.utmTerm.get,
|
||||
eventQueryFilters.continent.get,
|
||||
eventQueryFilters.country.get,
|
||||
eventQueryFilters.region.get,
|
||||
eventQueryFilters.city.get,
|
||||
eventQueryFilters.browser.get,
|
||||
eventQueryFilters.browserVersion.get,
|
||||
eventQueryFilters.os.get,
|
||||
eventQueryFilters.osVersion.get,
|
||||
eventQueryFilters.model.get,
|
||||
eventQueryFilters.brand.get,
|
||||
]);
|
||||
|
||||
return filters;
|
||||
}
|
||||
|
||||
export function getEventFilters({
|
||||
path,
|
||||
device,
|
||||
referrer,
|
||||
referrerName,
|
||||
referrerType,
|
||||
utmSource,
|
||||
utmMedium,
|
||||
utmCampaign,
|
||||
utmContent,
|
||||
utmTerm,
|
||||
continent,
|
||||
country,
|
||||
region,
|
||||
city,
|
||||
browser,
|
||||
browserVersion,
|
||||
os,
|
||||
osVersion,
|
||||
brand,
|
||||
model,
|
||||
}: {
|
||||
path: string | null;
|
||||
device: string | null;
|
||||
referrer: string | null;
|
||||
referrerName: string | null;
|
||||
referrerType: string | null;
|
||||
utmSource: string | null;
|
||||
utmMedium: string | null;
|
||||
utmCampaign: string | null;
|
||||
utmContent: string | null;
|
||||
utmTerm: string | null;
|
||||
continent: string | null;
|
||||
country: string | null;
|
||||
region: string | null;
|
||||
city: string | null;
|
||||
browser: string | null;
|
||||
browserVersion: string | null;
|
||||
os: string | null;
|
||||
osVersion: string | null;
|
||||
brand: string | null;
|
||||
model: string | null;
|
||||
}) {
|
||||
const filters: IChartInput['events'][number]['filters'] = [];
|
||||
|
||||
if (path) {
|
||||
filters.push({
|
||||
id: 'path',
|
||||
operator: 'is',
|
||||
name: 'path' as const,
|
||||
value: [path],
|
||||
});
|
||||
}
|
||||
|
||||
if (device) {
|
||||
filters.push({
|
||||
id: 'device',
|
||||
operator: 'is',
|
||||
name: 'device' as const,
|
||||
value: [device],
|
||||
});
|
||||
}
|
||||
|
||||
if (referrer) {
|
||||
filters.push({
|
||||
id: 'referrer',
|
||||
operator: 'is',
|
||||
name: 'referrer' as const,
|
||||
value: [referrer],
|
||||
});
|
||||
}
|
||||
|
||||
if (referrerName) {
|
||||
filters.push({
|
||||
id: 'referrerName',
|
||||
operator: 'is',
|
||||
name: 'referrer_name' as const,
|
||||
value: [referrerName],
|
||||
});
|
||||
}
|
||||
|
||||
if (referrerType) {
|
||||
filters.push({
|
||||
id: 'referrerType',
|
||||
operator: 'is',
|
||||
name: 'referrer_type' as const,
|
||||
value: [referrerType],
|
||||
});
|
||||
}
|
||||
|
||||
if (utmSource) {
|
||||
filters.push({
|
||||
id: 'utmSource',
|
||||
operator: 'is',
|
||||
name: 'properties.query.utm_source' as const,
|
||||
value: [utmSource],
|
||||
});
|
||||
}
|
||||
|
||||
if (utmMedium) {
|
||||
filters.push({
|
||||
id: 'utmMedium',
|
||||
operator: 'is',
|
||||
name: 'properties.query.utm_medium' as const,
|
||||
value: [utmMedium],
|
||||
});
|
||||
}
|
||||
|
||||
if (utmCampaign) {
|
||||
filters.push({
|
||||
id: 'utmCampaign',
|
||||
operator: 'is',
|
||||
name: 'properties.query.utm_campaign' as const,
|
||||
value: [utmCampaign],
|
||||
});
|
||||
}
|
||||
|
||||
if (utmContent) {
|
||||
filters.push({
|
||||
id: 'utmContent',
|
||||
operator: 'is',
|
||||
name: 'properties.query.utm_content' as const,
|
||||
value: [utmContent],
|
||||
});
|
||||
}
|
||||
|
||||
if (utmTerm) {
|
||||
filters.push({
|
||||
id: 'utmTerm',
|
||||
operator: 'is',
|
||||
name: 'properties.query.utm_term' as const,
|
||||
value: [utmTerm],
|
||||
});
|
||||
}
|
||||
|
||||
if (continent) {
|
||||
filters.push({
|
||||
id: 'continent',
|
||||
operator: 'is',
|
||||
name: 'continent' as const,
|
||||
value: [continent],
|
||||
});
|
||||
}
|
||||
|
||||
if (country) {
|
||||
filters.push({
|
||||
id: 'country',
|
||||
operator: 'is',
|
||||
name: 'country' as const,
|
||||
value: [country],
|
||||
});
|
||||
}
|
||||
|
||||
if (region) {
|
||||
filters.push({
|
||||
id: 'region',
|
||||
operator: 'is',
|
||||
name: 'region' as const,
|
||||
value: [region],
|
||||
});
|
||||
}
|
||||
|
||||
if (city) {
|
||||
filters.push({
|
||||
id: 'city',
|
||||
operator: 'is',
|
||||
name: 'city' as const,
|
||||
value: [city],
|
||||
});
|
||||
}
|
||||
|
||||
if (browser) {
|
||||
filters.push({
|
||||
id: 'browser',
|
||||
operator: 'is',
|
||||
name: 'browser' as const,
|
||||
value: [browser],
|
||||
});
|
||||
}
|
||||
|
||||
if (browserVersion) {
|
||||
filters.push({
|
||||
id: 'browserVersion',
|
||||
operator: 'is',
|
||||
name: 'browser_version' as const,
|
||||
value: [browserVersion],
|
||||
});
|
||||
}
|
||||
|
||||
if (os) {
|
||||
filters.push({
|
||||
id: 'os',
|
||||
operator: 'is',
|
||||
name: 'os' as const,
|
||||
value: [os],
|
||||
});
|
||||
}
|
||||
|
||||
if (osVersion) {
|
||||
filters.push({
|
||||
id: 'osVersion',
|
||||
operator: 'is',
|
||||
name: 'os_version' as const,
|
||||
value: [osVersion],
|
||||
});
|
||||
}
|
||||
|
||||
if (brand) {
|
||||
filters.push({
|
||||
id: 'brand',
|
||||
operator: 'is',
|
||||
name: 'brand' as const,
|
||||
value: [brand],
|
||||
});
|
||||
}
|
||||
|
||||
if (model) {
|
||||
filters.push({
|
||||
id: 'model',
|
||||
operator: 'is',
|
||||
name: 'model' as const,
|
||||
value: [model],
|
||||
});
|
||||
}
|
||||
|
||||
return filters;
|
||||
export function useEventQueryNamesFilter(options: NuqsOptions = {}) {
|
||||
return useQueryState('events', eventQueryNamesFilter.withOptions(options));
|
||||
}
|
||||
|
||||
15
apps/web/src/hooks/useEventValues.ts
Normal file
15
apps/web/src/hooks/useEventValues.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { api } from '@/app/_trpc/client';
|
||||
|
||||
export function useEventValues(
|
||||
projectId: string,
|
||||
event: string,
|
||||
property: string
|
||||
) {
|
||||
const query = api.chart.values.useQuery({
|
||||
projectId: projectId,
|
||||
event,
|
||||
property,
|
||||
});
|
||||
|
||||
return query.data?.values ?? [];
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { IInterval } from '@/types';
|
||||
import type { IInterval } from '@mixan/validation';
|
||||
|
||||
export function formatDateInterval(interval: IInterval, date: Date): string {
|
||||
if (interval === 'hour' || interval === 'minute') {
|
||||
|
||||
@@ -5,7 +5,6 @@ import { authMiddleware } from '@clerk/nextjs';
|
||||
// See https://clerk.com/docs/references/nextjs/auth-middleware for more information about configuring your Middleware
|
||||
export default authMiddleware({
|
||||
publicRoutes: ['/share/overview/:id', '/api/trpc(.*)'],
|
||||
debug: true,
|
||||
});
|
||||
|
||||
export const config = {
|
||||
|
||||
@@ -4,17 +4,18 @@ import { api, handleError } from '@/app/_trpc/client';
|
||||
import { ButtonContainer } from '@/components/ButtonContainer';
|
||||
import { InputWithLabel } from '@/components/forms/InputWithLabel';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import type { IClientWithProject } from '@/types';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { toast } from 'sonner';
|
||||
import { z } from 'zod';
|
||||
|
||||
import type { IServiceClient } from '@mixan/db';
|
||||
|
||||
import { popModal } from '.';
|
||||
import { ModalContent, ModalHeader } from './Modal/Container';
|
||||
|
||||
type EditClientProps = IClientWithProject;
|
||||
type EditClientProps = IServiceClient;
|
||||
|
||||
const validator = z.object({
|
||||
id: z.string().min(1),
|
||||
|
||||
@@ -4,17 +4,18 @@ import { api, handleError } from '@/app/_trpc/client';
|
||||
import { ButtonContainer } from '@/components/ButtonContainer';
|
||||
import { InputWithLabel } from '@/components/forms/InputWithLabel';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import type { IServiceDashboardWithProject } from '@/server/services/dashboard.service';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { toast } from 'sonner';
|
||||
import { z } from 'zod';
|
||||
|
||||
import type { IServiceDashboard } from '@mixan/db';
|
||||
|
||||
import { popModal } from '.';
|
||||
import { ModalContent, ModalHeader } from './Modal/Container';
|
||||
|
||||
type EditDashboardProps = IServiceDashboardWithProject;
|
||||
type EditDashboardProps = Exclude<IServiceDashboard, null>;
|
||||
|
||||
const validator = z.object({
|
||||
id: z.string().min(1),
|
||||
|
||||
@@ -4,17 +4,18 @@ import { api, handleError } from '@/app/_trpc/client';
|
||||
import { ButtonContainer } from '@/components/ButtonContainer';
|
||||
import { InputWithLabel } from '@/components/forms/InputWithLabel';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import type { IProject } from '@/types';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { toast } from 'sonner';
|
||||
import { z } from 'zod';
|
||||
|
||||
import type { IServiceProject } from '@mixan/db';
|
||||
|
||||
import { popModal } from '.';
|
||||
import { ModalContent, ModalHeader } from './Modal/Container';
|
||||
|
||||
type EditProjectProps = IProject;
|
||||
type EditProjectProps = Exclude<IServiceProject, null>;
|
||||
|
||||
const validator = z.object({
|
||||
id: z.string().min(1),
|
||||
|
||||
@@ -7,13 +7,14 @@ import { Button } from '@/components/ui/button';
|
||||
import { Combobox } from '@/components/ui/combobox';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { useAppParams } from '@/hooks/useAppParams';
|
||||
import type { IChartInput } from '@/types';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { toast } from 'sonner';
|
||||
import { z } from 'zod';
|
||||
|
||||
import type { IChartInput } from '@mixan/validation';
|
||||
|
||||
import { popModal } from '.';
|
||||
import { ModalContent, ModalHeader } from './Modal/Container';
|
||||
|
||||
|
||||
@@ -6,13 +6,14 @@ import { InputWithLabel } from '@/components/forms/InputWithLabel';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { useAppParams } from '@/hooks/useAppParams';
|
||||
import { zShareOverview } from '@/utils/validation';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { toast } from 'sonner';
|
||||
import type { z } from 'zod';
|
||||
|
||||
import { zShareOverview } from '@mixan/validation';
|
||||
|
||||
import { popModal } from '.';
|
||||
import { ModalContent, ModalHeader } from './Modal/Container';
|
||||
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import { getChartSql } from '@/server/services/chart.service';
|
||||
import { round } from '@/utils/math';
|
||||
import * as mathjs from 'mathjs';
|
||||
import { sort } from 'ramda';
|
||||
|
||||
import { alphabetIds, NOT_SET_VALUE } from '@mixan/constants';
|
||||
import { chQuery, convertClickhouseDateToJs, getChartSql } from '@mixan/db';
|
||||
import type {
|
||||
IChartEvent,
|
||||
IChartInput,
|
||||
IGetChartDataInput,
|
||||
IInterval,
|
||||
} from '@/types';
|
||||
import { alphabetIds, NOT_SET_VALUE } from '@/utils/constants';
|
||||
import { round } from '@/utils/math';
|
||||
import * as mathjs from 'mathjs';
|
||||
import { sort } from 'ramda';
|
||||
|
||||
import { chQuery, convertClickhouseDateToJs } from '@mixan/db';
|
||||
} from '@mixan/validation';
|
||||
|
||||
export type GetChartDataResult = Awaited<ReturnType<typeof getChartData>>;
|
||||
export interface ResultItem {
|
||||
|
||||
@@ -3,14 +3,14 @@ import {
|
||||
protectedProcedure,
|
||||
publicProcedure,
|
||||
} from '@/server/api/trpc';
|
||||
import type { IChartEvent, IChartInput, IChartRange } from '@/types';
|
||||
import { getDaysOldDate } from '@/utils/date';
|
||||
import { average, max, min, round, sum } from '@/utils/math';
|
||||
import { zChartInput } from '@/utils/validation';
|
||||
import { flatten, map, pipe, prop, sort, uniq } from 'ramda';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { chQuery, createSqlBuilder } from '@mixan/db';
|
||||
import { zChartInput } from '@mixan/validation';
|
||||
import type { IChartEvent, IChartInput, IChartRange } from '@mixan/validation';
|
||||
|
||||
import { getChartData, withFormula } from './chart.helpers';
|
||||
|
||||
|
||||
@@ -107,11 +107,6 @@ export const dashboardRouter = createTRPCRouter({
|
||||
},
|
||||
});
|
||||
}
|
||||
await db.recentDashboards.deleteMany({
|
||||
where: {
|
||||
dashboard_id: id,
|
||||
},
|
||||
});
|
||||
await db.dashboard.delete({
|
||||
where: {
|
||||
id,
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { createTRPCRouter, protectedProcedure } from '@/server/api/trpc';
|
||||
import { getOrganizationBySlug } from '@/server/services/organization.service';
|
||||
import { zInviteUser } from '@/utils/validation';
|
||||
import { clerkClient } from '@clerk/nextjs';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { getOrganizationBySlug } from '@mixan/db';
|
||||
import { zInviteUser } from '@mixan/validation';
|
||||
|
||||
export const organizationRouter = createTRPCRouter({
|
||||
list: protectedProcedure.query(() => {
|
||||
return clerkClient.organizations.getOrganizationList();
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { createTRPCRouter, protectedProcedure } from '@/server/api/trpc';
|
||||
import { db } from '@/server/db';
|
||||
import { transformReport } from '@/server/services/reports.service';
|
||||
import { zChartInput } from '@/utils/validation';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { transformReport } from '@mixan/db';
|
||||
import { zChartInput } from '@mixan/validation';
|
||||
|
||||
export const reportRouter = createTRPCRouter({
|
||||
get: protectedProcedure
|
||||
.input(
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { createTRPCRouter, protectedProcedure } from '@/server/api/trpc';
|
||||
import { db } from '@/server/db';
|
||||
import { zShareOverview } from '@/utils/validation';
|
||||
import ShortUniqueId from 'short-unique-id';
|
||||
|
||||
import { zShareOverview } from '@mixan/validation';
|
||||
|
||||
const uid = new ShortUniqueId({ length: 6 });
|
||||
|
||||
export const shareRouter = createTRPCRouter({
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { createTRPCRouter, protectedProcedure } from '@/server/api/trpc';
|
||||
import { transformUser } from '@/server/services/user.service';
|
||||
import { clerkClient } from '@clerk/nextjs';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { transformUser } from '@mixan/db';
|
||||
|
||||
export const userRouter = createTRPCRouter({
|
||||
update: protectedProcedure
|
||||
.input(
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
import { getOrganizationBySlug } from './services/organization.service';
|
||||
import { getProjectById } from './services/project.service';
|
||||
import { getOrganizationBySlug, getProjectById } from '@mixan/db';
|
||||
|
||||
export async function getExists(organizationSlug: string, projectId?: string) {
|
||||
const promises: Promise<any>[] = [getOrganizationBySlug(organizationSlug)];
|
||||
|
||||
@@ -1,167 +0,0 @@
|
||||
import type { IGetChartDataInput } from '@/types';
|
||||
|
||||
import { createSqlBuilder, formatClickhouseDate } from '@mixan/db';
|
||||
|
||||
function log(sql: string) {
|
||||
const logs = ['--- START', sql, '--- END'];
|
||||
console.log(logs.join('\n'));
|
||||
return sql;
|
||||
}
|
||||
|
||||
export function getChartSql({
|
||||
event,
|
||||
breakdowns,
|
||||
interval,
|
||||
startDate,
|
||||
endDate,
|
||||
projectId,
|
||||
}: IGetChartDataInput) {
|
||||
const { sb, join, getWhere, getFrom, getSelect, getOrderBy, getGroupBy } =
|
||||
createSqlBuilder();
|
||||
|
||||
sb.where.projectId = `project_id = '${projectId}'`;
|
||||
if (event.name !== '*') {
|
||||
sb.select.label = `'${event.name}' as label`;
|
||||
sb.where.eventName = `name = '${event.name}'`;
|
||||
}
|
||||
|
||||
event.filters.forEach((filter, index) => {
|
||||
const id = `f${index}`;
|
||||
const { name, value, operator } = filter;
|
||||
|
||||
if (name.startsWith('properties.')) {
|
||||
const whereFrom = `mapValues(mapExtractKeyLike(properties, '${name
|
||||
.replace(/^properties\./, '')
|
||||
.replace('.*.', '.%.')}'))`;
|
||||
|
||||
switch (operator) {
|
||||
case 'is': {
|
||||
sb.where[id] = `arrayExists(x -> ${value
|
||||
.map((val) => `x = '${String(val).trim()}'`)
|
||||
.join(' OR ')}, ${whereFrom})`;
|
||||
break;
|
||||
}
|
||||
case 'isNot': {
|
||||
sb.where[id] = `arrayExists(x -> ${value
|
||||
.map((val) => `x != '${String(val).trim()}'`)
|
||||
.join(' OR ')}, ${whereFrom})`;
|
||||
break;
|
||||
}
|
||||
case 'contains': {
|
||||
sb.where[id] = `arrayExists(x -> ${value
|
||||
.map((val) => `x LIKE '%${String(val).trim()}%'`)
|
||||
.join(' OR ')}, ${whereFrom})`;
|
||||
break;
|
||||
}
|
||||
case 'doesNotContain': {
|
||||
sb.where[id] = `arrayExists(x -> ${value
|
||||
.map((val) => `x NOT LIKE '%${String(val).trim()}%'`)
|
||||
.join(' OR ')}, ${whereFrom})`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
switch (operator) {
|
||||
case 'is': {
|
||||
sb.where[id] = `${name} IN (${value
|
||||
.map((val) => `'${String(val).trim()}'`)
|
||||
.join(', ')})`;
|
||||
break;
|
||||
}
|
||||
case 'isNot': {
|
||||
sb.where[id] = `${name} NOT IN (${value
|
||||
.map((val) => `'${String(val).trim()}'`)
|
||||
.join(', ')})`;
|
||||
break;
|
||||
}
|
||||
case 'contains': {
|
||||
sb.where[id] = value
|
||||
.map((val) => `${name} LIKE '%${String(val).trim()}%'`)
|
||||
.join(' OR ');
|
||||
break;
|
||||
}
|
||||
case 'doesNotContain': {
|
||||
sb.where[id] = value
|
||||
.map((val) => `${name} NOT LIKE '%${String(val).trim()}%'`)
|
||||
.join(' OR ');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
sb.select.count = `count(*) as count`;
|
||||
switch (interval) {
|
||||
case 'minute': {
|
||||
sb.select.date = `toStartOfMinute(created_at) as date`;
|
||||
break;
|
||||
}
|
||||
case 'hour': {
|
||||
sb.select.date = `toStartOfHour(created_at) as date`;
|
||||
break;
|
||||
}
|
||||
case 'day': {
|
||||
sb.select.date = `toStartOfDay(created_at) as date`;
|
||||
break;
|
||||
}
|
||||
case 'month': {
|
||||
sb.select.date = `toStartOfMonth(created_at) as date`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
sb.groupBy.date = 'date';
|
||||
sb.orderBy.date = 'date ASC';
|
||||
|
||||
if (startDate) {
|
||||
sb.where.startDate = `created_at >= '${formatClickhouseDate(startDate)}'`;
|
||||
}
|
||||
|
||||
if (endDate) {
|
||||
sb.where.endDate = `created_at <= '${formatClickhouseDate(endDate)}'`;
|
||||
}
|
||||
|
||||
const breakdown = breakdowns[0]!;
|
||||
if (breakdown) {
|
||||
const value = breakdown.name.startsWith('properties.')
|
||||
? `mapValues(mapExtractKeyLike(properties, '${breakdown.name
|
||||
.replace(/^properties\./, '')
|
||||
.replace('.*.', '.%.')}'))`
|
||||
: breakdown.name;
|
||||
sb.select.label = breakdown.name.startsWith('properties.')
|
||||
? `arrayElement(${value}, 1) as label`
|
||||
: `${breakdown.name} as label`;
|
||||
sb.groupBy.label = `label`;
|
||||
}
|
||||
|
||||
if (event.segment === 'user') {
|
||||
sb.select.count = `countDistinct(profile_id) as count`;
|
||||
}
|
||||
|
||||
if (event.segment === 'user_average') {
|
||||
sb.select.count = `COUNT(*)::float / COUNT(DISTINCT profile_id)::float as count`;
|
||||
}
|
||||
|
||||
if (event.segment === 'property_sum' && event.property) {
|
||||
sb.select.count = `sum(${event.property}) as count`;
|
||||
}
|
||||
|
||||
if (event.segment === 'property_average' && event.property) {
|
||||
sb.select.count = `avg(${event.property}) as count`;
|
||||
}
|
||||
|
||||
if (event.segment === 'one_event_per_user') {
|
||||
sb.from = `(
|
||||
SELECT DISTINCT ON (profile_id) * from events WHERE ${join(
|
||||
sb.where,
|
||||
' AND '
|
||||
)}
|
||||
ORDER BY profile_id, created_at DESC
|
||||
) as subQuery`;
|
||||
|
||||
return log(`${getSelect()} ${getFrom()} ${getGroupBy()} ${getOrderBy()}`);
|
||||
}
|
||||
|
||||
return log(
|
||||
`${getSelect()} ${getFrom()} ${getWhere()} ${getGroupBy()} ${getOrderBy()}`
|
||||
);
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import { db } from '@mixan/db';
|
||||
|
||||
export function getClientsByOrganizationId(organizationId: string) {
|
||||
return db.client.findMany({
|
||||
where: {
|
||||
organization_slug: organizationId,
|
||||
},
|
||||
include: {
|
||||
project: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
import { db } from '../db';
|
||||
|
||||
export type IServiceDashboard = Awaited<ReturnType<typeof getDashboardById>>;
|
||||
export type IServiceDashboards = Awaited<
|
||||
ReturnType<typeof getDashboardsByProjectId>
|
||||
>;
|
||||
|
||||
export async function getDashboardById(id: string) {
|
||||
const dashboard = await db.dashboard.findUnique({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
include: {
|
||||
project: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!dashboard) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return dashboard;
|
||||
}
|
||||
|
||||
export async function getDashboardsByOrganization(organizationSlug: string) {
|
||||
return db.dashboard.findMany({
|
||||
where: {
|
||||
organization_slug: organizationSlug,
|
||||
},
|
||||
include: {
|
||||
project: true,
|
||||
},
|
||||
orderBy: {
|
||||
reports: {
|
||||
_count: 'desc',
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function getDashboardsByProjectId(projectId: string) {
|
||||
return db.dashboard.findMany({
|
||||
where: {
|
||||
project_id: projectId,
|
||||
},
|
||||
include: {
|
||||
project: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user