web: delete dashboards
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
)}
|
||||
|
||||
@@ -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={() => {
|
||||
|
||||
@@ -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
8
pnpm-lock.yaml
generated
@@ -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'}
|
||||
|
||||
Reference in New Issue
Block a user