Skip to content

Commit dce868a

Browse files
committed
feat: add permissions form and permission table
Fixes #2246
1 parent d650be1 commit dce868a

16 files changed

+620
-28
lines changed

src/App.tsx

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
MdOutlineIntegrationInstructions,
1313
MdOutlineSupportAgent
1414
} from "react-icons/md";
15+
import { RiShieldUserFill } from "react-icons/ri";
1516
import { VscJson } from "react-icons/vsc";
1617
import {
1718
BrowserRouter,
@@ -55,6 +56,7 @@ import { ConnectionsPage } from "./pages/Settings/ConnectionsPage";
5556
import { EventQueueStatusPage } from "./pages/Settings/EventQueueStatus";
5657
import { FeatureFlagsPage } from "./pages/Settings/FeatureFlagsPage";
5758
import NotificationSilencePage from "./pages/Settings/NotificationSilencePage";
59+
import { PermissionsPage } from "./pages/Settings/PermissionsPage";
5860
import { TopologyCardPage } from "./pages/TopologyCard";
5961
import { UsersPage } from "./pages/UsersPage";
6062
import { ConfigInsightsPage } from "./pages/config/ConfigInsightsList";
@@ -152,7 +154,7 @@ export type SettingsNavigationItems = {
152154

153155
const settingsNav: SettingsNavigationItems = {
154156
name: "Settings",
155-
icon: AdjustmentsIcon,
157+
icon: RiShieldUserFill,
156158
checkPath: false,
157159
submenu: [
158160
{
@@ -162,6 +164,13 @@ const settingsNav: SettingsNavigationItems = {
162164
featureName: features["settings.connections"],
163165
resourceName: tables.connections
164166
},
167+
{
168+
name: "Permissions",
169+
href: "/settings/permissions",
170+
icon: AdjustmentsIcon,
171+
featureName: features["settings.permissions"],
172+
resourceName: tables.permissions
173+
},
165174
...(process.env.NEXT_PUBLIC_AUTH_IS_CLERK === "true"
166175
? []
167176
: [
@@ -358,6 +367,14 @@ export function IncidentManagerRoutes({ sidebar }: { sidebar: ReactNode }) {
358367
true
359368
)}
360369
/>
370+
<Route
371+
path="permissions"
372+
element={withAuthorizationAccessCheck(
373+
<PermissionsPage />,
374+
tables.permissions,
375+
"read"
376+
)}
377+
/>
361378
<Route
362379
path="users"
363380
element={withAuthorizationAccessCheck(

src/api/services/permissions.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { AVATAR_INFO } from "@flanksource-ui/constants";
22
import { IncidentCommander } from "../axios";
33
import { resolvePostGrestRequestWithPagination } from "../resolve";
4-
import { PermissionAPIResponse } from "../types/permissions";
4+
import { PermissionAPIResponse, PermissionTable } from "../types/permissions";
55

66
export type FetchPermissionsInput = {
77
componentId?: string;
@@ -48,7 +48,7 @@ function composeQueryParamForFetchPermissions({
4848
if (connectionId) {
4949
return `connection_id=eq.${connectionId}`;
5050
}
51-
return undefined;
51+
return "";
5252
}
5353

5454
export function fetchPermissions(
@@ -79,3 +79,20 @@ export function fetchPermissions(
7979
IncidentCommander.get<PermissionAPIResponse[]>(url)
8080
);
8181
}
82+
83+
export function addPermission(permission: PermissionTable) {
84+
return IncidentCommander.post<PermissionTable>("/permissions", permission);
85+
}
86+
87+
export function updatePermission(permission: PermissionTable) {
88+
return IncidentCommander.patch<PermissionTable>(
89+
`/permissions?id=eq.${permission.id}`,
90+
permission
91+
);
92+
}
93+
94+
export function deletePermission(id: string) {
95+
return IncidentCommander.patch(`/permissions?id=eq.${id}`, {
96+
deleted_at: "now()"
97+
});
98+
}

src/api/types/permissions.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,14 @@ export type PermissionTable = {
1414
canary_id?: string;
1515
playbook_id?: string;
1616
created_by: string;
17+
connection_id?: string;
1718
person_id?: string;
1819
team_id?: string;
1920
updated_by: string;
2021
created_at: string;
2122
updated_at: string;
2223
until?: string;
24+
source?: string;
2325
};
2426

2527
export type PermissionAPIResponse = PermissionTable & {

src/components/Forms/Formik/FormikConnectionField.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ export default function FormikConnectionField({
4545
return (
4646
<FormikSelectDropdown
4747
name={name}
48-
className="h-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm"
48+
className="h-full rounded-md border-gray-300 py-2 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm"
4949
options={connections}
5050
label={label}
5151
isLoading={isLoading}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { useGetPlaybookNames } from "@flanksource-ui/api/query-hooks/playbooks";
2+
import PlaybookSpecIcon from "@flanksource-ui/components/Playbooks/Settings/PlaybookSpecIcon";
3+
import { useMemo } from "react";
4+
import FormikSelectDropdown from "./FormikSelectDropdown";
5+
6+
type FormikPlaybooksDropdownProps = {
7+
name: string;
8+
label?: string;
9+
required?: boolean;
10+
hint?: string;
11+
className?: string;
12+
};
13+
14+
export default function FormikPlaybooksDropdown({
15+
name,
16+
label,
17+
required = false,
18+
hint,
19+
className = "flex flex-col space-y-2 py-2"
20+
}: FormikPlaybooksDropdownProps) {
21+
const { isLoading, data: checks } = useGetPlaybookNames();
22+
23+
const options = useMemo(
24+
() =>
25+
checks?.map((playbook) => ({
26+
label: playbook.title || playbook.name,
27+
value: playbook.id,
28+
icon: <PlaybookSpecIcon playbook={playbook} />
29+
})),
30+
[checks]
31+
);
32+
33+
return (
34+
<FormikSelectDropdown
35+
name={name}
36+
className={className}
37+
options={options}
38+
label={label}
39+
isLoading={isLoading}
40+
required={required}
41+
hint={hint}
42+
/>
43+
);
44+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { useState } from "react";
2+
import { AiFillPlusCircle } from "react-icons/ai";
3+
import PermissionForm from "./PermissionForm";
4+
5+
export default function AddPermissionButton() {
6+
const [isOpen, setIsOpen] = useState(false);
7+
8+
return (
9+
<>
10+
<button type="button" className="" onClick={() => setIsOpen(true)}>
11+
<AiFillPlusCircle size={32} className="text-blue-600" />
12+
</button>
13+
<PermissionForm isOpen={isOpen} onClose={() => setIsOpen(false)} />
14+
</>
15+
);
16+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { deletePermission } from "@flanksource-ui/api/services/permissions";
2+
import {
3+
toastError,
4+
toastSuccess
5+
} from "@flanksource-ui/components/Toast/toast";
6+
import { ConfirmationPromptDialog } from "@flanksource-ui/ui/AlertDialog/ConfirmationPromptDialog";
7+
import { Button } from "@flanksource-ui/ui/Buttons/Button";
8+
import { useMutation } from "@tanstack/react-query";
9+
import { AxiosError } from "axios";
10+
import { useCallback, useState } from "react";
11+
import { FaCircleNotch, FaTrash } from "react-icons/fa";
12+
13+
export default function DeletePermission({
14+
permissionId,
15+
onDeleted = () => {}
16+
}: {
17+
permissionId: string;
18+
onDeleted: () => void;
19+
}) {
20+
const [isConfirmDialogOpen, setIsConfirmDialogOpen] = useState(false);
21+
22+
const { mutate: deleteResource, isLoading } = useMutation({
23+
mutationFn: async (id: string) => {
24+
const res = await deletePermission(id);
25+
return res.data;
26+
},
27+
onSuccess: (_) => {
28+
toastSuccess("Permission deleted");
29+
onDeleted();
30+
},
31+
onError: (error: AxiosError) => {
32+
toastError(error.message);
33+
}
34+
});
35+
36+
const onDeleteResource = useCallback(() => {
37+
setIsConfirmDialogOpen(false);
38+
deleteResource(permissionId);
39+
}, [deleteResource, permissionId]);
40+
41+
return (
42+
<>
43+
<Button
44+
text="Delete"
45+
disabled={isLoading}
46+
icon={
47+
!isLoading ? <FaTrash /> : <FaCircleNotch className="animate-spin" />
48+
}
49+
className="btn-danger"
50+
onClick={() => setIsConfirmDialogOpen(true)}
51+
/>
52+
53+
{isConfirmDialogOpen && (
54+
<ConfirmationPromptDialog
55+
title="Delete Permission"
56+
description="Are you sure you want to permission?"
57+
onConfirm={onDeleteResource}
58+
isOpen={isConfirmDialogOpen}
59+
onClose={() => setIsConfirmDialogOpen(false)}
60+
className="z-[9999]"
61+
/>
62+
)}
63+
</>
64+
);
65+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import FormikCanaryDropdown from "@flanksource-ui/components/Forms/Formik/FormikCanaryDropdown";
2+
import FormikConnectionField from "@flanksource-ui/components/Forms/Formik/FormikConnectionField";
3+
import FormikPlaybooksDropdown from "@flanksource-ui/components/Forms/Formik/FormikPlaybooksDropdown";
4+
import FormikResourceSelectorDropdown from "@flanksource-ui/components/Forms/Formik/FormikResourceSelectorDropdown";
5+
import { Switch } from "@flanksource-ui/ui/FormControls/Switch";
6+
import { useFormikContext } from "formik";
7+
import { useState } from "react";
8+
9+
export default function FormikPermissionSelectResourceFields() {
10+
const { setFieldValue } = useFormikContext<Record<string, any>>();
11+
12+
const [switchOption, setSwitchOption] = useState<
13+
"Component" | "Catalog" | "Canary" | "Playbook" | "Connection"
14+
>("Catalog");
15+
16+
return (
17+
<div className="flex flex-col gap-2">
18+
<label className={`form-label`}>Resource</label>
19+
<div>
20+
<div className="flex w-full flex-row">
21+
<Switch
22+
options={[
23+
"Catalog",
24+
"Component",
25+
"Canary",
26+
"Connection",
27+
"Playbook"
28+
]}
29+
className="w-auto"
30+
itemsClassName=""
31+
defaultValue="Go Template"
32+
value={switchOption}
33+
onChange={(v) => {
34+
setSwitchOption(v);
35+
setFieldValue("config_id", undefined);
36+
setFieldValue("check_id", undefined);
37+
setFieldValue("canary_id", undefined);
38+
setFieldValue("component_id", undefined);
39+
setFieldValue("playbook_id", undefined);
40+
}}
41+
/>
42+
</div>
43+
44+
{switchOption === "Catalog" && (
45+
<FormikResourceSelectorDropdown
46+
required
47+
name="config_id"
48+
configResourceSelector={[{}]}
49+
/>
50+
)}
51+
52+
{switchOption === "Component" && (
53+
<FormikResourceSelectorDropdown
54+
required
55+
name="component_id"
56+
componentResourceSelector={[{}]}
57+
/>
58+
)}
59+
60+
{switchOption === "Playbook" && (
61+
<FormikPlaybooksDropdown required name="playbook_id" />
62+
)}
63+
64+
{switchOption === "Canary" && (
65+
<FormikCanaryDropdown required name="canary_id" />
66+
)}
67+
68+
{switchOption === "Connection" && (
69+
<FormikConnectionField required name="connection_id" />
70+
)}
71+
</div>
72+
</div>
73+
);
74+
}

0 commit comments

Comments
 (0)