well deserved clean up (#1)
This commit is contained in:
@@ -48,6 +48,7 @@
|
|||||||
"@trpc/server": "^10.45.1",
|
"@trpc/server": "^10.45.1",
|
||||||
"@types/d3": "^7.4.3",
|
"@types/d3": "^7.4.3",
|
||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
|
"bind-event-listener": "^3.0.0",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.0",
|
"clsx": "^2.1.0",
|
||||||
"cmdk": "^0.2.1",
|
"cmdk": "^0.2.1",
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { api, handleErrorToastOptions } from '@/app/_trpc/client';
|
import { api, handleErrorToastOptions } from '@/app/_trpc/client';
|
||||||
import { Card, CardActions, CardActionsItem } from '@/components/Card';
|
import { Card, CardActions, CardActionsItem } from '@/components/card';
|
||||||
import { FullPageEmptyState } from '@/components/FullPageEmptyState';
|
import { FullPageEmptyState } from '@/components/full-page-empty-state';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { ToastAction } from '@/components/ui/toast';
|
|
||||||
import { useAppParams } from '@/hooks/useAppParams';
|
import { useAppParams } from '@/hooks/useAppParams';
|
||||||
import { pushModal } from '@/modals';
|
import { pushModal } from '@/modals';
|
||||||
import { LayoutPanelTopIcon, Pencil, PlusIcon, Trash } from 'lucide-react';
|
import { LayoutPanelTopIcon, Pencil, PlusIcon, Trash } from 'lucide-react';
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Fragment } from 'react';
|
import { Fragment } from 'react';
|
||||||
import { Widget, WidgetHead } from '@/components/Widget';
|
import { Widget, WidgetHead } from '@/components/widget';
|
||||||
import { isSameDay } from 'date-fns';
|
import { isSameDay } from 'date-fns';
|
||||||
|
|
||||||
import type { IServiceCreateEventPayload } from '@openpanel/db';
|
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';
|
import { db, getEvents } from '@openpanel/db';
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Fragment, Suspense } from 'react';
|
import { Fragment, Suspense } from 'react';
|
||||||
import { FullPageEmptyState } from '@/components/FullPageEmptyState';
|
import { FullPageEmptyState } from '@/components/full-page-empty-state';
|
||||||
import { Pagination } from '@/components/Pagination';
|
import { Pagination } from '@/components/pagination';
|
||||||
import { ChartSwitch, ChartSwitchShortcut } from '@/components/report/chart';
|
import { ChartSwitch, ChartSwitchShortcut } from '@/components/report/chart';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { useAppParams } from '@/hooks/useAppParams';
|
import { useAppParams } from '@/hooks/useAppParams';
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Logo } from '@/components/Logo';
|
import { Logo } from '@/components/logo';
|
||||||
import { buttonVariants } from '@/components/ui/button';
|
import { buttonVariants } from '@/components/ui/button';
|
||||||
import { cn } from '@/utils/cn';
|
import { cn } from '@/utils/cn';
|
||||||
import { Rotate as Hamburger } from 'hamburger-react';
|
import { Rotate as Hamburger } from 'hamburger-react';
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
import PageLayout from '@/app/(app)/[organizationId]/[projectId]/page-layout';
|
import PageLayout from '@/app/(app)/[organizationId]/[projectId]/page-layout';
|
||||||
import { OverviewFiltersButtons } from '@/components/overview/filters/overview-filters-buttons';
|
import { OverviewFiltersButtons } from '@/components/overview/filters/overview-filters-buttons';
|
||||||
import { OverviewFiltersDrawer } from '@/components/overview/filters/overview-filters-drawer';
|
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 { ChartSwitch } from '@/components/report/chart';
|
||||||
import { SerieIcon } from '@/components/report/chart/SerieIcon';
|
import { SerieIcon } from '@/components/report/chart/SerieIcon';
|
||||||
import { Widget, WidgetBody, WidgetHead } from '@/components/Widget';
|
import { Widget, WidgetBody, WidgetHead } from '@/components/widget';
|
||||||
import {
|
import {
|
||||||
eventQueryFiltersParser,
|
eventQueryFiltersParser,
|
||||||
eventQueryNamesFilter,
|
eventQueryNamesFilter,
|
||||||
} from '@/hooks/useEventQueryFilters';
|
} from '@/hooks/useEventQueryFilters';
|
||||||
import { getExists } from '@/server/pageExists';
|
import { getExists } from '@/server/pageExists';
|
||||||
import { getProfileName } from '@/utils/getters';
|
|
||||||
import { notFound } from 'next/navigation';
|
import { notFound } from 'next/navigation';
|
||||||
import { parseAsInteger, parseAsString } from 'nuqs';
|
import { parseAsInteger, parseAsString } from 'nuqs';
|
||||||
|
|
||||||
@@ -20,6 +19,7 @@ import {
|
|||||||
getEventList,
|
getEventList,
|
||||||
getEventsCount,
|
getEventsCount,
|
||||||
getProfileById,
|
getProfileById,
|
||||||
|
getProfileName,
|
||||||
} from '@openpanel/db';
|
} from '@openpanel/db';
|
||||||
import type { IChartEvent, IChartInput } from '@openpanel/validation';
|
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,
|
TooltipContent,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from '@/components/ui/tooltip';
|
} from '@/components/ui/tooltip';
|
||||||
import { Widget, WidgetBody, WidgetHead } from '@/components/Widget';
|
import { Widget, WidgetBody, WidgetHead } from '@/components/widget';
|
||||||
import { cn } from '@/utils/cn';
|
import { cn } from '@/utils/cn';
|
||||||
|
|
||||||
import { chQuery } from '@openpanel/db';
|
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';
|
'use client';
|
||||||
|
|
||||||
import { ListPropertiesIcon } from '@/components/events/ListPropertiesIcon';
|
import { ListPropertiesIcon } from '@/components/events/list-properties-icon';
|
||||||
import { FullPageEmptyState } from '@/components/FullPageEmptyState';
|
import { FullPageEmptyState } from '@/components/full-page-empty-state';
|
||||||
import { Pagination } from '@/components/Pagination';
|
import { Pagination } from '@/components/pagination';
|
||||||
import { ProfileAvatar } from '@/components/profiles/ProfileAvatar';
|
import { ProfileAvatar } from '@/components/profiles/profile-avatar';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Tooltiper } from '@/components/ui/tooltip';
|
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 { WidgetTable } from '@/components/widget-table';
|
||||||
import { useAppParams } from '@/hooks/useAppParams';
|
import { useAppParams } from '@/hooks/useAppParams';
|
||||||
import { useCursor } from '@/hooks/useCursor';
|
import { useCursor } from '@/hooks/useCursor';
|
||||||
import { UsersIcon } from 'lucide-react';
|
import { UsersIcon } from 'lucide-react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
|
import { getProfileName } from '@openpanel/db';
|
||||||
import type { IServiceProfile } from '@openpanel/db';
|
import type { IServiceProfile } from '@openpanel/db';
|
||||||
|
|
||||||
interface ProfileListProps {
|
interface ProfileListProps {
|
||||||
@@ -49,7 +50,7 @@ export function ProfileList({ data, count }: ProfileListProps) {
|
|||||||
className="flex gap-2 items-center font-medium"
|
className="flex gap-2 items-center font-medium"
|
||||||
>
|
>
|
||||||
<ProfileAvatar size="sm" {...profile} />
|
<ProfileAvatar size="sm" {...profile} />
|
||||||
{profile.firstName} {profile.lastName}
|
{getProfileName(profile)}
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { ListPropertiesIcon } from '@/components/events/ListPropertiesIcon';
|
import { ListPropertiesIcon } from '@/components/events/list-properties-icon';
|
||||||
import { ProfileAvatar } from '@/components/profiles/ProfileAvatar';
|
import { ProfileAvatar } from '@/components/profiles/profile-avatar';
|
||||||
import { Widget, WidgetHead } from '@/components/Widget';
|
import { Widget, WidgetHead } from '@/components/widget';
|
||||||
import { WidgetTable } from '@/components/widget-table';
|
import { WidgetTable } from '@/components/widget-table';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
import { chQuery, getProfiles } from '@openpanel/db';
|
import { chQuery, getProfileName, getProfiles } from '@openpanel/db';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
@@ -24,7 +24,7 @@ export default async function ProfileTopServer({
|
|||||||
const list = res.map((item) => {
|
const list = res.map((item) => {
|
||||||
return {
|
return {
|
||||||
count: item.count,
|
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>
|
</WidgetHead>
|
||||||
<WidgetTable
|
<WidgetTable
|
||||||
data={list.filter((item) => !!item.id)}
|
data={list.filter((item) => !!item.id)}
|
||||||
keyExtractor={(item) => item.id!}
|
keyExtractor={(item) => item.id}
|
||||||
columns={[
|
columns={[
|
||||||
{
|
{
|
||||||
name: 'Name',
|
name: 'Name',
|
||||||
@@ -46,7 +46,7 @@ export default async function ProfileTopServer({
|
|||||||
className="flex gap-2 items-center font-medium"
|
className="flex gap-2 items-center font-medium"
|
||||||
>
|
>
|
||||||
<ProfileAvatar size="sm" {...profile} />
|
<ProfileAvatar size="sm" {...profile} />
|
||||||
{profile.firstName} {profile.lastName}
|
{getProfileName(profile)}
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,9 +2,8 @@
|
|||||||
|
|
||||||
import { StickyBelowHeader } from '@/app/(app)/[organizationId]/[projectId]/layout-sticky-below-header';
|
import { StickyBelowHeader } from '@/app/(app)/[organizationId]/[projectId]/layout-sticky-below-header';
|
||||||
import { columns } from '@/components/clients/table';
|
import { columns } from '@/components/clients/table';
|
||||||
import { DataTable } from '@/components/DataTable';
|
import { DataTable } from '@/components/data-table';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { useAppParams } from '@/hooks/useAppParams';
|
|
||||||
import { pushModal } from '@/modals';
|
import { pushModal } from '@/modals';
|
||||||
import { PlusIcon } from 'lucide-react';
|
import { PlusIcon } from 'lucide-react';
|
||||||
|
|
||||||
@@ -14,17 +13,12 @@ interface ListClientsProps {
|
|||||||
clients: Awaited<ReturnType<typeof getClientsByOrganizationId>>;
|
clients: Awaited<ReturnType<typeof getClientsByOrganizationId>>;
|
||||||
}
|
}
|
||||||
export default function ListClients({ clients }: ListClientsProps) {
|
export default function ListClients({ clients }: ListClientsProps) {
|
||||||
const organizationId = useAppParams().organizationId;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<StickyBelowHeader>
|
<StickyBelowHeader>
|
||||||
<div className="p-4 flex items-center justify-between">
|
<div className="p-4 flex items-center justify-between">
|
||||||
<div />
|
<div />
|
||||||
<Button
|
<Button icon={PlusIcon} onClick={() => pushModal('AddClient')}>
|
||||||
icon={PlusIcon}
|
|
||||||
onClick={() => pushModal('AddClient', { organizationId })}
|
|
||||||
>
|
|
||||||
<span className="max-sm:hidden">Create client</span>
|
<span className="max-sm:hidden">Create client</span>
|
||||||
<span className="sm:hidden">Client</span>
|
<span className="sm:hidden">Client</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { api, handleError } from '@/app/_trpc/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 { 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 { useRouter } from 'next/navigation';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { api } from '@/app/_trpc/client';
|
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 { Button } from '@/components/ui/button';
|
||||||
import { useAppParams } from '@/hooks/useAppParams';
|
import { useAppParams } from '@/hooks/useAppParams';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from '@/components/ui/table';
|
} from '@/components/ui/table';
|
||||||
import { Widget, WidgetBody, WidgetHead } from '@/components/Widget';
|
import { Widget, WidgetBody, WidgetHead } from '@/components/widget';
|
||||||
|
|
||||||
import type { IServiceInvites } from '@openpanel/db';
|
import type { IServiceInvites } from '@openpanel/db';
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { api, handleError } from '@/app/_trpc/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 { 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 { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { buttonVariants } from '@/components/ui/button';
|
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';
|
import { SignOutButton } from '@clerk/nextjs';
|
||||||
|
|
||||||
export function Logout() {
|
export function Logout() {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { StickyBelowHeader } from '@/app/(app)/[organizationId]/[projectId]/layout-sticky-below-header';
|
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 { columns } from '@/components/projects/table';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { useAppParams } from '@/hooks/useAppParams';
|
import { useAppParams } from '@/hooks/useAppParams';
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { StickyBelowHeader } from '@/app/(app)/[organizationId]/[projectId]/layout-sticky-below-header';
|
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 { columns } from '@/components/references/table';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { pushModal } from '@/modals';
|
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';
|
'use client';
|
||||||
|
|
||||||
import { LogoSquare } from '@/components/Logo';
|
import { LogoSquare } from '@/components/logo';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Label } from '@/components/ui/label';
|
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 { ProjectCard } from '@/components/projects/project-card';
|
||||||
import { notFound, redirect } from 'next/navigation';
|
import { notFound, redirect } from 'next/navigation';
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { CreateClientSuccess } from '@/components/clients/create-client-success';
|
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 { Button, buttonVariants } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Label } from '@/components/ui/label';
|
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 { cn } from '@/utils/cn';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { SaveIcon, WallpaperIcon } from 'lucide-react';
|
import { SaveIcon, WallpaperIcon } from 'lucide-react';
|
||||||
import Link from 'next/link';
|
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import type { SubmitHandler } from 'react-hook-form';
|
import type { SubmitHandler } from 'react-hook-form';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// import { CreateOrganization } from '@clerk/nextjs';
|
// import { CreateOrganization } from '@clerk/nextjs';
|
||||||
|
|
||||||
import { LogoSquare } from '@/components/Logo';
|
import { LogoSquare } from '@/components/logo';
|
||||||
import { redirect } from 'next/navigation';
|
import { redirect } from 'next/navigation';
|
||||||
|
|
||||||
import { getCurrentOrganizations, isWaitlistUserAccepted } from '@openpanel/db';
|
import { getCurrentOrganizations, isWaitlistUserAccepted } from '@openpanel/db';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { StickyBelowHeader } from '@/app/(app)/[organizationId]/[projectId]/layout-sticky-below-header';
|
import { StickyBelowHeader } from '@/app/(app)/[organizationId]/[projectId]/layout-sticky-below-header';
|
||||||
import { OverviewReportRange } from '@/app/(app)/[organizationId]/[projectId]/overview-sticky-header';
|
import { OverviewReportRange } from '@/app/(app)/[organizationId]/[projectId]/overview-sticky-header';
|
||||||
import { Logo } from '@/components/Logo';
|
import { Logo } from '@/components/logo';
|
||||||
import { OverviewFiltersButtons } from '@/components/overview/filters/overview-filters-buttons';
|
import { OverviewFiltersButtons } from '@/components/overview/filters/overview-filters-buttons';
|
||||||
import ServerLiveCounter from '@/components/overview/live-counter';
|
import ServerLiveCounter from '@/components/overview/live-counter';
|
||||||
import { OverviewLiveHistogram } from '@/components/overview/overview-live-histogram';
|
import { OverviewLiveHistogram } from '@/components/overview/overview-live-histogram';
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { InputWithLabel } from '@/components/forms/InputWithLabel';
|
import { InputWithLabel } from '@/components/forms/input-with-label';
|
||||||
import { Logo } from '@/components/Logo';
|
import { Logo } from '@/components/logo';
|
||||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
|
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
|
||||||
import { Button } from '@/components/ui/button';
|
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 { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { KeySquareIcon } from 'lucide-react';
|
import { KeySquareIcon } from 'lucide-react';
|
||||||
import { signIn } from 'next-auth/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 type { IServiceClientWithProject } from '@openpanel/db';
|
||||||
|
|
||||||
import { ClientActions } from './ClientActions';
|
import { ClientActions } from './client-actions';
|
||||||
|
|
||||||
export const columns: ColumnDef<IServiceClientWithProject>[] = [
|
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 { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
||||||
import { cn } from '@/utils/cn';
|
import { cn } from '@/utils/cn';
|
||||||
|
|
||||||
import { Widget, WidgetBody } from '../../Widget';
|
import { Widget, WidgetBody } from '../../widget';
|
||||||
import { WidgetButtons, WidgetHead } from '../overview-widget';
|
import { WidgetButtons, WidgetHead } from '../overview-widget';
|
||||||
import { useOverviewOptions } from '../useOverviewOptions';
|
import { useOverviewOptions } from '../useOverviewOptions';
|
||||||
import { useOverviewWidget } from '../useOverviewWidget';
|
import { useOverviewWidget } from '../useOverviewWidget';
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import { WidgetHead } from '@/components/overview/overview-widget';
|
import { WidgetHead } from '@/components/overview/overview-widget';
|
||||||
import { useOverviewOptions } from '@/components/overview/useOverviewOptions';
|
import { useOverviewOptions } from '@/components/overview/useOverviewOptions';
|
||||||
import { ChartSwitch } from '@/components/report/chart';
|
import { ChartSwitch } from '@/components/report/chart';
|
||||||
import { Widget, WidgetBody } from '@/components/Widget';
|
import { Widget, WidgetBody } from '@/components/widget';
|
||||||
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
||||||
import { cn } from '@/utils/cn';
|
import { cn } from '@/utils/cn';
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { ChartSwitch } from '@/components/report/chart';
|
|||||||
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
||||||
import { cn } from '@/utils/cn';
|
import { cn } from '@/utils/cn';
|
||||||
|
|
||||||
import { Widget, WidgetBody } from '../Widget';
|
import { Widget, WidgetBody } from '../widget';
|
||||||
import { OverviewChartToggle } from './overview-chart-toggle';
|
import { OverviewChartToggle } from './overview-chart-toggle';
|
||||||
import { WidgetButtons, WidgetHead } from './overview-widget';
|
import { WidgetButtons, WidgetHead } from './overview-widget';
|
||||||
import { useOverviewOptions } from './useOverviewOptions';
|
import { useOverviewOptions } from './useOverviewOptions';
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
|||||||
import { cn } from '@/utils/cn';
|
import { cn } from '@/utils/cn';
|
||||||
import { BarChartIcon, LineChart, LineChartIcon } from 'lucide-react';
|
import { BarChartIcon, LineChart, LineChartIcon } from 'lucide-react';
|
||||||
|
|
||||||
import { Widget, WidgetBody } from '../../Widget';
|
import { Widget, WidgetBody } from '../../widget';
|
||||||
import { OverviewChartToggle } from '../overview-chart-toggle';
|
import { OverviewChartToggle } from '../overview-chart-toggle';
|
||||||
import { WidgetButtons, WidgetHead } from '../overview-widget';
|
import { WidgetButtons, WidgetHead } from '../overview-widget';
|
||||||
import { useOverviewOptions } from '../useOverviewOptions';
|
import { useOverviewOptions } from '../useOverviewOptions';
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { ChartSwitch } from '@/components/report/chart';
|
|||||||
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
||||||
import { cn } from '@/utils/cn';
|
import { cn } from '@/utils/cn';
|
||||||
|
|
||||||
import { Widget, WidgetBody } from '../Widget';
|
import { Widget, WidgetBody } from '../widget';
|
||||||
import { OverviewChartToggle } from './overview-chart-toggle';
|
import { OverviewChartToggle } from './overview-chart-toggle';
|
||||||
import { WidgetButtons, WidgetHead } from './overview-widget';
|
import { WidgetButtons, WidgetHead } from './overview-widget';
|
||||||
import { useOverviewOptions } from './useOverviewOptions';
|
import { useOverviewOptions } from './useOverviewOptions';
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { ChartSwitch } from '@/components/report/chart';
|
|||||||
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
||||||
import { cn } from '@/utils/cn';
|
import { cn } from '@/utils/cn';
|
||||||
|
|
||||||
import { Widget, WidgetBody } from '../Widget';
|
import { Widget, WidgetBody } from '../widget';
|
||||||
import { OverviewChartToggle } from './overview-chart-toggle';
|
import { OverviewChartToggle } from './overview-chart-toggle';
|
||||||
import { WidgetButtons, WidgetHead } from './overview-widget';
|
import { WidgetButtons, WidgetHead } from './overview-widget';
|
||||||
import { useOverviewOptions } from './useOverviewOptions';
|
import { useOverviewOptions } from './useOverviewOptions';
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { ChartSwitch } from '@/components/report/chart';
|
|||||||
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
||||||
import { cn } from '@/utils/cn';
|
import { cn } from '@/utils/cn';
|
||||||
|
|
||||||
import { Widget, WidgetBody } from '../Widget';
|
import { Widget, WidgetBody } from '../widget';
|
||||||
import { OverviewChartToggle } from './overview-chart-toggle';
|
import { OverviewChartToggle } from './overview-chart-toggle';
|
||||||
import { WidgetButtons, WidgetHead } from './overview-widget';
|
import { WidgetButtons, WidgetHead } from './overview-widget';
|
||||||
import { useOverviewOptions } from './useOverviewOptions';
|
import { useOverviewOptions } from './useOverviewOptions';
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ import {
|
|||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from '../ui/dropdown-menu';
|
} from '../ui/dropdown-menu';
|
||||||
import type { WidgetHeadProps } from '../Widget';
|
import type { WidgetHeadProps } from '../widget';
|
||||||
import { WidgetHead as WidgetHeadBase } from '../Widget';
|
import { WidgetHead as WidgetHeadBase } from '../widget';
|
||||||
|
|
||||||
export function WidgetHead({ className, ...props }: WidgetHeadProps) {
|
export function WidgetHead({ className, ...props }: WidgetHeadProps) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
import { formatDate } from '@/utils/date';
|
import { formatDate } from '@/utils/date';
|
||||||
import type { ColumnDef } from '@tanstack/react-table';
|
import type { ColumnDef } from '@tanstack/react-table';
|
||||||
|
|
||||||
import { IServiceProject } from '@openpanel/db';
|
import type { IServiceProject } from '@openpanel/db';
|
||||||
import type { Project as IProject } from '@openpanel/db';
|
|
||||||
|
|
||||||
import { ProjectActions } from './ProjectActions';
|
import { ProjectActions } from './project-actions';
|
||||||
|
|
||||||
export type Project = IProject;
|
export type Project = IServiceProject;
|
||||||
export const columns: ColumnDef<Project>[] = [
|
export const columns: ColumnDef<IServiceProject>[] = [
|
||||||
{
|
{
|
||||||
accessorKey: 'name',
|
accessorKey: 'name',
|
||||||
header: 'Name',
|
header: 'Name',
|
||||||
|
|||||||
@@ -7,9 +7,8 @@ import {
|
|||||||
PopoverTrigger,
|
PopoverTrigger,
|
||||||
} from '@/components/ui/popover';
|
} from '@/components/ui/popover';
|
||||||
import { useBreakpoint } from '@/hooks/useBreakpoint';
|
import { useBreakpoint } from '@/hooks/useBreakpoint';
|
||||||
import { useDispatch, useSelector } from '@/redux';
|
|
||||||
import { cn } from '@/utils/cn';
|
import { cn } from '@/utils/cn';
|
||||||
import { endOfDay, format, startOfDay } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
import { CalendarIcon, ChevronsUpDownIcon } from 'lucide-react';
|
import { CalendarIcon, ChevronsUpDownIcon } from 'lucide-react';
|
||||||
import type { SelectRangeEventHandler } from 'react-day-picker';
|
import type { SelectRangeEventHandler } from 'react-day-picker';
|
||||||
|
|
||||||
@@ -18,7 +17,6 @@ import type { IChartRange } from '@openpanel/validation';
|
|||||||
|
|
||||||
import type { ExtendedComboboxProps } from '../ui/combobox';
|
import type { ExtendedComboboxProps } from '../ui/combobox';
|
||||||
import { ToggleGroup, ToggleGroupItem } from '../ui/toggle-group';
|
import { ToggleGroup, ToggleGroupItem } from '../ui/toggle-group';
|
||||||
import { changeDates, changeEndDate, changeStartDate } from './reportSlice';
|
|
||||||
|
|
||||||
export function ReportRange({
|
export function ReportRange({
|
||||||
range,
|
range,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { FullPageEmptyState } from '@/components/FullPageEmptyState';
|
import { FullPageEmptyState } from '@/components/full-page-empty-state';
|
||||||
import { cn } from '@/utils/cn';
|
import { cn } from '@/utils/cn';
|
||||||
|
|
||||||
import { useChartContext } from './ChartProvider';
|
import { useChartContext } from './ChartProvider';
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import type { IChartData } from '@/app/_trpc/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 { fancyMinutes, useNumber } from '@/hooks/useNumerFormatter';
|
||||||
import { theme } from '@/utils/theme';
|
import { theme } from '@/utils/theme';
|
||||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||||
|
|||||||
@@ -44,7 +44,6 @@ export function ReportLineChart({
|
|||||||
const { series, setVisibleSeries } = useVisibleSeries(data);
|
const { series, setVisibleSeries } = useVisibleSeries(data);
|
||||||
const rechartData = useRechartDataModel(series);
|
const rechartData = useRechartDataModel(series);
|
||||||
const number = useNumber();
|
const number = useNumber();
|
||||||
console.log(references.map((ref) => ref.createdAt.getTime()));
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { IChartData } from '@/app/_trpc/client';
|
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 { useVisibleSeries } from '@/hooks/useVisibleSeries';
|
||||||
import { cn } from '@/utils/cn';
|
import { cn } from '@/utils/cn';
|
||||||
import { round } from '@/utils/math';
|
import { round } from '@/utils/math';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import type { IChartData } from '@/app/_trpc/client';
|
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 { Badge } from '@/components/ui/badge';
|
||||||
import { Checkbox } from '@/components/ui/checkbox';
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
import {
|
import {
|
||||||
@@ -30,12 +30,14 @@ interface ReportTableProps {
|
|||||||
setVisibleSeries: React.Dispatch<React.SetStateAction<string[]>>;
|
setVisibleSeries: React.Dispatch<React.SetStateAction<string[]>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ROWS_LIMIT = 50;
|
||||||
|
|
||||||
export function ReportTable({
|
export function ReportTable({
|
||||||
data,
|
data,
|
||||||
visibleSeries,
|
visibleSeries,
|
||||||
setVisibleSeries,
|
setVisibleSeries,
|
||||||
}: ReportTableProps) {
|
}: ReportTableProps) {
|
||||||
const { setPage, paginate, page } = usePagination(50);
|
const { setPage, paginate, page } = usePagination(ROWS_LIMIT);
|
||||||
const number = useNumber();
|
const number = useNumber();
|
||||||
const interval = useSelector((state) => state.report.interval);
|
const interval = useSelector((state) => state.report.interval);
|
||||||
const formatDate = useFormatDateInterval(interval);
|
const formatDate = useFormatDateInterval(interval);
|
||||||
@@ -162,7 +164,12 @@ export function ReportTable({
|
|||||||
<Badge>Min: {number.format(data.metrics.min)}</Badge>
|
<Badge>Min: {number.format(data.metrics.min)}</Badge>
|
||||||
<Badge>Max: {number.format(data.metrics.max)}</Badge>
|
<Badge>Max: {number.format(data.metrics.max)}</Badge>
|
||||||
</div>
|
</div>
|
||||||
<Pagination cursor={page} setCursor={setPage} />
|
<Pagination
|
||||||
|
cursor={page}
|
||||||
|
setCursor={setPage}
|
||||||
|
take={ROWS_LIMIT}
|
||||||
|
count={data.series.length}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { api } from '@/app/_trpc/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 { Combobox } from '@/components/ui/combobox';
|
||||||
import { useAppParams } from '@/hooks/useAppParams';
|
import { useAppParams } from '@/hooks/useAppParams';
|
||||||
import { useDispatch, useSelector } from '@/redux';
|
import { useDispatch, useSelector } from '@/redux';
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ColorSquare } from '@/components/ColorSquare';
|
import { ColorSquare } from '@/components/color-square';
|
||||||
import { Dropdown } from '@/components/Dropdown';
|
|
||||||
import { Checkbox } from '@/components/ui/checkbox';
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
import { Combobox } from '@/components/ui/combobox';
|
import { Combobox } from '@/components/ui/combobox';
|
||||||
|
import { DropdownMenuComposed } from '@/components/ui/dropdown-menu';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { useAppParams } from '@/hooks/useAppParams';
|
import { useAppParams } from '@/hooks/useAppParams';
|
||||||
import { useDebounceFn } from '@/hooks/useDebounceFn';
|
import { useDebounceFn } from '@/hooks/useDebounceFn';
|
||||||
@@ -94,7 +94,7 @@ export function ReportEvents() {
|
|||||||
|
|
||||||
{/* Segment and Filter buttons */}
|
{/* Segment and Filter buttons */}
|
||||||
<div className="flex gap-2 p-2 pt-0 text-sm">
|
<div className="flex gap-2 p-2 pt-0 text-sm">
|
||||||
<Dropdown
|
<DropdownMenuComposed
|
||||||
onChange={(segment) => {
|
onChange={(segment) => {
|
||||||
dispatch(
|
dispatch(
|
||||||
changeEvent({
|
changeEvent({
|
||||||
@@ -166,7 +166,7 @@ export function ReportEvents() {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</Dropdown>
|
</DropdownMenuComposed>
|
||||||
{/* */}
|
{/* */}
|
||||||
<FiltersCombobox event={event} />
|
<FiltersCombobox event={event} />
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { api } from '@/app/_trpc/client';
|
import { api } from '@/app/_trpc/client';
|
||||||
import { ColorSquare } from '@/components/ColorSquare';
|
import { ColorSquare } from '@/components/color-square';
|
||||||
import { Dropdown } from '@/components/Dropdown';
|
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { ComboboxAdvanced } from '@/components/ui/combobox-advanced';
|
import { ComboboxAdvanced } from '@/components/ui/combobox-advanced';
|
||||||
|
import { DropdownMenuComposed } from '@/components/ui/dropdown-menu';
|
||||||
import { RenderDots } from '@/components/ui/RenderDots';
|
import { RenderDots } from '@/components/ui/RenderDots';
|
||||||
import { useAppParams } from '@/hooks/useAppParams';
|
import { useAppParams } from '@/hooks/useAppParams';
|
||||||
import { useMappings } from '@/hooks/useMappings';
|
import { useMappings } from '@/hooks/useMappings';
|
||||||
@@ -104,7 +104,7 @@ export function FilterItem({ filter, event }: FilterProps) {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-1">
|
<div className="flex gap-1">
|
||||||
<Dropdown
|
<DropdownMenuComposed
|
||||||
onChange={changeFilterOperator}
|
onChange={changeFilterOperator}
|
||||||
items={mapKeys(operators).map((key) => ({
|
items={mapKeys(operators).map((key) => ({
|
||||||
value: key,
|
value: key,
|
||||||
@@ -115,7 +115,7 @@ export function FilterItem({ filter, event }: FilterProps) {
|
|||||||
<Button variant={'ghost'} className="whitespace-nowrap">
|
<Button variant={'ghost'} className="whitespace-nowrap">
|
||||||
{operators[filter.operator]}
|
{operators[filter.operator]}
|
||||||
</Button>
|
</Button>
|
||||||
</Dropdown>
|
</DropdownMenuComposed>
|
||||||
<ComboboxAdvanced
|
<ComboboxAdvanced
|
||||||
items={valuesCombobox}
|
items={valuesCombobox}
|
||||||
value={filter.value}
|
value={filter.value}
|
||||||
|
|||||||
@@ -180,6 +180,50 @@ const DropdownMenuShortcut = ({
|
|||||||
};
|
};
|
||||||
DropdownMenuShortcut.displayName = '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 {
|
export {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
|
|||||||
@@ -1,17 +1,16 @@
|
|||||||
import { isValidElement } from 'react';
|
|
||||||
import { cn } from '@/utils/cn';
|
import { cn } from '@/utils/cn';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
interface KeyValueProps {
|
interface KeyValueProps {
|
||||||
name: string;
|
name: string;
|
||||||
value: unknown;
|
value: any;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
href?: string;
|
href?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function KeyValue({ href, onClick, name, value }: KeyValueProps) {
|
export function KeyValue({ href, onClick, name, value }: KeyValueProps) {
|
||||||
const clickable = href || onClick;
|
const clickable = href || onClick;
|
||||||
const Component = href ? (Link as any) : onClick ? 'button' : 'div';
|
const Component = (href ? Link : onClick ? 'button' : 'div') as 'button';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Component
|
<Component
|
||||||
@@ -33,24 +32,3 @@ export function KeyValue({ href, onClick, name, value }: KeyValueProps) {
|
|||||||
</Component>
|
</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';
|
import { api } from '@/app/_trpc/client';
|
||||||
|
|
||||||
export function useProfileProperties(projectId: string, event?: string) {
|
export function useProfileProperties(projectId: string) {
|
||||||
const query = api.profile.properties.useQuery({
|
const query = api.profile.properties.useQuery({
|
||||||
projectId: projectId,
|
projectId: projectId,
|
||||||
event,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return query.data ?? [];
|
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",
|
"id": "123",
|
||||||
"name": "Mjölkproteinfritt"
|
"name": "123"
|
||||||
},
|
|
||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { api, handleError } from '@/app/_trpc/client';
|
import { api, handleError } from '@/app/_trpc/client';
|
||||||
import { ButtonContainer } from '@/components/ButtonContainer';
|
import { ButtonContainer } from '@/components/button-container';
|
||||||
import { InputWithLabel } from '@/components/forms/InputWithLabel';
|
import { InputWithLabel } from '@/components/forms/input-with-label';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { useAppParams } from '@/hooks/useAppParams';
|
import { useAppParams } from '@/hooks/useAppParams';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { api, handleError } from '@/app/_trpc/client';
|
import { api, handleError } from '@/app/_trpc/client';
|
||||||
import { ButtonContainer } from '@/components/ButtonContainer';
|
import { ButtonContainer } from '@/components/button-container';
|
||||||
import { InputWithLabel } from '@/components/forms/InputWithLabel';
|
import { InputWithLabel } from '@/components/forms/input-with-label';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { api, handleError } from '@/app/_trpc/client';
|
import { api, handleError } from '@/app/_trpc/client';
|
||||||
import { ButtonContainer } from '@/components/ButtonContainer';
|
import { ButtonContainer } from '@/components/button-container';
|
||||||
import { InputWithLabel } from '@/components/forms/InputWithLabel';
|
import { InputWithLabel } from '@/components/forms/input-with-label';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Calendar } from '@/components/ui/calendar';
|
import { Calendar } from '@/components/ui/calendar';
|
||||||
import { useAppParams } from '@/hooks/useAppParams';
|
import { useAppParams } from '@/hooks/useAppParams';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ButtonContainer } from '@/components/ButtonContainer';
|
import { ButtonContainer } from '@/components/button-container';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
|
|
||||||
import { popModal } from '.';
|
import { popModal } from '.';
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { api, handleError } from '@/app/_trpc/client';
|
import { api, handleError } from '@/app/_trpc/client';
|
||||||
import { ButtonContainer } from '@/components/ButtonContainer';
|
import { ButtonContainer } from '@/components/button-container';
|
||||||
import { InputWithLabel } from '@/components/forms/InputWithLabel';
|
import { InputWithLabel } from '@/components/forms/input-with-label';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { api, handleError } from '@/app/_trpc/client';
|
import { api, handleError } from '@/app/_trpc/client';
|
||||||
import { ButtonContainer } from '@/components/ButtonContainer';
|
import { ButtonContainer } from '@/components/button-container';
|
||||||
import { InputWithLabel } from '@/components/forms/InputWithLabel';
|
import { InputWithLabel } from '@/components/forms/input-with-label';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { api, handleError } from '@/app/_trpc/client';
|
import { api, handleError } from '@/app/_trpc/client';
|
||||||
import { ButtonContainer } from '@/components/ButtonContainer';
|
import { ButtonContainer } from '@/components/button-container';
|
||||||
import { InputWithLabel } from '@/components/forms/InputWithLabel';
|
import { InputWithLabel } from '@/components/forms/input-with-label';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ButtonContainer } from '@/components/ButtonContainer';
|
import { ButtonContainer } from '@/components/button-container';
|
||||||
import { InputWithLabel } from '@/components/forms/InputWithLabel';
|
import { InputWithLabel } from '@/components/forms/input-with-label';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import type { SubmitHandler } from 'react-hook-form';
|
import type { SubmitHandler } from 'react-hook-form';
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { api, handleError } from '@/app/_trpc/client';
|
import { api, handleError } from '@/app/_trpc/client';
|
||||||
import { ButtonContainer } from '@/components/ButtonContainer';
|
import { ButtonContainer } from '@/components/button-container';
|
||||||
import { InputWithLabel } from '@/components/forms/InputWithLabel';
|
import { InputWithLabel } from '@/components/forms/input-with-label';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Combobox } from '@/components/ui/combobox';
|
import { Combobox } from '@/components/ui/combobox';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
@@ -36,7 +36,7 @@ export default function SaveReport({ report }: SaveReportProps) {
|
|||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const dashboardId = searchParams?.get('dashboardId') ?? undefined;
|
const dashboardId = searchParams?.get('dashboardId') ?? undefined;
|
||||||
|
|
||||||
const save = api.report.save.useMutation({
|
const save = api.report.create.useMutation({
|
||||||
onError: handleError,
|
onError: handleError,
|
||||||
onSuccess(res) {
|
onSuccess(res) {
|
||||||
toast('Success', {
|
toast('Success', {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { api, handleError } from '@/app/_trpc/client';
|
import { api, handleError } from '@/app/_trpc/client';
|
||||||
import { ButtonContainer } from '@/components/ButtonContainer';
|
import { ButtonContainer } from '@/components/button-container';
|
||||||
import { InputWithLabel } from '@/components/forms/InputWithLabel';
|
import { InputWithLabel } from '@/components/forms/input-with-label';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Checkbox } from '@/components/ui/checkbox';
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
import { useAppParams } from '@/hooks/useAppParams';
|
import { useAppParams } from '@/hooks/useAppParams';
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import { projectRouter } from './routers/project';
|
|||||||
import { referenceRouter } from './routers/reference';
|
import { referenceRouter } from './routers/reference';
|
||||||
import { reportRouter } from './routers/report';
|
import { reportRouter } from './routers/report';
|
||||||
import { shareRouter } from './routers/share';
|
import { shareRouter } from './routers/share';
|
||||||
import { uiRouter } from './routers/ui';
|
|
||||||
import { userRouter } from './routers/user';
|
import { userRouter } from './routers/user';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -29,7 +28,6 @@ export const appRouter = createTRPCRouter({
|
|||||||
client: clientRouter,
|
client: clientRouter,
|
||||||
event: eventRouter,
|
event: eventRouter,
|
||||||
profile: profileRouter,
|
profile: profileRouter,
|
||||||
ui: uiRouter,
|
|
||||||
share: shareRouter,
|
share: shareRouter,
|
||||||
onboarding: onboardingRouter,
|
onboarding: onboardingRouter,
|
||||||
reference: referenceRouter,
|
reference: referenceRouter,
|
||||||
|
|||||||
@@ -1,10 +1,17 @@
|
|||||||
import { getDaysOldDate } from '@/utils/date';
|
|
||||||
import { round } from '@/utils/math';
|
import { round } from '@/utils/math';
|
||||||
|
import { subDays } from 'date-fns';
|
||||||
import * as mathjs from 'mathjs';
|
import * as mathjs from 'mathjs';
|
||||||
import { sort } from 'ramda';
|
import { repeat, reverse, sort } from 'ramda';
|
||||||
|
|
||||||
import { alphabetIds, NOT_SET_VALUE } from '@openpanel/constants';
|
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 {
|
import type {
|
||||||
IChartEvent,
|
IChartEvent,
|
||||||
IChartInput,
|
IChartInput,
|
||||||
@@ -317,7 +324,7 @@ export function getDatesFromRange(range: IChartRange) {
|
|||||||
let days = 1;
|
let days = 1;
|
||||||
|
|
||||||
if (range === '24h') {
|
if (range === '24h') {
|
||||||
const startDate = getDaysOldDate(days);
|
const startDate = subDays(new Date(), days);
|
||||||
const endDate = new Date();
|
const endDate = new Date();
|
||||||
return {
|
return {
|
||||||
startDate: startDate.toUTCString(),
|
startDate: startDate.toUTCString(),
|
||||||
@@ -337,7 +344,7 @@ export function getDatesFromRange(range: IChartRange) {
|
|||||||
days = 365;
|
days = 365;
|
||||||
}
|
}
|
||||||
|
|
||||||
const startDate = getDaysOldDate(days);
|
const startDate = subDays(new Date(), days);
|
||||||
startDate.setUTCHours(0, 0, 0, 0);
|
startDate.setUTCHours(0, 0, 0, 0);
|
||||||
const endDate = new Date();
|
const endDate = new Date();
|
||||||
endDate.setUTCHours(23, 59, 59, 999);
|
endDate.setUTCHours(23, 59, 59, 999);
|
||||||
@@ -347,10 +354,197 @@ export function getDatesFromRange(range: IChartRange) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getChartStartEndDate(
|
export function getChartStartEndDate({
|
||||||
input: Pick<IChartInput, 'endDate' | 'startDate' | 'range'>
|
startDate,
|
||||||
) {
|
endDate,
|
||||||
return input.startDate && input.endDate
|
range,
|
||||||
? { startDate: input.startDate, endDate: input.endDate }
|
}: Pick<IChartInput, 'endDate' | 'startDate' | 'range'>) {
|
||||||
: getDatesFromRange(input.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,
|
publicProcedure,
|
||||||
} from '@/server/api/trpc';
|
} from '@/server/api/trpc';
|
||||||
import { average, max, min, round, sum } from '@/utils/math';
|
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 { z } from 'zod';
|
||||||
|
|
||||||
import {
|
import { chQuery, createSqlBuilder } from '@openpanel/db';
|
||||||
chQuery,
|
|
||||||
createSqlBuilder,
|
|
||||||
formatClickhouseDate,
|
|
||||||
getEventFiltersWhereClause,
|
|
||||||
} from '@openpanel/db';
|
|
||||||
import { zChartInput } from '@openpanel/validation';
|
import { zChartInput } from '@openpanel/validation';
|
||||||
import type { IChartEvent, IChartInput } from '@openpanel/validation';
|
import type { IChartEvent, IChartInput } from '@openpanel/validation';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getChartData,
|
getChartPrevStartEndDate,
|
||||||
getChartStartEndDate,
|
getChartStartEndDate,
|
||||||
getDatesFromRange,
|
getFunnelData,
|
||||||
withFormula,
|
getSeriesFromEvents,
|
||||||
} from './chart.helpers';
|
} 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 = {
|
type PreviousValue = {
|
||||||
value: number;
|
value: number;
|
||||||
diff: number | null;
|
diff: number | null;
|
||||||
@@ -243,7 +131,7 @@ export const chartRouter = createTRPCRouter({
|
|||||||
.replace(/^properties\./, '')
|
.replace(/^properties\./, '')
|
||||||
.replace('.*.', '.%.')}')) as values`;
|
.replace('.*.', '.%.')}')) as values`;
|
||||||
} else {
|
} else {
|
||||||
sb.select.values = `${property} as values`;
|
sb.select.values = `distinct ${property} as values`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const events = await chQuery<{ values: string[] }>(getSql());
|
const events = await chQuery<{ values: string[] }>(getSql());
|
||||||
@@ -266,57 +154,19 @@ export const chartRouter = createTRPCRouter({
|
|||||||
|
|
||||||
// TODO: Make this private
|
// TODO: Make this private
|
||||||
chart: publicProcedure.input(zChartInput).query(async ({ input }) => {
|
chart: publicProcedure.input(zChartInput).query(async ({ input }) => {
|
||||||
const { startDate, endDate } = getChartStartEndDate(input);
|
const currentPeriod = getChartStartEndDate(input);
|
||||||
let diff = 0;
|
const previousPeriod = getChartPrevStartEndDate({
|
||||||
|
range: input.range,
|
||||||
|
...currentPeriod,
|
||||||
|
});
|
||||||
|
|
||||||
switch (input.range) {
|
const promises = [getSeriesFromEvents({ ...input, ...currentPeriod })];
|
||||||
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)];
|
|
||||||
|
|
||||||
if (input.previous) {
|
if (input.previous) {
|
||||||
promises.push(
|
promises.push(
|
||||||
getSeriesFromEvents({
|
getSeriesFromEvents({
|
||||||
...input,
|
...input,
|
||||||
...{
|
...previousPeriod,
|
||||||
startDate: new Date(
|
|
||||||
new Date(startDate).getTime() - diff
|
|
||||||
).toISOString(),
|
|
||||||
endDate: new Date(new Date(endDate).getTime() - diff).toISOString(),
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -407,11 +257,11 @@ export const chartRouter = createTRPCRouter({
|
|||||||
final.metrics.max = max(final.series.map((item) => item.metrics.max));
|
final.metrics.max = max(final.series.map((item) => item.metrics.max));
|
||||||
final.metrics.previous = {
|
final.metrics.previous = {
|
||||||
sum: getPreviousMetric(
|
sum: getPreviousMetric(
|
||||||
sum(final.series.map((item) => item.metrics.sum)),
|
final.metrics.sum,
|
||||||
sum(final.series.map((item) => item.metrics.previous.sum?.value ?? 0))
|
sum(final.series.map((item) => item.metrics.previous.sum?.value ?? 0))
|
||||||
),
|
),
|
||||||
average: getPreviousMetric(
|
average: getPreviousMetric(
|
||||||
round(average(final.series.map((item) => item.metrics.average)), 2),
|
final.metrics.average,
|
||||||
round(
|
round(
|
||||||
average(
|
average(
|
||||||
final.series.map(
|
final.series.map(
|
||||||
@@ -422,15 +272,16 @@ export const chartRouter = createTRPCRouter({
|
|||||||
)
|
)
|
||||||
),
|
),
|
||||||
min: getPreviousMetric(
|
min: getPreviousMetric(
|
||||||
min(final.series.map((item) => item.metrics.min)),
|
final.metrics.min,
|
||||||
min(final.series.map((item) => item.metrics.previous.min?.value ?? 0))
|
min(final.series.map((item) => item.metrics.previous.min?.value ?? 0))
|
||||||
),
|
),
|
||||||
max: getPreviousMetric(
|
max: getPreviousMetric(
|
||||||
max(final.series.map((item) => item.metrics.max)),
|
final.metrics.max,
|
||||||
max(final.series.map((item) => item.metrics.previous.max?.value ?? 0))
|
max(final.series.map((item) => item.metrics.previous.max?.value ?? 0))
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Sort by sum
|
||||||
final.series = final.series.sort((a, b) => {
|
final.series = final.series.sort((a, b) => {
|
||||||
if (input.chartType === 'linear') {
|
if (input.chartType === 'linear') {
|
||||||
const sumA = a.data.reduce((acc, item) => acc + (item.count ?? 0), 0);
|
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;
|
return final;
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
function getPreviousMetric(
|
export function getPreviousMetric(
|
||||||
current: number,
|
current: number,
|
||||||
previous: number | null
|
previous: number | null
|
||||||
): PreviousValue {
|
): PreviousValue {
|
||||||
@@ -483,28 +329,3 @@ function getPreviousMetric(
|
|||||||
value: previous,
|
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 { randomUUID } from 'crypto';
|
||||||
import { createTRPCRouter, protectedProcedure } from '@/server/api/trpc';
|
import { createTRPCRouter, protectedProcedure } from '@/server/api/trpc';
|
||||||
import { db } from '@/server/db';
|
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { hashPassword, stripTrailingSlash } from '@openpanel/common';
|
import { hashPassword, stripTrailingSlash } from '@openpanel/common';
|
||||||
|
import { db, transformClient } from '@openpanel/db';
|
||||||
|
|
||||||
export const clientRouter = createTRPCRouter({
|
export const clientRouter = createTRPCRouter({
|
||||||
list: protectedProcedure
|
list: protectedProcedure
|
||||||
@@ -76,7 +76,7 @@ export const clientRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...client,
|
...transformClient(client),
|
||||||
secret: input.cors ? null : secret,
|
secret: input.cors ? null : secret,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -1,60 +1,20 @@
|
|||||||
import { createTRPCRouter, protectedProcedure } from '@/server/api/trpc';
|
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 { PrismaError } from 'prisma-error-enum';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { db, getDashboardsByProjectId } from '@openpanel/db';
|
||||||
import type { Prisma } from '@openpanel/db';
|
import type { Prisma } from '@openpanel/db';
|
||||||
|
|
||||||
export const dashboardRouter = createTRPCRouter({
|
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
|
list: protectedProcedure
|
||||||
.input(
|
.input(
|
||||||
z
|
z.object({
|
||||||
.object({
|
projectId: z.string(),
|
||||||
projectId: z.string(),
|
})
|
||||||
})
|
|
||||||
.or(
|
|
||||||
z.object({
|
|
||||||
organizationId: z.string(),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
if ('projectId' in input) {
|
return getDashboardsByProjectId(input.projectId);
|
||||||
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',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}),
|
}),
|
||||||
create: protectedProcedure
|
create: protectedProcedure
|
||||||
.input(
|
.input(
|
||||||
|
|||||||
@@ -4,7 +4,12 @@ import { clerkClient } from '@clerk/nextjs';
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { hashPassword, stripTrailingSlash } from '@openpanel/common';
|
import { hashPassword, stripTrailingSlash } from '@openpanel/common';
|
||||||
import { db } from '@openpanel/db';
|
import {
|
||||||
|
db,
|
||||||
|
transformClient,
|
||||||
|
transformOrganization,
|
||||||
|
transformProject,
|
||||||
|
} from '@openpanel/db';
|
||||||
|
|
||||||
export const onboardingRouter = createTRPCRouter({
|
export const onboardingRouter = createTRPCRouter({
|
||||||
organziation: protectedProcedure
|
organziation: protectedProcedure
|
||||||
@@ -41,12 +46,12 @@ export const onboardingRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
client: {
|
client: transformClient({
|
||||||
...client,
|
...client,
|
||||||
secret: input.cors ? null : secret,
|
secret: input.cors ? null : secret,
|
||||||
},
|
}),
|
||||||
project,
|
project: transformProject(project),
|
||||||
organization: org,
|
organization: transformOrganization(org),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ export const organizationRouter = createTRPCRouter({
|
|||||||
list: protectedProcedure.query(() => {
|
list: protectedProcedure.query(() => {
|
||||||
return clerkClient.organizations.getOrganizationList();
|
return clerkClient.organizations.getOrganizationList();
|
||||||
}),
|
}),
|
||||||
// first: protectedProcedure.query(() => getCurrentOrganization()),
|
|
||||||
get: protectedProcedure
|
get: protectedProcedure
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
|
|||||||
@@ -3,71 +3,12 @@ import {
|
|||||||
protectedProcedure,
|
protectedProcedure,
|
||||||
publicProcedure,
|
publicProcedure,
|
||||||
} from '@/server/api/trpc';
|
} from '@/server/api/trpc';
|
||||||
import { db } from '@/server/db';
|
|
||||||
import { flatten, map, pipe, prop, sort, uniq } from 'ramda';
|
import { flatten, map, pipe, prop, sort, uniq } from 'ramda';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { chQuery, createSqlBuilder } from '@openpanel/db';
|
import { chQuery, createSqlBuilder } from '@openpanel/db';
|
||||||
|
|
||||||
export const profileRouter = createTRPCRouter({
|
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
|
properties: protectedProcedure
|
||||||
.input(z.object({ projectId: z.string() }))
|
.input(z.object({ projectId: z.string() }))
|
||||||
.query(async ({ input: { projectId } }) => {
|
.query(async ({ input: { projectId } }) => {
|
||||||
@@ -88,6 +29,7 @@ export const profileRouter = createTRPCRouter({
|
|||||||
uniq
|
uniq
|
||||||
)(properties);
|
)(properties);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
values: publicProcedure
|
values: publicProcedure
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { createTRPCRouter, protectedProcedure } from '@/server/api/trpc';
|
import { createTRPCRouter, protectedProcedure } from '@/server/api/trpc';
|
||||||
import { db, getId } from '@/server/db';
|
import { getId } from '@/utils/getDbId';
|
||||||
import { slug } from '@/utils/slug';
|
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { db, getProjectsByOrganizationSlug } from '@openpanel/db';
|
||||||
|
|
||||||
export const projectRouter = createTRPCRouter({
|
export const projectRouter = createTRPCRouter({
|
||||||
list: protectedProcedure
|
list: protectedProcedure
|
||||||
.input(
|
.input(
|
||||||
@@ -12,26 +13,9 @@ export const projectRouter = createTRPCRouter({
|
|||||||
)
|
)
|
||||||
.query(async ({ input: { organizationId } }) => {
|
.query(async ({ input: { organizationId } }) => {
|
||||||
if (organizationId === null) return [];
|
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
|
update: protectedProcedure
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
|
|||||||
@@ -1,57 +1,11 @@
|
|||||||
import { createTRPCRouter, protectedProcedure } from '@/server/api/trpc';
|
import { createTRPCRouter, protectedProcedure } from '@/server/api/trpc';
|
||||||
import { db } from '@/server/db';
|
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { transformReport } from '@openpanel/db';
|
import { db } from '@openpanel/db';
|
||||||
import { zChartInput } from '@openpanel/validation';
|
import { zChartInput } from '@openpanel/validation';
|
||||||
|
|
||||||
export const reportRouter = createTRPCRouter({
|
export const reportRouter = createTRPCRouter({
|
||||||
get: protectedProcedure
|
create: 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
|
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
report: zChartInput.omit({ projectId: true }),
|
report: zChartInput.omit({ projectId: true }),
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createTRPCRouter, protectedProcedure } from '@/server/api/trpc';
|
import { createTRPCRouter, protectedProcedure } from '@/server/api/trpc';
|
||||||
import { db } from '@/server/db';
|
|
||||||
import ShortUniqueId from 'short-unique-id';
|
import ShortUniqueId from 'short-unique-id';
|
||||||
|
|
||||||
|
import { db } from '@openpanel/db';
|
||||||
import { zShareOverview } from '@openpanel/validation';
|
import { zShareOverview } from '@openpanel/validation';
|
||||||
|
|
||||||
const uid = new ShortUniqueId({ length: 6 });
|
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 */
|
/* Rechart */
|
||||||
|
|
||||||
.recharts-wrapper .recharts-cartesian-grid-horizontal line:first-child,
|
.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) {
|
export function dateDifferanceInDays(date1: Date, date2: Date) {
|
||||||
const diffTime = Math.abs(date2.getTime() - date1.getTime());
|
const diffTime = Math.abs(date2.getTime() - date1.getTime());
|
||||||
return Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
return Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import { slug } from '@/utils/slug';
|
|
||||||
|
|
||||||
import { db } from '@openpanel/db';
|
import { db } from '@openpanel/db';
|
||||||
|
|
||||||
export { db } from '@openpanel/db';
|
import { slug } from './slug';
|
||||||
|
|
||||||
export async function getId(tableName: 'project' | 'dashboard', name: string) {
|
export async function getId(tableName: 'project' | 'dashboard', name: string) {
|
||||||
const newId = slug(name);
|
const newId = slug(name);
|
||||||
@@ -15,7 +13,7 @@ export async function getId(tableName: 'project' | 'dashboard', name: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
const existingProject = await db[tableName]!.findUnique({
|
const existingProject = await db[tableName].findUnique({
|
||||||
where: {
|
where: {
|
||||||
id: newId,
|
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 type { Client } from '../prisma-client';
|
||||||
import { db } from '../prisma-client';
|
import { db } from '../prisma-client';
|
||||||
|
import { transformProject } from './project.service';
|
||||||
import type { IServiceProject } from './project.service';
|
import type { IServiceProject } from './project.service';
|
||||||
|
|
||||||
export type IServiceClient = Client;
|
export type IServiceClient = ReturnType<typeof transformClient>;
|
||||||
export type IServiceClientWithProject = Client & {
|
export type IServiceClientWithProject = IServiceClient & {
|
||||||
project: Exclude<IServiceProject, null>;
|
project: Exclude<IServiceProject, null>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getClientsByOrganizationId(organizationId: string) {
|
export function transformClient({ organization_slug, ...client }: Client) {
|
||||||
return db.client.findMany({
|
return {
|
||||||
|
...client,
|
||||||
|
organizationSlug: organization_slug,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getClientsByOrganizationId(organizationId: string) {
|
||||||
|
const clients = await db.client.findMany({
|
||||||
where: {
|
where: {
|
||||||
organization_slug: organizationId,
|
organization_slug: organizationId,
|
||||||
},
|
},
|
||||||
@@ -16,4 +24,15 @@ export function getClientsByOrganizationId(organizationId: string) {
|
|||||||
project: true,
|
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