improvement: add select polices modal

This commit is contained in:
Scott Wilson
2025-05-07 13:50:27 -07:00
parent 0610416677
commit 68c549f1c6
8 changed files with 309 additions and 377 deletions
@@ -6,15 +6,15 @@ import { twMerge } from "tailwind-merge";
import { z } from "zod"; import { z } from "zod";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Input, Modal, ModalContent, ModalTrigger } from "@app/components/v2"; import { Button, FormControl, Input } from "@app/components/v2";
import { ProjectPermissionSub } from "@app/context"; import { ProjectPermissionSub } from "@app/context";
import { isCustomProjectRole } from "@app/helpers/roles"; import { isCustomProjectRole } from "@app/helpers/roles";
import { usePopUp } from "@app/hooks"; import { usePopUp } from "@app/hooks";
import { TProjectTemplate, useUpdateProjectTemplate } from "@app/hooks/api/projectTemplates"; import { TProjectTemplate, useUpdateProjectTemplate } from "@app/hooks/api/projectTemplates";
import { slugSchema } from "@app/lib/schemas"; import { slugSchema } from "@app/lib/schemas";
import { GeneralPermissionPolicies } from "@app/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionPolicies"; import { GeneralPermissionPolicies } from "@app/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionPolicies";
import { NewPermissionRule } from "@app/pages/project/RoleDetailsBySlugPage/components/NewPermissionRule";
import { PermissionEmptyState } from "@app/pages/project/RoleDetailsBySlugPage/components/PermissionEmptyState"; import { PermissionEmptyState } from "@app/pages/project/RoleDetailsBySlugPage/components/PermissionEmptyState";
import { PolicySelectionModal } from "@app/pages/project/RoleDetailsBySlugPage/components/PolicySelectionModal";
import { import {
formRolePermission2API, formRolePermission2API,
PROJECT_PERMISSION_OBJECT, PROJECT_PERMISSION_OBJECT,
@@ -44,7 +44,7 @@ export const ProjectTemplateEditRoleForm = ({
role, role,
isDisabled isDisabled
}: Props) => { }: Props) => {
const { popUp, handlePopUpToggle } = usePopUp(["createPolicy"] as const); const { popUp, handlePopUpToggle } = usePopUp(["addPolicy"] as const);
const formMethods = useForm<TFormSchema>({ const formMethods = useForm<TFormSchema>({
values: role ? { ...role, permissions: rolePermission2Form(role.permissions) } : undefined, values: role ? { ...role, permissions: rolePermission2Form(role.permissions) } : undefined,
@@ -119,34 +119,29 @@ export const ProjectTemplateEditRoleForm = ({
<Button <Button
variant="outline_bg" variant="outline_bg"
type="submit" type="submit"
className={twMerge("h-10 rounded-r-none", isDirty && "bg-primary text-black")} className={twMerge(
"h-10 rounded-r-none border border-primary",
isDirty && "bg-primary text-black"
)}
isDisabled={isSubmitting || !isDirty || isDisabled} isDisabled={isSubmitting || !isDirty || isDisabled}
isLoading={isSubmitting} isLoading={isSubmitting}
leftIcon={<FontAwesomeIcon icon={faSave} />} leftIcon={<FontAwesomeIcon icon={faSave} />}
> >
Save Save
</Button> </Button>
<Modal <Button
isOpen={popUp.createPolicy.isOpen} className="h-10 rounded-l-none"
onOpenChange={(isOpen) => handlePopUpToggle("createPolicy", isOpen)} variant="outline_bg"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
isDisabled={isDisabled}
onClick={() => handlePopUpToggle("addPolicy")}
> >
<ModalTrigger asChild> Add Policies
<Button </Button>
className="h-10 rounded-l-none" <PolicySelectionModal
variant="outline_bg" isOpen={popUp.addPolicy.isOpen}
leftIcon={<FontAwesomeIcon icon={faPlus} />} onOpenChange={(isOpen) => handlePopUpToggle("addPolicy", isOpen)}
isDisabled={isDisabled} />
>
New Policy
</Button>
</ModalTrigger>
<ModalContent
title="New Policy"
subTitle="Policies grant additional permissions."
>
<NewPermissionRule onClose={() => handlePopUpToggle("createPolicy")} />
</ModalContent>
</Modal>
</div> </div>
</div> </div>
)} )}
@@ -1,3 +1,4 @@
import { useMemo } from "react";
import { useRouterState } from "@tanstack/react-router"; import { useRouterState } from "@tanstack/react-router";
import { ProjectType } from "@app/hooks/api/workspace/types"; import { ProjectType } from "@app/hooks/api/workspace/types";
@@ -5,9 +6,17 @@ import { ProjectType } from "@app/hooks/api/workspace/types";
export const useGetProjectTypeFromRoute = () => { export const useGetProjectTypeFromRoute = () => {
const { location } = useRouterState(); const { location } = useRouterState();
const segment = location.pathname.split("/")[2]; return useMemo(() => {
const segments = location.pathname.split("/");
if (!Object.values(ProjectType).includes(segment as ProjectType)) return undefined; let type: ProjectType | undefined;
return segment as ProjectType; // location of project type can vary in router path, so we need to check all possible values
segments.forEach((segment) => {
if (Object.values(ProjectType).includes(segment as ProjectType))
type = segment as ProjectType;
});
return type;
}, [location]);
}; };
@@ -18,10 +18,6 @@ import { TtlFormLabel } from "@app/components/features";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { import {
Button, Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
FormControl, FormControl,
FormLabel, FormLabel,
Input, Input,
@@ -37,6 +33,7 @@ import {
useProjectPermission, useProjectPermission,
useWorkspace useWorkspace
} from "@app/context"; } from "@app/context";
import { usePopUp } from "@app/hooks";
import { import {
useCreateIdentityProjectAdditionalPrivilege, useCreateIdentityProjectAdditionalPrivilege,
useGetIdentityProjectPrivilegeDetails, useGetIdentityProjectPrivilegeDetails,
@@ -45,12 +42,11 @@ import {
import { IdentityProjectAdditionalPrivilegeTemporaryMode } from "@app/hooks/api/identityProjectAdditionalPrivilege/types"; import { IdentityProjectAdditionalPrivilegeTemporaryMode } from "@app/hooks/api/identityProjectAdditionalPrivilege/types";
import { GeneralPermissionPolicies } from "@app/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionPolicies"; import { GeneralPermissionPolicies } from "@app/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionPolicies";
import { PermissionEmptyState } from "@app/pages/project/RoleDetailsBySlugPage/components/PermissionEmptyState"; import { PermissionEmptyState } from "@app/pages/project/RoleDetailsBySlugPage/components/PermissionEmptyState";
import { PolicySelectionModal } from "@app/pages/project/RoleDetailsBySlugPage/components/PolicySelectionModal";
import { import {
formRolePermission2API, formRolePermission2API,
isConditionalSubjects,
PROJECT_PERMISSION_OBJECT, PROJECT_PERMISSION_OBJECT,
projectRoleFormSchema, projectRoleFormSchema,
ProjectTypePermissionSubjects,
rolePermission2Form rolePermission2Form
} from "@app/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils"; } from "@app/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils";
import { renderConditionalComponents } from "@app/pages/project/RoleDetailsBySlugPage/components/RolePermissionsSection"; import { renderConditionalComponents } from "@app/pages/project/RoleDetailsBySlugPage/components/RolePermissionsSection";
@@ -101,6 +97,7 @@ export const IdentityProjectAdditionalPrivilegeModifySection = ({
ProjectPermissionIdentityActions.Edit, ProjectPermissionIdentityActions.Edit,
subject(ProjectPermissionSub.Identity, { identityId }) subject(ProjectPermissionSub.Identity, { identityId })
); );
const { popUp, handlePopUpToggle } = usePopUp(["addPolicy"] as const);
const form = useForm<TFormSchema>({ const form = useForm<TFormSchema>({
values: privilegeDetails values: privilegeDetails
@@ -195,30 +192,6 @@ export const IdentityProjectAdditionalPrivilegeModifySection = ({
} }
} }
const onNewPolicy = (selectedSubject: ProjectPermissionSub) => {
const rootPolicyValue = form.getValues(`permissions.${selectedSubject}`);
if (rootPolicyValue && isConditionalSubjects(selectedSubject)) {
form.setValue(
`permissions.${selectedSubject}`,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore-error akhilmhdh: this is because of ts collision with both
[...rootPolicyValue, {}],
{ shouldDirty: true, shouldTouch: true }
);
} else {
form.setValue(
`permissions.${selectedSubject}`,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore-error akhilmhdh: this is because of ts collision with both
[{}],
{
shouldDirty: true,
shouldTouch: true
}
);
}
};
return ( return (
<form <form
onSubmit={handleSubmit(onSubmit)} onSubmit={handleSubmit(onSubmit)}
@@ -250,52 +223,25 @@ export const IdentityProjectAdditionalPrivilegeModifySection = ({
<Button <Button
variant="outline_bg" variant="outline_bg"
type="submit" type="submit"
className={twMerge("h-10 rounded-r-none", isDirty && "bg-primary text-black")} className={twMerge(
"h-10 rounded-r-none border border-primary",
isDirty && "bg-primary text-black"
)}
isDisabled={isSubmitting || !isDirty || isDisabled} isDisabled={isSubmitting || !isDirty || isDisabled}
isLoading={isSubmitting} isLoading={isSubmitting}
leftIcon={<FontAwesomeIcon icon={faSave} />} leftIcon={<FontAwesomeIcon icon={faSave} />}
> >
Save Save
</Button> </Button>
<DropdownMenu> <Button
<DropdownMenuTrigger> isDisabled={isDisabled}
<Button className="h-10 rounded-l-none"
isDisabled={isDisabled} variant="outline_bg"
className="h-10 rounded-l-none" leftIcon={<FontAwesomeIcon icon={faPlus} />}
variant="outline_bg" onClick={() => handlePopUpToggle("addPolicy")}
leftIcon={<FontAwesomeIcon icon={faPlus} />} >
> Add Policies
New policy </Button>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="thin-scrollbar max-h-96" align="end">
{Object.keys(PROJECT_PERMISSION_OBJECT)
.filter(
(sub) =>
ProjectTypePermissionSubjects[currentWorkspace.type][
sub as ProjectPermissionSub
]
)
.sort((a, b) =>
PROJECT_PERMISSION_OBJECT[a as keyof typeof PROJECT_PERMISSION_OBJECT].title
.toLowerCase()
.localeCompare(
PROJECT_PERMISSION_OBJECT[
b as keyof typeof PROJECT_PERMISSION_OBJECT
].title.toLowerCase()
)
)
.map((permissionSubject) => (
<DropdownMenuItem
key={`permission-create-${permissionSubject}`}
className="py-3"
onClick={() => onNewPolicy(permissionSubject as ProjectPermissionSub)}
>
{PROJECT_PERMISSION_OBJECT[permissionSubject as ProjectPermissionSub].title}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</div> </div>
</div> </div>
</div> </div>
@@ -436,6 +382,10 @@ export const IdentityProjectAdditionalPrivilegeModifySection = ({
) )
)} )}
</div> </div>
<PolicySelectionModal
isOpen={popUp.addPolicy.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("addPolicy", isOpen)}
/>
</FormProvider> </FormProvider>
</form> </form>
); );
@@ -17,10 +17,6 @@ import { TtlFormLabel } from "@app/components/features";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { import {
Button, Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
FormControl, FormControl,
FormLabel, FormLabel,
Input, Input,
@@ -36,6 +32,7 @@ import {
useProjectPermission, useProjectPermission,
useWorkspace useWorkspace
} from "@app/context"; } from "@app/context";
import { usePopUp } from "@app/hooks";
import { import {
useCreateProjectUserAdditionalPrivilege, useCreateProjectUserAdditionalPrivilege,
useGetProjectUserPrivilegeDetails, useGetProjectUserPrivilegeDetails,
@@ -44,12 +41,11 @@ import {
import { ProjectUserAdditionalPrivilegeTemporaryMode } from "@app/hooks/api/projectUserAdditionalPrivilege/types"; import { ProjectUserAdditionalPrivilegeTemporaryMode } from "@app/hooks/api/projectUserAdditionalPrivilege/types";
import { GeneralPermissionPolicies } from "@app/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionPolicies"; import { GeneralPermissionPolicies } from "@app/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionPolicies";
import { PermissionEmptyState } from "@app/pages/project/RoleDetailsBySlugPage/components/PermissionEmptyState"; import { PermissionEmptyState } from "@app/pages/project/RoleDetailsBySlugPage/components/PermissionEmptyState";
import { PolicySelectionModal } from "@app/pages/project/RoleDetailsBySlugPage/components/PolicySelectionModal";
import { import {
formRolePermission2API, formRolePermission2API,
isConditionalSubjects,
PROJECT_PERMISSION_OBJECT, PROJECT_PERMISSION_OBJECT,
projectRoleFormSchema, projectRoleFormSchema,
ProjectTypePermissionSubjects,
rolePermission2Form rolePermission2Form
} from "@app/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils"; } from "@app/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils";
import { renderConditionalComponents } from "@app/pages/project/RoleDetailsBySlugPage/components/RolePermissionsSection"; import { renderConditionalComponents } from "@app/pages/project/RoleDetailsBySlugPage/components/RolePermissionsSection";
@@ -87,6 +83,8 @@ export const MembershipProjectAdditionalPrivilegeModifySection = ({
projectMembershipId, projectMembershipId,
isDisabled isDisabled
}: Props) => { }: Props) => {
const { popUp, handlePopUpToggle } = usePopUp(["addPolicy"] as const);
const isCreate = !privilegeId; const isCreate = !privilegeId;
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const projectId = currentWorkspace?.id || ""; const projectId = currentWorkspace?.id || "";
@@ -167,30 +165,6 @@ export const MembershipProjectAdditionalPrivilegeModifySection = ({
} }
}; };
const onNewPolicy = (selectedSubject: ProjectPermissionSub) => {
const rootPolicyValue = form.getValues(`permissions.${selectedSubject}`);
if (rootPolicyValue && isConditionalSubjects(selectedSubject)) {
form.setValue(
`permissions.${selectedSubject}`,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore-error akhilmhdh: this is because of ts collision with both
[...rootPolicyValue, {}],
{ shouldDirty: true, shouldTouch: true }
);
} else {
form.setValue(
`permissions.${selectedSubject}`,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore-error akhilmhdh: this is because of ts collision with both
[{}],
{
shouldDirty: true,
shouldTouch: true
}
);
}
};
const privilegeTemporaryAccess = form.watch("temporaryAccess"); const privilegeTemporaryAccess = form.watch("temporaryAccess");
const isTemporary = privilegeTemporaryAccess?.isTemporary; const isTemporary = privilegeTemporaryAccess?.isTemporary;
const isExpired = const isExpired =
@@ -246,52 +220,25 @@ export const MembershipProjectAdditionalPrivilegeModifySection = ({
<Button <Button
variant="outline_bg" variant="outline_bg"
type="submit" type="submit"
className={twMerge("h-10 rounded-r-none", isDirty && "bg-primary text-black")} className={twMerge(
"h-10 rounded-r-none border border-primary",
isDirty && "bg-primary text-black"
)}
isDisabled={isSubmitting || !isDirty || isDisabled} isDisabled={isSubmitting || !isDirty || isDisabled}
isLoading={isSubmitting} isLoading={isSubmitting}
leftIcon={<FontAwesomeIcon icon={faSave} />} leftIcon={<FontAwesomeIcon icon={faSave} />}
> >
Save Save
</Button> </Button>
<DropdownMenu> <Button
<DropdownMenuTrigger> isDisabled={isDisabled}
<Button className="h-10 rounded-l-none"
isDisabled={isDisabled} variant="outline_bg"
className="h-10 rounded-l-none" leftIcon={<FontAwesomeIcon icon={faPlus} />}
variant="outline_bg" onClick={() => handlePopUpToggle("addPolicy")}
leftIcon={<FontAwesomeIcon icon={faPlus} />} >
> Add Policies
New policy </Button>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="thin-scrollbar max-h-96" align="end">
{Object.keys(PROJECT_PERMISSION_OBJECT)
.filter(
(subject) =>
ProjectTypePermissionSubjects[currentWorkspace.type][
subject as ProjectPermissionSub
]
)
.sort((a, b) =>
PROJECT_PERMISSION_OBJECT[a as keyof typeof PROJECT_PERMISSION_OBJECT].title
.toLowerCase()
.localeCompare(
PROJECT_PERMISSION_OBJECT[
b as keyof typeof PROJECT_PERMISSION_OBJECT
].title.toLowerCase()
)
)
.map((subject) => (
<DropdownMenuItem
key={`permission-create-${subject}`}
className="py-3"
onClick={() => onNewPolicy(subject as ProjectPermissionSub)}
>
{PROJECT_PERMISSION_OBJECT[subject as ProjectPermissionSub].title}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</div> </div>
</div> </div>
</div> </div>
@@ -430,6 +377,10 @@ export const MembershipProjectAdditionalPrivilegeModifySection = ({
</GeneralPermissionPolicies> </GeneralPermissionPolicies>
))} ))}
</div> </div>
<PolicySelectionModal
isOpen={popUp.addPolicy.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("addPolicy", isOpen)}
/>
</FormProvider> </FormProvider>
</form> </form>
); );
@@ -1,136 +0,0 @@
import { Controller, useForm, useFormContext } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import {
Button,
Checkbox,
FormControl,
FormLabel,
ModalClose,
Select,
SelectItem
} from "@app/components/v2";
import { ProjectPermissionSub } from "@app/context";
import { useGetProjectTypeFromRoute } from "@app/hooks";
import { ProjectType } from "@app/hooks/api/workspace/types";
import {
isConditionalSubjects,
PROJECT_PERMISSION_OBJECT,
projectRoleFormSchema,
ProjectTypePermissionSubjects,
TFormSchema
} from "./ProjectRoleModifySection.utils";
type Props = {
onClose: () => void;
};
export const NewPermissionRule = ({ onClose }: Props) => {
const rootForm = useFormContext<TFormSchema>();
const form = useForm<{
type: ProjectPermissionSub;
permissions: NonNullable<TFormSchema["permissions"]>;
}>({
resolver: zodResolver(
projectRoleFormSchema
.pick({ permissions: true })
.extend({ type: z.nativeEnum(ProjectPermissionSub) })
),
defaultValues: {
type: ProjectPermissionSub.Project
}
});
const selectedSubject = form.watch("type");
const projectType = useGetProjectTypeFromRoute();
return (
<div>
<Controller
control={form.control}
name="type"
defaultValue={ProjectPermissionSub.Project}
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl label="Subject" errorText={error?.message} isError={Boolean(error)}>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
>
{Object.keys(PROJECT_PERMISSION_OBJECT)
.filter(
(subject) =>
ProjectTypePermissionSubjects[projectType ?? ProjectType.SecretManager][
subject as ProjectPermissionSub
]
)
.map((subject) => (
<SelectItem value={subject} key={`permission-create-${subject}`}>
{PROJECT_PERMISSION_OBJECT[subject as ProjectPermissionSub].title}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<FormLabel label="Actions" className="my-2" />
<div className="flex flex-grow flex-wrap justify-start gap-8">
{PROJECT_PERMISSION_OBJECT?.[selectedSubject]?.actions?.map(({ label, value }) => (
<Controller
key={`create-permission-${selectedSubject}-${label}`}
name={`permissions.${selectedSubject}.0.${value as any}` as any}
control={form.control}
defaultValue={false}
render={({ field }) => (
<div className="flex items-center justify-center">
<Checkbox
isChecked={field.value}
onCheckedChange={field.onChange}
id={`new-permissions.${selectedSubject}.0.${String(value)}`}
>
{label}
</Checkbox>
</div>
)}
/>
))}
</div>
<div className="mt-8 flex space-x-4">
<Button
onClick={form.handleSubmit((el) => {
const rootPolicyValue = rootForm.getValues("permissions")?.[el.type];
if (rootPolicyValue && isConditionalSubjects(selectedSubject)) {
rootForm.setValue(
`permissions.${el.type}`,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore-error akhilmhdh: this is because of ts collision with both
[...rootPolicyValue, ...(el?.permissions[el.type] || [])],
{ shouldDirty: true, shouldTouch: true }
);
} else {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore-error akhilmhdh: this is because of ts collision with both
rootForm.setValue(`permissions.${el.type}`, el?.permissions?.[el.type], {
shouldDirty: true,
shouldTouch: true
});
}
onClose();
})}
>
Create
</Button>
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</ModalClose>
</div>
</div>
);
};
@@ -0,0 +1,214 @@
import { useState } from "react";
import { Controller, useForm, useFormContext } from "react-hook-form";
import { faCheck, faSearch, faXmark, faXmarkCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
Button,
EmptyState,
IconButton,
Input,
Modal,
ModalClose,
ModalContent,
Table,
TableContainer,
TBody,
Td,
Tooltip,
Tr
} from "@app/components/v2";
import { ProjectPermissionSub } from "@app/context";
import { useGetProjectTypeFromRoute } from "@app/hooks";
import { ProjectType } from "@app/hooks/api/workspace/types";
import {
isConditionalSubjects,
PROJECT_PERMISSION_OBJECT,
ProjectTypePermissionSubjects,
TFormSchema
} from "./ProjectRoleModifySection.utils";
type Props = {
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
};
type ContentProps = {
onClose: () => void;
};
type TForm = { permissions: Record<ProjectPermissionSub, boolean> };
const Content = ({ onClose }: ContentProps) => {
const rootForm = useFormContext<TFormSchema>();
const [search, setSearch] = useState("");
const {
control,
handleSubmit,
formState: { isDirty },
setValue,
reset
} = useForm<TForm>({
defaultValues: {
permissions: Object.fromEntries(
Object.values(ProjectPermissionSub).map((subject) => [subject, false])
)
}
});
const projectType = useGetProjectTypeFromRoute();
const filteredPolicies = Object.entries(PROJECT_PERMISSION_OBJECT)
.filter(
([subject, { title }]) =>
ProjectTypePermissionSubjects[projectType ?? ProjectType.SecretManager][
subject as ProjectPermissionSub
] && (search ? title.toLowerCase().includes(search.toLowerCase()) : true)
)
.sort((a, b) => a[1].title.localeCompare(b[1].title))
.map(([subject]) => subject);
const onSubmit = () =>
handleSubmit((form) => {
Object.entries(form.permissions).forEach(([subject, add]) => {
if (!add) return;
const type = subject as ProjectPermissionSub;
const rootPolicyValue = rootForm.getValues("permissions")?.[type];
if (rootPolicyValue && isConditionalSubjects(subject as ProjectPermissionSub)) {
rootForm.setValue(
`permissions.${type}`,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore-error akhilmhdh: this is because of ts collision with both
[...rootPolicyValue, {}],
{ shouldDirty: true, shouldTouch: true }
);
} else {
rootForm.setValue(
`permissions.${type}`,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore-error akhilmhdh: this is because of ts collision with both
[{}],
{
shouldDirty: true,
shouldTouch: true
}
);
}
});
onClose();
})();
return (
<>
<Input
placeholder="Search policies..."
value={search}
onChange={(e) => setSearch(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faSearch} />}
rightIcon={
search ? (
<IconButton ariaLabel="Clear search" variant="plain" onClick={() => setSearch("")}>
<FontAwesomeIcon icon={faXmark} />
</IconButton>
) : null
}
/>
<TableContainer className="thin-scrollbar mt-4 max-h-[28rem]">
<div className="sticky top-0 z-30 flex justify-between border-b border-b-mineshaft-600 bg-mineshaft-800 py-3 pl-5 pr-4 font-inter text-sm font-medium text-bunker-300">
<span>Resource</span>
<div className="flex gap-2">
<Button
variant="plain"
className="p-0 text-mineshaft-400"
size="xs"
colorSchema="secondary"
onClick={() => {
setValue(
"permissions",
Object.fromEntries(
Object.values(ProjectPermissionSub).map((subject) => [subject, true])
) as TForm["permissions"],
{ shouldDirty: true }
);
}}
>
Select All
</Button>
<Tooltip content="Clear selection">
<IconButton
ariaLabel="Clear selection"
onClick={() => reset()}
variant="plain"
size="xs"
className={`text-mineshaft-400 ${!isDirty ? "pointer-events-none opacity-50" : ""} hover:text-red`}
isDisabled={!isDirty}
>
<FontAwesomeIcon icon={faXmarkCircle} />
</IconButton>
</Tooltip>
</div>
</div>
<Table>
<TBody>
{filteredPolicies.map((subject) => (
<Controller
control={control}
key={`permission-add-${subject}`}
render={({ field: { value, onChange } }) => (
<Tr
className={`${value ? "bg-mineshaft-600/30" : ""} cursor-pointer hover:bg-mineshaft-700`}
onClick={() => onChange(!value)}
>
<Td className={`${value ? "text-mineshaft-100" : "text-mineshaft-300"} w-full`}>
{PROJECT_PERMISSION_OBJECT[subject as ProjectPermissionSub].title}
</Td>
<Td>
{value ? <FontAwesomeIcon className="text-green" icon={faCheck} /> : null}
</Td>
</Tr>
)}
name={`permissions.${subject as ProjectPermissionSub}`}
/>
))}
</TBody>
</Table>
{!filteredPolicies.length && (
<EmptyState
iconSize="2x"
icon={faSearch}
className="!pb-4 !pt-8"
title="No polices match search"
/>
)}
</TableContainer>
<div className="mt-8 flex space-x-4">
<Button isDisabled={!isDirty} onClick={onSubmit}>
Add Policies
</Button>
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</ModalClose>
</div>
</>
);
};
export const PolicySelectionModal = ({ isOpen, onOpenChange }: Props) => {
return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent
title="Add Policies"
subTitle="Select one or more policies to add to this role."
className="max-w-3xl"
>
<Content onClose={() => onOpenChange(false)} />
</ModalContent>
</Modal>
);
};
@@ -913,7 +913,7 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
actions: [{ label: "Modify", value: "edit" }] actions: [{ label: "Modify", value: "edit" }]
}, },
[ProjectPermissionSub.Integrations]: { [ProjectPermissionSub.Integrations]: {
title: "Integrations", title: "Native Integrations",
actions: [ actions: [
{ label: "Read", value: "read" }, { label: "Read", value: "read" },
{ label: "Create", value: "create" }, { label: "Create", value: "create" },
@@ -8,19 +8,15 @@ import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { AccessTree } from "@app/components/permissions"; import { AccessTree } from "@app/components/permissions";
import { import { Button } from "@app/components/v2";
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger
} from "@app/components/v2";
import { ProjectPermissionSub, useWorkspace } from "@app/context"; import { ProjectPermissionSub, useWorkspace } from "@app/context";
import { ProjectPermissionSet } from "@app/context/ProjectPermissionContext"; import { ProjectPermissionSet } from "@app/context/ProjectPermissionContext";
import { evaluatePermissionsAbility } from "@app/helpers/permissions"; import { evaluatePermissionsAbility } from "@app/helpers/permissions";
import { usePopUp } from "@app/hooks";
import { useGetProjectRoleBySlug, useUpdateProjectRole } from "@app/hooks/api"; import { useGetProjectRoleBySlug, useUpdateProjectRole } from "@app/hooks/api";
import { ProjectMembershipRole } from "@app/hooks/api/roles/types"; import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
import { ProjectType } from "@app/hooks/api/workspace/types"; import { ProjectType } from "@app/hooks/api/workspace/types";
import { PolicySelectionModal } from "@app/pages/project/RoleDetailsBySlugPage/components/PolicySelectionModal";
import { DynamicSecretPermissionConditions } from "./DynamicSecretPermissionConditions"; import { DynamicSecretPermissionConditions } from "./DynamicSecretPermissionConditions";
import { GeneralPermissionConditions } from "./GeneralPermissionConditions"; import { GeneralPermissionConditions } from "./GeneralPermissionConditions";
@@ -90,6 +86,8 @@ export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => {
const { mutateAsync: updateRole } = useUpdateProjectRole(); const { mutateAsync: updateRole } = useUpdateProjectRole();
const { popUp, handlePopUpToggle } = usePopUp(["addPolicy"] as const);
const onSubmit = async (el: TFormSchema) => { const onSubmit = async (el: TFormSchema) => {
try { try {
if (!projectId || !role?.id) return; if (!projectId || !role?.id) return;
@@ -110,30 +108,6 @@ export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => {
(role?.slug ?? "") as ProjectMembershipRole (role?.slug ?? "") as ProjectMembershipRole
); );
const onNewPolicy = (selectedSubject: ProjectPermissionSub) => {
const rootPolicyValue = form.getValues(`permissions.${selectedSubject}`);
if (rootPolicyValue && isConditionalSubjects(selectedSubject)) {
form.setValue(
`permissions.${selectedSubject}`,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore-error akhilmhdh: this is because of ts collision with both
[...rootPolicyValue, {}],
{ shouldDirty: true, shouldTouch: true }
);
} else {
form.setValue(
`permissions.${selectedSubject}`,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore-error akhilmhdh: this is because of ts collision with both
[{}],
{
shouldDirty: true,
shouldTouch: true
}
);
}
};
const permissions = form.watch("permissions"); const permissions = form.watch("permissions");
const formattedPermissions = useMemo( const formattedPermissions = useMemo(
@@ -176,54 +150,25 @@ export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => {
<Button <Button
variant="outline_bg" variant="outline_bg"
type="submit" type="submit"
className={twMerge("h-10 rounded-r-none", isDirty && "bg-primary text-black")} className={twMerge(
"h-10 rounded-r-none border border-primary",
isDirty && "bg-primary text-black"
)}
isDisabled={isSubmitting || !isDirty} isDisabled={isSubmitting || !isDirty}
isLoading={isSubmitting} isLoading={isSubmitting}
leftIcon={<FontAwesomeIcon icon={faSave} />} leftIcon={<FontAwesomeIcon icon={faSave} />}
> >
Save Save
</Button> </Button>
<DropdownMenu> <Button
<DropdownMenuTrigger> isDisabled={isDisabled}
<Button className="h-10 rounded-l-none"
isDisabled={isDisabled} variant="outline_bg"
className="h-10 rounded-l-none" leftIcon={<FontAwesomeIcon icon={faPlus} />}
variant="outline_bg" onClick={() => handlePopUpToggle("addPolicy")}
leftIcon={<FontAwesomeIcon icon={faPlus} />} >
> Add Policy
New policy </Button>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="thin-scrollbar max-h-96" align="end">
{Object.keys(PROJECT_PERMISSION_OBJECT)
.filter(
(subject) =>
ProjectTypePermissionSubjects[currentWorkspace.type][
subject as ProjectPermissionSub
]
)
.sort((a, b) =>
PROJECT_PERMISSION_OBJECT[
a as keyof typeof PROJECT_PERMISSION_OBJECT
].title
.toLowerCase()
.localeCompare(
PROJECT_PERMISSION_OBJECT[
b as keyof typeof PROJECT_PERMISSION_OBJECT
].title.toLowerCase()
)
)
.map((subject) => (
<DropdownMenuItem
key={`permission-create-${subject}`}
className="py-3"
onClick={() => onNewPolicy(subject as ProjectPermissionSub)}
>
{PROJECT_PERMISSION_OBJECT[subject as ProjectPermissionSub].title}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</div> </div>
</> </>
)} )}
@@ -245,6 +190,10 @@ export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => {
</GeneralPermissionPolicies> </GeneralPermissionPolicies>
))} ))}
</div> </div>
<PolicySelectionModal
isOpen={popUp.addPolicy.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("addPolicy", isOpen)}
/>
</FormProvider> </FormProvider>
</form> </form>
</div> </div>