well deserved clean up (#1)
This commit is contained in:
@@ -48,6 +48,7 @@
|
||||
"@trpc/server": "^10.45.1",
|
||||
"@types/d3": "^7.4.3",
|
||||
"bcrypt": "^5.1.1",
|
||||
"bind-event-listener": "^3.0.0",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.0",
|
||||
"cmdk": "^0.2.1",
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import { api, handleErrorToastOptions } from '@/app/_trpc/client';
|
||||
import { Card, CardActions, CardActionsItem } from '@/components/Card';
|
||||
import { FullPageEmptyState } from '@/components/FullPageEmptyState';
|
||||
import { Card, CardActions, CardActionsItem } from '@/components/card';
|
||||
import { FullPageEmptyState } from '@/components/full-page-empty-state';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ToastAction } from '@/components/ui/toast';
|
||||
import { useAppParams } from '@/hooks/useAppParams';
|
||||
import { pushModal } from '@/modals';
|
||||
import { LayoutPanelTopIcon, Pencil, PlusIcon, Trash } from 'lucide-react';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { Fragment } from 'react';
|
||||
import { Widget, WidgetHead } from '@/components/Widget';
|
||||
import { Widget, WidgetHead } from '@/components/widget';
|
||||
import { isSameDay } from 'date-fns';
|
||||
|
||||
import type { IServiceCreateEventPayload } from '@openpanel/db';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Widget } from '@/components/Widget';
|
||||
import { Widget } from '@/components/widget';
|
||||
|
||||
import { db, getEvents } from '@openpanel/db';
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { Fragment, Suspense } from 'react';
|
||||
import { FullPageEmptyState } from '@/components/FullPageEmptyState';
|
||||
import { Pagination } from '@/components/Pagination';
|
||||
import { FullPageEmptyState } from '@/components/full-page-empty-state';
|
||||
import { Pagination } from '@/components/pagination';
|
||||
import { ChartSwitch, ChartSwitchShortcut } from '@/components/report/chart';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useAppParams } from '@/hooks/useAppParams';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Logo } from '@/components/Logo';
|
||||
import { Logo } from '@/components/logo';
|
||||
import { buttonVariants } from '@/components/ui/button';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { Rotate as Hamburger } from 'hamburger-react';
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
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 { ProfileAvatar } from '@/components/profiles/ProfileAvatar';
|
||||
import { ProfileAvatar } from '@/components/profiles/profile-avatar';
|
||||
import { ChartSwitch } from '@/components/report/chart';
|
||||
import { SerieIcon } from '@/components/report/chart/SerieIcon';
|
||||
import { Widget, WidgetBody, WidgetHead } from '@/components/Widget';
|
||||
import { Widget, WidgetBody, WidgetHead } from '@/components/widget';
|
||||
import {
|
||||
eventQueryFiltersParser,
|
||||
eventQueryNamesFilter,
|
||||
} from '@/hooks/useEventQueryFilters';
|
||||
import { getExists } from '@/server/pageExists';
|
||||
import { getProfileName } from '@/utils/getters';
|
||||
import { notFound } from 'next/navigation';
|
||||
import { parseAsInteger, parseAsString } from 'nuqs';
|
||||
|
||||
@@ -20,6 +19,7 @@ import {
|
||||
getEventList,
|
||||
getEventsCount,
|
||||
getProfileById,
|
||||
getProfileName,
|
||||
} from '@openpanel/db';
|
||||
import type { IChartEvent, IChartInput } from '@openpanel/validation';
|
||||
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { Suspense } from 'react';
|
||||
import { FullPageEmptyState } from '@/components/FullPageEmptyState';
|
||||
import { Pagination } from '@/components/Pagination';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useCursor } from '@/hooks/useCursor';
|
||||
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
||||
import { UsersIcon } from 'lucide-react';
|
||||
|
||||
import type { IServiceProfile } from '@openpanel/db';
|
||||
|
||||
import { ProfileListItem } from './profile-list-item';
|
||||
|
||||
interface ProfileListProps {
|
||||
data: IServiceProfile[];
|
||||
count: number;
|
||||
}
|
||||
export function ProfileList({ data, count }: ProfileListProps) {
|
||||
const { cursor, setCursor } = useCursor();
|
||||
const [filters] = useEventQueryFilters();
|
||||
|
||||
return (
|
||||
<Suspense>
|
||||
<div className="p-4">
|
||||
{data.length === 0 ? (
|
||||
<FullPageEmptyState title="No profiles here" icon={UsersIcon}>
|
||||
{cursor !== 0 ? (
|
||||
<>
|
||||
<p>Looks like you have reached the end of the list</p>
|
||||
<Button
|
||||
className="mt-4"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setCursor((p) => Math.max(0, p - 1))}
|
||||
>
|
||||
Go back
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{filters.length ? (
|
||||
<p>Could not find any profiles with your filter</p>
|
||||
) : (
|
||||
<p>No profiles have been created yet</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</FullPageEmptyState>
|
||||
) : (
|
||||
<>
|
||||
<Pagination
|
||||
cursor={cursor}
|
||||
setCursor={setCursor}
|
||||
count={count}
|
||||
take={50}
|
||||
/>
|
||||
<div className="flex flex-col gap-4 my-4">
|
||||
{data.map((item) => (
|
||||
<ProfileListItem key={item.id} {...item} />
|
||||
))}
|
||||
</div>
|
||||
<Pagination
|
||||
cursor={cursor}
|
||||
setCursor={setCursor}
|
||||
count={count}
|
||||
take={50}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip';
|
||||
import { Widget, WidgetBody, WidgetHead } from '@/components/Widget';
|
||||
import { Widget, WidgetBody, WidgetHead } from '@/components/widget';
|
||||
import { cn } from '@/utils/cn';
|
||||
|
||||
import { chQuery } from '@openpanel/db';
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { ExpandableListItem } from '@/components/general/ExpandableListItem';
|
||||
import { ProfileAvatar } from '@/components/profiles/ProfileAvatar';
|
||||
import { KeyValue, KeyValueSubtle } from '@/components/ui/key-value';
|
||||
import { useAppParams } from '@/hooks/useAppParams';
|
||||
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
||||
import { getProfileName } from '@/utils/getters';
|
||||
|
||||
import type { IServiceProfile } from '@openpanel/db';
|
||||
|
||||
type ProfileListItemProps = IServiceProfile;
|
||||
|
||||
export function ProfileListItem(props: ProfileListItemProps) {
|
||||
const { id, properties, createdAt } = props;
|
||||
const params = useAppParams();
|
||||
const [, setFilter] = useEventQueryFilters({ shallow: false });
|
||||
|
||||
const renderContent = () => {
|
||||
return (
|
||||
<>
|
||||
<KeyValueSubtle name="Time" value={createdAt.toLocaleString()} />
|
||||
<KeyValueSubtle
|
||||
href={`/${params.organizationId}/${params.projectId}/profiles/${id}`}
|
||||
name="Details"
|
||||
value={'See profile'}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<ExpandableListItem
|
||||
title={getProfileName(props)}
|
||||
content={renderContent()}
|
||||
image={<ProfileAvatar {...props} />}
|
||||
>
|
||||
<>
|
||||
{properties && (
|
||||
<div className="bg-white p-4 flex flex-col gap-4">
|
||||
<div className="font-medium">Properties</div>
|
||||
<div className="flex flex-wrap gap-x-4 gap-y-2">
|
||||
{Object.entries(properties)
|
||||
.filter(([, value]) => !!value)
|
||||
.map(([key, value]) => (
|
||||
<KeyValue
|
||||
onClick={() => setFilter(`properties.${key}`, value)}
|
||||
key={key}
|
||||
name={key}
|
||||
value={value}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
</ExpandableListItem>
|
||||
);
|
||||
}
|
||||
@@ -1,18 +1,19 @@
|
||||
'use client';
|
||||
|
||||
import { ListPropertiesIcon } from '@/components/events/ListPropertiesIcon';
|
||||
import { FullPageEmptyState } from '@/components/FullPageEmptyState';
|
||||
import { Pagination } from '@/components/Pagination';
|
||||
import { ProfileAvatar } from '@/components/profiles/ProfileAvatar';
|
||||
import { ListPropertiesIcon } from '@/components/events/list-properties-icon';
|
||||
import { FullPageEmptyState } from '@/components/full-page-empty-state';
|
||||
import { Pagination } from '@/components/pagination';
|
||||
import { ProfileAvatar } from '@/components/profiles/profile-avatar';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Tooltiper } from '@/components/ui/tooltip';
|
||||
import { Widget, WidgetHead } from '@/components/Widget';
|
||||
import { Widget, WidgetHead } from '@/components/widget';
|
||||
import { WidgetTable } from '@/components/widget-table';
|
||||
import { useAppParams } from '@/hooks/useAppParams';
|
||||
import { useCursor } from '@/hooks/useCursor';
|
||||
import { UsersIcon } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
|
||||
import { getProfileName } from '@openpanel/db';
|
||||
import type { IServiceProfile } from '@openpanel/db';
|
||||
|
||||
interface ProfileListProps {
|
||||
@@ -49,7 +50,7 @@ export function ProfileList({ data, count }: ProfileListProps) {
|
||||
className="flex gap-2 items-center font-medium"
|
||||
>
|
||||
<ProfileAvatar size="sm" {...profile} />
|
||||
{profile.firstName} {profile.lastName}
|
||||
{getProfileName(profile)}
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { ListPropertiesIcon } from '@/components/events/ListPropertiesIcon';
|
||||
import { ProfileAvatar } from '@/components/profiles/ProfileAvatar';
|
||||
import { Widget, WidgetHead } from '@/components/Widget';
|
||||
import { ListPropertiesIcon } from '@/components/events/list-properties-icon';
|
||||
import { ProfileAvatar } from '@/components/profiles/profile-avatar';
|
||||
import { Widget, WidgetHead } from '@/components/widget';
|
||||
import { WidgetTable } from '@/components/widget-table';
|
||||
import Link from 'next/link';
|
||||
|
||||
import { chQuery, getProfiles } from '@openpanel/db';
|
||||
import { chQuery, getProfileName, getProfiles } from '@openpanel/db';
|
||||
|
||||
interface Props {
|
||||
projectId: string;
|
||||
@@ -24,7 +24,7 @@ export default async function ProfileTopServer({
|
||||
const list = res.map((item) => {
|
||||
return {
|
||||
count: item.count,
|
||||
...(profiles.find((p) => p.id === item.profile_id) ?? {}),
|
||||
...(profiles.find((p) => p.id === item.profile_id)! ?? {}),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -35,7 +35,7 @@ export default async function ProfileTopServer({
|
||||
</WidgetHead>
|
||||
<WidgetTable
|
||||
data={list.filter((item) => !!item.id)}
|
||||
keyExtractor={(item) => item.id!}
|
||||
keyExtractor={(item) => item.id}
|
||||
columns={[
|
||||
{
|
||||
name: 'Name',
|
||||
@@ -46,7 +46,7 @@ export default async function ProfileTopServer({
|
||||
className="flex gap-2 items-center font-medium"
|
||||
>
|
||||
<ProfileAvatar size="sm" {...profile} />
|
||||
{profile.firstName} {profile.lastName}
|
||||
{getProfileName(profile)}
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -2,9 +2,8 @@
|
||||
|
||||
import { StickyBelowHeader } from '@/app/(app)/[organizationId]/[projectId]/layout-sticky-below-header';
|
||||
import { columns } from '@/components/clients/table';
|
||||
import { DataTable } from '@/components/DataTable';
|
||||
import { DataTable } from '@/components/data-table';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useAppParams } from '@/hooks/useAppParams';
|
||||
import { pushModal } from '@/modals';
|
||||
import { PlusIcon } from 'lucide-react';
|
||||
|
||||
@@ -14,17 +13,12 @@ interface ListClientsProps {
|
||||
clients: Awaited<ReturnType<typeof getClientsByOrganizationId>>;
|
||||
}
|
||||
export default function ListClients({ clients }: ListClientsProps) {
|
||||
const organizationId = useAppParams().organizationId;
|
||||
|
||||
return (
|
||||
<>
|
||||
<StickyBelowHeader>
|
||||
<div className="p-4 flex items-center justify-between">
|
||||
<div />
|
||||
<Button
|
||||
icon={PlusIcon}
|
||||
onClick={() => pushModal('AddClient', { organizationId })}
|
||||
>
|
||||
<Button icon={PlusIcon} onClick={() => pushModal('AddClient')}>
|
||||
<span className="max-sm:hidden">Create client</span>
|
||||
<span className="sm:hidden">Client</span>
|
||||
</Button>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import { api, handleError } from '@/app/_trpc/client';
|
||||
import { InputWithLabel } from '@/components/forms/InputWithLabel';
|
||||
import { InputWithLabel } from '@/components/forms/input-with-label';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Widget, WidgetBody, WidgetHead } from '@/components/Widget';
|
||||
import { Widget, WidgetBody, WidgetHead } from '@/components/widget';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { api } from '@/app/_trpc/client';
|
||||
import { InputWithLabel } from '@/components/forms/InputWithLabel';
|
||||
import { InputWithLabel } from '@/components/forms/input-with-label';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useAppParams } from '@/hooks/useAppParams';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import { Widget, WidgetBody, WidgetHead } from '@/components/Widget';
|
||||
import { Widget, WidgetBody, WidgetHead } from '@/components/widget';
|
||||
|
||||
import type { IServiceInvites } from '@openpanel/db';
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import { api, handleError } from '@/app/_trpc/client';
|
||||
import { InputWithLabel } from '@/components/forms/InputWithLabel';
|
||||
import { InputWithLabel } from '@/components/forms/input-with-label';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Widget, WidgetBody, WidgetHead } from '@/components/Widget';
|
||||
import { Widget, WidgetBody, WidgetHead } from '@/components/widget';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { buttonVariants } from '@/components/ui/button';
|
||||
import { Widget, WidgetBody, WidgetHead } from '@/components/Widget';
|
||||
import { Widget, WidgetBody, WidgetHead } from '@/components/widget';
|
||||
import { SignOutButton } from '@clerk/nextjs';
|
||||
|
||||
export function Logout() {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { StickyBelowHeader } from '@/app/(app)/[organizationId]/[projectId]/layout-sticky-below-header';
|
||||
import { DataTable } from '@/components/DataTable';
|
||||
import { DataTable } from '@/components/data-table';
|
||||
import { columns } from '@/components/projects/table';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useAppParams } from '@/hooks/useAppParams';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { StickyBelowHeader } from '@/app/(app)/[organizationId]/[projectId]/layout-sticky-below-header';
|
||||
import { DataTable } from '@/components/DataTable';
|
||||
import { DataTable } from '@/components/data-table';
|
||||
import { columns } from '@/components/references/table';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { pushModal } from '@/modals';
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import { Funnel } from '@/components/report/funnel';
|
||||
|
||||
import PageLayout from '../page-layout';
|
||||
|
||||
export const metadata = {
|
||||
title: 'Funnel - Openpanel.dev',
|
||||
};
|
||||
|
||||
interface PageProps {
|
||||
params: {
|
||||
organizationId: string;
|
||||
};
|
||||
}
|
||||
|
||||
export default function Page({ params: { organizationId } }: PageProps) {
|
||||
return (
|
||||
<PageLayout title="Funnel" organizationSlug={organizationId}>
|
||||
<Funnel />
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { LogoSquare } from '@/components/Logo';
|
||||
import { LogoSquare } from '@/components/logo';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { LogoSquare } from '@/components/Logo';
|
||||
import { LogoSquare } from '@/components/logo';
|
||||
import { ProjectCard } from '@/components/projects/project-card';
|
||||
import { notFound, redirect } from 'next/navigation';
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { CreateClientSuccess } from '@/components/clients/create-client-success';
|
||||
import { LogoSquare } from '@/components/Logo';
|
||||
import { LogoSquare } from '@/components/logo';
|
||||
import { Button, buttonVariants } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
@@ -10,7 +9,6 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { SaveIcon, WallpaperIcon } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import type { SubmitHandler } from 'react-hook-form';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// import { CreateOrganization } from '@clerk/nextjs';
|
||||
|
||||
import { LogoSquare } from '@/components/Logo';
|
||||
import { LogoSquare } from '@/components/logo';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
import { getCurrentOrganizations, isWaitlistUserAccepted } from '@openpanel/db';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { StickyBelowHeader } from '@/app/(app)/[organizationId]/[projectId]/layout-sticky-below-header';
|
||||
import { OverviewReportRange } from '@/app/(app)/[organizationId]/[projectId]/overview-sticky-header';
|
||||
import { Logo } from '@/components/Logo';
|
||||
import { Logo } from '@/components/logo';
|
||||
import { OverviewFiltersButtons } from '@/components/overview/filters/overview-filters-buttons';
|
||||
import ServerLiveCounter from '@/components/overview/live-counter';
|
||||
import { OverviewLiveHistogram } from '@/components/overview/overview-live-histogram';
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { InputWithLabel } from '@/components/forms/InputWithLabel';
|
||||
import { Logo } from '@/components/Logo';
|
||||
import { InputWithLabel } from '@/components/forms/input-with-label';
|
||||
import { Logo } from '@/components/logo';
|
||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Widget, WidgetBody } from '@/components/Widget';
|
||||
import { Widget, WidgetBody } from '@/components/widget';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { KeySquareIcon } from 'lucide-react';
|
||||
import { signIn } from 'next-auth/react';
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import type { HtmlProps } from '@/types';
|
||||
import { cn } from '@/utils/cn';
|
||||
|
||||
export function Container({ className, ...props }: HtmlProps<HTMLDivElement>) {
|
||||
return (
|
||||
<div
|
||||
className={cn('mx-auto w-full max-w-4xl px-4', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { cn } from '@/utils/cn';
|
||||
|
||||
interface ContentHeaderProps {
|
||||
title: string;
|
||||
text: string;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function ContentHeader({ title, text, children }: ContentHeaderProps) {
|
||||
return (
|
||||
<div className="flex items-center justify-between py-6 first:pt-0">
|
||||
<div>
|
||||
<h2 className="h2">{title}</h2>
|
||||
<p className="text-sm text-muted-foreground">{text}</p>
|
||||
</div>
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface ContentSectionProps {
|
||||
title: string;
|
||||
text?: string | React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
asCol?: boolean;
|
||||
}
|
||||
|
||||
export function ContentSection({
|
||||
title,
|
||||
text,
|
||||
children,
|
||||
asCol,
|
||||
}: ContentSectionProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'first:pt-0] flex py-6',
|
||||
asCol ? 'col flex' : 'justify-between'
|
||||
)}
|
||||
>
|
||||
{title && (
|
||||
<div className="max-w-[50%]">
|
||||
<h4 className="h4">{title}</h4>
|
||||
{text && <p className="text-sm text-muted-foreground">{text}</p>}
|
||||
</div>
|
||||
)}
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
import { cloneElement } from 'react';
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from './ui/dropdown-menu';
|
||||
|
||||
interface DropdownProps<Value> {
|
||||
children: React.ReactNode;
|
||||
label?: string;
|
||||
items: {
|
||||
label: string;
|
||||
value: Value;
|
||||
}[];
|
||||
onChange?: (value: Value) => void;
|
||||
}
|
||||
|
||||
export function Dropdown<Value extends string>({
|
||||
children,
|
||||
label,
|
||||
items,
|
||||
onChange,
|
||||
}: DropdownProps<Value>) {
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>{children}</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-56" align="start">
|
||||
{label && (
|
||||
<>
|
||||
<DropdownMenuLabel>{label}</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
</>
|
||||
)}
|
||||
<DropdownMenuGroup>
|
||||
{items.map((item) => (
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
key={item.value}
|
||||
onClick={() => {
|
||||
onChange?.(item.value);
|
||||
}}
|
||||
>
|
||||
{item.label}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import type { HtmlProps } from '@/types';
|
||||
|
||||
type PageTitleProps = HtmlProps<HTMLDivElement>;
|
||||
|
||||
export function PageTitle({ children }: PageTitleProps) {
|
||||
return (
|
||||
<div className="my-8 flex justify-between border-b border-border py-4">
|
||||
<h1 className="h1">{children}</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import type { HtmlProps } from '@/types';
|
||||
|
||||
type WithSidebarProps = HtmlProps<HTMLDivElement>;
|
||||
|
||||
export function WithSidebar({ children }: WithSidebarProps) {
|
||||
return (
|
||||
<div className="grid grid-cols-[200px_minmax(0,1fr)] gap-8">{children}</div>
|
||||
);
|
||||
}
|
||||
|
||||
type SidebarProps = HtmlProps<HTMLDivElement>;
|
||||
|
||||
export function Sidebar({ children }: SidebarProps) {
|
||||
return <div className="flex flex-col gap-1">{children}</div>;
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import type { ColumnDef } from '@tanstack/react-table';
|
||||
|
||||
import type { IServiceClientWithProject } from '@openpanel/db';
|
||||
|
||||
import { ClientActions } from './ClientActions';
|
||||
import { ClientActions } from './client-actions';
|
||||
|
||||
export const columns: ColumnDef<IServiceClientWithProject>[] = [
|
||||
{
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
interface InputErrorProps {
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export function InputError({ message }: InputErrorProps) {
|
||||
if (!message) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <div className="mt-1 text-sm text-red-600">{message}</div>;
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { ChevronUp } from 'lucide-react';
|
||||
import AnimateHeight from 'react-animate-height';
|
||||
|
||||
import { Button } from '../ui/button';
|
||||
|
||||
interface ExpandableListItemProps {
|
||||
children: React.ReactNode;
|
||||
content: React.ReactNode;
|
||||
title: React.ReactNode;
|
||||
image?: React.ReactNode;
|
||||
initialOpen?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
export function ExpandableListItem({
|
||||
title,
|
||||
content,
|
||||
image,
|
||||
initialOpen = false,
|
||||
children,
|
||||
className,
|
||||
}: ExpandableListItemProps) {
|
||||
const [open, setOpen] = useState(initialOpen ?? false);
|
||||
return (
|
||||
<div className={cn('card overflow-hidden', className)}>
|
||||
<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">
|
||||
<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}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="icon"
|
||||
onClick={() => setOpen((p) => !p)}
|
||||
className="bg-black/5 hover:bg-black/10"
|
||||
>
|
||||
<ChevronUp
|
||||
size={20}
|
||||
className={cn(
|
||||
'transition-transform',
|
||||
open ? 'rotate-180' : 'rotate-0'
|
||||
)}
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
<AnimateHeight duration={200} height={open ? 'auto' : 0}>
|
||||
<div className="border-t border-border">{children}</div>
|
||||
</AnimateHeight>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import { ChartSwitch } from '@/components/report/chart';
|
||||
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
||||
import { cn } from '@/utils/cn';
|
||||
|
||||
import { Widget, WidgetBody } from '../../Widget';
|
||||
import { Widget, WidgetBody } from '../../widget';
|
||||
import { WidgetButtons, WidgetHead } from '../overview-widget';
|
||||
import { useOverviewOptions } from '../useOverviewOptions';
|
||||
import { useOverviewWidget } from '../useOverviewWidget';
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { WidgetHead } from '@/components/overview/overview-widget';
|
||||
import { useOverviewOptions } from '@/components/overview/useOverviewOptions';
|
||||
import { ChartSwitch } from '@/components/report/chart';
|
||||
import { Widget, WidgetBody } from '@/components/Widget';
|
||||
import { Widget, WidgetBody } from '@/components/widget';
|
||||
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
||||
import { cn } from '@/utils/cn';
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { ChartSwitch } from '@/components/report/chart';
|
||||
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
||||
import { cn } from '@/utils/cn';
|
||||
|
||||
import { Widget, WidgetBody } from '../Widget';
|
||||
import { Widget, WidgetBody } from '../widget';
|
||||
import { OverviewChartToggle } from './overview-chart-toggle';
|
||||
import { WidgetButtons, WidgetHead } from './overview-widget';
|
||||
import { useOverviewOptions } from './useOverviewOptions';
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { BarChartIcon, LineChart, LineChartIcon } from 'lucide-react';
|
||||
|
||||
import { Widget, WidgetBody } from '../../Widget';
|
||||
import { Widget, WidgetBody } from '../../widget';
|
||||
import { OverviewChartToggle } from '../overview-chart-toggle';
|
||||
import { WidgetButtons, WidgetHead } from '../overview-widget';
|
||||
import { useOverviewOptions } from '../useOverviewOptions';
|
||||
|
||||
@@ -4,7 +4,7 @@ import { ChartSwitch } from '@/components/report/chart';
|
||||
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
||||
import { cn } from '@/utils/cn';
|
||||
|
||||
import { Widget, WidgetBody } from '../Widget';
|
||||
import { Widget, WidgetBody } from '../widget';
|
||||
import { OverviewChartToggle } from './overview-chart-toggle';
|
||||
import { WidgetButtons, WidgetHead } from './overview-widget';
|
||||
import { useOverviewOptions } from './useOverviewOptions';
|
||||
|
||||
@@ -4,7 +4,7 @@ import { ChartSwitch } from '@/components/report/chart';
|
||||
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
||||
import { cn } from '@/utils/cn';
|
||||
|
||||
import { Widget, WidgetBody } from '../Widget';
|
||||
import { Widget, WidgetBody } from '../widget';
|
||||
import { OverviewChartToggle } from './overview-chart-toggle';
|
||||
import { WidgetButtons, WidgetHead } from './overview-widget';
|
||||
import { useOverviewOptions } from './useOverviewOptions';
|
||||
|
||||
@@ -4,7 +4,7 @@ import { ChartSwitch } from '@/components/report/chart';
|
||||
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
||||
import { cn } from '@/utils/cn';
|
||||
|
||||
import { Widget, WidgetBody } from '../Widget';
|
||||
import { Widget, WidgetBody } from '../widget';
|
||||
import { OverviewChartToggle } from './overview-chart-toggle';
|
||||
import { WidgetButtons, WidgetHead } from './overview-widget';
|
||||
import { useOverviewOptions } from './useOverviewOptions';
|
||||
|
||||
@@ -13,8 +13,8 @@ import {
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '../ui/dropdown-menu';
|
||||
import type { WidgetHeadProps } from '../Widget';
|
||||
import { WidgetHead as WidgetHeadBase } from '../Widget';
|
||||
import type { WidgetHeadProps } from '../widget';
|
||||
import { WidgetHead as WidgetHeadBase } from '../widget';
|
||||
|
||||
export function WidgetHead({ className, ...props }: WidgetHeadProps) {
|
||||
return (
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { formatDate } from '@/utils/date';
|
||||
import type { ColumnDef } from '@tanstack/react-table';
|
||||
|
||||
import { IServiceProject } from '@openpanel/db';
|
||||
import type { Project as IProject } from '@openpanel/db';
|
||||
import type { IServiceProject } from '@openpanel/db';
|
||||
|
||||
import { ProjectActions } from './ProjectActions';
|
||||
import { ProjectActions } from './project-actions';
|
||||
|
||||
export type Project = IProject;
|
||||
export const columns: ColumnDef<Project>[] = [
|
||||
export type Project = IServiceProject;
|
||||
export const columns: ColumnDef<IServiceProject>[] = [
|
||||
{
|
||||
accessorKey: 'name',
|
||||
header: 'Name',
|
||||
|
||||
@@ -7,9 +7,8 @@ import {
|
||||
PopoverTrigger,
|
||||
} from '@/components/ui/popover';
|
||||
import { useBreakpoint } from '@/hooks/useBreakpoint';
|
||||
import { useDispatch, useSelector } from '@/redux';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { endOfDay, format, startOfDay } from 'date-fns';
|
||||
import { format } from 'date-fns';
|
||||
import { CalendarIcon, ChevronsUpDownIcon } from 'lucide-react';
|
||||
import type { SelectRangeEventHandler } from 'react-day-picker';
|
||||
|
||||
@@ -18,7 +17,6 @@ import type { IChartRange } from '@openpanel/validation';
|
||||
|
||||
import type { ExtendedComboboxProps } from '../ui/combobox';
|
||||
import { ToggleGroup, ToggleGroupItem } from '../ui/toggle-group';
|
||||
import { changeDates, changeEndDate, changeStartDate } from './reportSlice';
|
||||
|
||||
export function ReportRange({
|
||||
range,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FullPageEmptyState } from '@/components/FullPageEmptyState';
|
||||
import { FullPageEmptyState } from '@/components/full-page-empty-state';
|
||||
import { cn } from '@/utils/cn';
|
||||
|
||||
import { useChartContext } from './ChartProvider';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import type { IChartData } from '@/app/_trpc/client';
|
||||
import { ColorSquare } from '@/components/ColorSquare';
|
||||
import { ColorSquare } from '@/components/color-square';
|
||||
import { fancyMinutes, useNumber } from '@/hooks/useNumerFormatter';
|
||||
import { theme } from '@/utils/theme';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
|
||||
@@ -44,7 +44,6 @@ export function ReportLineChart({
|
||||
const { series, setVisibleSeries } = useVisibleSeries(data);
|
||||
const rechartData = useRechartDataModel(series);
|
||||
const number = useNumber();
|
||||
console.log(references.map((ref) => ref.createdAt.getTime()));
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { IChartData } from '@/app/_trpc/client';
|
||||
import { AutoSizer } from '@/components/AutoSizer';
|
||||
import { AutoSizer } from '@/components/react-virtualized-auto-sizer';
|
||||
import { useVisibleSeries } from '@/hooks/useVisibleSeries';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { round } from '@/utils/math';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import type { IChartData } from '@/app/_trpc/client';
|
||||
import { Pagination, usePagination } from '@/components/Pagination';
|
||||
import { Pagination, usePagination } from '@/components/pagination';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import {
|
||||
@@ -30,12 +30,14 @@ interface ReportTableProps {
|
||||
setVisibleSeries: React.Dispatch<React.SetStateAction<string[]>>;
|
||||
}
|
||||
|
||||
const ROWS_LIMIT = 50;
|
||||
|
||||
export function ReportTable({
|
||||
data,
|
||||
visibleSeries,
|
||||
setVisibleSeries,
|
||||
}: ReportTableProps) {
|
||||
const { setPage, paginate, page } = usePagination(50);
|
||||
const { setPage, paginate, page } = usePagination(ROWS_LIMIT);
|
||||
const number = useNumber();
|
||||
const interval = useSelector((state) => state.report.interval);
|
||||
const formatDate = useFormatDateInterval(interval);
|
||||
@@ -162,7 +164,12 @@ export function ReportTable({
|
||||
<Badge>Min: {number.format(data.metrics.min)}</Badge>
|
||||
<Badge>Max: {number.format(data.metrics.max)}</Badge>
|
||||
</div>
|
||||
<Pagination cursor={page} setCursor={setPage} />
|
||||
<Pagination
|
||||
cursor={page}
|
||||
setCursor={setPage}
|
||||
take={ROWS_LIMIT}
|
||||
count={data.series.length}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { api } from '@/app/_trpc/client';
|
||||
import { ColorSquare } from '@/components/ColorSquare';
|
||||
import { ColorSquare } from '@/components/color-square';
|
||||
import { Combobox } from '@/components/ui/combobox';
|
||||
import { useAppParams } from '@/hooks/useAppParams';
|
||||
import { useDispatch, useSelector } from '@/redux';
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import { ColorSquare } from '@/components/ColorSquare';
|
||||
import { Dropdown } from '@/components/Dropdown';
|
||||
import { ColorSquare } from '@/components/color-square';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Combobox } from '@/components/ui/combobox';
|
||||
import { DropdownMenuComposed } from '@/components/ui/dropdown-menu';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { useAppParams } from '@/hooks/useAppParams';
|
||||
import { useDebounceFn } from '@/hooks/useDebounceFn';
|
||||
@@ -94,7 +94,7 @@ export function ReportEvents() {
|
||||
|
||||
{/* Segment and Filter buttons */}
|
||||
<div className="flex gap-2 p-2 pt-0 text-sm">
|
||||
<Dropdown
|
||||
<DropdownMenuComposed
|
||||
onChange={(segment) => {
|
||||
dispatch(
|
||||
changeEvent({
|
||||
@@ -166,7 +166,7 @@ export function ReportEvents() {
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</Dropdown>
|
||||
</DropdownMenuComposed>
|
||||
{/* */}
|
||||
<FiltersCombobox event={event} />
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { api } from '@/app/_trpc/client';
|
||||
import { ColorSquare } from '@/components/ColorSquare';
|
||||
import { Dropdown } from '@/components/Dropdown';
|
||||
import { ColorSquare } from '@/components/color-square';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ComboboxAdvanced } from '@/components/ui/combobox-advanced';
|
||||
import { DropdownMenuComposed } from '@/components/ui/dropdown-menu';
|
||||
import { RenderDots } from '@/components/ui/RenderDots';
|
||||
import { useAppParams } from '@/hooks/useAppParams';
|
||||
import { useMappings } from '@/hooks/useMappings';
|
||||
@@ -104,7 +104,7 @@ export function FilterItem({ filter, event }: FilterProps) {
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex gap-1">
|
||||
<Dropdown
|
||||
<DropdownMenuComposed
|
||||
onChange={changeFilterOperator}
|
||||
items={mapKeys(operators).map((key) => ({
|
||||
value: key,
|
||||
@@ -115,7 +115,7 @@ export function FilterItem({ filter, event }: FilterProps) {
|
||||
<Button variant={'ghost'} className="whitespace-nowrap">
|
||||
{operators[filter.operator]}
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</DropdownMenuComposed>
|
||||
<ComboboxAdvanced
|
||||
items={valuesCombobox}
|
||||
value={filter.value}
|
||||
|
||||
@@ -180,6 +180,50 @@ const DropdownMenuShortcut = ({
|
||||
};
|
||||
DropdownMenuShortcut.displayName = 'DropdownMenuShortcut';
|
||||
|
||||
interface DropdownProps<Value> {
|
||||
children: React.ReactNode;
|
||||
label?: string;
|
||||
items: {
|
||||
label: string;
|
||||
value: Value;
|
||||
}[];
|
||||
onChange?: (value: Value) => void;
|
||||
}
|
||||
|
||||
export function DropdownMenuComposed<Value extends string>({
|
||||
children,
|
||||
label,
|
||||
items,
|
||||
onChange,
|
||||
}: DropdownProps<Value>) {
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>{children}</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-56" align="start">
|
||||
{label && (
|
||||
<>
|
||||
<DropdownMenuLabel>{label}</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
</>
|
||||
)}
|
||||
<DropdownMenuGroup>
|
||||
{items.map((item) => (
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
key={item.value}
|
||||
onClick={() => {
|
||||
onChange?.(item.value);
|
||||
}}
|
||||
>
|
||||
{item.label}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
import { isValidElement } from 'react';
|
||||
import { cn } from '@/utils/cn';
|
||||
import Link from 'next/link';
|
||||
|
||||
interface KeyValueProps {
|
||||
name: string;
|
||||
value: unknown;
|
||||
value: any;
|
||||
onClick?: () => void;
|
||||
href?: string;
|
||||
}
|
||||
|
||||
export function KeyValue({ href, onClick, name, value }: KeyValueProps) {
|
||||
const clickable = href || onClick;
|
||||
const Component = href ? (Link as any) : onClick ? 'button' : 'div';
|
||||
const Component = (href ? Link : onClick ? 'button' : 'div') as 'button';
|
||||
|
||||
return (
|
||||
<Component
|
||||
@@ -33,24 +32,3 @@ export function KeyValue({ href, onClick, name, value }: KeyValueProps) {
|
||||
</Component>
|
||||
);
|
||||
}
|
||||
|
||||
export function KeyValueSubtle({ href, onClick, name, value }: KeyValueProps) {
|
||||
const clickable = href || onClick;
|
||||
const Component = href ? (Link as any) : onClick ? 'button' : 'div';
|
||||
return (
|
||||
<Component
|
||||
className="group flex text-[10px] sm:text-xs gap-2 font-medium self-start min-w-0 max-w-full items-center"
|
||||
{...{ href, onClick }}
|
||||
>
|
||||
<div className="text-gray-400">{name}</div>
|
||||
<div
|
||||
className={cn(
|
||||
'bg-black/5 rounded p-0.5 px-1 sm:p-1 sm:px-2 text-gray-600 whitespace-nowrap text-ellipsis flex items-center gap-1',
|
||||
clickable && 'group-hover:underline'
|
||||
)}
|
||||
>
|
||||
{value}
|
||||
</div>
|
||||
</Component>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { api } from '@/app/_trpc/client';
|
||||
|
||||
export function useProfileProperties(projectId: string, event?: string) {
|
||||
export function useProfileProperties(projectId: string) {
|
||||
const query = api.profile.properties.useQuery({
|
||||
projectId: projectId,
|
||||
event,
|
||||
});
|
||||
|
||||
return query.data ?? [];
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import type { z } from 'zod';
|
||||
|
||||
export function useQueryParams<Z extends z.ZodTypeAny = z.ZodNever>(zod: Z) {
|
||||
const router = useRouter();
|
||||
const value = zod.safeParse(router.query);
|
||||
|
||||
return useMemo(() => {
|
||||
function setQueryParams(newValue: Partial<z.infer<Z>>) {
|
||||
return router
|
||||
.replace({
|
||||
pathname: router.pathname,
|
||||
query: {
|
||||
...router.query,
|
||||
...newValue,
|
||||
},
|
||||
})
|
||||
.catch(() => {
|
||||
// ignore
|
||||
});
|
||||
}
|
||||
|
||||
if (value.success) {
|
||||
return { ...value.data, setQueryParams } as z.infer<Z> & {
|
||||
setQueryParams: typeof setQueryParams;
|
||||
};
|
||||
}
|
||||
return { ...router.query, setQueryParams } as z.infer<Z> & {
|
||||
setQueryParams: typeof setQueryParams;
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [router.asPath, value.success]);
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
export function useRefetchActive() {
|
||||
const client = useQueryClient();
|
||||
return () => client.refetchQueries({ type: 'active' });
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
export function useRouterBeforeLeave(callback: () => void) {
|
||||
const router = useRouter();
|
||||
const prevUrl = useRef(router.asPath);
|
||||
|
||||
useEffect(() => {
|
||||
const handleRouteChange = (url: string) => {
|
||||
if (prevUrl.current !== url) {
|
||||
callback();
|
||||
}
|
||||
prevUrl.current = url;
|
||||
};
|
||||
|
||||
router.events.on('routeChangeStart', handleRouteChange);
|
||||
return () => {
|
||||
router.events.off('routeChangeStart', handleRouteChange);
|
||||
};
|
||||
}, [router, callback]);
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
|
||||
export function useSetCookie() {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
return (key: string, value: string, path?: string) => {
|
||||
fetch(`/api/cookie?${key}=${value}`).then(() => {
|
||||
if (path && path !== pathname) {
|
||||
router.refresh();
|
||||
router.replace(path);
|
||||
} else {
|
||||
router.refresh();
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -1,146 +1,6 @@
|
||||
[
|
||||
{
|
||||
"id": "clbow140n0228u99ui1t24l85",
|
||||
"name": "Mjölkproteinfritt"
|
||||
},
|
||||
{
|
||||
"id": "cl04a3trn0015ix50tzy40pyf",
|
||||
"name": "Måltider"
|
||||
},
|
||||
{
|
||||
"id": "cl04a5p7s0401ix505n75yfcn",
|
||||
"name": "Svårighetsgrad"
|
||||
},
|
||||
{
|
||||
"id": "cl04a7k3i0813ix50aau6yxqg",
|
||||
"name": "Tid"
|
||||
},
|
||||
{
|
||||
"id": "cl04a47fu0103ix50dqckz2vc",
|
||||
"name": "Frukost"
|
||||
},
|
||||
{
|
||||
"id": "cl04a4hvu0152ix50o0w4iy8l",
|
||||
"name": "Mellis"
|
||||
},
|
||||
{
|
||||
"id": "cl04a58ju0281ix50kdmcwst6",
|
||||
"name": "Dessert"
|
||||
},
|
||||
{
|
||||
"id": "cl04a5fjc0321ix50xiwhuydy",
|
||||
"name": "Smakportioner"
|
||||
},
|
||||
{
|
||||
"id": "cl04a5kcu0361ix50bnmbhoxz",
|
||||
"name": "Plockmat"
|
||||
},
|
||||
{
|
||||
"id": "cl04a60sk0496ix50et419drf",
|
||||
"name": "Medium"
|
||||
},
|
||||
{
|
||||
"id": "cl04a67lx0536ix50sstoxnhi",
|
||||
"name": "Avancerat"
|
||||
},
|
||||
{
|
||||
"id": "cl04a7qi60850ix50je7vaxo3",
|
||||
"name": "0-10 min"
|
||||
},
|
||||
{
|
||||
"id": "cl04a7vxi0890ix50veumcuyu",
|
||||
"name": "10-20 min"
|
||||
},
|
||||
{
|
||||
"id": "cl04a82bj0930ix50bboh3tl9",
|
||||
"name": "20-30 min"
|
||||
},
|
||||
{
|
||||
"id": "cl04a8a7a0970ix50uet02cqh",
|
||||
"name": "30-40 min"
|
||||
},
|
||||
{
|
||||
"id": "cl04a8g151010ix50z4cnf2kg",
|
||||
"name": "40-50 min"
|
||||
},
|
||||
{
|
||||
"id": "cl04a8mqy1050ix50z0d1ho1a",
|
||||
"name": "50-60 min"
|
||||
},
|
||||
{
|
||||
"id": "cl04a5ujg0447ix50vd3vor87",
|
||||
"name": "Lätt"
|
||||
},
|
||||
{
|
||||
"id": "cl04a4qv60201ix50b8q5kn9r",
|
||||
"name": "Lunch & Middag"
|
||||
},
|
||||
{
|
||||
"id": "clak50jem0072ri9ugwygg5ko",
|
||||
"name": "Annat"
|
||||
},
|
||||
{
|
||||
"id": "clak510qm0120ri9upqkca39s",
|
||||
"name": "För hela familjen"
|
||||
},
|
||||
{
|
||||
"id": "clak59l8x0085yd9uzllcuci5",
|
||||
"name": "Under 3 ingredienser"
|
||||
},
|
||||
{
|
||||
"id": "clak59l8y0087yd9u53qperp8",
|
||||
"name": "Under 5 ingredienser"
|
||||
},
|
||||
{
|
||||
"id": "claslo2sg0404no9uo2tckm5i",
|
||||
"name": "Huvudingredienser"
|
||||
},
|
||||
{
|
||||
"id": "claslv9s20491no9ugo4fd9ns",
|
||||
"name": "Fisk"
|
||||
},
|
||||
{
|
||||
"id": "claslv9s30493no9umug5po29",
|
||||
"name": "Kyckling"
|
||||
},
|
||||
{
|
||||
"id": "claslv9s40495no9umor61pql",
|
||||
"name": "Kött"
|
||||
},
|
||||
{
|
||||
"id": "claslv9s40497no9uttwkt47n",
|
||||
"name": "Korv"
|
||||
},
|
||||
{
|
||||
"id": "claslv9s50499no9uch0lhs9i",
|
||||
"name": "Vegetariskt"
|
||||
},
|
||||
{
|
||||
"id": "clb1y44f40128np9uufck0iqf",
|
||||
"name": "Årstider"
|
||||
},
|
||||
{
|
||||
"id": "clb1y4ks80202np9uh43c84ts",
|
||||
"name": "Jul"
|
||||
},
|
||||
{
|
||||
"id": "clbovy0fd0081u99u8dr0yplr",
|
||||
"name": "Allergier"
|
||||
},
|
||||
{
|
||||
"id": "clbow140p0230u99uk9e7g1u1",
|
||||
"name": "Äggfritt"
|
||||
},
|
||||
{
|
||||
"id": "clbow140q0232u99uy3lwukvc",
|
||||
"name": "Vetefritt"
|
||||
},
|
||||
{
|
||||
"id": "clbow140q0234u99uiyrujxd4",
|
||||
"name": "Glutenfritt"
|
||||
},
|
||||
{
|
||||
"id": "clbow140r0236u99u5333gpei",
|
||||
"name": "Nötfritt"
|
||||
"id": "123",
|
||||
"name": "123"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { api, handleError } from '@/app/_trpc/client';
|
||||
import { ButtonContainer } from '@/components/ButtonContainer';
|
||||
import { InputWithLabel } from '@/components/forms/InputWithLabel';
|
||||
import { ButtonContainer } from '@/components/button-container';
|
||||
import { InputWithLabel } from '@/components/forms/input-with-label';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useAppParams } from '@/hooks/useAppParams';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { api, handleError } from '@/app/_trpc/client';
|
||||
import { ButtonContainer } from '@/components/ButtonContainer';
|
||||
import { InputWithLabel } from '@/components/forms/InputWithLabel';
|
||||
import { ButtonContainer } from '@/components/button-container';
|
||||
import { InputWithLabel } from '@/components/forms/input-with-label';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { api, handleError } from '@/app/_trpc/client';
|
||||
import { ButtonContainer } from '@/components/ButtonContainer';
|
||||
import { InputWithLabel } from '@/components/forms/InputWithLabel';
|
||||
import { ButtonContainer } from '@/components/button-container';
|
||||
import { InputWithLabel } from '@/components/forms/input-with-label';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Calendar } from '@/components/ui/calendar';
|
||||
import { useAppParams } from '@/hooks/useAppParams';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { ButtonContainer } from '@/components/ButtonContainer';
|
||||
import { ButtonContainer } from '@/components/button-container';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
import { popModal } from '.';
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { api, handleError } from '@/app/_trpc/client';
|
||||
import { ButtonContainer } from '@/components/ButtonContainer';
|
||||
import { InputWithLabel } from '@/components/forms/InputWithLabel';
|
||||
import { ButtonContainer } from '@/components/button-container';
|
||||
import { InputWithLabel } from '@/components/forms/input-with-label';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { api, handleError } from '@/app/_trpc/client';
|
||||
import { ButtonContainer } from '@/components/ButtonContainer';
|
||||
import { InputWithLabel } from '@/components/forms/InputWithLabel';
|
||||
import { ButtonContainer } from '@/components/button-container';
|
||||
import { InputWithLabel } from '@/components/forms/input-with-label';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { api, handleError } from '@/app/_trpc/client';
|
||||
import { ButtonContainer } from '@/components/ButtonContainer';
|
||||
import { InputWithLabel } from '@/components/forms/InputWithLabel';
|
||||
import { ButtonContainer } from '@/components/button-container';
|
||||
import { InputWithLabel } from '@/components/forms/input-with-label';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { ButtonContainer } from '@/components/ButtonContainer';
|
||||
import { InputWithLabel } from '@/components/forms/InputWithLabel';
|
||||
import { ButtonContainer } from '@/components/button-container';
|
||||
import { InputWithLabel } from '@/components/forms/input-with-label';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import type { SubmitHandler } from 'react-hook-form';
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { api, handleError } from '@/app/_trpc/client';
|
||||
import { ButtonContainer } from '@/components/ButtonContainer';
|
||||
import { InputWithLabel } from '@/components/forms/InputWithLabel';
|
||||
import { ButtonContainer } from '@/components/button-container';
|
||||
import { InputWithLabel } from '@/components/forms/input-with-label';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Combobox } from '@/components/ui/combobox';
|
||||
import { Label } from '@/components/ui/label';
|
||||
@@ -36,7 +36,7 @@ export default function SaveReport({ report }: SaveReportProps) {
|
||||
const searchParams = useSearchParams();
|
||||
const dashboardId = searchParams?.get('dashboardId') ?? undefined;
|
||||
|
||||
const save = api.report.save.useMutation({
|
||||
const save = api.report.create.useMutation({
|
||||
onError: handleError,
|
||||
onSuccess(res) {
|
||||
toast('Success', {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { api, handleError } from '@/app/_trpc/client';
|
||||
import { ButtonContainer } from '@/components/ButtonContainer';
|
||||
import { InputWithLabel } from '@/components/forms/InputWithLabel';
|
||||
import { ButtonContainer } from '@/components/button-container';
|
||||
import { InputWithLabel } from '@/components/forms/input-with-label';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { useAppParams } from '@/hooks/useAppParams';
|
||||
|
||||
@@ -11,7 +11,6 @@ import { projectRouter } from './routers/project';
|
||||
import { referenceRouter } from './routers/reference';
|
||||
import { reportRouter } from './routers/report';
|
||||
import { shareRouter } from './routers/share';
|
||||
import { uiRouter } from './routers/ui';
|
||||
import { userRouter } from './routers/user';
|
||||
|
||||
/**
|
||||
@@ -29,7 +28,6 @@ export const appRouter = createTRPCRouter({
|
||||
client: clientRouter,
|
||||
event: eventRouter,
|
||||
profile: profileRouter,
|
||||
ui: uiRouter,
|
||||
share: shareRouter,
|
||||
onboarding: onboardingRouter,
|
||||
reference: referenceRouter,
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
import { getDaysOldDate } from '@/utils/date';
|
||||
import { round } from '@/utils/math';
|
||||
import { subDays } from 'date-fns';
|
||||
import * as mathjs from 'mathjs';
|
||||
import { sort } from 'ramda';
|
||||
import { repeat, reverse, sort } from 'ramda';
|
||||
|
||||
import { alphabetIds, NOT_SET_VALUE } from '@openpanel/constants';
|
||||
import { chQuery, convertClickhouseDateToJs, getChartSql } from '@openpanel/db';
|
||||
import {
|
||||
chQuery,
|
||||
convertClickhouseDateToJs,
|
||||
createSqlBuilder,
|
||||
formatClickhouseDate,
|
||||
getChartSql,
|
||||
getEventFiltersWhereClause,
|
||||
} from '@openpanel/db';
|
||||
import type {
|
||||
IChartEvent,
|
||||
IChartInput,
|
||||
@@ -317,7 +324,7 @@ export function getDatesFromRange(range: IChartRange) {
|
||||
let days = 1;
|
||||
|
||||
if (range === '24h') {
|
||||
const startDate = getDaysOldDate(days);
|
||||
const startDate = subDays(new Date(), days);
|
||||
const endDate = new Date();
|
||||
return {
|
||||
startDate: startDate.toUTCString(),
|
||||
@@ -337,7 +344,7 @@ export function getDatesFromRange(range: IChartRange) {
|
||||
days = 365;
|
||||
}
|
||||
|
||||
const startDate = getDaysOldDate(days);
|
||||
const startDate = subDays(new Date(), days);
|
||||
startDate.setUTCHours(0, 0, 0, 0);
|
||||
const endDate = new Date();
|
||||
endDate.setUTCHours(23, 59, 59, 999);
|
||||
@@ -347,10 +354,197 @@ export function getDatesFromRange(range: IChartRange) {
|
||||
};
|
||||
}
|
||||
|
||||
export function getChartStartEndDate(
|
||||
input: Pick<IChartInput, 'endDate' | 'startDate' | 'range'>
|
||||
) {
|
||||
return input.startDate && input.endDate
|
||||
? { startDate: input.startDate, endDate: input.endDate }
|
||||
: getDatesFromRange(input.range);
|
||||
export function getChartStartEndDate({
|
||||
startDate,
|
||||
endDate,
|
||||
range,
|
||||
}: Pick<IChartInput, 'endDate' | 'startDate' | 'range'>) {
|
||||
return startDate && endDate
|
||||
? { startDate: startDate, endDate: endDate }
|
||||
: getDatesFromRange(range);
|
||||
}
|
||||
|
||||
export function getChartPrevStartEndDate({
|
||||
startDate,
|
||||
endDate,
|
||||
range,
|
||||
}: {
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
range: IChartRange;
|
||||
}) {
|
||||
let diff = 0;
|
||||
|
||||
switch (range) {
|
||||
case '30min': {
|
||||
diff = 1000 * 60 * 30;
|
||||
break;
|
||||
}
|
||||
case '1h': {
|
||||
diff = 1000 * 60 * 60;
|
||||
break;
|
||||
}
|
||||
case '24h':
|
||||
case 'today': {
|
||||
diff = 1000 * 60 * 60 * 24;
|
||||
break;
|
||||
}
|
||||
case '7d': {
|
||||
diff = 1000 * 60 * 60 * 24 * 7;
|
||||
break;
|
||||
}
|
||||
case '14d': {
|
||||
diff = 1000 * 60 * 60 * 24 * 14;
|
||||
break;
|
||||
}
|
||||
case '1m': {
|
||||
diff = 1000 * 60 * 60 * 24 * 30;
|
||||
break;
|
||||
}
|
||||
case '3m': {
|
||||
diff = 1000 * 60 * 60 * 24 * 90;
|
||||
break;
|
||||
}
|
||||
case '6m': {
|
||||
diff = 1000 * 60 * 60 * 24 * 180;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
startDate: new Date(new Date(startDate).getTime() - diff).toISOString(),
|
||||
endDate: new Date(new Date(endDate).getTime() - diff).toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
export async function getFunnelData({ projectId, ...payload }: IChartInput) {
|
||||
const { startDate, endDate } = getChartStartEndDate(payload);
|
||||
|
||||
if (payload.events.length === 0) {
|
||||
return {
|
||||
totalSessions: 0,
|
||||
steps: [],
|
||||
};
|
||||
}
|
||||
|
||||
const funnels = payload.events.map((event) => {
|
||||
const { sb, getWhere } = createSqlBuilder();
|
||||
sb.where = getEventFiltersWhereClause(event.filters);
|
||||
sb.where.name = `name = '${event.name}'`;
|
||||
return getWhere().replace('WHERE ', '');
|
||||
});
|
||||
|
||||
const innerSql = `SELECT
|
||||
session_id,
|
||||
windowFunnel(6048000000000000,'strict_increase')(toUnixTimestamp(created_at), ${funnels.join(', ')}) AS level
|
||||
FROM events
|
||||
WHERE (project_id = '${projectId}' AND created_at >= '${formatClickhouseDate(startDate)}') AND (created_at <= '${formatClickhouseDate(endDate)}')
|
||||
GROUP BY session_id`;
|
||||
|
||||
const sql = `SELECT level, count() AS count FROM (${innerSql}) GROUP BY level ORDER BY level DESC`;
|
||||
|
||||
const [funnelRes, sessionRes] = await Promise.all([
|
||||
chQuery<{ level: number; count: number }>(sql),
|
||||
chQuery<{ count: number }>(
|
||||
`SELECT count(name) as count FROM events WHERE project_id = '${projectId}' AND name = 'session_start' AND (created_at >= '${formatClickhouseDate(startDate)}') AND (created_at <= '${formatClickhouseDate(endDate)}')`
|
||||
),
|
||||
]);
|
||||
|
||||
if (funnelRes[0]?.level !== payload.events.length) {
|
||||
funnelRes.unshift({
|
||||
level: payload.events.length,
|
||||
count: 0,
|
||||
});
|
||||
}
|
||||
|
||||
const totalSessions = sessionRes[0]?.count ?? 0;
|
||||
const filledFunnelRes = funnelRes.reduce(
|
||||
(acc, item, index) => {
|
||||
const diff =
|
||||
index !== 0 ? (acc[acc.length - 1]?.level ?? 0) - item.level : 1;
|
||||
|
||||
if (diff > 1) {
|
||||
acc.push(
|
||||
...reverse(
|
||||
repeat({}, diff - 1).map((_, index) => ({
|
||||
count: acc[acc.length - 1]?.count ?? 0,
|
||||
level: item.level + index + 1,
|
||||
}))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return [
|
||||
...acc,
|
||||
{
|
||||
count: item.count + (acc[acc.length - 1]?.count ?? 0),
|
||||
level: item.level,
|
||||
},
|
||||
];
|
||||
},
|
||||
[] as typeof funnelRes
|
||||
);
|
||||
|
||||
const steps = reverse(filledFunnelRes)
|
||||
.filter((item) => item.level !== 0)
|
||||
.reduce(
|
||||
(acc, item, index, list) => {
|
||||
const prev = list[index - 1] ?? { count: totalSessions };
|
||||
return [
|
||||
...acc,
|
||||
{
|
||||
event: payload.events[item.level - 1]!,
|
||||
before: prev.count,
|
||||
current: item.count,
|
||||
dropoff: {
|
||||
count: prev.count - item.count,
|
||||
percent: 100 - (item.count / prev.count) * 100,
|
||||
},
|
||||
percent: (item.count / totalSessions) * 100,
|
||||
prevPercent: (prev.count / totalSessions) * 100,
|
||||
},
|
||||
];
|
||||
},
|
||||
[] as {
|
||||
event: IChartEvent;
|
||||
before: number;
|
||||
current: number;
|
||||
dropoff: {
|
||||
count: number;
|
||||
percent: number;
|
||||
};
|
||||
percent: number;
|
||||
prevPercent: number;
|
||||
}[]
|
||||
);
|
||||
|
||||
return {
|
||||
totalSessions,
|
||||
steps,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getSeriesFromEvents(input: IChartInput) {
|
||||
const { startDate, endDate } =
|
||||
input.startDate && input.endDate
|
||||
? {
|
||||
startDate: input.startDate,
|
||||
endDate: input.endDate,
|
||||
}
|
||||
: getDatesFromRange(input.range);
|
||||
|
||||
const series = (
|
||||
await Promise.all(
|
||||
input.events.map(async (event) =>
|
||||
getChartData({
|
||||
...input,
|
||||
startDate,
|
||||
endDate,
|
||||
event,
|
||||
})
|
||||
)
|
||||
)
|
||||
).flat();
|
||||
|
||||
return withFormula(input, series);
|
||||
}
|
||||
|
||||
@@ -4,132 +4,20 @@ import {
|
||||
publicProcedure,
|
||||
} from '@/server/api/trpc';
|
||||
import { average, max, min, round, sum } from '@/utils/math';
|
||||
import { flatten, map, pipe, prop, repeat, reverse, sort, uniq } from 'ramda';
|
||||
import { flatten, map, pipe, prop, sort, uniq } from 'ramda';
|
||||
import { z } from 'zod';
|
||||
|
||||
import {
|
||||
chQuery,
|
||||
createSqlBuilder,
|
||||
formatClickhouseDate,
|
||||
getEventFiltersWhereClause,
|
||||
} from '@openpanel/db';
|
||||
import { chQuery, createSqlBuilder } from '@openpanel/db';
|
||||
import { zChartInput } from '@openpanel/validation';
|
||||
import type { IChartEvent, IChartInput } from '@openpanel/validation';
|
||||
|
||||
import {
|
||||
getChartData,
|
||||
getChartPrevStartEndDate,
|
||||
getChartStartEndDate,
|
||||
getDatesFromRange,
|
||||
withFormula,
|
||||
getFunnelData,
|
||||
getSeriesFromEvents,
|
||||
} from './chart.helpers';
|
||||
|
||||
async function getFunnelData({ projectId, ...payload }: IChartInput) {
|
||||
const { startDate, endDate } = getChartStartEndDate(payload);
|
||||
|
||||
if (payload.events.length === 0) {
|
||||
return {
|
||||
totalSessions: 0,
|
||||
steps: [],
|
||||
};
|
||||
}
|
||||
|
||||
const funnels = payload.events.map((event) => {
|
||||
const { sb, getWhere } = createSqlBuilder();
|
||||
sb.where = getEventFiltersWhereClause(event.filters);
|
||||
sb.where.name = `name = '${event.name}'`;
|
||||
return getWhere().replace('WHERE ', '');
|
||||
});
|
||||
|
||||
const innerSql = `SELECT
|
||||
session_id,
|
||||
windowFunnel(6048000000000000,'strict_increase')(toUnixTimestamp(created_at), ${funnels.join(', ')}) AS level
|
||||
FROM events
|
||||
WHERE (project_id = '${projectId}' AND created_at >= '${formatClickhouseDate(startDate)}') AND (created_at <= '${formatClickhouseDate(endDate)}')
|
||||
GROUP BY session_id`;
|
||||
|
||||
const sql = `SELECT level, count() AS count FROM (${innerSql}) GROUP BY level ORDER BY level DESC`;
|
||||
|
||||
const [funnelRes, sessionRes] = await Promise.all([
|
||||
chQuery<{ level: number; count: number }>(sql),
|
||||
chQuery<{ count: number }>(
|
||||
`SELECT count(name) as count FROM events WHERE project_id = '${projectId}' AND name = 'session_start' AND (created_at >= '${formatClickhouseDate(startDate)}') AND (created_at <= '${formatClickhouseDate(endDate)}')`
|
||||
),
|
||||
]);
|
||||
|
||||
if (funnelRes[0]?.level !== payload.events.length) {
|
||||
funnelRes.unshift({
|
||||
level: payload.events.length,
|
||||
count: 0,
|
||||
});
|
||||
}
|
||||
|
||||
const totalSessions = sessionRes[0]?.count ?? 0;
|
||||
const filledFunnelRes = funnelRes.reduce(
|
||||
(acc, item, index) => {
|
||||
const diff =
|
||||
index !== 0 ? (acc[acc.length - 1]?.level ?? 0) - item.level : 1;
|
||||
|
||||
if (diff > 1) {
|
||||
acc.push(
|
||||
...reverse(
|
||||
repeat({}, diff - 1).map((_, index) => ({
|
||||
count: acc[acc.length - 1]?.count ?? 0,
|
||||
level: item.level + index + 1,
|
||||
}))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return [
|
||||
...acc,
|
||||
{
|
||||
count: item.count + (acc[acc.length - 1]?.count ?? 0),
|
||||
level: item.level,
|
||||
},
|
||||
];
|
||||
},
|
||||
[] as typeof funnelRes
|
||||
);
|
||||
|
||||
const steps = reverse(filledFunnelRes)
|
||||
.filter((item) => item.level !== 0)
|
||||
.reduce(
|
||||
(acc, item, index, list) => {
|
||||
const prev = list[index - 1] ?? { count: totalSessions };
|
||||
return [
|
||||
...acc,
|
||||
{
|
||||
event: payload.events[item.level - 1]!,
|
||||
before: prev.count,
|
||||
current: item.count,
|
||||
dropoff: {
|
||||
count: prev.count - item.count,
|
||||
percent: 100 - (item.count / prev.count) * 100,
|
||||
},
|
||||
percent: (item.count / totalSessions) * 100,
|
||||
prevPercent: (prev.count / totalSessions) * 100,
|
||||
},
|
||||
];
|
||||
},
|
||||
[] as {
|
||||
event: IChartEvent;
|
||||
before: number;
|
||||
current: number;
|
||||
dropoff: {
|
||||
count: number;
|
||||
percent: number;
|
||||
};
|
||||
percent: number;
|
||||
prevPercent: number;
|
||||
}[]
|
||||
);
|
||||
|
||||
return {
|
||||
totalSessions,
|
||||
steps,
|
||||
};
|
||||
}
|
||||
|
||||
type PreviousValue = {
|
||||
value: number;
|
||||
diff: number | null;
|
||||
@@ -243,7 +131,7 @@ export const chartRouter = createTRPCRouter({
|
||||
.replace(/^properties\./, '')
|
||||
.replace('.*.', '.%.')}')) as values`;
|
||||
} else {
|
||||
sb.select.values = `${property} as values`;
|
||||
sb.select.values = `distinct ${property} as values`;
|
||||
}
|
||||
|
||||
const events = await chQuery<{ values: string[] }>(getSql());
|
||||
@@ -266,57 +154,19 @@ export const chartRouter = createTRPCRouter({
|
||||
|
||||
// TODO: Make this private
|
||||
chart: publicProcedure.input(zChartInput).query(async ({ input }) => {
|
||||
const { startDate, endDate } = getChartStartEndDate(input);
|
||||
let diff = 0;
|
||||
const currentPeriod = getChartStartEndDate(input);
|
||||
const previousPeriod = getChartPrevStartEndDate({
|
||||
range: input.range,
|
||||
...currentPeriod,
|
||||
});
|
||||
|
||||
switch (input.range) {
|
||||
case '30min': {
|
||||
diff = 1000 * 60 * 30;
|
||||
break;
|
||||
}
|
||||
case '1h': {
|
||||
diff = 1000 * 60 * 60;
|
||||
break;
|
||||
}
|
||||
case '24h':
|
||||
case 'today': {
|
||||
diff = 1000 * 60 * 60 * 24;
|
||||
break;
|
||||
}
|
||||
case '7d': {
|
||||
diff = 1000 * 60 * 60 * 24 * 7;
|
||||
break;
|
||||
}
|
||||
case '14d': {
|
||||
diff = 1000 * 60 * 60 * 24 * 14;
|
||||
break;
|
||||
}
|
||||
case '1m': {
|
||||
diff = 1000 * 60 * 60 * 24 * 30;
|
||||
break;
|
||||
}
|
||||
case '3m': {
|
||||
diff = 1000 * 60 * 60 * 24 * 90;
|
||||
break;
|
||||
}
|
||||
case '6m': {
|
||||
diff = 1000 * 60 * 60 * 24 * 180;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const promises = [getSeriesFromEvents(input)];
|
||||
const promises = [getSeriesFromEvents({ ...input, ...currentPeriod })];
|
||||
|
||||
if (input.previous) {
|
||||
promises.push(
|
||||
getSeriesFromEvents({
|
||||
...input,
|
||||
...{
|
||||
startDate: new Date(
|
||||
new Date(startDate).getTime() - diff
|
||||
).toISOString(),
|
||||
endDate: new Date(new Date(endDate).getTime() - diff).toISOString(),
|
||||
},
|
||||
...previousPeriod,
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -407,11 +257,11 @@ export const chartRouter = createTRPCRouter({
|
||||
final.metrics.max = max(final.series.map((item) => item.metrics.max));
|
||||
final.metrics.previous = {
|
||||
sum: getPreviousMetric(
|
||||
sum(final.series.map((item) => item.metrics.sum)),
|
||||
final.metrics.sum,
|
||||
sum(final.series.map((item) => item.metrics.previous.sum?.value ?? 0))
|
||||
),
|
||||
average: getPreviousMetric(
|
||||
round(average(final.series.map((item) => item.metrics.average)), 2),
|
||||
final.metrics.average,
|
||||
round(
|
||||
average(
|
||||
final.series.map(
|
||||
@@ -422,15 +272,16 @@ export const chartRouter = createTRPCRouter({
|
||||
)
|
||||
),
|
||||
min: getPreviousMetric(
|
||||
min(final.series.map((item) => item.metrics.min)),
|
||||
final.metrics.min,
|
||||
min(final.series.map((item) => item.metrics.previous.min?.value ?? 0))
|
||||
),
|
||||
max: getPreviousMetric(
|
||||
max(final.series.map((item) => item.metrics.max)),
|
||||
final.metrics.max,
|
||||
max(final.series.map((item) => item.metrics.previous.max?.value ?? 0))
|
||||
),
|
||||
};
|
||||
|
||||
// Sort by sum
|
||||
final.series = final.series.sort((a, b) => {
|
||||
if (input.chartType === 'linear') {
|
||||
const sumA = a.data.reduce((acc, item) => acc + (item.count ?? 0), 0);
|
||||
@@ -441,16 +292,11 @@ export const chartRouter = createTRPCRouter({
|
||||
}
|
||||
});
|
||||
|
||||
// await new Promise((res) => {
|
||||
// setTimeout(() => {
|
||||
// res();
|
||||
// }, 100);
|
||||
// });
|
||||
return final;
|
||||
}),
|
||||
});
|
||||
|
||||
function getPreviousMetric(
|
||||
export function getPreviousMetric(
|
||||
current: number,
|
||||
previous: number | null
|
||||
): PreviousValue {
|
||||
@@ -483,28 +329,3 @@ function getPreviousMetric(
|
||||
value: previous,
|
||||
};
|
||||
}
|
||||
|
||||
async function getSeriesFromEvents(input: IChartInput) {
|
||||
const { startDate, endDate } =
|
||||
input.startDate && input.endDate
|
||||
? {
|
||||
startDate: input.startDate,
|
||||
endDate: input.endDate,
|
||||
}
|
||||
: getDatesFromRange(input.range);
|
||||
|
||||
const series = (
|
||||
await Promise.all(
|
||||
input.events.map(async (event) =>
|
||||
getChartData({
|
||||
...input,
|
||||
startDate,
|
||||
endDate,
|
||||
event,
|
||||
})
|
||||
)
|
||||
)
|
||||
).flat();
|
||||
|
||||
return withFormula(input, series);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { randomUUID } from 'crypto';
|
||||
import { createTRPCRouter, protectedProcedure } from '@/server/api/trpc';
|
||||
import { db } from '@/server/db';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { hashPassword, stripTrailingSlash } from '@openpanel/common';
|
||||
import { db, transformClient } from '@openpanel/db';
|
||||
|
||||
export const clientRouter = createTRPCRouter({
|
||||
list: protectedProcedure
|
||||
@@ -76,7 +76,7 @@ export const clientRouter = createTRPCRouter({
|
||||
});
|
||||
|
||||
return {
|
||||
...client,
|
||||
...transformClient(client),
|
||||
secret: input.cors ? null : secret,
|
||||
};
|
||||
}),
|
||||
|
||||
@@ -1,60 +1,20 @@
|
||||
import { createTRPCRouter, protectedProcedure } from '@/server/api/trpc';
|
||||
import { db, getId } from '@/server/db';
|
||||
import { getId } from '@/utils/getDbId';
|
||||
import { PrismaError } from 'prisma-error-enum';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { db, getDashboardsByProjectId } from '@openpanel/db';
|
||||
import type { Prisma } from '@openpanel/db';
|
||||
|
||||
export const dashboardRouter = createTRPCRouter({
|
||||
get: protectedProcedure
|
||||
.input(z.object({ id: z.string() }))
|
||||
.query(async ({ input }) => {
|
||||
return db.dashboard.findUnique({
|
||||
where: {
|
||||
id: input.id,
|
||||
},
|
||||
});
|
||||
}),
|
||||
list: protectedProcedure
|
||||
.input(
|
||||
z
|
||||
.object({
|
||||
projectId: z.string(),
|
||||
})
|
||||
.or(
|
||||
z.object({
|
||||
organizationId: z.string(),
|
||||
})
|
||||
)
|
||||
z.object({
|
||||
projectId: z.string(),
|
||||
})
|
||||
)
|
||||
.query(async ({ input }) => {
|
||||
if ('projectId' in input) {
|
||||
return db.dashboard.findMany({
|
||||
where: {
|
||||
project_id: input.projectId,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
include: {
|
||||
project: true,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
return db.dashboard.findMany({
|
||||
where: {
|
||||
project: {
|
||||
organization_slug: input.organizationId,
|
||||
},
|
||||
},
|
||||
include: {
|
||||
project: true,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
});
|
||||
}
|
||||
return getDashboardsByProjectId(input.projectId);
|
||||
}),
|
||||
create: protectedProcedure
|
||||
.input(
|
||||
|
||||
@@ -4,7 +4,12 @@ import { clerkClient } from '@clerk/nextjs';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { hashPassword, stripTrailingSlash } from '@openpanel/common';
|
||||
import { db } from '@openpanel/db';
|
||||
import {
|
||||
db,
|
||||
transformClient,
|
||||
transformOrganization,
|
||||
transformProject,
|
||||
} from '@openpanel/db';
|
||||
|
||||
export const onboardingRouter = createTRPCRouter({
|
||||
organziation: protectedProcedure
|
||||
@@ -41,12 +46,12 @@ export const onboardingRouter = createTRPCRouter({
|
||||
});
|
||||
|
||||
return {
|
||||
client: {
|
||||
client: transformClient({
|
||||
...client,
|
||||
secret: input.cors ? null : secret,
|
||||
},
|
||||
project,
|
||||
organization: org,
|
||||
}),
|
||||
project: transformProject(project),
|
||||
organization: transformOrganization(org),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ export const organizationRouter = createTRPCRouter({
|
||||
list: protectedProcedure.query(() => {
|
||||
return clerkClient.organizations.getOrganizationList();
|
||||
}),
|
||||
// first: protectedProcedure.query(() => getCurrentOrganization()),
|
||||
get: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
|
||||
@@ -3,71 +3,12 @@ import {
|
||||
protectedProcedure,
|
||||
publicProcedure,
|
||||
} from '@/server/api/trpc';
|
||||
import { db } from '@/server/db';
|
||||
import { flatten, map, pipe, prop, sort, uniq } from 'ramda';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { chQuery, createSqlBuilder } from '@openpanel/db';
|
||||
|
||||
export const profileRouter = createTRPCRouter({
|
||||
list: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
query: z.string().nullable(),
|
||||
projectId: z.string(),
|
||||
take: z.number().default(100),
|
||||
skip: z.number().default(0),
|
||||
})
|
||||
)
|
||||
.query(async ({ input: { take, skip, projectId, query } }) => {
|
||||
return db.profile.findMany({
|
||||
take,
|
||||
skip,
|
||||
where: {
|
||||
project_id: projectId,
|
||||
...(query
|
||||
? {
|
||||
OR: [
|
||||
{
|
||||
first_name: {
|
||||
contains: query,
|
||||
mode: 'insensitive',
|
||||
},
|
||||
},
|
||||
{
|
||||
last_name: {
|
||||
contains: query,
|
||||
mode: 'insensitive',
|
||||
},
|
||||
},
|
||||
{
|
||||
email: {
|
||||
contains: query,
|
||||
mode: 'insensitive',
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
});
|
||||
}),
|
||||
get: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
})
|
||||
)
|
||||
.query(async ({ input: { id } }) => {
|
||||
return db.profile.findUniqueOrThrow({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
}),
|
||||
properties: protectedProcedure
|
||||
.input(z.object({ projectId: z.string() }))
|
||||
.query(async ({ input: { projectId } }) => {
|
||||
@@ -88,6 +29,7 @@ export const profileRouter = createTRPCRouter({
|
||||
uniq
|
||||
)(properties);
|
||||
}),
|
||||
|
||||
values: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { createTRPCRouter, protectedProcedure } from '@/server/api/trpc';
|
||||
import { db, getId } from '@/server/db';
|
||||
import { slug } from '@/utils/slug';
|
||||
import { getId } from '@/utils/getDbId';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { db, getProjectsByOrganizationSlug } from '@openpanel/db';
|
||||
|
||||
export const projectRouter = createTRPCRouter({
|
||||
list: protectedProcedure
|
||||
.input(
|
||||
@@ -12,26 +13,9 @@ export const projectRouter = createTRPCRouter({
|
||||
)
|
||||
.query(async ({ input: { organizationId } }) => {
|
||||
if (organizationId === null) return [];
|
||||
return getProjectsByOrganizationSlug(organizationId);
|
||||
}),
|
||||
|
||||
return db.project.findMany({
|
||||
where: {
|
||||
organization_slug: organizationId,
|
||||
},
|
||||
});
|
||||
}),
|
||||
get: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
})
|
||||
)
|
||||
.query(({ input: { id } }) => {
|
||||
return db.project.findUniqueOrThrow({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
}),
|
||||
update: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
|
||||
@@ -1,57 +1,11 @@
|
||||
import { createTRPCRouter, protectedProcedure } from '@/server/api/trpc';
|
||||
import { db } from '@/server/db';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { transformReport } from '@openpanel/db';
|
||||
import { db } from '@openpanel/db';
|
||||
import { zChartInput } from '@openpanel/validation';
|
||||
|
||||
export const reportRouter = createTRPCRouter({
|
||||
get: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
})
|
||||
)
|
||||
.query(({ input: { id } }) => {
|
||||
return db.report
|
||||
.findUniqueOrThrow({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
})
|
||||
.then(transformReport);
|
||||
}),
|
||||
list: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
projectId: z.string(),
|
||||
dashboardId: z.string(),
|
||||
})
|
||||
)
|
||||
.query(async ({ input: { projectId, dashboardId } }) => {
|
||||
const [dashboard, reports] = await db.$transaction([
|
||||
db.dashboard.findUniqueOrThrow({
|
||||
where: {
|
||||
id: dashboardId,
|
||||
},
|
||||
}),
|
||||
db.report.findMany({
|
||||
where: {
|
||||
project_id: projectId,
|
||||
dashboard_id: dashboardId,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
return {
|
||||
reports: reports.map(transformReport),
|
||||
dashboard,
|
||||
};
|
||||
}),
|
||||
save: protectedProcedure
|
||||
create: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
report: zChartInput.omit({ projectId: true }),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createTRPCRouter, protectedProcedure } from '@/server/api/trpc';
|
||||
import { db } from '@/server/db';
|
||||
import ShortUniqueId from 'short-unique-id';
|
||||
|
||||
import { db } from '@openpanel/db';
|
||||
import { zShareOverview } from '@openpanel/validation';
|
||||
|
||||
const uid = new ShortUniqueId({ length: 6 });
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import { createTRPCRouter, protectedProcedure } from '@/server/api/trpc';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const uiRouter = createTRPCRouter({
|
||||
breadcrumbs: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
url: z.string(),
|
||||
})
|
||||
)
|
||||
.query(({ input: { url } }) => {
|
||||
const parts = url.split('/').filter(Boolean);
|
||||
return parts;
|
||||
}),
|
||||
});
|
||||
@@ -1,3 +0,0 @@
|
||||
export async function getSession() {
|
||||
return true;
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
const cache = new Map<
|
||||
string,
|
||||
{
|
||||
expires: number;
|
||||
data: any;
|
||||
}
|
||||
>();
|
||||
|
||||
export function get(key: string) {
|
||||
const hit = cache.get(key);
|
||||
if (hit) {
|
||||
if (hit.expires > Date.now()) {
|
||||
return hit.data;
|
||||
}
|
||||
cache.delete(key);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function set(key: string, expires: number, data: any) {
|
||||
cache.set(key, {
|
||||
expires: Date.now() + expires,
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
export async function getOr<T>(
|
||||
key: string,
|
||||
expires: number,
|
||||
fn: () => Promise<T>
|
||||
): Promise<T> {
|
||||
const hit = get(key);
|
||||
if (hit) {
|
||||
return hit;
|
||||
}
|
||||
const data = await fn();
|
||||
set(key, expires, data);
|
||||
return data;
|
||||
}
|
||||
@@ -126,44 +126,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.resizer {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
width: 5px;
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
cursor: col-resize;
|
||||
user-select: none;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.resizer.isResizing {
|
||||
@apply bg-black;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
.resizer {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
*:hover > .resizer {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* For Webkit-based browsers (Chrome, Safari and Opera) */
|
||||
.scrollbar-hide::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* For IE, Edge and Firefox */
|
||||
.scrollbar-hide {
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
}
|
||||
|
||||
/* Rechart */
|
||||
|
||||
.recharts-wrapper .recharts-cartesian-grid-horizontal line:first-child,
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
export function getDaysOldDate(days: number) {
|
||||
const date = new Date();
|
||||
date.setDate(date.getDate() - days);
|
||||
return date;
|
||||
}
|
||||
|
||||
export function dateDifferanceInDays(date1: Date, date2: Date) {
|
||||
const diffTime = Math.abs(date2.getTime() - date1.getTime());
|
||||
return Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { slug } from '@/utils/slug';
|
||||
|
||||
import { db } from '@openpanel/db';
|
||||
|
||||
export { db } from '@openpanel/db';
|
||||
import { slug } from './slug';
|
||||
|
||||
export async function getId(tableName: 'project' | 'dashboard', name: string) {
|
||||
const newId = slug(name);
|
||||
@@ -15,7 +13,7 @@ export async function getId(tableName: 'project' | 'dashboard', name: string) {
|
||||
}
|
||||
|
||||
// @ts-expect-error
|
||||
const existingProject = await db[tableName]!.findUnique({
|
||||
const existingProject = await db[tableName].findUnique({
|
||||
where: {
|
||||
id: newId,
|
||||
},
|
||||
@@ -1,6 +0,0 @@
|
||||
import type { IServiceProfile } from '@openpanel/db';
|
||||
|
||||
export function getProfileName(profile: IServiceProfile | undefined | null) {
|
||||
if (!profile) return 'No profile';
|
||||
return [profile.first_name, profile.last_name].filter(Boolean).join(' ');
|
||||
}
|
||||
@@ -1,14 +1,22 @@
|
||||
import type { Client } from '../prisma-client';
|
||||
import { db } from '../prisma-client';
|
||||
import { transformProject } from './project.service';
|
||||
import type { IServiceProject } from './project.service';
|
||||
|
||||
export type IServiceClient = Client;
|
||||
export type IServiceClientWithProject = Client & {
|
||||
export type IServiceClient = ReturnType<typeof transformClient>;
|
||||
export type IServiceClientWithProject = IServiceClient & {
|
||||
project: Exclude<IServiceProject, null>;
|
||||
};
|
||||
|
||||
export function getClientsByOrganizationId(organizationId: string) {
|
||||
return db.client.findMany({
|
||||
export function transformClient({ organization_slug, ...client }: Client) {
|
||||
return {
|
||||
...client,
|
||||
organizationSlug: organization_slug,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getClientsByOrganizationId(organizationId: string) {
|
||||
const clients = await db.client.findMany({
|
||||
where: {
|
||||
organization_slug: organizationId,
|
||||
},
|
||||
@@ -16,4 +24,15 @@ export function getClientsByOrganizationId(organizationId: string) {
|
||||
project: true,
|
||||
},
|
||||
});
|
||||
|
||||
return clients
|
||||
.map((client) => {
|
||||
return {
|
||||
...transformClient(client),
|
||||
project: transformProject(client.project),
|
||||
};
|
||||
})
|
||||
.filter(
|
||||
(client): client is IServiceClientWithProject => client.project !== null
|
||||
);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user