feat: updated frontend for review changes

This commit is contained in:
=
2024-12-05 14:08:08 +05:30
parent bcd778457d
commit 882f6b22f5
6 changed files with 31 additions and 416 deletions

View File

@@ -1,4 +1,5 @@
import { Controller, FormProvider, useForm } from "react-hook-form";
import { subject } from "@casl/ability";
import {
faCaretDown,
faChevronLeft,
@@ -28,7 +29,8 @@ import {
PopoverContent,
PopoverTrigger,
Tag,
Tooltip} from "@app/components/v2";
Tooltip
} from "@app/components/v2";
import {
ProjectPermissionActions,
ProjectPermissionSub,
@@ -48,7 +50,8 @@ import {
isConditionalSubjects,
PROJECT_PERMISSION_OBJECT,
projectRoleFormSchema,
rolePermission2Form} from "@app/views/Project/RolePage/components/RolePermissionsSection/ProjectRoleModifySection.utils";
rolePermission2Form
} from "@app/views/Project/RolePage/components/RolePermissionsSection/ProjectRoleModifySection.utils";
import { renderConditionalComponents } from "@app/views/Project/RolePage/components/RolePermissionsSection/RolePermissionsSection";
type Props = {
@@ -95,7 +98,7 @@ export const IdentityProjectAdditionalPrivilegeModifySection = ({
const { permission } = useProjectPermission();
const isIdentityEditDisabled = permission.cannot(
ProjectPermissionActions.Edit,
ProjectPermissionSub.Identity
subject(ProjectPermissionSub.Identity, { identityId })
);
const form = useForm<TFormSchema>({
@@ -275,13 +278,13 @@ export const IdentityProjectAdditionalPrivilegeModifySection = ({
].title.toLowerCase()
)
)
.map((subject) => (
.map((permissionSubject) => (
<DropdownMenuItem
key={`permission-create-${subject}`}
key={`permission-create-${permissionSubject}`}
className="py-3"
onClick={() => onNewPolicy(subject as ProjectPermissionSub)}
onClick={() => onNewPolicy(permissionSubject as ProjectPermissionSub)}
>
{PROJECT_PERMISSION_OBJECT[subject as ProjectPermissionSub].title}
{PROJECT_PERMISSION_OBJECT[permissionSubject as ProjectPermissionSub].title}
</DropdownMenuItem>
))}
</DropdownMenuContent>
@@ -412,17 +415,19 @@ export const IdentityProjectAdditionalPrivilegeModifySection = ({
<div className="p-4">
<div className="mb-2 text-lg">Policies</div>
{(isCreate || !isLoading) && <PermissionEmptyState />}
{(Object.keys(PROJECT_PERMISSION_OBJECT) as ProjectPermissionSub[]).map((subject) => (
<GeneralPermissionPolicies
subject={subject}
actions={PROJECT_PERMISSION_OBJECT[subject].actions}
title={PROJECT_PERMISSION_OBJECT[subject].title}
key={`project-permission-${subject}`}
isDisabled={isDisabled}
>
{renderConditionalComponents(subject, isDisabled)}
</GeneralPermissionPolicies>
))}
{(Object.keys(PROJECT_PERMISSION_OBJECT) as ProjectPermissionSub[]).map(
(permissionSubject) => (
<GeneralPermissionPolicies
subject={permissionSubject}
actions={PROJECT_PERMISSION_OBJECT[permissionSubject].actions}
title={PROJECT_PERMISSION_OBJECT[permissionSubject].title}
key={`project-permission-${permissionSubject}`}
isDisabled={isDisabled}
>
{renderConditionalComponents(permissionSubject, isDisabled)}
</GeneralPermissionPolicies>
)
)}
</div>
</FormProvider>
</form>

View File

@@ -1,3 +1,4 @@
import { subject } from "@casl/ability";
import { faFolder, faPencil, faTrash } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { format, formatDistance } from "date-fns";
@@ -93,7 +94,9 @@ export const IdentityRoleDetailsSection = ({
<h3 className="text-lg font-semibold text-mineshaft-100">Project Roles</h3>
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.Identity}
a={subject(ProjectPermissionSub.Identity, {
identityId: identityMembershipDetails.identity.id
})}
renderTooltip
allowedLabel="Edit Role(s)"
>
@@ -175,7 +178,9 @@ export const IdentityRoleDetailsSection = ({
<div className="opacity-0 transition-opacity duration-300 group-hover:opacity-100">
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.Member}
a={subject(ProjectPermissionSub.Identity, {
identityId: identityMembershipDetails.identity.id
})}
renderTooltip
allowedLabel="Remove Role"
>

View File

@@ -1,354 +0,0 @@
/* eslint-disable no-nested-ternary */
import { Controller, useFieldArray, useForm } from "react-hook-form";
import { faCaretDown, faClock, faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { format, formatDistance } from "date-fns";
import ms from "ms";
import { twMerge } from "tailwind-merge";
import { z } from "zod";
import { TtlFormLabel } from "@app/components/features";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
Button,
FormControl,
IconButton,
Input,
Popover,
PopoverContent,
PopoverTrigger,
Select,
SelectItem,
Spinner,
Tag,
Tooltip
} from "@app/components/v2";
import {
ProjectPermissionActions,
ProjectPermissionSub,
useProjectPermission,
useSubscription,
useWorkspace
} from "@app/context";
import { useGetProjectRoles, useUpdateIdentityWorkspaceRole } from "@app/hooks/api";
import { IdentityMembership } from "@app/hooks/api/identities/types";
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
import { ProjectUserMembershipTemporaryMode } from "@app/hooks/api/workspace/types";
const roleFormSchema = z.object({
roles: z
.object({
slug: z.string(),
temporaryAccess: z.discriminatedUnion("isTemporary", [
z.object({
isTemporary: z.literal(true),
temporaryRange: z.string().min(1),
temporaryAccessStartTime: z.string().datetime(),
temporaryAccessEndTime: z.string().datetime().nullable().optional()
}),
z.object({
isTemporary: z.literal(false)
})
])
})
.array()
});
type TRoleForm = z.infer<typeof roleFormSchema>;
type Props = {
identityProjectMember: IdentityMembership;
onOpenUpgradeModal: (title: string) => void;
};
export const IdentityRbacSection = ({ identityProjectMember, onOpenUpgradeModal }: Props) => {
const { subscription } = useSubscription();
const { currentWorkspace } = useWorkspace();
const workspaceId = currentWorkspace?.id || "";
const { data: projectRoles, isLoading: isRolesLoading } = useGetProjectRoles(workspaceId);
const { permission } = useProjectPermission();
const isMemberEditDisabled = permission.cannot(
ProjectPermissionActions.Edit,
ProjectPermissionSub.Identity
);
const roleForm = useForm<TRoleForm>({
resolver: zodResolver(roleFormSchema),
values: {
roles: identityProjectMember?.roles?.map(({ customRoleSlug, role, ...dto }) => ({
slug: customRoleSlug || role,
temporaryAccess: dto.isTemporary
? {
isTemporary: true,
temporaryRange: dto.temporaryRange,
temporaryAccessEndTime: dto.temporaryAccessEndTime,
temporaryAccessStartTime: dto.temporaryAccessStartTime
}
: {
isTemporary: dto.isTemporary
}
}))
}
});
const selectedRoleList = useFieldArray({
name: "roles",
control: roleForm.control
});
const formRoleField = roleForm.watch("roles");
const updateMembershipRole = useUpdateIdentityWorkspaceRole();
const handleRoleUpdate = async (data: TRoleForm) => {
if (updateMembershipRole.isLoading) return;
const sanitizedRoles = data.roles.map((el) => {
const { isTemporary } = el.temporaryAccess;
if (!isTemporary) {
return { role: el.slug, isTemporary: false as const };
}
return {
role: el.slug,
isTemporary: true as const,
temporaryMode: ProjectUserMembershipTemporaryMode.Relative,
temporaryRange: el.temporaryAccess.temporaryRange,
temporaryAccessStartTime: el.temporaryAccess.temporaryAccessStartTime
};
});
const hasCustomRoleSelected = sanitizedRoles.some(
(el) => !Object.values(ProjectMembershipRole).includes(el.role as ProjectMembershipRole)
);
if (hasCustomRoleSelected && subscription && !subscription?.rbac) {
onOpenUpgradeModal(
"You can assign custom roles to members if you upgrade your Infisical plan."
);
return;
}
try {
await updateMembershipRole.mutateAsync({
workspaceId,
identityId: identityProjectMember.identity.id,
roles: sanitizedRoles
});
createNotification({ text: "Successfully updated roles", type: "success" });
roleForm.reset(undefined, { keepValues: true });
} catch (err) {
createNotification({ text: "Failed to update role", type: "error" });
}
};
if (isRolesLoading)
return (
<div className="flex w-full items-center justify-center p-8">
<Spinner />
</div>
);
return (
<div>
<div className="text-lg font-medium">Roles</div>
<p className="text-sm text-mineshaft-400">Select one of the pre-defined or custom roles.</p>
<div>
<form onSubmit={roleForm.handleSubmit(handleRoleUpdate)}>
<div className="mt-2 flex flex-col space-y-2">
{selectedRoleList.fields.map(({ id }, index) => {
const { temporaryAccess } = formRoleField[index];
const isTemporary = temporaryAccess?.isTemporary;
const isExpired =
temporaryAccess.isTemporary &&
new Date() > new Date(temporaryAccess.temporaryAccessEndTime || "");
return (
<div key={id} className="flex items-center space-x-2">
<Controller
control={roleForm.control}
name={`roles.${index}.slug`}
render={({ field: { onChange, ...field } }) => (
<Select
defaultValue={field.value}
{...field}
isDisabled={isMemberEditDisabled}
onValueChange={(e) => onChange(e)}
className="w-full bg-mineshaft-600"
>
{projectRoles?.map(({ name, slug, id: projectRoleId }) => (
<SelectItem value={slug} key={projectRoleId}>
{name}
</SelectItem>
))}
</Select>
)}
/>
<Popover>
<PopoverTrigger disabled={isMemberEditDisabled}>
<div>
<Tooltip
content={
temporaryAccess?.isTemporary
? isExpired
? "Timed Access Expired"
: `Until ${format(
new Date(temporaryAccess.temporaryAccessEndTime || ""),
"yyyy-MM-dd HH:mm:ss"
)}`
: "Non expiry access"
}
>
<Button
variant="outline_bg"
leftIcon={isTemporary ? <FontAwesomeIcon icon={faClock} /> : undefined}
rightIcon={<FontAwesomeIcon icon={faCaretDown} className="ml-2" />}
isDisabled={isMemberEditDisabled}
className={twMerge(
"border-none bg-mineshaft-600 py-2.5 text-xs capitalize hover:bg-mineshaft-500",
isTemporary && "text-primary",
isExpired && "text-red-600"
)}
>
{temporaryAccess?.isTemporary
? isExpired
? "Access Expired"
: formatDistance(
new Date(temporaryAccess.temporaryAccessEndTime || ""),
new Date()
)
: "Permanent"}
</Button>
</Tooltip>
</div>
</PopoverTrigger>
<PopoverContent
arrowClassName="fill-gray-600"
side="right"
sideOffset={12}
hideCloseBtn
className="border border-gray-600 pt-4"
>
<div className="flex flex-col space-y-4">
<div className="border-b border-b-gray-700 pb-2 text-sm text-mineshaft-300">
Configure timed access
</div>
{isExpired && <Tag colorSchema="red">Expired</Tag>}
<Controller
control={roleForm.control}
defaultValue="1h"
name={`roles.${index}.temporaryAccess.temporaryRange`}
render={({ field, fieldState: { error } }) => (
<FormControl
label={<TtlFormLabel label="Validity" />}
isError={Boolean(error?.message)}
errorText={error?.message}
>
<Input {...field} />
</FormControl>
)}
/>
<div className="flex items-center space-x-2">
<Button
size="xs"
onClick={() => {
const temporaryRange = roleForm.getValues(
`roles.${index}.temporaryAccess.temporaryRange`
);
if (!temporaryRange) {
roleForm.setError(
`roles.${index}.temporaryAccess.temporaryRange`,
{ type: "required", message: "Required" },
{ shouldFocus: true }
);
return;
}
roleForm.clearErrors(`roles.${index}.temporaryAccess.temporaryRange`);
roleForm.setValue(
`roles.${index}.temporaryAccess`,
{
isTemporary: true,
temporaryAccessStartTime: new Date().toISOString(),
temporaryRange,
temporaryAccessEndTime: new Date(
new Date().getTime() + ms(temporaryRange)
).toISOString()
},
{ shouldDirty: true }
);
}}
>
{temporaryAccess.isTemporary ? "Restart" : "Grant"}
</Button>
{temporaryAccess.isTemporary && (
<Button
size="xs"
variant="outline_bg"
colorSchema="danger"
onClick={() => {
roleForm.setValue(`roles.${index}.temporaryAccess`, {
isTemporary: false
});
}}
>
Revoke Access
</Button>
)}
</div>
</div>
</PopoverContent>
</Popover>
<IconButton
variant="outline_bg"
className="border border-mineshaft-500 bg-mineshaft-600 py-3 hover:border-red/70 hover:bg-red/20"
ariaLabel="delete-role"
isDisabled={isMemberEditDisabled || selectedRoleList.fields.length === 1}
onClick={() => {
if (selectedRoleList.fields.length > 1) {
selectedRoleList.remove(index);
}
}}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</div>
);
})}
</div>
<div className="mt-4 flex justify-between space-x-2">
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.Identity}
>
{(isAllowed) => (
<Button
variant="outline_bg"
isDisabled={!isAllowed}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() =>
selectedRoleList.append({
slug: ProjectMembershipRole.Member,
temporaryAccess: { isTemporary: false }
})
}
>
Add Role
</Button>
)}
</ProjectPermissionCan>
<Button
type="submit"
className={twMerge(
"transition-all",
"cursor-default opacity-0",
roleForm.formState.isDirty && "cursor-pointer opacity-100"
)}
isDisabled={!roleForm.formState.isDirty}
isLoading={roleForm.formState.isSubmitting}
>
Save Roles
</Button>
</div>
</form>
</div>
</div>
);
};

View File

@@ -1,40 +0,0 @@
import Link from "next/link";
import { Alert, AlertDescription } from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { IdentityMembership } from "@app/hooks/api/identities/types";
import { IdentityRbacSection } from "./IdentityRbacSection";
type Props = {
identityProjectMember: IdentityMembership;
onOpenUpgradeModal: (title: string) => void;
};
export const IdentityRoleForm = ({ identityProjectMember, onOpenUpgradeModal }: Props) => {
const { currentWorkspace } = useWorkspace();
return (
<div>
<IdentityRbacSection
identityProjectMember={identityProjectMember}
onOpenUpgradeModal={onOpenUpgradeModal}
/>
<Alert
title="Additional privileges have been moved and now offer full permission customization."
className="mt-4 border-primary/50 bg-primary/10"
>
<AlertDescription>
<Link
href={`/project/${currentWorkspace?.id || ""}/identities/${
identityProjectMember?.identity?.id
}`}
>
<span className="cursor-pointer text-primary underline underline-offset-2">
Click here to access them now
</span>
</Link>
</AlertDescription>
</Alert>
</div>
);
};

View File

@@ -1 +0,0 @@
export { IdentityRoleForm } from "./IdentityRoleForm";

View File

@@ -494,7 +494,7 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
title: "Machine Identity Management",
actions: [
{ label: "Read", value: "read" },
{ label: "Create", value: "create" },
{ label: "Add", value: "create" },
{ label: "Modify", value: "edit" },
{ label: "Remove", value: "delete" }
]