feat(rbac): made org role section permission into one component and fixed some small issues

This commit is contained in:
Akhil Mohan
2023-09-08 21:22:36 +05:30
parent f1a291a52a
commit cb9ee00ed3
16 changed files with 134 additions and 984 deletions
@@ -658,7 +658,7 @@ export const DashboardPage = withProjectPermission(
}, [selectedEnv?.slug, (popUp?.deleteFolder?.data as TDeleteFolderForm)?.id]); }, [selectedEnv?.slug, (popUp?.deleteFolder?.data as TDeleteFolderForm)?.id]);
// SECRET IMPORT SECTION // SECRET IMPORT SECTION
const handleSecretImportCreate = async (env: string, secretPath: string) => { const handleSecretImportCreate = async (env: string, secPath: string) => {
try { try {
await createSecretImport({ await createSecretImport({
workspaceId, workspaceId,
@@ -666,7 +666,7 @@ export const DashboardPage = withProjectPermission(
folderId, folderId,
secretImport: { secretImport: {
environment: env, environment: env,
secretPath secretPath: secPath
} }
}); });
createNotification({ createNotification({
@@ -1,149 +0,0 @@
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(() => {
const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
const totalActions = PERMISSIONS.length;
const score = actions.map((key) => (rule[key] ? 1 : 0)).reduce((a, b) => a + b, 0 as number);
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(() => {
if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
}, [selectedPermissionCategory]);
const handlePermissionChange = (val: Permission) => {
if (val === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
switch (val) {
case Permission.NoAccess:
setValue(
"permissions.incident-contact",
{ read: false, edit: false, create: false, delete: false },
{ shouldDirty: true }
);
break;
case Permission.FullAccess:
setValue(
"permissions.incident-contact",
{ read: true, edit: true, create: true, delete: true },
{ shouldDirty: true }
);
break;
case Permission.ReadOnly:
setValue(
"permissions.incident-contact",
{ read: true, edit: false, create: false, delete: false },
{ shouldDirty: true }
);
break;
default:
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>
);
};
@@ -1,149 +0,0 @@
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 rule = useWatch({
control,
name: "permissions.member"
});
const [isCustom, setIsCustom] = useToggle();
const selectedPermissionCategory = useMemo(() => {
const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
const totalActions = PERMISSIONS.length;
const score = actions.map((key) => (rule[key] ? 1 : 0)).reduce((a, b) => a + b, 0 as number);
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(() => {
if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
}, [selectedPermissionCategory]);
const handlePermissionChange = (val: Permission) => {
if (val === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
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>
);
};
@@ -1,6 +1,15 @@
import { useState } from "react"; 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,
faCog,
faContactCard,
faMagnifyingGlass,
faMoneyBill,
faSignIn,
faUserCog,
faUsers
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
@@ -10,19 +19,13 @@ 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 { RolePermission } from "./RolePermission"; import { SimpleLevelPermissionOption } from "./SimpleLevelPermissionOptions";
import { SecretScannigPermission } from "./SecretScanningPermission";
import { SettingsPermission } from "./SettingsPermission";
import { SsoPermission } from "./SsoPermission";
import { WorkspacePermission } from "./WorkspacePermission"; import { WorkspacePermission } from "./WorkspacePermission";
type Props = { type Props = {
@@ -30,6 +33,51 @@ type Props = {
onGoBack: VoidFunction; onGoBack: VoidFunction;
}; };
const SIMPLE_PERMISSION_OPTIONS = [
{
title: "Members",
subtitle: "Project member management control",
icon: faUsers,
formName: "member"
},
{
title: "Billing",
subtitle: "Billing management control",
icon: faMoneyBill,
formName: "billing"
},
{
title: "Role",
subtitle: "Org role management control",
icon: faUserCog,
formName: "role"
},
{
title: "Incident Contacts",
subtitle: "Incident contacts management control",
icon: faContactCard,
formName: "incident-contact"
},
{
title: "Settings",
subtitle: "Settings management control",
icon: faCog,
formName: "settings"
},
{
title: "Secret Scanning",
subtitle: "Secret scanning management control",
icon: faMagnifyingGlass,
formName: "secret-scanning"
},
{
title: "SSO",
subtitle: "SSO management control",
icon: faSignIn,
formName: "sso"
}
] as const;
export const OrgRoleModifySection = ({ role, onGoBack }: Props) => { export const OrgRoleModifySection = ({ role, onGoBack }: Props) => {
const [searchPermission, setSearchPermission] = useState(""); const [searchPermission, setSearchPermission] = useState("");
@@ -148,50 +196,26 @@ export const OrgRoleModifySection = ({ role, onGoBack }: Props) => {
/> />
</div> </div>
</div> </div>
<div className="flex flex-col space-y-4"> <div className="">
<WorkspacePermission <WorkspacePermission
isNonEditable={isNonEditable} isNonEditable={isNonEditable}
control={control} control={control}
setValue={setValue} setValue={setValue}
/> />
</div> </div>
<div className="flex flex-col space-y-4"> {SIMPLE_PERMISSION_OPTIONS.map(({ title, subtitle, icon, formName }) => (
<MemberPermission isNonEditable={isNonEditable} control={control} setValue={setValue} /> <div key={`permission-${title}`}>
</div> <SimpleLevelPermissionOption
<div className="flex flex-col space-y-4"> isNonEditable={isNonEditable}
<BillingPermission control={control}
isNonEditable={isNonEditable} setValue={setValue}
control={control} icon={icon}
setValue={setValue} title={title}
/> subtitle={subtitle}
</div> formName={formName}
<div className="flex flex-col space-y-4"> />
<RolePermission isNonEditable={isNonEditable} control={control} setValue={setValue} /> </div>
</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">
<SecretScannigPermission
isNonEditable={isNonEditable}
control={control}
setValue={setValue}
/>
</div>
<div className="flex flex-col space-y-4">
<SsoPermission isNonEditable={isNonEditable} control={control} setValue={setValue} />
</div>
</div> </div>
<div className="flex items-center space-x-4 mt-12"> <div className="flex items-center space-x-4 mt-12">
<Button <Button
@@ -3,22 +3,26 @@ import { z } from "zod";
import { TPermission } from "@app/hooks/api/roles/types"; import { TPermission } from "@app/hooks/api/roles/types";
const generalPermissionSchema = z.object({ const generalPermissionSchema = z
read: z.boolean().optional(), .object({
edit: z.boolean().optional(), read: z.boolean().optional(),
delete: z.boolean().optional(), edit: z.boolean().optional(),
create: z.boolean().optional() delete: z.boolean().optional(),
}); create: z.boolean().optional()
})
.optional();
export const formSchema = z.object({ export const formSchema = z.object({
name: z.string(), name: z.string(),
description: z.string().optional(), description: z.string().optional(),
slug: z.string(), slug: z.string(),
permissions: z.object({ permissions: z.object({
workspace: z.object({ workspace: z
read: z.boolean().optional(), .object({
create: z.boolean().optional() read: z.boolean().optional(),
}), create: z.boolean().optional()
})
.optional(),
member: generalPermissionSchema, member: generalPermissionSchema,
role: generalPermissionSchema, role: generalPermissionSchema,
settings: generalPermissionSchema, settings: generalPermissionSchema,
@@ -47,8 +51,10 @@ export const rolePermission2Form = (permissions: TPermission[] = []) => {
}; };
permissions.forEach((permission) => { permissions.forEach((permission) => {
// akhilmhdh: this is typecast as workspace key else i would need an if loop with same condition on both side // akhilmhdh: this is typecast as something other than workspace key else i would need an if loop with same condition on both side
formVal[permission.subject][permission.action as keyof typeof formVal.workspace] = true; const key = permission.subject as keyof TFormSchema["permissions"];
(formVal[key] as Exclude<TFormSchema["permissions"]["member"], undefined>)[permission.action] =
true;
}); });
return formVal; return formVal;
@@ -59,13 +65,13 @@ export const formRolePermission2API = (formVal: TFormSchema["permissions"]) => {
(Object.keys(formVal) as Array<keyof typeof formVal>).forEach((rule) => { (Object.keys(formVal) as Array<keyof typeof formVal>).forEach((rule) => {
// all these type annotations are due to Object.keys of ts cannot infer and put it just a string[] // all these type annotations are due to Object.keys of ts cannot infer and put it just a string[]
// quite annoying i know // quite annoying i know
const actions = Object.keys(formVal[rule]) as Array< const actions = Object.keys(formVal[rule] || {}) as Array<
keyof z.infer<typeof generalPermissionSchema> keyof z.infer<typeof generalPermissionSchema>
>; >;
actions.forEach((action) => { actions.forEach((action) => {
// akhilmhdh: set it as any due to the union type bug i would end up writing an if else with same condition on both side // akhilmhdh: set it as any due to the union type bug i would end up writing an if else with same condition on both side
if (formVal[rule][action as keyof typeof formVal.workspace]) { if (formVal?.[rule]?.[action as keyof typeof formVal.workspace]) {
permissions.push({ subject: rule, action } as any); permissions.push({ subject: rule, action } as any);
} }
}); });
@@ -1,149 +0,0 @@
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 rule = useWatch({
control,
name: "permissions.role"
});
const [isCustom, setIsCustom] = useToggle();
const selectedPermissionCategory = useMemo(() => {
const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
const totalActions = PERMISSIONS.length;
const score = actions.map((key) => (rule[key] ? 1 : 0)).reduce((a, b) => a + b, 0 as number);
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(() => {
if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
}, [selectedPermissionCategory]);
const handlePermissionChange = (val: Permission) => {
if (val === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
switch (val) {
case Permission.NoAccess:
setValue(
"permissions.role",
{ read: false, edit: false, create: false, delete: false },
{ shouldDirty: true }
);
break;
case Permission.FullAccess:
setValue(
"permissions.role",
{ read: true, edit: true, create: true, delete: true },
{ shouldDirty: true }
);
break;
case Permission.ReadOnly:
setValue(
"permissions.role",
{ read: true, edit: false, create: false, delete: false },
{ shouldDirty: true }
);
break;
default:
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>
);
};
@@ -1,149 +0,0 @@
import { useEffect, useMemo } from "react";
import { Control, Controller, UseFormSetValue, useWatch } from "react-hook-form";
import { faMagnifyingGlass } 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 SecretScannigPermission = ({ isNonEditable, setValue, control }: Props) => {
const rule = useWatch({
control,
name: "permissions.secret-scanning"
});
const [isCustom, setIsCustom] = useToggle();
const selectedPermissionCategory = useMemo(() => {
const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
const totalActions = PERMISSIONS.length;
const score = actions.map((key) => (rule[key] ? 1 : 0)).reduce((a, b) => a + b, 0 as number);
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(() => {
if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
}, [selectedPermissionCategory]);
const handlePermissionChange = (val: Permission) => {
if (val === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
switch (val) {
case Permission.NoAccess:
setValue(
"permissions.secret-scanning",
{ read: false, edit: false, create: false, delete: false },
{ shouldDirty: true }
);
break;
case Permission.FullAccess:
setValue(
"permissions.secret-scanning",
{ read: true, edit: true, create: true, delete: true },
{ shouldDirty: true }
);
break;
case Permission.ReadOnly:
setValue(
"permissions.secret-scanning",
{ read: true, edit: false, create: false, delete: false },
{ shouldDirty: true }
);
break;
default:
setValue(
"permissions.secret-scanning",
{ 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={faMagnifyingGlass} className="text-4xl" />
</div>
<div className="flex-grow flex flex-col">
<div className="font-medium mb-1 text-lg">Secret Scanning</div>
<div className="text-xs font-light">Secret scanning 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>
);
};
@@ -1,149 +0,0 @@
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(() => {
const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
const totalActions = PERMISSIONS.length;
const score = actions.map((key) => (rule[key] ? 1 : 0)).reduce((a, b) => a + b, 0 as number);
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(() => {
if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
}, [selectedPermissionCategory]);
const handlePermissionChange = (val: Permission) => {
if (val === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
switch (val) {
case Permission.NoAccess:
setValue(
"permissions.settings",
{ read: false, edit: false, create: false, delete: false },
{ shouldDirty: true }
);
break;
case Permission.FullAccess:
setValue(
"permissions.settings",
{ read: true, edit: true, create: true, delete: true },
{ shouldDirty: true }
);
break;
case Permission.ReadOnly:
setValue(
"permissions.settings",
{ read: true, edit: false, create: false, delete: false },
{ shouldDirty: true }
);
break;
default:
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>
);
};
@@ -1,6 +1,6 @@
import { useEffect, useMemo } from "react"; import { useEffect, useMemo } from "react";
import { Control, Controller, UseFormSetValue, useWatch } from "react-hook-form"; import { Control, Controller, UseFormSetValue, useWatch } from "react-hook-form";
import { faMoneyBill } from "@fortawesome/free-solid-svg-icons"; import { IconProp } from "@fortawesome/fontawesome-svg-core";
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 { twMerge } from "tailwind-merge";
@@ -11,9 +11,13 @@ import { useToggle } from "@app/hooks";
import { TFormSchema } from "./OrgRoleModifySection.utils"; import { TFormSchema } from "./OrgRoleModifySection.utils";
type Props = { type Props = {
formName: keyof Omit<TFormSchema["permissions"], "workspace">;
isNonEditable?: boolean; isNonEditable?: boolean;
setValue: UseFormSetValue<TFormSchema>; setValue: UseFormSetValue<TFormSchema>;
control: Control<TFormSchema>; control: Control<TFormSchema>;
title: string;
subtitle: string;
icon: IconProp;
}; };
enum Permission { enum Permission {
@@ -24,28 +28,36 @@ enum Permission {
} }
const PERMISSIONS = [ const PERMISSIONS = [
{ action: "read", label: "Read" }, { action: "read", label: "View" },
{ action: "create", label: "Create" }, { action: "create", label: "Create" },
{ action: "edit", label: "Update" }, { action: "edit", label: "Modify" },
{ action: "delete", label: "Remove" } { action: "delete", label: "Remove" }
] as const; ] as const;
export const BillingPermission = ({ isNonEditable, setValue, control }: Props) => { export const SimpleLevelPermissionOption = ({
isNonEditable,
setValue,
control,
formName,
subtitle,
title,
icon
}: Props) => {
const rule = useWatch({ const rule = useWatch({
control, control,
name: "permissions.billing" name: `permissions.${formName}`
}); });
const [isCustom, setIsCustom] = useToggle(); const [isCustom, setIsCustom] = useToggle();
const selectedPermissionCategory = useMemo(() => { const selectedPermissionCategory = useMemo(() => {
const actions = Object.keys(rule || {}) as Array<keyof typeof rule>; const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
const totalActions = PERMISSIONS.length; const totalActions = PERMISSIONS.length;
const score = actions.map((key) => (rule[key] ? 1 : 0)).reduce((a, b) => a + b, 0 as number); const score = actions.map((key) => (rule?.[key] ? 1 : 0)).reduce((a, b) => a + b, 0 as number);
if (isCustom) return Permission.Custom; if (isCustom) return Permission.Custom;
if (score === 0) return Permission.NoAccess; if (score === 0) return Permission.NoAccess;
if (score === totalActions) return Permission.FullAccess; if (score === totalActions) return Permission.FullAccess;
if (score === 1 && rule.read) return Permission.ReadOnly; if (score === 1 && rule?.read) return Permission.ReadOnly;
return Permission.Custom; return Permission.Custom;
}, [rule, isCustom]); }, [rule, isCustom]);
@@ -62,28 +74,28 @@ export const BillingPermission = ({ isNonEditable, setValue, control }: Props) =
switch (val) { switch (val) {
case Permission.NoAccess: case Permission.NoAccess:
setValue( setValue(
"permissions.billing", `permissions.${formName}`,
{ read: false, edit: false, create: false, delete: false }, { read: false, edit: false, create: false, delete: false },
{ shouldDirty: true } { shouldDirty: true }
); );
break; break;
case Permission.FullAccess: case Permission.FullAccess:
setValue( setValue(
"permissions.billing", `permissions.${formName}`,
{ read: true, edit: true, create: true, delete: true }, { read: true, edit: true, create: true, delete: true },
{ shouldDirty: true } { shouldDirty: true }
); );
break; break;
case Permission.ReadOnly: case Permission.ReadOnly:
setValue( setValue(
"permissions.billing", `permissions.${formName}`,
{ read: true, edit: false, create: false, delete: false }, { read: true, edit: false, create: false, delete: false },
{ shouldDirty: true } { shouldDirty: true }
); );
break; break;
default: default:
setValue( setValue(
"permissions.billing", `permissions.${formName}`,
{ read: false, edit: false, create: false, delete: false }, { read: false, edit: false, create: false, delete: false },
{ shouldDirty: true } { shouldDirty: true }
); );
@@ -100,11 +112,11 @@ export const BillingPermission = ({ isNonEditable, setValue, control }: Props) =
> >
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
<div> <div>
<FontAwesomeIcon icon={faMoneyBill} className="text-4xl" /> <FontAwesomeIcon icon={icon} className="text-4xl" />
</div> </div>
<div className="flex-grow flex flex-col"> <div className="flex-grow flex flex-col">
<div className="font-medium mb-1 text-lg">Billing</div> <div className="font-medium mb-1 text-lg">{title}</div>
<div className="text-xs font-light">Billing management control</div> <div className="text-xs font-light">{subtitle}</div>
</div> </div>
<div> <div>
<Select <Select
@@ -128,14 +140,14 @@ export const BillingPermission = ({ isNonEditable, setValue, control }: Props) =
{isCustom && {isCustom &&
PERMISSIONS.map(({ action, label }) => ( PERMISSIONS.map(({ action, label }) => (
<Controller <Controller
name={`permissions.billing.${action}`} name={`permissions.${formName}.${action}`}
key={`permissions.billing.${action}`} key={`permissions.${formName}.${action}`}
control={control} control={control}
render={({ field }) => ( render={({ field }) => (
<Checkbox <Checkbox
isChecked={field.value} isChecked={field.value}
onCheckedChange={field.onChange} onCheckedChange={field.onChange}
id={`permissions.billing.${action}`} id={`permissions.${formName}.${action}`}
isDisabled={isNonEditable} isDisabled={isNonEditable}
> >
{label} {label}
@@ -1,149 +0,0 @@
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(() => {
const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
const totalActions = PERMISSIONS.length;
const score = actions.map((key) => (rule[key] ? 1 : 0)).reduce((a, b) => a + b, 0 as number);
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(() => {
if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
}, [selectedPermissionCategory]);
const handlePermissionChange = (val: Permission) => {
if (val === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
switch (val) {
case Permission.NoAccess:
setValue(
"permissions.sso",
{ read: false, edit: false, create: false, delete: false },
{ shouldDirty: true }
);
break;
case Permission.FullAccess:
setValue(
"permissions.sso",
{ read: true, edit: true, create: true, delete: true },
{ shouldDirty: true }
);
break;
case Permission.ReadOnly:
setValue(
"permissions.sso",
{ read: true, edit: false, create: false, delete: false },
{ shouldDirty: true }
);
break;
default:
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>
);
};
@@ -24,7 +24,7 @@ enum Permission {
} }
const PERMISSIONS = [ const PERMISSIONS = [
{ action: "read", label: "Read" }, { action: "read", label: "View" },
{ action: "create", label: "Create" } { action: "create", label: "Create" }
] as const; ] as const;
@@ -38,12 +38,12 @@ export const WorkspacePermission = ({ isNonEditable, setValue, control }: Props)
const selectedPermissionCategory = useMemo(() => { const selectedPermissionCategory = useMemo(() => {
const actions = Object.keys(rule || {}) as Array<keyof typeof rule>; const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
const totalActions = PERMISSIONS.length; const totalActions = PERMISSIONS.length;
const score = actions.map((key) => (rule[key] ? 1 : 0)).reduce((a, b) => a + b, 0 as number); const score = actions.map((key) => (rule?.[key] ? 1 : 0)).reduce((a, b) => a + b, 0 as number);
if (isCustom) return Permission.Custom; if (isCustom) return Permission.Custom;
if (score === 0) return Permission.NoAccess; if (score === 0) return Permission.NoAccess;
if (score === totalActions) return Permission.FullAccess; if (score === totalActions) return Permission.FullAccess;
if (score === 1 && rule.read) return Permission.ReadOnly; if (score === 1 && rule?.read) return Permission.ReadOnly;
return Permission.Custom; return Permission.Custom;
}, [rule, isCustom]); }, [rule, isCustom]);
@@ -135,9 +135,9 @@ export const MultiEnvProjectPermission = ({
<Tr> <Tr>
<Th /> <Th />
<Th className="min-w-[8rem]">Secret Path</Th> <Th className="min-w-[8rem]">Secret Path</Th>
<Th className="text-center">Read</Th> <Th className="text-center">View</Th>
<Th className="text-center">Create</Th> <Th className="text-center">Create</Th>
<Th className="text-center">Edit</Th> <Th className="text-center">Modify</Th>
<Th className="text-center">Delete</Th> <Th className="text-center">Delete</Th>
</Tr> </Tr>
</THead> </THead>
@@ -260,7 +260,7 @@ export const ProjectRoleModifySection = ({ role, onGoBack }: Props) => {
/> />
</div> </div>
{SINGLE_PERMISSION_LIST.map(({ title, subtitle, icon, formName }) => ( {SINGLE_PERMISSION_LIST.map(({ title, subtitle, icon, formName }) => (
<div className="flex flex-col space-y-4" key={`permission-${title}`}> <div key={`permission-${title}`}>
<SingleProjectPermission <SingleProjectPermission
isNonEditable={isNonEditable} isNonEditable={isNonEditable}
control={control} control={control}
@@ -94,7 +94,10 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
permissions.forEach((permission) => { permissions.forEach((permission) => {
if (["secrets", "folders", "secret-imports"].includes(permission.subject)) { if (["secrets", "folders", "secret-imports"].includes(permission.subject)) {
multiEnvApi2Form(formVal[permission.subject], permission); multiEnvApi2Form(
formVal[permission.subject] as TFormSchema["permissions"]["secrets"],
permission
);
} else { } else {
// everything else follows same pattern // everything else follows same pattern
// formVal[settings][read | write] = true // formVal[settings][read | write] = true
@@ -135,7 +138,7 @@ const multiEnvForm2Api = (
// if not full access for an action // if not full access for an action
if (!formVal?.all?.[action] && action !== "secretPath" && formVal?.[slug]?.[action]) { if (!formVal?.all?.[action] && action !== "secretPath" && formVal?.[slug]?.[action]) {
const conditions: Record<string, unknown> = { environment: slug }; const conditions: Record<string, unknown> = { environment: slug };
if (formVal[slug]?.secretPath) conditions.secretPath = formVal[slug].secretPath; if (formVal[slug]?.secretPath) conditions.secretPath = formVal?.[slug]?.secretPath;
permissions.push({ action, subject, conditions }); permissions.push({ action, subject, conditions });
} }
@@ -38,9 +38,9 @@ enum Permission {
} }
const PERMISSIONS = [ const PERMISSIONS = [
{ action: "read", label: "Read" }, { action: "read", label: "View" },
{ action: "create", label: "Create" }, { action: "create", label: "Create" },
{ action: "edit", label: "Update" }, { action: "edit", label: "Modify" },
{ action: "delete", label: "Remove" } { action: "delete", label: "Remove" }
] as const; ] as const;
@@ -42,7 +42,6 @@ import {
useDeleteSecretV3, useDeleteSecretV3,
useGetFoldersByEnv, useGetFoldersByEnv,
useGetProjectSecretsAllEnv, useGetProjectSecretsAllEnv,
useGetUserWsEnvironments,
useGetUserWsKey, useGetUserWsKey,
useUpdateSecretV3 useUpdateSecretV3
} from "@app/hooks/api"; } from "@app/hooks/api";