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
|
- [*] View profiles in a list
|
||||||
- [ ] Invite users
|
- [ ] Invite users
|
||||||
- [ ] Drag n Drop reports on dashboard
|
- [ ] Drag n Drop reports on dashboard
|
||||||
- [ ] Manage dashboards
|
- [x] Manage dashboards
|
||||||
- [ ] Support more chart types
|
- [ ] Support more chart types
|
||||||
- [x] Bar
|
- [x] Bar
|
||||||
- [ ] Pie
|
- [ ] Pie
|
||||||
|
|||||||
@@ -45,6 +45,7 @@
|
|||||||
"mitt": "^3.0.1",
|
"mitt": "^3.0.1",
|
||||||
"next": "13.4",
|
"next": "13.4",
|
||||||
"next-auth": "^4.23.0",
|
"next-auth": "^4.23.0",
|
||||||
|
"prisma-error-enum": "^0.1.3",
|
||||||
"ramda": "^0.29.1",
|
"ramda": "^0.29.1",
|
||||||
"random-animal-name": "^0.1.1",
|
"random-animal-name": "^0.1.1",
|
||||||
"react": "18.2.0",
|
"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 type { HtmlProps } from '@/types';
|
||||||
import { cn } from '@/utils/cn';
|
import { cn } from '@/utils/cn';
|
||||||
|
import { MoreHorizontal } from 'lucide-react';
|
||||||
|
|
||||||
type CardProps = HtmlProps<HTMLDivElement> & {
|
type CardProps = HtmlProps<HTMLDivElement> & {
|
||||||
hover?: boolean;
|
hover?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function Card({ children, hover }: CardProps) {
|
export function Card({ children, hover, className }: CardProps) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'border border-border rounded',
|
'border border-border rounded relative',
|
||||||
hover &&
|
hover && 'transition-all hover:shadow hover:border-black',
|
||||||
'transition-all hover:-translate-y-0.5 hover:shadow hover:border-black'
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</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 { useOrganizationParams } from '@/hooks/useOrganizationParams';
|
||||||
import { cn } from '@/utils/cn';
|
import { cn } from '@/utils/cn';
|
||||||
import { strip } from '@/utils/object';
|
import { strip } from '@/utils/object';
|
||||||
|
import type { LinkProps } from 'next/link';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
import { NavbarUserDropdown } from './NavbarUserDropdown';
|
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() {
|
export function NavbarMenu() {
|
||||||
const params = useOrganizationParams();
|
const params = useOrganizationParams();
|
||||||
return (
|
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 && (
|
{params.project && (
|
||||||
<Link shallow href={`/${params.organization}/${params.project}`}>
|
<Item href={`/${params.organization}/${params.project}`}>Home</Item>
|
||||||
Home
|
|
||||||
</Link>
|
|
||||||
)}
|
)}
|
||||||
{params.project && (
|
{params.project && (
|
||||||
<Link shallow href={`/${params.organization}/${params.project}/events`}>
|
<Item href={`/${params.organization}/${params.project}/events`}>
|
||||||
Events
|
Events
|
||||||
</Link>
|
</Item>
|
||||||
)}
|
)}
|
||||||
{params.project && (
|
{params.project && (
|
||||||
<Link
|
<Item href={`/${params.organization}/${params.project}/profiles`}>
|
||||||
shallow
|
|
||||||
href={`/${params.organization}/${params.project}/profiles`}
|
|
||||||
>
|
|
||||||
Profiles
|
Profiles
|
||||||
</Link>
|
</Item>
|
||||||
)}
|
)}
|
||||||
{params.project && (
|
{params.project && (
|
||||||
<Link
|
<Item
|
||||||
shallow
|
|
||||||
href={{
|
href={{
|
||||||
pathname: `/${params.organization}/${params.project}/reports`,
|
pathname: `/${params.organization}/${params.project}/reports`,
|
||||||
query: strip({
|
query: strip({
|
||||||
@@ -38,7 +49,7 @@ export function NavbarMenu() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Create report
|
Create report
|
||||||
</Link>
|
</Item>
|
||||||
)}
|
)}
|
||||||
<NavbarUserDropdown />
|
<NavbarUserDropdown />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,20 +9,20 @@ import {
|
|||||||
} from '@/components/ui/dropdown-menu';
|
} from '@/components/ui/dropdown-menu';
|
||||||
import { useOrganizationParams } from '@/hooks/useOrganizationParams';
|
import { useOrganizationParams } from '@/hooks/useOrganizationParams';
|
||||||
import { User } from 'lucide-react';
|
import { User } from 'lucide-react';
|
||||||
import { signOut } from 'next-auth/react';
|
import { signOut, useSession } from 'next-auth/react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
export function NavbarUserDropdown() {
|
export function NavbarUserDropdown() {
|
||||||
const params = useOrganizationParams();
|
const params = useOrganizationParams();
|
||||||
|
const session = useSession();
|
||||||
|
const user = session.data?.user;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger>
|
||||||
<button>
|
<Avatar>
|
||||||
<Avatar>
|
<AvatarFallback>{user?.name?.charAt(0) ?? '🤠'}</AvatarFallback>
|
||||||
<AvatarFallback>CL</AvatarFallback>
|
</Avatar>
|
||||||
</Avatar>
|
|
||||||
</button>
|
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end" className="w-[200px]">
|
<DropdownMenuContent align="end" className="w-[200px]">
|
||||||
<DropdownMenuGroup>
|
<DropdownMenuGroup>
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ const DropdownMenuItem = React.forwardRef<
|
|||||||
<DropdownMenuPrimitive.Item
|
<DropdownMenuPrimitive.Item
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
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',
|
inset && 'pl-8',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import { Card } from '@/components/Card';
|
import { Card, CardActions, CardActionsItem } from '@/components/Card';
|
||||||
import { Container } from '@/components/Container';
|
import { Container } from '@/components/Container';
|
||||||
import { MainLayout } from '@/components/layouts/MainLayout';
|
import { MainLayout } from '@/components/layouts/MainLayout';
|
||||||
import { PageTitle } from '@/components/PageTitle';
|
import { PageTitle } from '@/components/PageTitle';
|
||||||
import { useOrganizationParams } from '@/hooks/useOrganizationParams';
|
import { useOrganizationParams } from '@/hooks/useOrganizationParams';
|
||||||
|
import { useRefetchActive } from '@/hooks/useRefetchActive';
|
||||||
import { pushModal } from '@/modals';
|
import { pushModal } from '@/modals';
|
||||||
import { createServerSideProps } from '@/server/getServerSideProps';
|
import { createServerSideProps } from '@/server/getServerSideProps';
|
||||||
import { api } from '@/utils/api';
|
import { api, handleError } from '@/utils/api';
|
||||||
import { Plus } from 'lucide-react';
|
import { Plus, Trash } from 'lucide-react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
export const getServerSideProps = createServerSideProps();
|
export const getServerSideProps = createServerSideProps();
|
||||||
@@ -22,6 +23,12 @@ export default function Home() {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
const dashboards = query.data ?? [];
|
const dashboards = query.data ?? [];
|
||||||
|
const deletion = api.dashboard.delete.useMutation({
|
||||||
|
onError: handleError,
|
||||||
|
onSuccess() {
|
||||||
|
query.refetch();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MainLayout>
|
<MainLayout>
|
||||||
@@ -37,9 +44,24 @@ export default function Home() {
|
|||||||
>
|
>
|
||||||
{item.name}
|
{item.name}
|
||||||
</Link>
|
</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>
|
||||||
))}
|
))}
|
||||||
<Card hover>
|
<Card hover className="border-dashed">
|
||||||
<button
|
<button
|
||||||
className="flex items-center justify-between w-full p-4 font-medium leading-none"
|
className="flex items-center justify-between w-full p-4 font-medium leading-none"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { db } from '@/server/db';
|
|||||||
import { getDashboardBySlug } from '@/server/services/dashboard.service';
|
import { getDashboardBySlug } from '@/server/services/dashboard.service';
|
||||||
import { getProjectBySlug } from '@/server/services/project.service';
|
import { getProjectBySlug } from '@/server/services/project.service';
|
||||||
import { slug } from '@/utils/slug';
|
import { slug } from '@/utils/slug';
|
||||||
|
import { Prisma } from '@prisma/client';
|
||||||
|
import { PrismaError } from 'prisma-error-enum';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
export const dashboardRouter = createTRPCRouter({
|
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:
|
next-auth:
|
||||||
specifier: ^4.23.0
|
specifier: ^4.23.0
|
||||||
version: 4.24.4(next@13.4.19)(react-dom@18.2.0)(react@18.2.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:
|
ramda:
|
||||||
specifier: ^0.29.1
|
specifier: ^0.29.1
|
||||||
version: 0.29.1
|
version: 0.29.1
|
||||||
@@ -4919,6 +4922,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==}
|
resolution: {integrity: sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/prisma-error-enum@0.1.3:
|
||||||
|
resolution: {integrity: sha512-Ybu1gnxsj4IMCdGjOqdQ7Jaf9XnDzrnnVK+LBOCQBG9OCUuBxUikgwUzBeJX3oKvZ0ni//lHqsTga0Qvh3ZfCQ==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/prisma@5.5.2:
|
/prisma@5.5.2:
|
||||||
resolution: {integrity: sha512-WQtG6fevOL053yoPl6dbHV+IWgKo25IRN4/pwAGqcWmg7CrtoCzvbDbN9fXUc7QS2KK0LimHIqLsaCOX/vHl8w==}
|
resolution: {integrity: sha512-WQtG6fevOL053yoPl6dbHV+IWgKo25IRN4/pwAGqcWmg7CrtoCzvbDbN9fXUc7QS2KK0LimHIqLsaCOX/vHl8w==}
|
||||||
engines: {node: '>=16.13'}
|
engines: {node: '>=16.13'}
|
||||||
|
|||||||
Reference in New Issue
Block a user