web: delete dashboards

This commit is contained in:
Carl-Gerhard Lindesvärd
2023-12-12 21:55:42 +01:00
parent 61561e65c9
commit fc141a80e0
9 changed files with 129 additions and 31 deletions

View File

@@ -31,7 +31,7 @@ As of today (2023-12-12) I have more then 1.2 million events in PSQL and perform
- [*] View profiles in a list
- [ ] Invite users
- [ ] Drag n Drop reports on dashboard
- [ ] Manage dashboards
- [x] Manage dashboards
- [ ] Support more chart types
- [x] Bar
- [ ] Pie

View File

@@ -45,6 +45,7 @@
"mitt": "^3.0.1",
"next": "13.4",
"next-auth": "^4.23.0",
"prisma-error-enum": "^0.1.3",
"ramda": "^0.29.1",
"random-animal-name": "^0.1.1",
"react": "18.2.0",

View File

@@ -1,20 +1,48 @@
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import type { HtmlProps } from '@/types';
import { cn } from '@/utils/cn';
import { MoreHorizontal } from 'lucide-react';
type CardProps = HtmlProps<HTMLDivElement> & {
hover?: boolean;
};
export function Card({ children, hover }: CardProps) {
export function Card({ children, hover, className }: CardProps) {
return (
<div
className={cn(
'border border-border rounded',
hover &&
'transition-all hover:-translate-y-0.5 hover:shadow hover:border-black'
'border border-border rounded relative',
hover && 'transition-all hover:shadow hover:border-black',
className
)}
>
{children}
</div>
);
}
interface CardActionsProps {
children: React.ReactNode;
}
export function CardActions({ children }: CardActionsProps) {
return (
<div className="absolute top-2 right-2 z-10">
<DropdownMenu>
<DropdownMenuTrigger className="h-8 w-8 hover:border rounded justify-center items-center flex">
<MoreHorizontal size={16} />
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-[200px]">
<DropdownMenuGroup>{children}</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
</div>
);
}
export const CardActionsItem = DropdownMenuItem;

View File

@@ -1,35 +1,46 @@
import { useOrganizationParams } from '@/hooks/useOrganizationParams';
import { cn } from '@/utils/cn';
import { strip } from '@/utils/object';
import type { LinkProps } from 'next/link';
import Link from 'next/link';
import { NavbarUserDropdown } from './NavbarUserDropdown';
function Item({
children,
...props
}: LinkProps & { children: React.ReactNode }) {
return (
<Link
{...props}
className="h-9 items-center flex px-3 leading-none relative [&>div]:hover:opacity-100 [&>div]:hover:ring-1"
shallow
>
<div className="opacity-0 absolute inset-0 transition-all bg-gradient-to-r from-blue-50 to-purple-50 rounded ring-0 ring-purple-900" />
<span className="relative">{children}</span>
</Link>
);
}
export function NavbarMenu() {
const params = useOrganizationParams();
return (
<div className={cn('flex gap-6 items-center text-sm', 'max-sm:flex-col')}>
<div className={cn('flex gap-1 items-center text-sm', 'max-sm:flex-col')}>
{params.project && (
<Link shallow href={`/${params.organization}/${params.project}`}>
Home
</Link>
<Item href={`/${params.organization}/${params.project}`}>Home</Item>
)}
{params.project && (
<Link shallow href={`/${params.organization}/${params.project}/events`}>
<Item href={`/${params.organization}/${params.project}/events`}>
Events
</Link>
</Item>
)}
{params.project && (
<Link
shallow
href={`/${params.organization}/${params.project}/profiles`}
>
<Item href={`/${params.organization}/${params.project}/profiles`}>
Profiles
</Link>
</Item>
)}
{params.project && (
<Link
shallow
<Item
href={{
pathname: `/${params.organization}/${params.project}/reports`,
query: strip({
@@ -38,7 +49,7 @@ export function NavbarMenu() {
}}
>
Create report
</Link>
</Item>
)}
<NavbarUserDropdown />
</div>

View File

@@ -9,20 +9,20 @@ import {
} from '@/components/ui/dropdown-menu';
import { useOrganizationParams } from '@/hooks/useOrganizationParams';
import { User } from 'lucide-react';
import { signOut } from 'next-auth/react';
import { signOut, useSession } from 'next-auth/react';
import Link from 'next/link';
export function NavbarUserDropdown() {
const params = useOrganizationParams();
const session = useSession();
const user = session.data?.user;
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button>
<Avatar>
<AvatarFallback>CL</AvatarFallback>
</Avatar>
</button>
<DropdownMenuTrigger>
<Avatar>
<AvatarFallback>{user?.name?.charAt(0) ?? '🤠'}</AvatarFallback>
</Avatar>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-[200px]">
<DropdownMenuGroup>

View File

@@ -80,7 +80,7 @@ const DropdownMenuItem = React.forwardRef<
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
'relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:mr-2',
inset && 'pl-8',
className
)}

View File

@@ -1,12 +1,13 @@
import { Card } from '@/components/Card';
import { Card, CardActions, CardActionsItem } from '@/components/Card';
import { Container } from '@/components/Container';
import { MainLayout } from '@/components/layouts/MainLayout';
import { PageTitle } from '@/components/PageTitle';
import { useOrganizationParams } from '@/hooks/useOrganizationParams';
import { useRefetchActive } from '@/hooks/useRefetchActive';
import { pushModal } from '@/modals';
import { createServerSideProps } from '@/server/getServerSideProps';
import { api } from '@/utils/api';
import { Plus } from 'lucide-react';
import { api, handleError } from '@/utils/api';
import { Plus, Trash } from 'lucide-react';
import Link from 'next/link';
export const getServerSideProps = createServerSideProps();
@@ -22,6 +23,12 @@ export default function Home() {
}
);
const dashboards = query.data ?? [];
const deletion = api.dashboard.delete.useMutation({
onError: handleError,
onSuccess() {
query.refetch();
},
});
return (
<MainLayout>
@@ -37,9 +44,24 @@ export default function Home() {
>
{item.name}
</Link>
<CardActions>
<CardActionsItem className="text-destructive w-full" asChild>
<button
onClick={() => {
deletion.mutate({
id: item.id,
});
}}
>
<Trash size={16} />
Delete
</button>
</CardActionsItem>
</CardActions>
</Card>
))}
<Card hover>
<Card hover className="border-dashed">
<button
className="flex items-center justify-between w-full p-4 font-medium leading-none"
onClick={() => {

View File

@@ -3,6 +3,8 @@ import { db } from '@/server/db';
import { getDashboardBySlug } from '@/server/services/dashboard.service';
import { getProjectBySlug } from '@/server/services/project.service';
import { slug } from '@/utils/slug';
import { Prisma } from '@prisma/client';
import { PrismaError } from 'prisma-error-enum';
import { z } from 'zod';
export const dashboardRouter = createTRPCRouter({
@@ -58,4 +60,30 @@ export const dashboardRouter = createTRPCRouter({
},
});
}),
delete: protectedProcedure
.input(
z.object({
id: z.string(),
})
)
.mutation(async ({ input: { id } }) => {
try {
await db.dashboard.delete({
where: {
id,
},
});
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
switch (error.code) {
case PrismaError.ForeignConstraintViolation:
throw new Error(
'Cannot delete dashboard with associated reports'
);
default:
throw new Error('Unknown error deleting dashboard');
}
}
}
}),
});

8
pnpm-lock.yaml generated
View File

@@ -187,6 +187,9 @@ importers:
next-auth:
specifier: ^4.23.0
version: 4.24.4(next@13.4.19)(react-dom@18.2.0)(react@18.2.0)
prisma-error-enum:
specifier: ^0.1.3
version: 0.1.3
ramda:
specifier: ^0.29.1
version: 0.29.1
@@ -4919,6 +4922,11 @@ packages:
resolution: {integrity: sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==}
dev: false
/prisma-error-enum@0.1.3:
resolution: {integrity: sha512-Ybu1gnxsj4IMCdGjOqdQ7Jaf9XnDzrnnVK+LBOCQBG9OCUuBxUikgwUzBeJX3oKvZ0ni//lHqsTga0Qvh3ZfCQ==}
engines: {node: '>=10'}
dev: false
/prisma@5.5.2:
resolution: {integrity: sha512-WQtG6fevOL053yoPl6dbHV+IWgKo25IRN4/pwAGqcWmg7CrtoCzvbDbN9fXUc7QS2KK0LimHIqLsaCOX/vHl8w==}
engines: {node: '>=16.13'}