mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-08 18:28:12 +00:00
feat(rbac): added rest of permissions in ui and backend api for org level
This commit is contained in:
@@ -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",
|
||||||
|
|||||||
@@ -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();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Generated
+15
-1
@@ -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=="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
+147
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
+151
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
+146
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
+48
-4
@@ -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}
|
||||||
|
|||||||
+63
-28
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
+150
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
+150
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
+150
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
+150
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
+25
-18
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user