diff --git a/apps/dashboard/src/app/(app)/[organizationId]/[projectId]/layout-menu.tsx b/apps/dashboard/src/app/(app)/[organizationId]/[projectId]/layout-menu.tsx index aa1efffc..6e0d0192 100644 --- a/apps/dashboard/src/app/(app)/[organizationId]/[projectId]/layout-menu.tsx +++ b/apps/dashboard/src/app/(app)/[organizationId]/[projectId]/layout-menu.tsx @@ -117,11 +117,6 @@ export default function LayoutMenu({ dashboards }: LayoutMenuProps) { label="Projects" href={`/${params.organizationId}/${projectId}/settings/projects`} /> - >; -} -export default function ListClients({ clients }: ListClientsProps) { - return ( - <> - -
-
- -
- -
- -
- - ); -} diff --git a/apps/dashboard/src/app/(app)/[organizationId]/[projectId]/settings/clients/page.tsx b/apps/dashboard/src/app/(app)/[organizationId]/[projectId]/settings/clients/page.tsx deleted file mode 100644 index 685c94c6..00000000 --- a/apps/dashboard/src/app/(app)/[organizationId]/[projectId]/settings/clients/page.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import PageLayout from '@/app/(app)/[organizationId]/[projectId]/page-layout'; - -import { getClientsByOrganizationId } from '@openpanel/db'; - -import ListClients from './list-clients'; - -interface PageProps { - params: { - organizationId: string; - }; -} - -export default async function Page({ params: { organizationId } }: PageProps) { - const clients = await getClientsByOrganizationId(organizationId); - - return ( - - - - ); -} diff --git a/apps/dashboard/src/app/(app)/[organizationId]/[projectId]/settings/projects/list-projects.tsx b/apps/dashboard/src/app/(app)/[organizationId]/[projectId]/settings/projects/list-projects.tsx index d04efdfe..cc25c480 100644 --- a/apps/dashboard/src/app/(app)/[organizationId]/[projectId]/settings/projects/list-projects.tsx +++ b/apps/dashboard/src/app/(app)/[organizationId]/[projectId]/settings/projects/list-projects.tsx @@ -1,19 +1,29 @@ 'use client'; import { StickyBelowHeader } from '@/app/(app)/[organizationId]/[projectId]/layout-sticky-below-header'; -import { DataTable } from '@/components/data-table'; -import { columns } from '@/components/projects/table'; +import { ClientActions } from '@/components/clients/client-actions'; +import { ProjectActions } from '@/components/projects/project-actions'; +// import { columns } from '@/components/projects/table'; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from '@/components/ui/accordion'; +import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import { Button } from '@/components/ui/button'; +import { Tooltiper } from '@/components/ui/tooltip'; import { useAppParams } from '@/hooks/useAppParams'; import { pushModal } from '@/modals'; -import { PlusIcon } from 'lucide-react'; +import { InfoIcon, PlusIcon, PlusSquareIcon } from 'lucide-react'; -import type { getProjectsByOrganizationSlug } from '@openpanel/db'; +import type { IServiceClientWithProject, IServiceProject } from '@openpanel/db'; interface ListProjectsProps { - projects: Awaited>; + projects: IServiceProject[]; + clients: IServiceClientWithProject[]; } -export default function ListProjects({ projects }: ListProjectsProps) { +export default function ListProjects({ projects, clients }: ListProjectsProps) { const organizationId = useAppParams().organizationId; return ( <> @@ -34,7 +44,83 @@ export default function ListProjects({ projects }: ListProjectsProps) {
- +
+ + + What is a project + + A project can be a website, mobile app or any other application + that you want to track event for. Each project can have one or + more clients. The client is used to send events to the project. + + + + {projects.map((project) => { + const pClients = clients.filter( + (client) => client.project_id === project.id + ); + return ( + + +
+ {project.name} + + {pClients.length > 0 + ? `(${pClients.length} clients)` + : 'No clients created yet'} + +
+
+
+ + +
+ {pClients.map((item) => { + return ( +
+
{item.name}
+ + Client ID: ...{item.id.slice(-12)} + +
+ {item.secret + ? 'Secret: Hidden' + : `Website: ${item.cors}`} +
+
+ +
+
+ ); + })} + +
+
+
+ ); + })} +
+
); diff --git a/apps/dashboard/src/app/(app)/[organizationId]/[projectId]/settings/projects/page.tsx b/apps/dashboard/src/app/(app)/[organizationId]/[projectId]/settings/projects/page.tsx index 86b2c09a..131a96ce 100644 --- a/apps/dashboard/src/app/(app)/[organizationId]/[projectId]/settings/projects/page.tsx +++ b/apps/dashboard/src/app/(app)/[organizationId]/[projectId]/settings/projects/page.tsx @@ -1,6 +1,9 @@ import PageLayout from '@/app/(app)/[organizationId]/[projectId]/page-layout'; -import { getProjectsByOrganizationSlug } from '@openpanel/db'; +import { + getClientsByOrganizationId, + getProjectsByOrganizationSlug, +} from '@openpanel/db'; import ListProjects from './list-projects'; @@ -11,11 +14,14 @@ interface PageProps { } export default async function Page({ params: { organizationId } }: PageProps) { - const projects = await getProjectsByOrganizationSlug(organizationId); + const [projects, clients] = await Promise.all([ + getProjectsByOrganizationSlug(organizationId), + getClientsByOrganizationId(organizationId), + ]); return ( - + ); } diff --git a/apps/dashboard/src/components/projects/project-actions.tsx b/apps/dashboard/src/components/projects/project-actions.tsx index 5c946b36..34a7f743 100644 --- a/apps/dashboard/src/components/projects/project-actions.tsx +++ b/apps/dashboard/src/components/projects/project-actions.tsx @@ -2,22 +2,13 @@ import { pushModal, showConfirm } from '@/modals'; import { api } from '@/trpc/client'; -import { clipboard } from '@/utils/clipboard'; -import { MoreHorizontal } from 'lucide-react'; +import { Edit2Icon, TrashIcon } from 'lucide-react'; import { useRouter } from 'next/navigation'; import { toast } from 'sonner'; import type { IServiceProject } from '@openpanel/db'; import { Button } from '../ui/button'; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from '../ui/dropdown-menu'; export function ProjectActions(project: Exclude) { const { id } = project; @@ -32,43 +23,34 @@ export function ProjectActions(project: Exclude) { }); return ( - - - - - - Actions - clipboard(id)}> - Copy project ID - - { - pushModal('EditProject', project); - }} - > - Edit - - - { - showConfirm({ - title: 'Delete project', - text: 'This will delete all events for this project. This action cannot be undone.', - onConfirm() { - deletion.mutate({ - id, - }); - }, - }); - }} - > - Delete - - - +
+ + +
); } diff --git a/apps/dashboard/src/components/ui/accordion.tsx b/apps/dashboard/src/components/ui/accordion.tsx index 4f43db91..1c45b241 100644 --- a/apps/dashboard/src/components/ui/accordion.tsx +++ b/apps/dashboard/src/components/ui/accordion.tsx @@ -25,7 +25,7 @@ const AccordionTrigger = React.forwardRef< svg]:rotate-180', + 'flex flex-1 items-center justify-between py-4 font-medium transition-all [&[data-state=closed]]:hover:bg-muted/30 [&[data-state=open]>svg]:rotate-180', className )} {...props} diff --git a/apps/dashboard/src/components/ui/tooltip.tsx b/apps/dashboard/src/components/ui/tooltip.tsx index 5fd47f4d..ab596a06 100644 --- a/apps/dashboard/src/components/ui/tooltip.tsx +++ b/apps/dashboard/src/components/ui/tooltip.tsx @@ -33,14 +33,22 @@ TooltipContent.displayName = TooltipPrimitive.Content.displayName; export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }; interface TooltiperProps { - asChild: boolean; + asChild?: boolean; content: string; children: React.ReactNode; + className?: string; } -export function Tooltiper({ asChild, content, children }: TooltiperProps) { +export function Tooltiper({ + asChild, + content, + children, + className, +}: TooltiperProps) { return ( - {children} + + {children} + {content} ); diff --git a/apps/dashboard/src/modals/AddClient.tsx b/apps/dashboard/src/modals/AddClient.tsx index 318ba53c..ee651099 100644 --- a/apps/dashboard/src/modals/AddClient.tsx +++ b/apps/dashboard/src/modals/AddClient.tsx @@ -40,7 +40,10 @@ const validation = z type IForm = z.infer; -export default function AddClient() { +interface Props { + projectId: string; +} +export default function AddClient(props: Props) { const { organizationId, projectId } = useAppParams(); const router = useRouter(); const form = useForm({ @@ -49,7 +52,7 @@ export default function AddClient() { name: '', cors: '', tab: 'website', - projectId, + projectId: props.projectId ?? projectId, }, }); const mutation = api.client.create.useMutation({ diff --git a/apps/worker/src/jobs/events.ts b/apps/worker/src/jobs/events.ts index 82acc921..0313211b 100644 --- a/apps/worker/src/jobs/events.ts +++ b/apps/worker/src/jobs/events.ts @@ -1,6 +1,7 @@ import type { Job } from 'bullmq'; +import { escape } from 'sqlstring'; -import { createEvent } from '@openpanel/db'; +import { chQuery, createEvent, db } from '@openpanel/db'; import type { EventsQueuePayload, EventsQueuePayloadCreateSessionEnd, @@ -19,7 +20,17 @@ export async function eventsJob(job: Job) { if (job.attemptsStarted > 1 && job.data.payload.duration < 0) { job.data.payload.duration = 0; } - return await createEvent(job.data.payload); + const createdEvent = await createEvent(job.data.payload); + try { + await updateEventsCount(job.data.payload.projectId); + } catch (e) { + if (e instanceof Error) { + job.log(`Failed to update events count: ${e.message}`); + } else { + job.log(`Failed to update events count: Unknown issue`); + } + } + return createdEvent; } case 'createSessionEnd': { return await createSessionEnd( @@ -28,3 +39,20 @@ export async function eventsJob(job: Job) { } } } + +async function updateEventsCount(projectId: string) { + const res = await chQuery<{ count: number }>( + `SELECT * FROM events WHERE project_id = ${escape(projectId)}` + ); + const count = res[0]?.count; + if (count) { + await db.project.update({ + where: { + id: projectId, + }, + data: { + eventsCount: count, + }, + }); + } +} diff --git a/packages/db/src/services/project.service.ts b/packages/db/src/services/project.service.ts index 4c70831e..1c82d38e 100644 --- a/packages/db/src/services/project.service.ts +++ b/packages/db/src/services/project.service.ts @@ -32,6 +32,9 @@ export async function getProjectsByOrganizationSlug(slug: string) { where: { organization_slug: slug, }, + orderBy: { + createdAt: 'desc', + }, }); return res.map(transformProject);