add delete confirm modal to resources and sites

This commit is contained in:
Milo Schwartz 2024-11-11 23:00:51 -05:00
parent 36bbb412dd
commit 93ea7e4620
No known key found for this signature in database
6 changed files with 379 additions and 131 deletions

View file

@ -0,0 +1,85 @@
import { internal } from "@app/api";
import { authCookieHeader } from "@app/api/cookies";
import { SidebarSettings } from "@app/components/SidebarSettings";
import { verifySession } from "@app/lib/auth/verifySession";
import OrgProvider from "@app/providers/OrgProvider";
import OrgUserProvider from "@app/providers/OrgUserProvider";
import { GetOrgResponse } from "@server/routers/org";
import { GetOrgUserResponse } from "@server/routers/user";
import { AxiosResponse } from "axios";
import { redirect } from "next/navigation";
import { cache } from "react";
type GeneralSettingsProps = {
children: React.ReactNode;
params: Promise<{ orgId: string }>;
};
export default async function GeneralSettingsPage({
children,
params,
}: GeneralSettingsProps) {
const { orgId } = await params;
const getUser = cache(verifySession);
const user = await getUser();
if (!user) {
redirect("/auth/login");
}
let orgUser = null;
try {
const getOrgUser = cache(async () =>
internal.get<AxiosResponse<GetOrgUserResponse>>(
`/org/${orgId}/user/${user.userId}`,
await authCookieHeader()
)
);
const res = await getOrgUser();
orgUser = res.data.data;
} catch {
redirect(`/${orgId}`);
}
let org = null;
try {
const getOrg = cache(async () =>
internal.get<AxiosResponse<GetOrgResponse>>(
`/org/${orgId}`,
await authCookieHeader()
)
);
const res = await getOrg();
org = res.data.data;
} catch {
redirect(`/${orgId}`);
}
const sidebarNavItems = [
{
title: "General",
href: `/{orgId}/settings/general`,
},
];
return (
<>
<OrgProvider org={org}>
<OrgUserProvider orgUser={orgUser}>
<div className="space-y-0.5 select-none mb-6">
<h2 className="text-2xl font-bold tracking-tight">
General
</h2>
<p className="text-muted-foreground">
Configure your organization's general settings
</p>
</div>
<SidebarSettings sidebarNavItems={sidebarNavItems}>
{children}
</SidebarSettings>
</OrgUserProvider>
</OrgProvider>
</>
);
}

View file

@ -0,0 +1,58 @@
"use client";
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
import { Button } from "@app/components/ui/button";
import { useOrgContext } from "@app/hooks/useOrgContext";
import { userOrgUserContext } from "@app/hooks/useOrgUserContext";
import { useState } from "react";
export default function GeneralPage() {
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const { orgUser } = userOrgUserContext();
const { org } = useOrgContext();
async function deleteOrg() {
console.log("not implemented");
}
return (
<>
<ConfirmDeleteDialog
open={isDeleteModalOpen}
setOpen={(val) => {
setIsDeleteModalOpen(val);
}}
dialog={
<div>
<p className="mb-2">
Are you sure you want to delete the organization{" "}
<b>{org?.org.name}?</b>
</p>
<p className="mb-2">
This action is irreversible and will delete all
associated data.
</p>
<p>
To confirm, type the name of the organization below.
</p>
</div>
}
buttonText="Confirm delete organization"
onConfirm={deleteOrg}
string={org?.org.name || ""}
title="Delete organization"
/>
{orgUser.isOwner ? (
<Button onClick={() => setIsDeleteModalOpen(true)}>
Delete Organization
</Button>
) : (
<p>Nothing to see here</p>
)}
</>
);
}

View file

@ -96,99 +96,113 @@ export default function GeneralForm() {
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>
This is the display name of the resource.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="siteId"
render={({ field }) => (
<FormItem className="flex flex-col">
<FormLabel>Site</FormLabel>
<Popover>
<PopoverTrigger asChild>
<FormControl>
<Button
variant="outline"
role="combobox"
className={cn(
"w-[350px] justify-between",
!field.value &&
"text-muted-foreground"
)}
>
{field.value
? sites.find(
(site) =>
site.siteId ===
field.value
)?.name
: "Select site"}
<CaretSortIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="w-[350px] p-0">
<Command>
<CommandInput placeholder="Search site..." />
<CommandList>
<CommandEmpty>
No site found.
</CommandEmpty>
<CommandGroup>
{sites.map((site) => (
<CommandItem
value={site.name}
key={site.siteId}
onSelect={() => {
form.setValue(
"siteId",
site.siteId
);
}}
>
<CheckIcon
className={cn(
"mr-2 h-4 w-4",
site.siteId ===
field.value
? "opacity-100"
: "opacity-0"
)}
/>
{site.name}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
<FormDescription>
This is the site that will be used in the
dashboard.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Update Resource</Button>
</form>
</Form>
<>
<div className="space-y-0.5 select-none mb-6">
<h2 className="text-2xl font-bold tracking-tight">
General Settings
</h2>
<p className="text-muted-foreground">
Configure the general settings for this resource
</p>
</div>
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-4"
>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>
This is the display name of the resource.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="siteId"
render={({ field }) => (
<FormItem className="flex flex-col">
<FormLabel>Site</FormLabel>
<Popover>
<PopoverTrigger asChild>
<FormControl>
<Button
variant="outline"
role="combobox"
className={cn(
"w-[350px] justify-between",
!field.value &&
"text-muted-foreground"
)}
>
{field.value
? sites.find(
(site) =>
site.siteId ===
field.value
)?.name
: "Select site"}
<CaretSortIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="w-[350px] p-0">
<Command>
<CommandInput placeholder="Search site..." />
<CommandList>
<CommandEmpty>
No site found.
</CommandEmpty>
<CommandGroup>
{sites.map((site) => (
<CommandItem
value={site.name}
key={site.siteId}
onSelect={() => {
form.setValue(
"siteId",
site.siteId
);
}}
>
<CheckIcon
className={cn(
"mr-2 h-4 w-4",
site.siteId ===
field.value
? "opacity-100"
: "opacity-0"
)}
/>
{site.name}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
<FormDescription>
This is the site that will be used in the
dashboard.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Update Resource</Button>
</form>
</Form>
</>
);
}

View file

@ -25,11 +25,9 @@ const isValidIPAddress = (ip: string) => {
return ipv4Regex.test(ip);
};
export default function ReverseProxyTargets(
props: {
params: Promise<{ resourceId: number }>;
}
) {
export default function ReverseProxyTargets(props: {
params: Promise<{ resourceId: number }>;
}) {
const params = use(props.params);
const [targets, setTargets] = useState<ListTargetsResponse["targets"]>([]);
const [nextId, setNextId] = useState(1);
@ -39,7 +37,7 @@ export default function ReverseProxyTargets(
if (typeof window !== "undefined") {
const fetchSites = async () => {
const res = await api.get<AxiosResponse<ListTargetsResponse>>(
`/resource/${params.resourceId}/targets`,
`/resource/${params.resourceId}/targets`
);
setTargets(res.data.data.targets);
};
@ -93,7 +91,7 @@ export default function ReverseProxyTargets(
})
.then((res) => {
setTargets(
targets.filter((target) => target.targetId !== targetId),
targets.filter((target) => target.targetId !== targetId)
);
});
};
@ -103,8 +101,8 @@ export default function ReverseProxyTargets(
targets.map((target) =>
target.targetId === targetId
? { ...target, enabled: !target.enabled }
: target,
),
: target
)
);
api.post(`/target/${targetId}`, {
enabled: !targets.find((target) => target.targetId === targetId)
@ -115,7 +113,14 @@ export default function ReverseProxyTargets(
};
return (
<div className="space-y-6">
<div>
<div className="space-y-0.5 select-none mb-6">
<h2 className="text-2xl font-bold tracking-tight">Targets</h2>
<p className="text-muted-foreground">
Setup the targets for the reverse proxy
</p>
</div>
<form
onSubmit={(e) => {
e.preventDefault();
@ -192,9 +197,7 @@ export default function ReverseProxyTargets(
</Select>
</div>
</div>
<Button type="submit">
<PlusCircle className="mr-2 h-4 w-4" /> Add Target
</Button>
<Button type="submit">Add Target</Button>
</form>
<div className="space-y-4">

View file

@ -15,6 +15,8 @@ import { useRouter } from "next/navigation";
import api from "@app/api";
import CreateResourceForm from "./CreateResourceForm";
import { useState } from "react";
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
import { set } from "zod";
export type ResourceRow = {
id: number;
@ -33,6 +35,20 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
const router = useRouter();
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [selectedResource, setSelectedResource] =
useState<ResourceRow | null>();
const deleteResource = (resourceId: number) => {
api.delete(`/resource/${resourceId}`)
.catch((e) => {
console.error("Error deleting resource", e);
})
.then(() => {
router.refresh();
setIsDeleteModalOpen(false);
});
};
const columns: ColumnDef<ResourceRow>[] = [
{
@ -78,16 +94,6 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
const resourceRow = row.original;
const deleteResource = (resourceId: number) => {
api.delete(`/resource/${resourceId}`)
.catch((e) => {
console.error("Error deleting resource", e);
})
.then(() => {
router.refresh();
});
};
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
@ -106,9 +112,10 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
</DropdownMenuItem>
<DropdownMenuItem>
<button
onClick={() =>
deleteResource(resourceRow.id)
}
onClick={() => {
setSelectedResource(resourceRow);
setIsDeleteModalOpen(true);
}}
className="text-red-600 hover:text-red-800 hover:underline cursor-pointer"
>
Delete
@ -128,6 +135,43 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
setOpen={setIsCreateModalOpen}
/>
{selectedResource && (
<ConfirmDeleteDialog
open={isDeleteModalOpen}
setOpen={(val) => {
setIsDeleteModalOpen(val);
setSelectedResource(null);
}}
dialog={
<div>
<p className="mb-2">
Are you sure you want to remove the resource{" "}
<b>
{selectedResource?.name ||
selectedResource?.id}
</b>{" "}
from the organization?
</p>
<p className="mb-2">
Once removed, the resource will no longer be
accessible. All targets attached to the resource
will be removed.
</p>
<p>
To confirm, please type the name of the resource
below.
</p>
</div>
}
buttonText="Confirm delete resource"
onConfirm={async () => deleteResource(selectedResource!.id)}
string={selectedResource.name}
title="Delete resource"
/>
)}
<ResourcesDataTable
columns={columns}
data={resources}

View file

@ -16,6 +16,7 @@ import api from "@app/api";
import { AxiosResponse } from "axios";
import { useState } from "react";
import CreateSiteForm from "./CreateSiteForm";
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
export type SiteRow = {
id: number;
@ -35,12 +36,25 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
const router = useRouter();
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [selectedSite, setSelectedSite] = useState<SiteRow | null>(null);
const callApi = async () => {
const res = await api.put<AxiosResponse<any>>(`/newt`);
console.log(res);
};
const deleteSite = (siteId: number) => {
api.delete(`/site/${siteId}`)
.catch((e) => {
console.error("Error deleting site", e);
})
.then(() => {
router.refresh();
setIsDeleteModalOpen(false);
});
};
const columns: ColumnDef<SiteRow>[] = [
{
accessorKey: "name",
@ -89,16 +103,6 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
const siteRow = row.original;
const deleteSite = (siteId: number) => {
api.delete(`/site/${siteId}`)
.catch((e) => {
console.error("Error deleting site", e);
})
.then(() => {
router.refresh();
});
};
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
@ -117,7 +121,10 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
</DropdownMenuItem>
<DropdownMenuItem>
<button
onClick={() => deleteSite(siteRow.id)}
onClick={() => {
setSelectedSite(siteRow);
setIsDeleteModalOpen(true);
}}
className="text-red-600 hover:text-red-800"
>
Delete
@ -137,6 +144,43 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
setOpen={setIsCreateModalOpen}
/>
{selectedSite && (
<ConfirmDeleteDialog
open={isDeleteModalOpen}
setOpen={(val) => {
setIsDeleteModalOpen(val);
setSelectedSite(null);
}}
dialog={
<div>
<p className="mb-2">
Are you sure you want to remove the site{" "}
<b>{selectedSite?.name || selectedSite?.id}</b>{" "}
from the organization?
</p>
<p className="mb-2">
Once removed, the site will no longer be
accessible.{" "}
<b>
All resources and targets associated with
the site will also be removed.
</b>
</p>
<p>
To confirm, please type the name of the site
below.
</p>
</div>
}
buttonText="Confirm delete site"
onConfirm={async () => deleteSite(selectedSite!.id)}
string={selectedSite.name}
title="Delete site"
/>
)}
<SitesDataTable
columns={columns}
data={sites}