move clients settings into projects

This commit is contained in:
Carl-Gerhard Lindesvärd
2024-04-02 22:33:13 +02:00
parent 1245047a7c
commit d9e3045a5b
11 changed files with 182 additions and 124 deletions

View File

@@ -117,11 +117,6 @@ export default function LayoutMenu({ dashboards }: LayoutMenuProps) {
label="Projects"
href={`/${params.organizationId}/${projectId}/settings/projects`}
/>
<LinkWithIcon
icon={KeySquareIcon}
label="Clients"
href={`/${params.organizationId}/${projectId}/settings/clients`}
/>
<LinkWithIcon
icon={UserIcon}
label="Profile (yours)"

View File

@@ -1,32 +0,0 @@
'use client';
import { StickyBelowHeader } from '@/app/(app)/[organizationId]/[projectId]/layout-sticky-below-header';
import { columns } from '@/components/clients/table';
import { DataTable } from '@/components/data-table';
import { Button } from '@/components/ui/button';
import { pushModal } from '@/modals';
import { PlusIcon } from 'lucide-react';
import type { getClientsByOrganizationId } from '@openpanel/db';
interface ListClientsProps {
clients: Awaited<ReturnType<typeof getClientsByOrganizationId>>;
}
export default function ListClients({ clients }: ListClientsProps) {
return (
<>
<StickyBelowHeader>
<div className="flex items-center justify-between p-4">
<div />
<Button icon={PlusIcon} onClick={() => pushModal('AddClient')}>
<span className="max-sm:hidden">Create client</span>
<span className="sm:hidden">Client</span>
</Button>
</div>
</StickyBelowHeader>
<div className="p-4">
<DataTable data={clients} columns={columns} />
</div>
</>
);
}

View File

@@ -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 (
<PageLayout title="Clients" organizationSlug={organizationId}>
<ListClients clients={clients} />
</PageLayout>
);
}

View File

@@ -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<ReturnType<typeof getProjectsByOrganizationSlug>>;
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) {
</div>
</StickyBelowHeader>
<div className="p-4">
<DataTable data={projects} columns={columns} />
<div className="card p-4">
<Alert className="mb-4">
<InfoIcon size={16} />
<AlertTitle>What is a project</AlertTitle>
<AlertDescription>
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.
</AlertDescription>
</Alert>
<Accordion type="single" collapsible className="-mx-4">
{projects.map((project) => {
const pClients = clients.filter(
(client) => client.project_id === project.id
);
return (
<AccordionItem
value={project.id}
key={project.id}
className="last:border-b-0"
>
<AccordionTrigger className="px-4">
<div className="flex-1 text-left">
{project.name}
<span className="ml-2 text-muted-foreground">
{pClients.length > 0
? `(${pClients.length} clients)`
: 'No clients created yet'}
</span>
</div>
<div className="mx-4"></div>
</AccordionTrigger>
<AccordionContent className="px-4">
<ProjectActions {...project} />
<div className="mt-4 grid gap-4 md:grid-cols-3">
{pClients.map((item) => {
return (
<div
className="relative rounded border border-border p-4"
key={item.id}
>
<div className="mb-1 font-medium">{item.name}</div>
<Tooltiper
className="text-muted-foreground"
content={item.id}
>
Client ID: ...{item.id.slice(-12)}
</Tooltiper>
<div className="text-muted-foreground">
{item.secret
? 'Secret: Hidden'
: `Website: ${item.cors}`}
</div>
<div className="absolute right-4 top-4">
<ClientActions {...item} />
</div>
</div>
);
})}
<button
onClick={() => {
pushModal('AddClient', {
projectId: project.id,
});
}}
className="flex items-center justify-center gap-4 rounded bg-muted p-4"
>
<PlusSquareIcon />
<div className="font-medium">New client</div>
</button>
</div>
</AccordionContent>
</AccordionItem>
);
})}
</Accordion>
</div>
</div>
</>
);

View File

@@ -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 (
<PageLayout title="Projects" organizationSlug={organizationId}>
<ListProjects projects={projects} />
<ListProjects projects={projects} clients={clients} />
</PageLayout>
);
}

View File

@@ -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<IServiceProject, null>) {
const { id } = project;
@@ -32,43 +23,34 @@ export function ProjectActions(project: Exclude<IServiceProject, null>) {
});
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<span className="sr-only">Open menu</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuItem onClick={() => clipboard(id)}>
Copy project ID
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
pushModal('EditProject', project);
}}
>
Edit
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
className="text-destructive"
onClick={() => {
showConfirm({
title: 'Delete project',
text: 'This will delete all events for this project. This action cannot be undone.',
onConfirm() {
deletion.mutate({
id,
});
},
});
}}
>
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<div className="flex gap-2">
<Button
variant="secondary"
onClick={() => {
pushModal('EditProject', project);
}}
icon={Edit2Icon}
>
Edit project
</Button>
<Button
variant="secondary"
className="text-destructive"
onClick={() => {
showConfirm({
title: 'Delete project',
text: 'This will delete all events for this project. This action cannot be undone.',
onConfirm() {
deletion.mutate({
id,
});
},
});
}}
icon={TrashIcon}
>
Delete project
</Button>
</div>
);
}

View File

@@ -25,7 +25,7 @@ const AccordionTrigger = React.forwardRef<
<AccordionPrimitive.Trigger
ref={ref}
className={cn(
'flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>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}

View File

@@ -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 (
<Tooltip>
<TooltipTrigger asChild={asChild}>{children}</TooltipTrigger>
<TooltipTrigger asChild={asChild} className={className}>
{children}
</TooltipTrigger>
<TooltipContent>{content}</TooltipContent>
</Tooltip>
);

View File

@@ -40,7 +40,10 @@ const validation = z
type IForm = z.infer<typeof validation>;
export default function AddClient() {
interface Props {
projectId: string;
}
export default function AddClient(props: Props) {
const { organizationId, projectId } = useAppParams();
const router = useRouter();
const form = useForm<IForm>({
@@ -49,7 +52,7 @@ export default function AddClient() {
name: '',
cors: '',
tab: 'website',
projectId,
projectId: props.projectId ?? projectId,
},
});
const mutation = api.client.create.useMutation({