feat(rbac): added rest of permissions in ui and backend api for org level

This commit is contained in:
Akhil Mohan
2023-09-08 21:20:29 +05:30
parent c5f76b1e6f
commit 96437fd1b7
16 changed files with 1253 additions and 58 deletions
@@ -135,18 +135,21 @@ export const getRoles = async (req: Request, res: Response) => {
data: { data: {
roles: [ roles: [
{ {
_id: "owner",
name: "Owner", name: "Owner",
slug: "owner", slug: "owner",
description: "Complete administration access over the organization.", description: "Complete administration access over the organization.",
permissions: adminPermissions.rules permissions: adminPermissions.rules
}, },
{ {
_id: "admin",
name: "Admin", name: "Admin",
slug: "admin", slug: "admin",
description: "Complete administration access over the organization", description: "Complete administration access over the organization",
permissions: adminPermissions.rules permissions: adminPermissions.rules
}, },
{ {
_id: "member",
name: "Member", name: "Member",
slug: "member", slug: "member",
description: "Non-administrative role in an organization", description: "Non-administrative role in an organization",
+50 -3
View File
@@ -12,12 +12,24 @@ export enum GeneralPermissionActions {
export enum OrgPermissionSubjects { export enum OrgPermissionSubjects {
Workspace = "workspace", Workspace = "workspace",
Role = "role" Role = "role",
Member = "member",
Settings = "settings",
ServiceAccount = "service-account",
IncidentAccount = "incident-contact",
Sso = "sso",
Billing = "billing"
} }
export type OrgPermissionSet = export type OrgPermissionSet =
| [GeneralPermissionActions, OrgPermissionSubjects.Workspace] | [GeneralPermissionActions, OrgPermissionSubjects.Workspace]
| [GeneralPermissionActions, OrgPermissionSubjects.Role]; | [GeneralPermissionActions, OrgPermissionSubjects.Role]
| [GeneralPermissionActions, OrgPermissionSubjects.Member]
| [GeneralPermissionActions, OrgPermissionSubjects.Settings]
| [GeneralPermissionActions, OrgPermissionSubjects.ServiceAccount]
| [GeneralPermissionActions, OrgPermissionSubjects.IncidentAccount]
| [GeneralPermissionActions, OrgPermissionSubjects.Sso]
| [GeneralPermissionActions, OrgPermissionSubjects.Billing];
const buildAdminPermission = () => { const buildAdminPermission = () => {
const { can, build } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility); const { can, build } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility);
@@ -32,6 +44,36 @@ const buildAdminPermission = () => {
can(GeneralPermissionActions.Edit, OrgPermissionSubjects.Role); can(GeneralPermissionActions.Edit, OrgPermissionSubjects.Role);
can(GeneralPermissionActions.Delete, OrgPermissionSubjects.Role); can(GeneralPermissionActions.Delete, OrgPermissionSubjects.Role);
can(GeneralPermissionActions.Read, OrgPermissionSubjects.Member);
can(GeneralPermissionActions.Create, OrgPermissionSubjects.Member);
can(GeneralPermissionActions.Edit, OrgPermissionSubjects.Member);
can(GeneralPermissionActions.Delete, OrgPermissionSubjects.Member);
can(GeneralPermissionActions.Read, OrgPermissionSubjects.Settings);
can(GeneralPermissionActions.Create, OrgPermissionSubjects.Settings);
can(GeneralPermissionActions.Edit, OrgPermissionSubjects.Settings);
can(GeneralPermissionActions.Delete, OrgPermissionSubjects.Settings);
can(GeneralPermissionActions.Read, OrgPermissionSubjects.ServiceAccount);
can(GeneralPermissionActions.Create, OrgPermissionSubjects.ServiceAccount);
can(GeneralPermissionActions.Edit, OrgPermissionSubjects.ServiceAccount);
can(GeneralPermissionActions.Delete, OrgPermissionSubjects.ServiceAccount);
can(GeneralPermissionActions.Read, OrgPermissionSubjects.IncidentAccount);
can(GeneralPermissionActions.Create, OrgPermissionSubjects.IncidentAccount);
can(GeneralPermissionActions.Edit, OrgPermissionSubjects.IncidentAccount);
can(GeneralPermissionActions.Delete, OrgPermissionSubjects.IncidentAccount);
can(GeneralPermissionActions.Read, OrgPermissionSubjects.Sso);
can(GeneralPermissionActions.Create, OrgPermissionSubjects.Sso);
can(GeneralPermissionActions.Edit, OrgPermissionSubjects.Sso);
can(GeneralPermissionActions.Delete, OrgPermissionSubjects.Sso);
can(GeneralPermissionActions.Read, OrgPermissionSubjects.Billing);
can(GeneralPermissionActions.Create, OrgPermissionSubjects.Billing);
can(GeneralPermissionActions.Edit, OrgPermissionSubjects.Billing);
can(GeneralPermissionActions.Delete, OrgPermissionSubjects.Billing);
return build(); return build();
}; };
@@ -40,9 +82,14 @@ export const adminPermissions = buildAdminPermission();
const buildMemberPermission = () => { const buildMemberPermission = () => {
const { can, build } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility); const { can, build } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility);
// ws permissions
can(GeneralPermissionActions.Read, OrgPermissionSubjects.Workspace); can(GeneralPermissionActions.Read, OrgPermissionSubjects.Workspace);
can(GeneralPermissionActions.Read, OrgPermissionSubjects.Member);
can(GeneralPermissionActions.Read, OrgPermissionSubjects.Role); can(GeneralPermissionActions.Read, OrgPermissionSubjects.Role);
can(GeneralPermissionActions.Read, OrgPermissionSubjects.Settings);
can(GeneralPermissionActions.Read, OrgPermissionSubjects.Billing);
can(GeneralPermissionActions.Read, OrgPermissionSubjects.Sso);
can(GeneralPermissionActions.Read, OrgPermissionSubjects.IncidentAccount);
can(GeneralPermissionActions.Read, OrgPermissionSubjects.ServiceAccount);
return build(); return build();
}; };
+2 -2
View File
@@ -11,7 +11,7 @@ export const CreateRoleSchema = z.object({
.object({ .object({
subject: z.string(), subject: z.string(),
action: z.string(), action: z.string(),
condition: z.record(z.union([z.string(), z.number()])) condition: z.record(z.union([z.string(), z.number()])).optional()
}) })
.array() .array()
}) })
@@ -31,7 +31,7 @@ export const UpdateRoleSchema = z.object({
.object({ .object({
subject: z.string(), subject: z.string(),
action: z.string(), action: z.string(),
condition: z.record(z.union([z.string(), z.number()])) condition: z.record(z.union([z.string(), z.number()])).optional()
}) })
.array() .array()
.optional() .optional()
+15 -1
View File
@@ -90,7 +90,8 @@
"uuid": "^8.3.2", "uuid": "^8.3.2",
"uuidv4": "^6.2.13", "uuidv4": "^6.2.13",
"yaml": "^2.2.2", "yaml": "^2.2.2",
"yup": "^0.32.11" "yup": "^0.32.11",
"zod": "^3.22.0"
}, },
"devDependencies": { "devDependencies": {
"@storybook/addon-essentials": "^7.0.23", "@storybook/addon-essentials": "^7.0.23",
@@ -23634,6 +23635,14 @@
"engines": { "engines": {
"node": ">=10" "node": ">=10"
} }
},
"node_modules/zod": {
"version": "3.22.0",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.22.0.tgz",
"integrity": "sha512-y5KZY/ssf5n7hCGDGGtcJO/EBJEm5Pa+QQvFBeyMOtnFYOSflalxIFFvdaYevPhePcmcKC4aTbFkCcXN7D0O8Q==",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
} }
}, },
"dependencies": { "dependencies": {
@@ -40759,6 +40768,11 @@
"property-expr": "^2.0.4", "property-expr": "^2.0.4",
"toposort": "^2.0.2" "toposort": "^2.0.2"
} }
},
"zod": {
"version": "3.22.0",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.22.0.tgz",
"integrity": "sha512-y5KZY/ssf5n7hCGDGGtcJO/EBJEm5Pa+QQvFBeyMOtnFYOSflalxIFFvdaYevPhePcmcKC4aTbFkCcXN7D0O8Q=="
} }
} }
} }
+2 -1
View File
@@ -98,7 +98,8 @@
"uuid": "^8.3.2", "uuid": "^8.3.2",
"uuidv4": "^6.2.13", "uuidv4": "^6.2.13",
"yaml": "^2.2.2", "yaml": "^2.2.2",
"yup": "^0.32.11" "yup": "^0.32.11",
"zod": "^3.22.0"
}, },
"devDependencies": { "devDependencies": {
"@storybook/addon-essentials": "^7.0.23", "@storybook/addon-essentials": "^7.0.23",
@@ -0,0 +1,147 @@
import { useEffect, useMemo } from "react";
import { Control, Controller, UseFormSetValue, useWatch } from "react-hook-form";
import { faMoneyBill } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { motion } from "framer-motion";
import { twMerge } from "tailwind-merge";
import { Checkbox, Select, SelectItem } from "@app/components/v2";
import { useToggle } from "@app/hooks";
import { TFormSchema } from "./OrgRoleModifySection.utils";
type Props = {
isNonEditable?: boolean;
setValue: UseFormSetValue<TFormSchema>;
control: Control<TFormSchema>;
};
enum Permission {
NoAccess = "no-access",
ReadOnly = "read-only",
FullAccess = "full-acess",
Custom = "custom"
}
const PERMISSIONS = [
{ action: "read", label: "Read" },
{ action: "create", label: "Create" },
{ action: "edit", label: "Update" },
{ action: "delete", label: "Remove" }
] as const;
export const BillingPermission = ({ isNonEditable, setValue, control }: Props) => {
const rule = useWatch({
control,
name: "permissions.billing"
});
const [isCustom, setIsCustom] = useToggle();
const selectedPermissionCategory = useMemo(() => {
let score = 0;
const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
const totalActions = PERMISSIONS.length;
actions.forEach((key) => (score += rule[key] ? 1 : 0));
if (isCustom) return Permission.Custom;
if (score === 0) return Permission.NoAccess;
if (score === totalActions) return Permission.FullAccess;
if (score === 1 && rule.read) return Permission.ReadOnly;
return Permission.Custom;
}, [rule, isCustom]);
useEffect(() => {
selectedPermissionCategory === Permission.Custom ? setIsCustom.on() : setIsCustom.off();
}, [selectedPermissionCategory]);
const handlePermissionChange = (val: Permission) => {
val === Permission.Custom ? setIsCustom.on() : setIsCustom.off();
switch (val) {
case Permission.NoAccess:
setValue(
"permissions.billing",
{ read: false, edit: false, create: false, delete: false },
{ shouldDirty: true }
);
break;
case Permission.FullAccess:
setValue(
"permissions.billing",
{ read: true, edit: true, create: true, delete: true },
{ shouldDirty: true }
);
break;
case Permission.ReadOnly:
setValue(
"permissions.billing",
{ read: true, edit: false, create: false, delete: false },
{ shouldDirty: true }
);
break;
default:
setValue(
"permissions.billing",
{ read: false, edit: false, create: false, delete: false },
{ shouldDirty: true }
);
break;
}
};
return (
<div
className={twMerge(
"px-10 py-6 bg-mineshaft-800 rounded-md",
selectedPermissionCategory !== Permission.NoAccess && "border-l-2 border-primary-600"
)}
>
<div className="flex items-center space-x-4">
<div>
<FontAwesomeIcon icon={faMoneyBill} className="text-4xl" />
</div>
<div className="flex-grow flex flex-col">
<div className="font-medium mb-1 text-lg">Billing</div>
<div className="text-xs font-light">Billing management control</div>
</div>
<div>
<Select
defaultValue={Permission.NoAccess}
isDisabled={isNonEditable}
value={selectedPermissionCategory}
onValueChange={handlePermissionChange}
>
<SelectItem value={Permission.NoAccess}>No Access</SelectItem>
<SelectItem value={Permission.ReadOnly}>Read Only</SelectItem>
<SelectItem value={Permission.FullAccess}>Full Access</SelectItem>
<SelectItem value={Permission.Custom}>Custom</SelectItem>
</Select>
</div>
</div>
<motion.div
initial={false}
animate={{ height: isCustom ? "2.5rem" : 0, paddingTop: isCustom ? "1rem" : 0 }}
className="overflow-hidden grid gap-8 grid-flow-col auto-cols-min"
>
{isCustom &&
PERMISSIONS.map(({ action, label }) => (
<Controller
name={`permissions.billing.${action}`}
key={`permissions.billing.${action}`}
control={control}
render={({ field }) => (
<Checkbox
isChecked={field.value}
onCheckedChange={field.onChange}
id={`permissions.billing.${action}`}
isDisabled={isNonEditable}
>
{label}
</Checkbox>
)}
/>
))}
</motion.div>
</div>
);
};
@@ -0,0 +1,151 @@
import { useEffect, useMemo } from "react";
import { Control, Controller, UseFormSetValue, useWatch } from "react-hook-form";
import { faContactCard } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { motion } from "framer-motion";
import { twMerge } from "tailwind-merge";
import { Checkbox, Select, SelectItem } from "@app/components/v2";
import { useToggle } from "@app/hooks";
import { TFormSchema } from "./OrgRoleModifySection.utils";
type Props = {
isNonEditable?: boolean;
setValue: UseFormSetValue<TFormSchema>;
control: Control<TFormSchema>;
};
enum Permission {
NoAccess = "no-access",
ReadOnly = "read-only",
FullAccess = "full-acess",
Custom = "custom"
}
const PERMISSIONS = [
{ action: "read", label: "Read" },
{ action: "create", label: "Create" },
{ action: "edit", label: "Update" },
{ action: "delete", label: "Remove" }
] as const;
export const IncidentContactPermission = ({ isNonEditable, setValue, control }: Props) => {
const rule = useWatch({
control,
name: "permissions.incident-contact"
});
const [isCustom, setIsCustom] = useToggle();
const selectedPermissionCategory = useMemo(() => {
let score = 0;
const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
const totalActions = PERMISSIONS.length;
actions.forEach((key) => (score += rule[key] ? 1 : 0));
if (isCustom) return Permission.Custom;
if (score === 0) return Permission.NoAccess;
if (score === totalActions) return Permission.FullAccess;
if (score === 1 && rule.read) return Permission.ReadOnly;
return Permission.Custom;
}, [rule, isCustom]);
useEffect(() => {
selectedPermissionCategory === Permission.Custom ? setIsCustom.on() : setIsCustom.off();
}, [selectedPermissionCategory]);
const handlePermissionChange = (val: Permission) => {
val === Permission.Custom ? setIsCustom.on() : setIsCustom.off();
switch (val) {
case Permission.NoAccess:
setIsCustom.off();
setValue(
"permissions.incident-contact",
{ read: false, edit: false, create: false, delete: false },
{ shouldDirty: true }
);
break;
case Permission.FullAccess:
setIsCustom.off();
setValue(
"permissions.incident-contact",
{ read: true, edit: true, create: true, delete: true },
{ shouldDirty: true }
);
break;
case Permission.ReadOnly:
setIsCustom.off();
setValue(
"permissions.incident-contact",
{ read: true, edit: false, create: false, delete: false },
{ shouldDirty: true }
);
break;
default:
setIsCustom.on();
setValue(
"permissions.incident-contact",
{ read: false, edit: false, create: false, delete: false },
{ shouldDirty: true }
);
break;
}
};
return (
<div
className={twMerge(
"px-10 py-6 bg-mineshaft-800 rounded-md",
selectedPermissionCategory !== Permission.NoAccess && "border-l-2 border-primary-600"
)}
>
<div className="flex items-center space-x-4">
<div>
<FontAwesomeIcon icon={faContactCard} className="text-4xl" />
</div>
<div className="flex-grow flex flex-col">
<div className="font-medium mb-1 text-lg">Incident Contact</div>
<div className="text-xs font-light">Incident Contacts management control</div>
</div>
<div>
<Select
defaultValue={Permission.NoAccess}
isDisabled={isNonEditable}
value={selectedPermissionCategory}
onValueChange={handlePermissionChange}
>
<SelectItem value={Permission.NoAccess}>No Access</SelectItem>
<SelectItem value={Permission.ReadOnly}>Read Only</SelectItem>
<SelectItem value={Permission.FullAccess}>Full Access</SelectItem>
<SelectItem value={Permission.Custom}>Custom</SelectItem>
</Select>
</div>
</div>
<motion.div
initial={false}
animate={{ height: isCustom ? "2.5rem" : 0, paddingTop: isCustom ? "1rem" : 0 }}
className="overflow-hidden grid gap-8 grid-flow-col auto-cols-min"
>
{isCustom &&
PERMISSIONS.map(({ action, label }) => (
<Controller
name={`permissions.incident-contact.${action}`}
key={`permissions.incident-contact.${action}`}
control={control}
render={({ field }) => (
<Checkbox
isChecked={field.value}
onCheckedChange={field.onChange}
id={`permissions.incident-contact.${action}`}
isDisabled={isNonEditable}
>
{label}
</Checkbox>
)}
/>
))}
</motion.div>
</div>
);
};
@@ -0,0 +1,146 @@
import { useEffect, useMemo } from "react";
import { Control, Controller, UseFormSetValue, useWatch } from "react-hook-form";
import { faUsers } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { motion } from "framer-motion";
import { twMerge } from "tailwind-merge";
import { Checkbox, Select, SelectItem } from "@app/components/v2";
import { useToggle } from "@app/hooks";
import { TFormSchema } from "./OrgRoleModifySection.utils";
type Props = {
isNonEditable?: boolean;
setValue: UseFormSetValue<TFormSchema>;
control: Control<TFormSchema>;
};
enum Permission {
NoAccess = "no-access",
ReadOnly = "read-only",
FullAccess = "full-acess",
Custom = "custom"
}
const PERMISSIONS = [
{ action: "read", label: "Read" },
{ action: "create", label: "Invite" },
{ action: "edit", label: "Update" },
{ action: "delete", label: "Remove" }
] as const;
export const MemberPermission = ({ isNonEditable, setValue, control }: Props) => {
const memberRule = useWatch({
control,
name: "permissions.member"
});
const [isCustom, setIsCustom] = useToggle();
const selectedPermissionCategory = useMemo(() => {
let score = 0;
const actions = Object.keys(memberRule || {}) as Array<keyof typeof memberRule>;
const totalActions = PERMISSIONS.length;
actions.forEach((key) => (score += memberRule[key] ? 1 : 0));
if (isCustom) return Permission.Custom;
if (score === 0) return Permission.NoAccess;
if (score === totalActions) return Permission.FullAccess;
if (score === 1 && memberRule.read) return Permission.ReadOnly;
return Permission.Custom;
}, [memberRule, isCustom]);
useEffect(() => {
selectedPermissionCategory === Permission.Custom ? setIsCustom.on() : setIsCustom.off();
}, [selectedPermissionCategory]);
const handlePermissionChange = (val: Permission) => {
switch (val) {
case Permission.NoAccess:
setValue(
"permissions.member",
{ read: false, edit: false, create: false, delete: false },
{ shouldDirty: true }
);
break;
case Permission.FullAccess:
setValue(
"permissions.member",
{ read: true, edit: true, create: true, delete: true },
{ shouldDirty: true }
);
break;
case Permission.ReadOnly:
setValue(
"permissions.member",
{ read: true, edit: false, create: false, delete: false },
{ shouldDirty: true }
);
break;
default:
setValue(
"permissions.member",
{ read: false, edit: false, create: false, delete: false },
{ shouldDirty: true }
);
break;
}
};
return (
<div
className={twMerge(
"px-10 py-6 bg-mineshaft-800 rounded-md",
selectedPermissionCategory !== Permission.NoAccess && "border-l-2 border-primary-600"
)}
>
<div className="flex items-center space-x-4">
<div>
<FontAwesomeIcon icon={faUsers} className="text-4xl" />
</div>
<div className="flex-grow flex flex-col">
<div className="font-medium mb-1 text-lg">Members</div>
<div className="text-xs font-light">Project member management control</div>
</div>
<div>
<Select
defaultValue={Permission.NoAccess}
isDisabled={isNonEditable}
value={selectedPermissionCategory}
onValueChange={handlePermissionChange}
>
<SelectItem value={Permission.NoAccess}>No Access</SelectItem>
<SelectItem value={Permission.ReadOnly}>Read Only</SelectItem>
<SelectItem value={Permission.FullAccess}>Full Access</SelectItem>
<SelectItem value={Permission.Custom}>Custom</SelectItem>
</Select>
</div>
</div>
<motion.div
initial={false}
animate={{ height: isCustom ? "2.5rem" : 0, paddingTop: isCustom ? "1rem" : 0 }}
className="overflow-hidden grid gap-8 grid-flow-col auto-cols-min"
>
{isCustom &&
PERMISSIONS.map(({ action, label }) => (
<Controller
name={`permissions.member.${action}`}
key={`permissions.member.${action}`}
control={control}
render={({ field }) => (
<Checkbox
isChecked={field.value}
onCheckedChange={field.onChange}
id={`permissions.member.${action}`}
isDisabled={isNonEditable}
>
{label}
</Checkbox>
)}
/>
))}
</motion.div>
</div>
);
};
@@ -2,7 +2,7 @@ import { useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { faArrowLeft, faMagnifyingGlass } from "@fortawesome/free-solid-svg-icons"; import { faArrowLeft, faMagnifyingGlass } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { yupResolver } from "@hookform/resolvers/yup"; import { zodResolver } from "@hookform/resolvers/zod";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { Button, FormControl, Input } from "@app/components/v2"; import { Button, FormControl, Input } from "@app/components/v2";
@@ -10,13 +10,20 @@ import { useOrganization } from "@app/context";
import { useCreateRole, useUpdateRole } from "@app/hooks/api"; import { useCreateRole, useUpdateRole } from "@app/hooks/api";
import { TRole } from "@app/hooks/api/roles/types"; import { TRole } from "@app/hooks/api/roles/types";
import { BillingPermission } from "./BillingPermission";
import { IncidentContactPermission } from "./IncidentContactPermission";
import { MemberPermission } from "./MemberPermission";
import { import {
formRolePermission2API, formRolePermission2API,
formSchema, formSchema,
rolePermission2Form, rolePermission2Form,
TFormSchema TFormSchema
} from "./OrgRoleModifySection.utils"; } from "./OrgRoleModifySection.utils";
import { OrgRoleWorkspacePermission } from "./OrgRoleWorkspacePermission"; import { RolePermission } from "./RolePermission";
import { ServiceAccountPermission } from "./ServiceAccountPermission";
import { SettingsPermission } from "./SettingsPermission";
import { SsoPermission } from "./SsoPermission";
import { WorkspacePermission } from "./WorkspacePermission";
type Props = { type Props = {
role?: TRole; role?: TRole;
@@ -40,7 +47,7 @@ export const OrgRoleModifySection = ({ role, onGoBack }: Props) => {
control control
} = useForm<TFormSchema>({ } = useForm<TFormSchema>({
defaultValues: role ? { ...role, permissions: rolePermission2Form(role.permissions) } : {}, defaultValues: role ? { ...role, permissions: rolePermission2Form(role.permissions) } : {},
resolver: yupResolver(formSchema) resolver: zodResolver(formSchema)
}); });
const { mutateAsync: createRole } = useCreateRole(); const { mutateAsync: createRole } = useCreateRole();
@@ -142,7 +149,44 @@ export const OrgRoleModifySection = ({ role, onGoBack }: Props) => {
</div> </div>
</div> </div>
<div className="flex flex-col space-y-4"> <div className="flex flex-col space-y-4">
<OrgRoleWorkspacePermission <WorkspacePermission
isNonEditable={isNonEditable}
control={control}
setValue={setValue}
/>
</div>
<div className="flex flex-col space-y-4">
<MemberPermission isNonEditable={isNonEditable} control={control} setValue={setValue} />
</div>
<div className="flex flex-col space-y-4">
<BillingPermission
isNonEditable={isNonEditable}
control={control}
setValue={setValue}
/>
</div>
<div className="flex flex-col space-y-4">
<RolePermission isNonEditable={isNonEditable} control={control} setValue={setValue} />
</div>
<div className="flex flex-col space-y-4">
<IncidentContactPermission
isNonEditable={isNonEditable}
control={control}
setValue={setValue}
/>
</div>
<div className="flex flex-col space-y-4">
<SettingsPermission
isNonEditable={isNonEditable}
control={control}
setValue={setValue}
/>
</div>
<div className="flex flex-col space-y-4">
<SsoPermission isNonEditable={isNonEditable} control={control} setValue={setValue} />
</div>
<div className="flex flex-col space-y-4">
<ServiceAccountPermission
isNonEditable={isNonEditable} isNonEditable={isNonEditable}
control={control} control={control}
setValue={setValue} setValue={setValue}
@@ -1,34 +1,34 @@
/* eslint-disable no-param-reassign */ /* eslint-disable no-param-reassign */
import * as yup from "yup"; import { z } from "zod";
import { TPermission } from "@app/hooks/api/roles/types"; import { TPermission } from "@app/hooks/api/roles/types";
const PERMISSION_ACTIONS = ["read", "create", "edit", "delete"] as const; const PERMISSION_ACTIONS = ["read", "create", "edit", "delete"] as const;
export const formSchema = yup.object({ const generalPermissionSchema = z.object({
name: yup.string().required().label("Name"), read: z.boolean().optional(),
description: yup.string(), edit: z.boolean().optional(),
slug: yup.string().required().label("Slug"), delete: z.boolean().optional(),
permissions: yup.object({ create: z.boolean().optional()
workspace: yup.lazy((val) => });
yup.object(
Object.fromEntries( export const formSchema = z.object({
Object.entries(val || {}).map(([k]) => [ name: z.string(),
k, description: z.string().optional(),
yup.object({ slug: z.string(),
read: yup.bool(), permissions: z.object({
edit: yup.bool(), workspace: z.record(generalPermissionSchema),
delete: yup.bool(), member: generalPermissionSchema,
create: yup.bool() role: generalPermissionSchema,
}) settings: generalPermissionSchema,
]) "service-account": generalPermissionSchema,
) "incident-contact": generalPermissionSchema,
) sso: generalPermissionSchema,
) billing: generalPermissionSchema
}) })
}); });
export type TFormSchema = yup.InferType<typeof formSchema>; export type TFormSchema = z.infer<typeof formSchema>;
const api2FormWorkspace = ( const api2FormWorkspace = (
formVal: TFormSchema["permissions"]["workspace"], formVal: TFormSchema["permissions"]["workspace"],
@@ -36,24 +36,43 @@ const api2FormWorkspace = (
) => { ) => {
if (permission.subject !== "workspace") return; if (permission.subject !== "workspace") return;
const isCustomRule = Boolean(permission?.condition?.id); const isCustomRule = Boolean(permission?.condition?.id);
// full access
if (isCustomRule && !formVal?.custom) { if (isCustomRule && !formVal?.custom) {
formVal.custom = { read: true, edit: true, delete: true, create: true }; formVal.custom = { read: true, edit: true, delete: true, create: true };
} }
const workspaceId = permission?.condition?.id || "all"; const workspaceId = permission?.condition?.id || "all";
if (!formVal?.[workspaceId]) // initalize
if (!formVal?.[workspaceId]) {
formVal[workspaceId] = { read: false, edit: false, create: false, delete: false }; formVal[workspaceId] = { read: false, edit: false, create: false, delete: false };
}
formVal[workspaceId][permission.action] = true; formVal[workspaceId][permission.action] = true;
}; };
// convert role permission to form compatiable data structure // convert role permission to form compatiable data structure
export const rolePermission2Form = (permissions: TPermission[] = []) => { export const rolePermission2Form = (permissions: TPermission[] = []) => {
const formVal: TFormSchema["permissions"] = { const formVal: TFormSchema["permissions"] = {
workspace: {} workspace: {},
billing: {},
settings: {},
role: {},
sso: {},
member: {},
"service-account": {},
"incident-contact": {}
}; };
permissions.forEach((permission) => { permissions.forEach((permission) => {
api2FormWorkspace(formVal?.workspace, permission); switch (permission.subject) {
case "workspace":
api2FormWorkspace(formVal?.workspace, permission);
break;
default:
// everything else follows same pattern
// formVal[settings][read | write] = true
formVal[permission.subject as keyof TFormSchema["permissions"]][permission.action] = true;
break;
}
}); });
return formVal; return formVal;
@@ -64,7 +83,7 @@ const form2ApiWorkspace = (
workspace: TFormSchema["permissions"]["workspace"] workspace: TFormSchema["permissions"]["workspace"]
) => { ) => {
const isFullAccess = PERMISSION_ACTIONS.every((action) => workspace?.all?.[action]); const isFullAccess = PERMISSION_ACTIONS.every((action) => workspace?.all?.[action]);
// if any of them is set in all push it without any condition
PERMISSION_ACTIONS.forEach((action) => { PERMISSION_ACTIONS.forEach((action) => {
if (workspace?.all?.[action]) permissions.push({ action, subject: "workspace" }); if (workspace?.all?.[action]) permissions.push({ action, subject: "workspace" });
}); });
@@ -86,8 +105,24 @@ const form2ApiWorkspace = (
export const formRolePermission2API = (formVal: TFormSchema["permissions"]) => { export const formRolePermission2API = (formVal: TFormSchema["permissions"]) => {
const permissions: TPermission[] = []; const permissions: TPermission[] = [];
// easy deep copy if (formVal?.workspace) {
if (formVal?.workspace) // easy deep copy
form2ApiWorkspace(permissions, JSON.parse(JSON.stringify(formVal.workspace))); form2ApiWorkspace(permissions, JSON.parse(JSON.stringify(formVal.workspace)));
}
// other than workspace everything else follows same
// if in future there is a different follow the above on how workspace is done
const { workspace, ...rules } = formVal;
(Object.keys(rules) as Array<keyof typeof rules>).forEach((rule) => {
// all these type annotations are due to Object.keys of ts cannot infer and put it just a string[]
// quite annoying i know
const actions = Object.keys(rules[rule]) as Array<
keyof z.infer<typeof generalPermissionSchema>
>;
actions.forEach((action) => {
if (rules[rule][action]) {
permissions.push({ action, subject: rule });
}
});
});
return permissions; return permissions;
}; };
@@ -0,0 +1,150 @@
import { useEffect, useMemo } from "react";
import { Control, Controller, UseFormSetValue, useWatch } from "react-hook-form";
import { faUserCog } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { motion } from "framer-motion";
import { twMerge } from "tailwind-merge";
import { Checkbox, Select, SelectItem } from "@app/components/v2";
import { useToggle } from "@app/hooks";
import { TFormSchema } from "./OrgRoleModifySection.utils";
type Props = {
isNonEditable?: boolean;
setValue: UseFormSetValue<TFormSchema>;
control: Control<TFormSchema>;
};
enum Permission {
NoAccess = "no-access",
ReadOnly = "read-only",
FullAccess = "full-acess",
Custom = "custom"
}
const PERMISSIONS = [
{ action: "read", label: "Read" },
{ action: "create", label: "Create" },
{ action: "edit", label: "Update" },
{ action: "delete", label: "Remove" }
] as const;
export const RolePermission = ({ isNonEditable, setValue, control }: Props) => {
const roleRule = useWatch({
control,
name: "permissions.role"
});
const [isCustom, setIsCustom] = useToggle();
const selectedPermissionCategory = useMemo(() => {
let score = 0;
const actions = Object.keys(roleRule || {}) as Array<keyof typeof roleRule>;
const totalActions = PERMISSIONS.length;
actions.forEach((key) => (score += roleRule[key] ? 1 : 0));
if (isCustom) return Permission.Custom;
if (score === 0) return Permission.NoAccess;
if (score === totalActions) return Permission.FullAccess;
if (score === 1 && roleRule.read) return Permission.ReadOnly;
return Permission.Custom;
}, [roleRule, isCustom]);
useEffect(() => {
selectedPermissionCategory === Permission.Custom ? setIsCustom.on() : setIsCustom.off();
}, [selectedPermissionCategory]);
const handlePermissionChange = (val: Permission) => {
switch (val) {
case Permission.NoAccess:
setIsCustom.off();
setValue(
"permissions.role",
{ read: false, edit: false, create: false, delete: false },
{ shouldDirty: true }
);
break;
case Permission.FullAccess:
setIsCustom.off();
setValue(
"permissions.role",
{ read: true, edit: true, create: true, delete: true },
{ shouldDirty: true }
);
break;
case Permission.ReadOnly:
setIsCustom.off();
setValue(
"permissions.role",
{ read: true, edit: false, create: false, delete: false },
{ shouldDirty: true }
);
break;
default:
setIsCustom.on();
setValue(
"permissions.role",
{ read: false, edit: false, create: false, delete: false },
{ shouldDirty: true }
);
break;
}
};
return (
<div
className={twMerge(
"px-10 py-6 bg-mineshaft-800 rounded-md",
selectedPermissionCategory !== Permission.NoAccess && "border-l-2 border-primary-600"
)}
>
<div className="flex items-center space-x-4">
<div>
<FontAwesomeIcon icon={faUserCog} className="text-4xl" />
</div>
<div className="flex-grow flex flex-col">
<div className="font-medium mb-1 text-lg">Role</div>
<div className="text-xs font-light">Project role management control</div>
</div>
<div>
<Select
defaultValue={Permission.NoAccess}
isDisabled={isNonEditable}
value={selectedPermissionCategory}
onValueChange={handlePermissionChange}
>
<SelectItem value={Permission.NoAccess}>No Access</SelectItem>
<SelectItem value={Permission.ReadOnly}>Read Only</SelectItem>
<SelectItem value={Permission.FullAccess}>Full Access</SelectItem>
<SelectItem value={Permission.Custom}>Custom</SelectItem>
</Select>
</div>
</div>
<motion.div
initial={false}
animate={{ height: isCustom ? "2.5rem" : 0, paddingTop: isCustom ? "1rem" : 0 }}
className="overflow-hidden grid gap-8 grid-flow-col auto-cols-min"
>
{isCustom &&
PERMISSIONS.map(({ action, label }) => (
<Controller
name={`permissions.role.${action}`}
key={`permissions.role.${action}`}
control={control}
render={({ field }) => (
<Checkbox
isChecked={field.value}
onCheckedChange={field.onChange}
id={`permissions.role.${action}`}
isDisabled={isNonEditable}
>
{label}
</Checkbox>
)}
/>
))}
</motion.div>
</div>
);
};
@@ -0,0 +1,150 @@
import { useEffect, useMemo } from "react";
import { Control, Controller, UseFormSetValue, useWatch } from "react-hook-form";
import { faLaptopCode } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { motion } from "framer-motion";
import { twMerge } from "tailwind-merge";
import { Checkbox, Select, SelectItem } from "@app/components/v2";
import { useToggle } from "@app/hooks";
import { TFormSchema } from "./OrgRoleModifySection.utils";
type Props = {
isNonEditable?: boolean;
setValue: UseFormSetValue<TFormSchema>;
control: Control<TFormSchema>;
};
enum Permission {
NoAccess = "no-access",
ReadOnly = "read-only",
FullAccess = "full-acess",
Custom = "custom"
}
const PERMISSIONS = [
{ action: "read", label: "Read" },
{ action: "create", label: "Create" },
{ action: "edit", label: "Update" },
{ action: "delete", label: "Remove" }
] as const;
export const ServiceAccountPermission = ({ isNonEditable, setValue, control }: Props) => {
const rule = useWatch({
control,
name: "permissions.service-account"
});
const [isCustom, setIsCustom] = useToggle();
const selectedPermissionCategory = useMemo(() => {
let score = 0;
const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
const totalActions = PERMISSIONS.length;
actions.forEach((key) => (score += rule[key] ? 1 : 0));
if (isCustom) return Permission.Custom;
if (score === 0) return Permission.NoAccess;
if (score === totalActions) return Permission.FullAccess;
if (score === 1 && rule.read) return Permission.ReadOnly;
return Permission.Custom;
}, [rule, isCustom]);
useEffect(() => {
selectedPermissionCategory === Permission.Custom ? setIsCustom.on() : setIsCustom.off();
}, [selectedPermissionCategory]);
const handlePermissionChange = (val: Permission) => {
switch (val) {
case Permission.NoAccess:
setIsCustom.off();
setValue(
"permissions.service-account",
{ read: false, edit: false, create: false, delete: false },
{ shouldDirty: true }
);
break;
case Permission.FullAccess:
setIsCustom.off();
setValue(
"permissions.service-account",
{ read: true, edit: true, create: true, delete: true },
{ shouldDirty: true }
);
break;
case Permission.ReadOnly:
setIsCustom.off();
setValue(
"permissions.service-account",
{ read: true, edit: false, create: false, delete: false },
{ shouldDirty: true }
);
break;
default:
setIsCustom.on();
setValue(
"permissions.service-account",
{ read: false, edit: false, create: false, delete: false },
{ shouldDirty: true }
);
break;
}
};
return (
<div
className={twMerge(
"px-10 py-6 bg-mineshaft-800 rounded-md",
selectedPermissionCategory !== Permission.NoAccess && "border-l-2 border-primary-600"
)}
>
<div className="flex items-center space-x-4">
<div>
<FontAwesomeIcon icon={faLaptopCode} className="text-4xl" />
</div>
<div className="flex-grow flex flex-col">
<div className="font-medium mb-1 text-lg">Service Accounts</div>
<div className="text-xs font-light">Service Account management control</div>
</div>
<div>
<Select
defaultValue={Permission.NoAccess}
isDisabled={isNonEditable}
value={selectedPermissionCategory}
onValueChange={handlePermissionChange}
>
<SelectItem value={Permission.NoAccess}>No Access</SelectItem>
<SelectItem value={Permission.ReadOnly}>Read Only</SelectItem>
<SelectItem value={Permission.FullAccess}>Full Access</SelectItem>
<SelectItem value={Permission.Custom}>Custom</SelectItem>
</Select>
</div>
</div>
<motion.div
initial={false}
animate={{ height: isCustom ? "2.5rem" : 0, paddingTop: isCustom ? "1rem" : 0 }}
className="overflow-hidden grid gap-8 grid-flow-col auto-cols-min"
>
{isCustom &&
PERMISSIONS.map(({ action, label }) => (
<Controller
name={`permissions.service-account.${action}`}
key={`permissions.service-account.${action}`}
control={control}
render={({ field }) => (
<Checkbox
isChecked={field.value}
onCheckedChange={field.onChange}
id={`permissions.service-account.${action}`}
isDisabled={isNonEditable}
>
{label}
</Checkbox>
)}
/>
))}
</motion.div>
</div>
);
};
@@ -0,0 +1,150 @@
import { useEffect, useMemo } from "react";
import { Control, Controller, UseFormSetValue, useWatch } from "react-hook-form";
import { faCog } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { motion } from "framer-motion";
import { twMerge } from "tailwind-merge";
import { Checkbox, Select, SelectItem } from "@app/components/v2";
import { useToggle } from "@app/hooks";
import { TFormSchema } from "./OrgRoleModifySection.utils";
type Props = {
isNonEditable?: boolean;
setValue: UseFormSetValue<TFormSchema>;
control: Control<TFormSchema>;
};
enum Permission {
NoAccess = "no-access",
ReadOnly = "read-only",
FullAccess = "full-acess",
Custom = "custom"
}
const PERMISSIONS = [
{ action: "read", label: "Read" },
{ action: "create", label: "Create" },
{ action: "edit", label: "Update" },
{ action: "delete", label: "Remove" }
] as const;
export const SettingsPermission = ({ isNonEditable, setValue, control }: Props) => {
const rule = useWatch({
control,
name: "permissions.settings"
});
const [isCustom, setIsCustom] = useToggle();
const selectedPermissionCategory = useMemo(() => {
let score = 0;
const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
const totalActions = PERMISSIONS.length;
actions.forEach((key) => (score += rule[key] ? 1 : 0));
if (isCustom) return Permission.Custom;
if (score === 0) return Permission.NoAccess;
if (score === totalActions) return Permission.FullAccess;
if (score === 1 && rule.read) return Permission.ReadOnly;
return Permission.Custom;
}, [rule, isCustom]);
useEffect(() => {
selectedPermissionCategory === Permission.Custom ? setIsCustom.on() : setIsCustom.off();
}, [selectedPermissionCategory]);
const handlePermissionChange = (val: Permission) => {
switch (val) {
case Permission.NoAccess:
setIsCustom.off();
setValue(
"permissions.settings",
{ read: false, edit: false, create: false, delete: false },
{ shouldDirty: true }
);
break;
case Permission.FullAccess:
setIsCustom.off();
setValue(
"permissions.settings",
{ read: true, edit: true, create: true, delete: true },
{ shouldDirty: true }
);
break;
case Permission.ReadOnly:
setIsCustom.off();
setValue(
"permissions.settings",
{ read: true, edit: false, create: false, delete: false },
{ shouldDirty: true }
);
break;
default:
setIsCustom.on();
setValue(
"permissions.settings",
{ read: false, edit: false, create: false, delete: false },
{ shouldDirty: true }
);
break;
}
};
return (
<div
className={twMerge(
"px-10 py-6 bg-mineshaft-800 rounded-md",
selectedPermissionCategory !== Permission.NoAccess && "border-l-2 border-primary-600"
)}
>
<div className="flex items-center space-x-4">
<div>
<FontAwesomeIcon icon={faCog} className="text-4xl" />
</div>
<div className="flex-grow flex flex-col">
<div className="font-medium mb-1 text-lg">Settings</div>
<div className="text-xs font-light">Settings management control</div>
</div>
<div>
<Select
defaultValue={Permission.NoAccess}
isDisabled={isNonEditable}
value={selectedPermissionCategory}
onValueChange={handlePermissionChange}
>
<SelectItem value={Permission.NoAccess}>No Access</SelectItem>
<SelectItem value={Permission.ReadOnly}>Read Only</SelectItem>
<SelectItem value={Permission.FullAccess}>Full Access</SelectItem>
<SelectItem value={Permission.Custom}>Custom</SelectItem>
</Select>
</div>
</div>
<motion.div
initial={false}
animate={{ height: isCustom ? "2.5rem" : 0, paddingTop: isCustom ? "1rem" : 0 }}
className="overflow-hidden grid gap-8 grid-flow-col auto-cols-min"
>
{isCustom &&
PERMISSIONS.map(({ action, label }) => (
<Controller
name={`permissions.service-account.${action}`}
key={`permissions.service-account.${action}`}
control={control}
render={({ field }) => (
<Checkbox
isChecked={field.value}
onCheckedChange={field.onChange}
id={`permissions.service-account.${action}`}
isDisabled={isNonEditable}
>
{label}
</Checkbox>
)}
/>
))}
</motion.div>
</div>
);
};
@@ -0,0 +1,150 @@
import { useEffect, useMemo } from "react";
import { Control, Controller, UseFormSetValue, useWatch } from "react-hook-form";
import { faSignIn } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { motion } from "framer-motion";
import { twMerge } from "tailwind-merge";
import { Checkbox, Select, SelectItem } from "@app/components/v2";
import { useToggle } from "@app/hooks";
import { TFormSchema } from "./OrgRoleModifySection.utils";
type Props = {
isNonEditable?: boolean;
setValue: UseFormSetValue<TFormSchema>;
control: Control<TFormSchema>;
};
enum Permission {
NoAccess = "no-access",
ReadOnly = "read-only",
FullAccess = "full-acess",
Custom = "custom"
}
const PERMISSIONS = [
{ action: "read", label: "Read" },
{ action: "create", label: "Create" },
{ action: "edit", label: "Update" },
{ action: "delete", label: "Remove" }
] as const;
export const SsoPermission = ({ isNonEditable, setValue, control }: Props) => {
const rule = useWatch({
control,
name: "permissions.sso"
});
const [isCustom, setIsCustom] = useToggle();
const selectedPermissionCategory = useMemo(() => {
let score = 0;
const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
const totalActions = PERMISSIONS.length;
actions.forEach((key) => (score += rule[key] ? 1 : 0));
if (isCustom) return Permission.Custom;
if (score === 0) return Permission.NoAccess;
if (score === totalActions) return Permission.FullAccess;
if (score === 1 && rule.read) return Permission.ReadOnly;
return Permission.Custom;
}, [rule, isCustom]);
useEffect(() => {
selectedPermissionCategory === Permission.Custom ? setIsCustom.on() : setIsCustom.off();
}, [selectedPermissionCategory]);
const handlePermissionChange = (val: Permission) => {
switch (val) {
case Permission.NoAccess:
setIsCustom.off();
setValue(
"permissions.sso",
{ read: false, edit: false, create: false, delete: false },
{ shouldDirty: true }
);
break;
case Permission.FullAccess:
setIsCustom.off();
setValue(
"permissions.sso",
{ read: true, edit: true, create: true, delete: true },
{ shouldDirty: true }
);
break;
case Permission.ReadOnly:
setIsCustom.off();
setValue(
"permissions.sso",
{ read: true, edit: false, create: false, delete: false },
{ shouldDirty: true }
);
break;
default:
setIsCustom.on();
setValue(
"permissions.sso",
{ read: false, edit: false, create: false, delete: false },
{ shouldDirty: true }
);
break;
}
};
return (
<div
className={twMerge(
"px-10 py-6 bg-mineshaft-800 rounded-md",
selectedPermissionCategory !== Permission.NoAccess && "border-l-2 border-primary-600"
)}
>
<div className="flex items-center space-x-4">
<div>
<FontAwesomeIcon icon={faSignIn} className="text-4xl" />
</div>
<div className="flex-grow flex flex-col">
<div className="font-medium mb-1 text-lg">SSO</div>
<div className="text-xs font-light">SSO management control</div>
</div>
<div>
<Select
defaultValue={Permission.NoAccess}
isDisabled={isNonEditable}
value={selectedPermissionCategory}
onValueChange={handlePermissionChange}
>
<SelectItem value={Permission.NoAccess}>No Access</SelectItem>
<SelectItem value={Permission.ReadOnly}>Read Only</SelectItem>
<SelectItem value={Permission.FullAccess}>Full Access</SelectItem>
<SelectItem value={Permission.Custom}>Custom</SelectItem>
</Select>
</div>
</div>
<motion.div
initial={false}
animate={{ height: isCustom ? "2.5rem" : 0, paddingTop: isCustom ? "1rem" : 0 }}
className="overflow-hidden grid gap-8 grid-flow-col auto-cols-min"
>
{isCustom &&
PERMISSIONS.map(({ action, label }) => (
<Controller
name={`permissions.sso.${action}`}
key={`permissions.sso.${action}`}
control={control}
render={({ field }) => (
<Checkbox
isChecked={field.value}
onCheckedChange={field.onChange}
id={`permissions.sso.${action}`}
isDisabled={isNonEditable}
>
{label}
</Checkbox>
)}
/>
))}
</motion.div>
</div>
);
};
@@ -3,6 +3,7 @@ import { Control, Controller, UseFormSetValue, useWatch } from "react-hook-form"
import { faClipboardList } from "@fortawesome/free-solid-svg-icons"; import { faClipboardList } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { twMerge } from "tailwind-merge";
import { import {
Checkbox, Checkbox,
@@ -26,14 +27,14 @@ type Props = {
control: Control<TFormSchema>; control: Control<TFormSchema>;
}; };
enum WorkspacePermission { enum Permission {
NoAccess = "no-access", NoAccess = "no-access",
ReadOnly = "read-only", ReadOnly = "read-only",
FullAccess = "full-acess", FullAccess = "full-acess",
Custom = "custom" Custom = "custom"
} }
export const OrgRoleWorkspacePermission = ({ isNonEditable, setValue, control }: Props) => { export const WorkspacePermission = ({ isNonEditable, setValue, control }: Props) => {
const { workspaces } = useWorkspace(); const { workspaces } = useWorkspace();
const customWorkspaceRule = useWatch({ const customWorkspaceRule = useWatch({
@@ -43,26 +44,26 @@ export const OrgRoleWorkspacePermission = ({ isNonEditable, setValue, control }:
const isCustom = Boolean(customWorkspaceRule); const isCustom = Boolean(customWorkspaceRule);
const allWorkspaceRule = useWatch({ control, name: "permissions.workspace.all" }); const allWorkspaceRule = useWatch({ control, name: "permissions.workspace.all" });
const selectedWsTopVal = useMemo(() => { const selectedPermissionCategory = useMemo(() => {
const { read, delete: del, edit, create } = allWorkspaceRule || {}; const { read, delete: del, edit, create } = allWorkspaceRule || {};
if (read && del && edit && create) return WorkspacePermission.FullAccess; if (read && del && edit && create) return Permission.FullAccess;
if (read) return WorkspacePermission.ReadOnly; if (read) return Permission.ReadOnly;
return WorkspacePermission.NoAccess; return Permission.NoAccess;
}, [allWorkspaceRule]); }, [allWorkspaceRule]);
const handleTopLevelPermissionChange = (val: WorkspacePermission) => { const handlePermissionChange = (val: Permission) => {
switch (val) { switch (val) {
case WorkspacePermission.NoAccess: case Permission.NoAccess:
setValue("permissions.workspace", {}, { shouldDirty: true }); setValue("permissions.workspace", {}, { shouldDirty: true });
break; break;
case WorkspacePermission.FullAccess: case Permission.FullAccess:
setValue( setValue(
"permissions.workspace", "permissions.workspace",
{ all: { read: true, edit: true, create: true, delete: true } }, { all: { read: true, edit: true, create: true, delete: true } },
{ shouldDirty: true } { shouldDirty: true }
); );
break; break;
case WorkspacePermission.ReadOnly: case Permission.ReadOnly:
setValue( setValue(
"permissions.workspace", "permissions.workspace",
{ all: { read: true, edit: false, create: false, delete: false } }, { all: { read: true, edit: false, create: false, delete: false } },
@@ -80,7 +81,13 @@ export const OrgRoleWorkspacePermission = ({ isNonEditable, setValue, control }:
}; };
return ( return (
<div className="px-10 py-6 bg-mineshaft-800 rounded-md"> <div
className={twMerge(
"px-10 py-6 bg-mineshaft-800 rounded-md",
(selectedPermissionCategory !== Permission.NoAccess || isCustom) &&
"border-l-2 border-primary-600"
)}
>
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
<div> <div>
<FontAwesomeIcon icon={faClipboardList} className="text-4xl" /> <FontAwesomeIcon icon={faClipboardList} className="text-4xl" />
@@ -91,15 +98,15 @@ export const OrgRoleWorkspacePermission = ({ isNonEditable, setValue, control }:
</div> </div>
<div> <div>
<Select <Select
defaultValue={WorkspacePermission.NoAccess} defaultValue={Permission.NoAccess}
isDisabled={isNonEditable} isDisabled={isNonEditable}
value={isCustom ? WorkspacePermission.Custom : selectedWsTopVal} value={isCustom ? Permission.Custom : selectedPermissionCategory}
onValueChange={handleTopLevelPermissionChange} onValueChange={handlePermissionChange}
> >
<SelectItem value={WorkspacePermission.NoAccess}>No Access</SelectItem> <SelectItem value={Permission.NoAccess}>No Access</SelectItem>
<SelectItem value={WorkspacePermission.ReadOnly}>Read Only</SelectItem> <SelectItem value={Permission.ReadOnly}>Read Only</SelectItem>
<SelectItem value={WorkspacePermission.FullAccess}>Full Access</SelectItem> <SelectItem value={Permission.FullAccess}>Full Access</SelectItem>
<SelectItem value={WorkspacePermission.Custom}>Custom</SelectItem> <SelectItem value={Permission.Custom}>Custom</SelectItem>
</Select> </Select>
</div> </div>
</div> </div>
@@ -88,7 +88,7 @@ export const OrgRoleTable = ({ isRolesLoading, roles = [], onSelectRole }: Props
const isNonMutatable = ["owner", "admin", "member"].includes(slug); const isNonMutatable = ["owner", "admin", "member"].includes(slug);
return ( return (
<Tr key={id}> <Tr key={`role-list-${id}`}>
<Td>{name}</Td> <Td>{name}</Td>
<Td>{slug}</Td> <Td>{slug}</Td>
<Td> <Td>