Skip to content

Commit 0b79a63

Browse files
authored
feat(workspace): add delete workspace with cascade delete (#285)
1 parent a4b40fd commit 0b79a63

File tree

26 files changed

+581
-343
lines changed

26 files changed

+581
-343
lines changed

console/src/pages/asset-group/asset-groups.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,6 @@ export function AssetGroups() {
6767
const assetGroups = data?.data ?? [];
6868
const total = data?.total ?? 0;
6969

70-
if (!data && !isLoading) return <div>Error loading asset groups.</div>;
71-
7270
return (
7371
<Page
7472
title="Groups"

console/src/pages/assets/list-assets.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ export function ListAssets() {
5959
};
6060

6161
if (workspaces.length === 0) return <CreateWorkspace />;
62+
6263
return (
6364
<div className="w-full">
6465
<div className="mb-4">
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { Button } from '@/components/ui/button';
2+
import { ConfirmDialog } from '@/components/ui/confirm-dialog';
3+
import {
4+
useWorkspacesControllerDeleteWorkspace,
5+
type Workspace,
6+
} from '@/services/apis/gen/queries';
7+
import { useQueryClient } from '@tanstack/react-query';
8+
import { Trash2Icon } from 'lucide-react';
9+
import { toast } from 'sonner';
10+
11+
interface DeleteWorkspaceProps {
12+
workspace: Workspace;
13+
}
14+
const DeleteWorkspace = (props: DeleteWorkspaceProps) => {
15+
const queryClient = useQueryClient();
16+
17+
const { workspace } = props;
18+
const { mutate } = useWorkspacesControllerDeleteWorkspace();
19+
const handleDelete = () => {
20+
mutate(
21+
{
22+
id: workspace.id,
23+
},
24+
{
25+
onSuccess: () => {
26+
toast.success('Workspace deleted');
27+
queryClient.invalidateQueries({ queryKey: ['workspaces'] });
28+
},
29+
onError: () => {
30+
toast.error('Failed to delete workspace');
31+
},
32+
},
33+
);
34+
};
35+
return (
36+
<ConfirmDialog
37+
title="Delete workspace"
38+
description={`Are you sure you want to delete workspace "${workspace.name}"? All related data including assets, vulnerabilities, scan results, and settings will be permanently deleted and cannot be recovered.`}
39+
onConfirm={handleDelete}
40+
confirmText={'Delete'}
41+
typeToConfirm={'delete'}
42+
trigger={
43+
<Button
44+
variant="ghost"
45+
size="icon"
46+
className="text-muted-foreground hover:text-muted-foreground/80"
47+
>
48+
<Trash2Icon />
49+
</Button>
50+
}
51+
/>
52+
);
53+
};
54+
55+
export default DeleteWorkspace;
Lines changed: 51 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,104 +1,101 @@
1-
import { Button } from "@/components/ui/button";
1+
import { Button } from '@/components/ui/button';
22
import {
33
Dialog,
44
DialogContent,
55
DialogDescription,
66
DialogFooter,
77
DialogHeader,
88
DialogTitle,
9-
} from "@/components/ui/dialog";
9+
} from '@/components/ui/dialog';
1010
import {
1111
Form,
1212
FormControl,
1313
FormField,
1414
FormItem,
1515
FormLabel,
1616
FormMessage,
17-
} from "@/components/ui/form";
18-
import { Input } from "@/components/ui/input";
19-
import { Textarea } from "@/components/ui/textarea";
20-
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
21-
import { useWorkspacesControllerUpdateWorkspace, type Workspace } from "@/services/apis/gen/queries";
22-
import { zodResolver } from "@hookform/resolvers/zod";
23-
import { useQueryClient } from "@tanstack/react-query";
24-
import { Edit } from "lucide-react";
25-
import { useState } from "react";
26-
import { useForm } from "react-hook-form";
27-
import { toast } from "sonner";
28-
import { z } from "zod";
17+
} from '@/components/ui/form';
18+
import { Input } from '@/components/ui/input';
19+
import { Textarea } from '@/components/ui/textarea';
20+
import {
21+
useWorkspacesControllerUpdateWorkspace,
22+
type Workspace,
23+
} from '@/services/apis/gen/queries';
24+
import { zodResolver } from '@hookform/resolvers/zod';
25+
import { useQueryClient } from '@tanstack/react-query';
26+
import { Edit } from 'lucide-react';
27+
import { useState } from 'react';
28+
import { useForm } from 'react-hook-form';
29+
import { toast } from 'sonner';
30+
import { z } from 'zod';
2931

3032
const formSchema = z.object({
31-
name: z.string().min(1, "Name is required"),
33+
name: z.string().min(1, 'Name is required'),
3234
description: z.string().optional(),
3335
});
3436

3537
interface EditWorkspaceDialogProps {
36-
workspace: Workspace
38+
workspace: Workspace;
3739
}
3840

39-
export function EditWorkspaceDialog({
40-
workspace,
41-
}: EditWorkspaceDialogProps) {
41+
export function EditWorkspaceDialog({ workspace }: EditWorkspaceDialogProps) {
4242
const form = useForm<z.infer<typeof formSchema>>({
4343
resolver: zodResolver(formSchema),
4444
defaultValues: {
4545
name: workspace.name,
46-
description: workspace.description || "",
46+
description: workspace.description || '',
4747
},
4848
});
4949

50-
const { mutate } = useWorkspacesControllerUpdateWorkspace()
50+
const { mutate } = useWorkspacesControllerUpdateWorkspace();
5151

5252
const queryClient = useQueryClient();
5353

5454
function handleSubmit(data: z.infer<typeof formSchema>) {
55-
mutate({
56-
id: workspace.id,
57-
data,
58-
}, {
59-
onSuccess: () => {
60-
setOpen(false);
61-
toast.success("Workspace updated successfully")
62-
queryClient.invalidateQueries({ queryKey: ["workspaces"] });
55+
mutate(
56+
{
57+
id: workspace.id,
58+
data,
59+
},
60+
{
61+
onSuccess: () => {
62+
setOpen(false);
63+
toast.success('Workspace updated successfully');
64+
queryClient.invalidateQueries({ queryKey: ['workspaces'] });
65+
},
66+
onError: () => {
67+
toast.error('Failed to update workspace');
68+
},
6369
},
64-
onError: () => {
65-
toast.error("Failed to update workspace")
66-
}
67-
})
70+
);
6871
}
6972

7073
const [open, setOpen] = useState(false);
7174

7275
return (
7376
<div>
74-
<TooltipProvider>
75-
<Tooltip>
76-
<TooltipTrigger asChild>
77-
<Button
78-
variant="ghost"
79-
size="icon"
80-
className="text-muted-foreground hover:text-muted-foreground/80"
81-
onClick={() => setOpen(true)}
82-
>
83-
<Edit className="h-4 w-4" />
84-
<span className="sr-only">Edit</span>
85-
</Button>
86-
</TooltipTrigger>
87-
<TooltipContent>
88-
<p>Edit workspace</p>
89-
</TooltipContent>
90-
</Tooltip>
91-
</TooltipProvider>
77+
<Button
78+
variant="ghost"
79+
size="icon"
80+
className="text-muted-foreground hover:text-muted-foreground/80"
81+
onClick={() => setOpen(true)}
82+
>
83+
<Edit className="h-4 w-4" />
84+
<span className="sr-only">Edit</span>
85+
</Button>
9286
<Dialog open={open} onOpenChange={setOpen}>
93-
<DialogContent className="sm:max-w-[425px]">
87+
<DialogContent className="sm:max-w-106.25">
9488
<DialogHeader>
9589
<DialogTitle>Edit Workspace</DialogTitle>
9690
<DialogDescription>
9791
Update the name and description of your workspace.
9892
</DialogDescription>
9993
</DialogHeader>
10094
<Form {...form}>
101-
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4">
95+
<form
96+
onSubmit={form.handleSubmit(handleSubmit)}
97+
className="space-y-4"
98+
>
10299
<FormField
103100
control={form.control}
104101
name="name"
@@ -137,6 +134,5 @@ export function EditWorkspaceDialog({
137134
</DialogContent>
138135
</Dialog>
139136
</div>
140-
141137
);
142-
}
138+
}
Lines changed: 61 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,66 @@
1-
import { Badge } from "@/components/ui/badge";
2-
import type { Workspace } from "@/services/apis/gen/queries";
3-
import { type ColumnDef } from "@tanstack/react-table";
4-
import ArchivedWorkspace from "./archived-workspace";
5-
import { EditWorkspaceDialog } from "./edit-workspace-dialog";
6-
import ShowNameWorkspace from "./show-name-workspace";
7-
1+
import { Badge } from '@/components/ui/badge';
2+
import type { Workspace } from '@/services/apis/gen/queries';
3+
import { type ColumnDef } from '@tanstack/react-table';
4+
import DeleteWorkspace from './delete-workspace';
5+
import { EditWorkspaceDialog } from './edit-workspace-dialog';
6+
import ShowNameWorkspace from './show-name-workspace';
87

98
export const workspaceColumns: ColumnDef<Workspace, unknown>[] = [
10-
{
11-
accessorKey: "name",
12-
header: "Workspace Name",
13-
cell: ({ row }) => (
14-
<ShowNameWorkspace workspace={row.original as Workspace} />
15-
),
16-
},
17-
{
18-
accessorKey: "description",
19-
header: "Description",
20-
cell: ({ row }) => (
21-
<div className="text-gray-500">{row.getValue("description") || "No description"}</div>
22-
),
23-
},
24-
{
25-
accessorKey: "createdAt",
26-
header: "Created At",
27-
cell: ({ row }) => (
28-
<div className="text-gray-500">
29-
{new Date(row.getValue("createdAt")).toLocaleDateString()}
30-
</div>
31-
),
32-
},
33-
{
34-
accessorKey: "status",
35-
header: "Status",
36-
cell: ({ row }) => {
37-
const workspace = row.original as Workspace;
38-
return (
39-
<Badge className={workspace.archivedAt ? "bg-gray-500 text-white" : "bg-green-500 text-white"}>
40-
{workspace.archivedAt ? "Archived" : "Active"}
41-
</Badge>
42-
);
43-
}
9+
{
10+
accessorKey: 'name',
11+
header: 'Workspace Name',
12+
cell: ({ row }) => (
13+
<ShowNameWorkspace workspace={row.original as Workspace} />
14+
),
15+
},
16+
{
17+
accessorKey: 'description',
18+
header: 'Description',
19+
cell: ({ row }) => (
20+
<div className="text-gray-500">
21+
{row.getValue('description') || 'No description'}
22+
</div>
23+
),
24+
},
25+
{
26+
accessorKey: 'createdAt',
27+
header: 'Created At',
28+
cell: ({ row }) => (
29+
<div className="text-gray-500">
30+
{new Date(row.getValue('createdAt')).toLocaleDateString()}
31+
</div>
32+
),
33+
},
34+
{
35+
accessorKey: 'status',
36+
header: 'Status',
37+
cell: ({ row }) => {
38+
const workspace = row.original as Workspace;
39+
return (
40+
<Badge
41+
className={
42+
workspace.archivedAt
43+
? 'bg-gray-500 text-white'
44+
: 'bg-green-500 text-white'
45+
}
46+
>
47+
{workspace.archivedAt ? 'Archived' : 'Active'}
48+
</Badge>
49+
);
4450
},
45-
{
46-
accessorKey: "actions",
47-
header: "Actions",
48-
enableSorting: false,
49-
cell: ({ row }) => {
50-
const workspace = row.original as Workspace;
51-
return (
52-
<div className="flex justify-start gap-2">
53-
<EditWorkspaceDialog workspace={workspace} />
54-
<ArchivedWorkspace workspace={workspace} />
55-
</div>
56-
);
57-
}
51+
},
52+
{
53+
accessorKey: 'actions',
54+
header: 'Actions',
55+
enableSorting: false,
56+
cell: ({ row }) => {
57+
const workspace = row.original as Workspace;
58+
return (
59+
<div className="flex justify-start gap-2">
60+
<EditWorkspaceDialog workspace={workspace} />
61+
<DeleteWorkspace workspace={workspace} />
62+
</div>
63+
);
5864
},
65+
},
5966
];

0 commit comments

Comments
 (0)