From 8c244a8c7dbaa540b296bd8235fb720d40e0bc46 Mon Sep 17 00:00:00 2001 From: gaspard Date: Sat, 27 Jul 2024 17:00:50 +0200 Subject: [PATCH] feat: track permission changes --- .../admin/users/editGroup/EditGroup.tsx | 2 +- .../AclPermissionCheckbox.tsx | 15 +++-- .../AclTable.module.scss | 0 .../AclTable.tsx | 41 ++----------- .../users/editPermissions/AreaAccess.tsx | 29 ++++++++++ .../GroupPermissions.tsx | 34 +++-------- .../GroupPermissionsEditor.tsx | 55 ++++++++++++++++++ .../PermissionCheckbox.tsx | 14 +++-- .../permission.utils.test.ts} | 31 +++++++++- .../users/editPermissions/permission.utils.ts | 57 +++++++++++++++++++ src/mockServer/fixtures/permissionDTO.ts | 5 +- 11 files changed, 208 insertions(+), 75 deletions(-) rename src/components/accessories/admin/users/{editGroup => editPermissions}/AclPermissionCheckbox.tsx (73%) rename src/components/accessories/admin/users/{editGroup => editPermissions}/AclTable.module.scss (100%) rename src/components/accessories/admin/users/{editGroup => editPermissions}/AclTable.tsx (59%) create mode 100644 src/components/accessories/admin/users/editPermissions/AreaAccess.tsx rename src/components/accessories/admin/users/{editGroup => editPermissions}/GroupPermissions.tsx (54%) create mode 100644 src/components/accessories/admin/users/editPermissions/GroupPermissionsEditor.tsx rename src/components/accessories/admin/users/{editGroup => editPermissions}/PermissionCheckbox.tsx (72%) rename src/components/accessories/admin/users/{editGroup/GroupPermissions.test.ts => editPermissions/permission.utils.test.ts} (64%) create mode 100644 src/components/accessories/admin/users/editPermissions/permission.utils.ts diff --git a/src/components/accessories/admin/users/editGroup/EditGroup.tsx b/src/components/accessories/admin/users/editGroup/EditGroup.tsx index 5ef985f16..6e9d73856 100644 --- a/src/components/accessories/admin/users/editGroup/EditGroup.tsx +++ b/src/components/accessories/admin/users/editGroup/EditGroup.tsx @@ -20,7 +20,7 @@ import { createUserGroup, createUserGroupReset, } from "../../../../../state/usergroups/actions"; -import { GroupPermissions } from "./GroupPermissions"; +import { GroupPermissions } from "../editPermissions/GroupPermissions"; export const EditGroup = () => { const dispatch = useDispatch(); diff --git a/src/components/accessories/admin/users/editGroup/AclPermissionCheckbox.tsx b/src/components/accessories/admin/users/editPermissions/AclPermissionCheckbox.tsx similarity index 73% rename from src/components/accessories/admin/users/editGroup/AclPermissionCheckbox.tsx rename to src/components/accessories/admin/users/editPermissions/AclPermissionCheckbox.tsx index 96fc23ee1..4f3ec9242 100644 --- a/src/components/accessories/admin/users/editGroup/AclPermissionCheckbox.tsx +++ b/src/components/accessories/admin/users/editPermissions/AclPermissionCheckbox.tsx @@ -2,17 +2,18 @@ import React from "react"; import { Checkbox, Popper } from "@material-ui/core"; import { PermissionDTO } from "../../../../../generated"; +import { computeNewPermission } from "./permission.utils"; interface IProps { permission: PermissionDTO; onChange: (permission: PermissionDTO) => void; - thisGroup: string; + thisGroupId: string; } export const AclPermissionCheckbox = ({ permission, onChange, - thisGroup, + thisGroupId, }: IProps) => { const [anchorEl, setAnchorEl] = React.useState(null); const open = Boolean(anchorEl); @@ -21,8 +22,12 @@ export const AclPermissionCheckbox = ({ <> group === thisGroup)} - onChange={() => console.log} + checked={ + !!permission.userGroupIds.find((group) => group === thisGroupId) + } + onChange={(_ev, val) => + onChange(computeNewPermission(thisGroupId, permission, val)) + } name={permission.id.toString()} onMouseEnter={(event: React.MouseEvent) => { setAnchorEl(anchorEl ? null : event.currentTarget); @@ -45,6 +50,8 @@ export const AclPermissionCheckbox = ({ }} > {permission.name || "unknown"} + {/**- TODO REMOVE DEBUG */} + •{permission.userGroupIds.join(",")} {thisGroupId} diff --git a/src/components/accessories/admin/users/editGroup/AclTable.module.scss b/src/components/accessories/admin/users/editPermissions/AclTable.module.scss similarity index 100% rename from src/components/accessories/admin/users/editGroup/AclTable.module.scss rename to src/components/accessories/admin/users/editPermissions/AclTable.module.scss diff --git a/src/components/accessories/admin/users/editGroup/AclTable.tsx b/src/components/accessories/admin/users/editPermissions/AclTable.tsx similarity index 59% rename from src/components/accessories/admin/users/editGroup/AclTable.tsx rename to src/components/accessories/admin/users/editPermissions/AclTable.tsx index 48552a218..d30a063af 100644 --- a/src/components/accessories/admin/users/editGroup/AclTable.tsx +++ b/src/components/accessories/admin/users/editPermissions/AclTable.tsx @@ -3,44 +3,15 @@ import React, { useMemo } from "react"; import { PermissionDTO } from "../../../../../generated"; import { AclPermissionCheckbox } from "./AclPermissionCheckbox"; import classes from "./AclTable.module.scss"; +import { Crud, groupPermissions } from "./permission.utils"; interface IProps { permissions: PermissionDTO[]; userGroupId: string; + onChange: (permission: PermissionDTO) => void; } -enum Crud { - CREATE = "create", - READ = "read", - UPDATE = "update", - DELETE = "delete", -} - -export const groupPermissions = ( - permissions: PermissionDTO[] -): Map> => { - let permissionNames = new Map(); - for (let i = 0; i < permissions.length; i++) { - const matches = - permissions[i].name && - /([a-z]+)\.(create|read|update|delete)$/.exec(permissions[i].name || ""); - // no match: skip - if (!matches) continue; - const [, key, access] = matches; - - if (!!permissionNames.get(key)?.[access]) { - throw new Error(`duplicate permission ${key}.${access}`); - } - - permissionNames.set(key, { - ...permissionNames.get(key), - [access]: permissions[i], - }); - } - return permissionNames; -}; - -export const AclTable = ({ permissions, userGroupId }: IProps) => { +export const AclTable = ({ permissions, userGroupId, onChange }: IProps) => { const crudPermissions = useMemo(() => { return groupPermissions(permissions); }, [permissions]); @@ -69,12 +40,12 @@ export const AclTable = ({ permissions, userGroupId }: IProps) => { console.log} - thisGroup={userGroupId} + onChange={onChange} + thisGroupId={userGroupId} /> ) : ( - + ); diff --git a/src/components/accessories/admin/users/editPermissions/AreaAccess.tsx b/src/components/accessories/admin/users/editPermissions/AreaAccess.tsx new file mode 100644 index 000000000..fce9dd2d8 --- /dev/null +++ b/src/components/accessories/admin/users/editPermissions/AreaAccess.tsx @@ -0,0 +1,29 @@ +import React from "react"; + +import { PermissionDTO } from "../../../../../generated"; +import { PermissionCheckbox } from "./PermissionCheckbox"; + +interface IProps { + permissions: PermissionDTO[]; + thisGroupId: string; + onChange: (permission: PermissionDTO) => void; +} +export const AreaAccess = ({ permissions, thisGroupId, onChange }: IProps) => { + return ( +
    + {permissions + .filter( + (perm: PermissionDTO) => perm.name && /\.access$/.test(perm.name) + ) + .map((perm, index) => ( +
  • + +
  • + ))} +
+ ); +}; diff --git a/src/components/accessories/admin/users/editGroup/GroupPermissions.tsx b/src/components/accessories/admin/users/editPermissions/GroupPermissions.tsx similarity index 54% rename from src/components/accessories/admin/users/editGroup/GroupPermissions.tsx rename to src/components/accessories/admin/users/editPermissions/GroupPermissions.tsx index a71affe74..1bf12a871 100644 --- a/src/components/accessories/admin/users/editGroup/GroupPermissions.tsx +++ b/src/components/accessories/admin/users/editPermissions/GroupPermissions.tsx @@ -3,11 +3,9 @@ import { useDispatch, useSelector } from "react-redux"; import { getAllPermissions } from "../../../../../state/permissions/actions"; import { IState } from "../../../../../types"; -import { UserGroupDTO, PermissionDTO } from "../../../../../generated"; +import { UserGroupDTO } from "../../../../../generated"; import InfoBox from "../../../infoBox/InfoBox"; - -import { PermissionCheckbox } from "./PermissionCheckbox"; -import { AclTable } from "./AclTable"; +import { GroupPermissionsEditor } from "./GroupPermissionsEditor"; interface IProps { userGroupId: Pick | string; @@ -31,29 +29,11 @@ export const GroupPermissions = ({ userGroupId }: IProps) => { ); if (!permissionsState.hasSucceeded || !permissionsState.data) return <>...; if (!permissionsState.data.length) return <>no permissions; + return ( - <> -

Areas access

-
    - {permissionsState.data - .filter( - (perm: PermissionDTO) => perm.name && /\.access$/.test(perm.name) - ) - .map((perm, index) => ( -
  • - console.log} - thisGroup={userGroupId as string} - /> -
  • - ))} -
-

Access-control list

- - + ); }; diff --git a/src/components/accessories/admin/users/editPermissions/GroupPermissionsEditor.tsx b/src/components/accessories/admin/users/editPermissions/GroupPermissionsEditor.tsx new file mode 100644 index 000000000..2426dd934 --- /dev/null +++ b/src/components/accessories/admin/users/editPermissions/GroupPermissionsEditor.tsx @@ -0,0 +1,55 @@ +import React, { useEffect, useState } from "react"; + +import { PermissionDTO } from "../../../../../generated"; +import { AclTable } from "./AclTable"; +import { AreaAccess } from "./AreaAccess"; + +interface IProps { + permissions: PermissionDTO[]; + thisGroupId: string; +} +export const GroupPermissionsEditor = ({ + permissions, + thisGroupId, +}: IProps) => { + const [permissionsStack, setPermissionsStack] = useState([]); + const [permissionsState, setPermissionsState] = useState([]); + + useEffect(() => { + setPermissionsState(permissions); + }, [permissions]); + + const handleChange = (newPermission: PermissionDTO) => { + const otherPermissions = permissionsStack.filter( + ({ id }) => id !== newPermission.id + ); + setPermissionsStack([...otherPermissions, newPermission]); + const newState = permissionsState.map((perm) => + perm.id === newPermission.id ? newPermission : perm + ); + setPermissionsState(newState); + }; + + return ( + <> +

Areas access

+ + +

Access-control list

+ + {permissionsStack.length && ( +

+ Editing permissions: {permissionsStack.map(({ id }) => id).join(",")} +

+ )} + + ); +}; diff --git a/src/components/accessories/admin/users/editGroup/PermissionCheckbox.tsx b/src/components/accessories/admin/users/editPermissions/PermissionCheckbox.tsx similarity index 72% rename from src/components/accessories/admin/users/editGroup/PermissionCheckbox.tsx rename to src/components/accessories/admin/users/editPermissions/PermissionCheckbox.tsx index ea9b64a16..02aec7c3c 100644 --- a/src/components/accessories/admin/users/editGroup/PermissionCheckbox.tsx +++ b/src/components/accessories/admin/users/editPermissions/PermissionCheckbox.tsx @@ -1,27 +1,29 @@ import React from "react"; - -import { PermissionDTO } from "../../../../../generated"; import { Checkbox, FormControlLabel } from "@material-ui/core"; +import { PermissionDTO } from "../../../../../generated"; +import { computeNewPermission } from "./permission.utils"; interface IProps { permission: PermissionDTO; onChange: (permission: PermissionDTO) => void; - thisGroup: string; + thisGroupId: string; } export const PermissionCheckbox = ({ permission, onChange, - thisGroup, + thisGroupId, }: IProps) => { return ( group === thisGroup) + !!permission.userGroupIds.find((group) => group === thisGroupId) + } + onChange={(_ev, val) => + onChange(computeNewPermission(thisGroupId, permission, val)) } - onChange={() => console.log} name={permission.id.toString()} /> } diff --git a/src/components/accessories/admin/users/editGroup/GroupPermissions.test.ts b/src/components/accessories/admin/users/editPermissions/permission.utils.test.ts similarity index 64% rename from src/components/accessories/admin/users/editGroup/GroupPermissions.test.ts rename to src/components/accessories/admin/users/editPermissions/permission.utils.test.ts index 0e9eb9db6..b6096c7e2 100644 --- a/src/components/accessories/admin/users/editGroup/GroupPermissions.test.ts +++ b/src/components/accessories/admin/users/editPermissions/permission.utils.test.ts @@ -1,5 +1,5 @@ import { PermissionDTO } from "../../../../../generated"; -import { groupPermissions } from "./AclTable"; +import { computeNewPermission, groupPermissions } from "./permission.utils"; describe("groupPermissions", () => { it("should ignore non-crud actions and badly formatted permission names", () => { @@ -57,3 +57,32 @@ describe("groupPermissions", () => { expect(() => groupPermissions(doublePermissions)).toThrow(); }); }); + +describe("computeNewPermission", () => { + const samplePermission: PermissionDTO = { + description: "", + id: 163, + name: "laboratories.access", + userGroupIds: ["foo", "bar"], + }; + it("should remove a group to permission", () => { + expect( + computeNewPermission("bar", { ...samplePermission }, false).userGroupIds + ).toEqual(["foo"]); + }); + it("should add a group to permission", () => { + expect( + computeNewPermission("con", { ...samplePermission }, true).userGroupIds + ).toEqual(["foo", "bar", "con"]); + }); + it("should leave a group as it (do not add twice a group)", () => { + expect( + computeNewPermission("bar", { ...samplePermission }, true).userGroupIds + ).toEqual(["foo", "bar"]); + }); + it("should leave a group as it (can't remove a non-existant group)", () => { + expect( + computeNewPermission("baz", { ...samplePermission }, false).userGroupIds + ).toEqual(["foo", "bar"]); + }); +}); diff --git a/src/components/accessories/admin/users/editPermissions/permission.utils.ts b/src/components/accessories/admin/users/editPermissions/permission.utils.ts new file mode 100644 index 000000000..af4e1a79c --- /dev/null +++ b/src/components/accessories/admin/users/editPermissions/permission.utils.ts @@ -0,0 +1,57 @@ +import { PermissionDTO } from "../../../../../generated"; + +export enum Crud { + CREATE = "create", + READ = "read", + UPDATE = "update", + DELETE = "delete", +} + +export const groupPermissions = ( + permissions: PermissionDTO[] +): Map> => { + let permissionNames = new Map(); + for (let i = 0; i < permissions.length; i++) { + const matches = + permissions[i].name && + /([a-z]+)\.(create|read|update|delete)$/.exec(permissions[i].name || ""); + // no match: skip + if (!matches) continue; + const [, key, access] = matches; + + if (!!permissionNames.get(key)?.[access]) { + throw new Error(`duplicate permission ${key}.${access}`); + } + + permissionNames.set(key, { + ...permissionNames.get(key), + [access]: permissions[i], + }); + } + return permissionNames; +}; + +export const computeNewPermission = ( + thisGroupId: string, + permission: PermissionDTO, + val: boolean +): PermissionDTO => { + const getUserGroups = () => { + if (val) { + if (permission.userGroupIds.includes(thisGroupId)) + return permission.userGroupIds; + return [...permission.userGroupIds, thisGroupId]; + } + let userGroupIds: string[] = []; + for (let index = 0; index < permission.userGroupIds.length; index++) { + if (permission.userGroupIds[index] !== thisGroupId) + userGroupIds = [...userGroupIds, permission.userGroupIds[index]]; + } + return userGroupIds; + }; + + return { + ...permission, + userGroupIds: getUserGroups(), + }; +}; diff --git a/src/mockServer/fixtures/permissionDTO.ts b/src/mockServer/fixtures/permissionDTO.ts index 00ed75171..a1397b304 100644 --- a/src/mockServer/fixtures/permissionDTO.ts +++ b/src/mockServer/fixtures/permissionDTO.ts @@ -7,6 +7,9 @@ export const permissionDTO: PermissionDTO[] = Array.from(permissionList).map( name: permissionName, id: i, description: "", - userGroupIds: ["admin", userGroupsDTO[i % userGroupsDTO.length].code], + userGroupIds: [ + userGroupsDTO[0].code, //admin + userGroupsDTO[(i + 1) % (userGroupsDTO.length - 1)].code, //others + ], }) );