Skip to content

Commit

Permalink
feat: add permissions page with ability to manage permissions
Browse files Browse the repository at this point in the history
Fixes #2246

chore: enable hiding resource column

fix: make minor adjustments to resource display
  • Loading branch information
mainawycliffe committed Oct 14, 2024
1 parent ba4fa25 commit 826bff2
Show file tree
Hide file tree
Showing 14 changed files with 850 additions and 2 deletions.
17 changes: 17 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
MdOutlineIntegrationInstructions,
MdOutlineSupportAgent
} from "react-icons/md";
import { RiShieldUserFill } from "react-icons/ri";
import { VscJson } from "react-icons/vsc";
import {
BrowserRouter,
Expand Down Expand Up @@ -53,6 +54,7 @@ import {
import { ConnectionsPage } from "./pages/Settings/ConnectionsPage";
import { EventQueueStatusPage } from "./pages/Settings/EventQueueStatus";
import { FeatureFlagsPage } from "./pages/Settings/FeatureFlagsPage";
import { PermissionsPage } from "./pages/Settings/PermissionsPage";
import NotificationSilencedAddPage from "./pages/Settings/notifications/NotificationSilencedAddPage";
import NotificationsPage from "./pages/Settings/notifications/NotificationsPage";
import NotificationRulesPage from "./pages/Settings/notifications/NotificationsRulesPage";
Expand Down Expand Up @@ -164,6 +166,13 @@ const settingsNav: SettingsNavigationItems = {
featureName: features["settings.connections"],
resourceName: tables.connections
},
{
name: "Permissions",
href: "/settings/permissions",
icon: RiShieldUserFill,
featureName: features["settings.permissions"],
resourceName: tables.permissions
},
...(process.env.NEXT_PUBLIC_AUTH_IS_CLERK === "true"
? []
: [
Expand Down Expand Up @@ -397,6 +406,14 @@ export function IncidentManagerRoutes({ sidebar }: { sidebar: ReactNode }) {
true
)}
/>
<Route
path="permissions"
element={withAuthorizationAccessCheck(
<PermissionsPage />,
tables.permissions,
"read"
)}
/>
<Route
path="users"
element={withAuthorizationAccessCheck(
Expand Down
98 changes: 98 additions & 0 deletions src/api/services/permissions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { AVATAR_INFO } from "@flanksource-ui/constants";
import { IncidentCommander } from "../axios";
import { resolvePostGrestRequestWithPagination } from "../resolve";
import { PermissionAPIResponse, PermissionTable } from "../types/permissions";

export type FetchPermissionsInput = {
componentId?: string;
personId?: string;
teamId?: string;
configId?: string;
checkId?: string;
canaryId?: string;
playbookId?: string;
connectionId?: string;
};

function composeQueryParamForFetchPermissions({
componentId,
personId,
teamId,
configId,
checkId,
canaryId,
playbookId,
connectionId
}: FetchPermissionsInput) {
if (componentId) {
return `component_id=eq.${componentId}`;
}
if (personId) {
return `person_id=eq.${personId}`;
}
if (teamId) {
return `team_id=eq.${teamId}`;
}
if (configId) {
return `config_id=eq.${configId}`;
}
if (checkId) {
return `check_id=eq.${checkId}`;
}
if (canaryId) {
return `canary_id=eq.${canaryId}`;
}
if (playbookId) {
return `playbook_id=eq.${playbookId}`;
}
if (connectionId) {
return `connection_id=eq.${connectionId}`;
}
return "";
}

export function fetchPermissions(
input: FetchPermissionsInput,
pagination: {
pageSize: number;
pageIndex: number;
}
) {
const queryParam = composeQueryParamForFetchPermissions(input);
const selectFields = [
"*",
// "checks:check_id(id, name, status, type)",
"catalog:config_id(id, name, type, config_class)",
"component:component_id(id, name, icon)",
"canary:canary_id(id, name)",
"playbook:playbook_id(id, title, name, icon)",
"team:team_id(id, name, icon)",
`person:person_id(${AVATAR_INFO})`,
`createdBy:created_by(${AVATAR_INFO})`,
`connection:connection_id(id,name,type)`
];

const { pageSize, pageIndex } = pagination;

const url = `/permissions?${queryParam}&select=${selectFields.join(",")}&limit=${pageSize}&offset=${pageIndex * pageSize}`;
return resolvePostGrestRequestWithPagination(
IncidentCommander.get<PermissionAPIResponse[]>(url)
);
}

export function addPermission(permission: PermissionTable) {
return IncidentCommander.post<PermissionTable>("/permissions", permission);
}

export function updatePermission(permission: PermissionTable) {
return IncidentCommander.patch<PermissionTable>(
`/permissions?id=eq.${permission.id}`,
permission
);
}

export function deletePermission(id: string) {
return IncidentCommander.patch(`/permissions?id=eq.${id}`, {
deleted_at: "now()"
});
}
40 changes: 40 additions & 0 deletions src/api/types/permissions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Connection } from "@flanksource-ui/components/Connections/ConnectionFormModal";
import { ConfigItem } from "./configs";
import { PlaybookSpec } from "./playbooks";
import { Topology } from "./topology";
import { Team, User } from "./users";

export type PermissionTable = {
id: string;
description: string;
action: string;
deny?: boolean;
component_id?: string;
config_id?: string;
canary_id?: string;
playbook_id?: string;
created_by: string;
connection_id?: string;
person_id?: string;
team_id?: string;
updated_by: string;
created_at: string;
updated_at: string;
until?: string;
source?: string;
};

export type PermissionAPIResponse = PermissionTable & {
// checks: Pick<HealthCheck, "id" | "name" | "type" | "status">;
catalog: Pick<ConfigItem, "id" | "name" | "type" | "config_class">;
component: Pick<Topology, "id" | "name" | "icon">;
canary: {
id: string;
name: string;
};
playbook: Pick<PlaybookSpec, "id" | "name" | "icon" | "title">;
team: Pick<Team, "id" | "name" | "icon">;
connection: Pick<Connection, "id" | "name" | "type">;
person: User;
createdBy: User;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { useState } from "react";
import { AiFillPlusCircle } from "react-icons/ai";
import PermissionForm from "./PermissionForm";

export default function AddPermissionButton() {
const [isOpen, setIsOpen] = useState(false);

return (
<>
<button type="button" className="" onClick={() => setIsOpen(true)}>
<AiFillPlusCircle size={32} className="text-blue-600" />
</button>
<PermissionForm isOpen={isOpen} onClose={() => setIsOpen(false)} />
</>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { deletePermission } from "@flanksource-ui/api/services/permissions";
import {
toastError,
toastSuccess
} from "@flanksource-ui/components/Toast/toast";
import { ConfirmationPromptDialog } from "@flanksource-ui/ui/AlertDialog/ConfirmationPromptDialog";
import { Button } from "@flanksource-ui/ui/Buttons/Button";
import { useMutation } from "@tanstack/react-query";
import { AxiosError } from "axios";
import { useCallback, useState } from "react";
import { FaCircleNotch, FaTrash } from "react-icons/fa";

export default function DeletePermission({
permissionId,
onDeleted = () => {}
}: {
permissionId: string;
onDeleted: () => void;
}) {
const [isConfirmDialogOpen, setIsConfirmDialogOpen] = useState(false);

const { mutate: deleteResource, isLoading } = useMutation({
mutationFn: async (id: string) => {
const res = await deletePermission(id);
return res.data;
},
onSuccess: (_) => {
toastSuccess("Permission deleted");
onDeleted();
},
onError: (error: AxiosError) => {
toastError(error.message);
}
});

const onDeleteResource = useCallback(() => {
setIsConfirmDialogOpen(false);
deleteResource(permissionId);
}, [deleteResource, permissionId]);

return (
<>
<Button
text="Delete"
disabled={isLoading}
icon={
!isLoading ? <FaTrash /> : <FaCircleNotch className="animate-spin" />
}
className="btn-danger"
onClick={() => setIsConfirmDialogOpen(true)}
/>

{isConfirmDialogOpen && (
<ConfirmationPromptDialog
title="Delete Permission"
description="Are you sure you want to permission?"
onConfirm={onDeleteResource}
isOpen={isConfirmDialogOpen}
onClose={() => setIsConfirmDialogOpen(false)}
className="z-[9999]"
/>
)}
</>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import FormikCanaryDropdown from "@flanksource-ui/components/Forms/Formik/FormikCanaryDropdown";
import FormikConnectionField from "@flanksource-ui/components/Forms/Formik/FormikConnectionField";
import FormikPlaybooksDropdown from "@flanksource-ui/components/Forms/Formik/FormikPlaybooksDropdown";
import FormikResourceSelectorDropdown from "@flanksource-ui/components/Forms/Formik/FormikResourceSelectorDropdown";
import { Switch } from "@flanksource-ui/ui/FormControls/Switch";
import { useFormikContext } from "formik";
import { useState } from "react";

export default function FormikPermissionSelectResourceFields() {
const { setFieldValue } = useFormikContext<Record<string, any>>();

const [switchOption, setSwitchOption] = useState<
"Component" | "Catalog" | "Canary" | "Playbook" | "Connection"
>("Catalog");

return (
<div className="flex flex-col gap-2">
<label className={`form-label`}>Resource</label>
<div>
<div className="flex w-full flex-row">
<Switch
options={["Catalog", "Component", "Connection", "Playbook"]}
className="w-auto"
itemsClassName=""
defaultValue="Go Template"
value={switchOption}
onChange={(v) => {
setSwitchOption(v);
setFieldValue("config_id", undefined);
setFieldValue("check_id", undefined);
setFieldValue("canary_id", undefined);
setFieldValue("component_id", undefined);
setFieldValue("playbook_id", undefined);
}}
/>
</div>

{switchOption === "Catalog" && (
<FormikResourceSelectorDropdown
required
name="config_id"
configResourceSelector={[{}]}
/>
)}

{switchOption === "Component" && (
<FormikResourceSelectorDropdown
required
name="component_id"
componentResourceSelector={[{}]}
/>
)}

{switchOption === "Playbook" && (
<FormikPlaybooksDropdown required name="playbook_id" />
)}

{switchOption === "Canary" && (
<FormikCanaryDropdown required name="canary_id" />
)}

{switchOption === "Connection" && (
<FormikConnectionField required name="connection_id" />
)}
</div>
</div>
);
}
Loading

0 comments on commit 826bff2

Please sign in to comment.