feat: improved ui based of feedback

This commit is contained in:
=
2025-12-09 01:51:24 +05:30
parent 10e9eebc25
commit 24d7bcf0b3
9 changed files with 225 additions and 18 deletions
@@ -51,7 +51,7 @@ export type ApprovalRequestStep = {
export type PamAccessRequestData = { export type PamAccessRequestData = {
accountPath: string; accountPath: string;
accessDuration: number; accessDuration: string;
}; };
export type TApprovalRequest = { export type TApprovalRequest = {
@@ -80,7 +80,6 @@ export const ApprovalStepsSection = ({ request }: Props) => {
<div className="space-y-6"> <div className="space-y-6">
{request.steps.map((step, index) => ( {request.steps.map((step, index) => (
<div key={step.id} className="relative"> <div key={step.id} className="relative">
{/* Connector line */}
{index < request.steps.length - 1 && ( {index < request.steps.length - 1 && (
<div className="absolute top-8 left-2 h-full w-0.5 bg-mineshaft-600" /> <div className="absolute top-8 left-2 h-full w-0.5 bg-mineshaft-600" />
)} )}
@@ -130,7 +129,7 @@ export const ApprovalStepsSection = ({ request }: Props) => {
<div className="flex flex-wrap gap-1.5"> <div className="flex flex-wrap gap-1.5">
{step.approvers.map((approver) => ( {step.approvers.map((approver) => (
<Badge variant="neutral"> <Badge variant="neutral">
{approver.type === ApproverType.Group ? <User /> : <Users />} {approver.type === ApproverType.User ? <User /> : <Users />}
{getApproverLabel(approver.id, approver.type)} {getApproverLabel(approver.id, approver.type)}
</Badge> </Badge>
))} ))}
@@ -98,7 +98,9 @@ export const PoliciesTable = ({ handlePopUpOpen }: Props) => {
<Tr> <Tr>
<Th className="w-10" /> <Th className="w-10" />
<Th>Policy Name</Th> <Th>Policy Name</Th>
<Th>Max Request TTL</Th> <Th>Max Approval Request TTL</Th>
<Th>Min Access Duration</Th>
<Th>Max Access Duration</Th>
<Th>Conditions</Th> <Th>Conditions</Th>
<Th className="w-5" /> <Th className="w-5" />
</Tr> </Tr>
@@ -107,7 +109,7 @@ export const PoliciesTable = ({ handlePopUpOpen }: Props) => {
{isPoliciesLoading && <TableSkeleton columns={5} innerKey="approval-policies" />} {isPoliciesLoading && <TableSkeleton columns={5} innerKey="approval-policies" />}
{!isPoliciesLoading && policies.length === 0 && ( {!isPoliciesLoading && policies.length === 0 && (
<Tr> <Tr>
<Td colSpan={5}> <Td colSpan={7}>
<EmptyState title="No policies found" icon={faUsers} /> <EmptyState title="No policies found" icon={faUsers} />
</Td> </Td>
</Tr> </Tr>
@@ -143,6 +145,8 @@ export const PoliciesTable = ({ handlePopUpOpen }: Props) => {
</Td> </Td>
<Td>{policy.name}</Td> <Td>{policy.name}</Td>
<Td>{maxTtl}</Td> <Td>{maxTtl}</Td>
<Td>{policy.constraints.constraints.accessDuration.min}</Td>
<Td>{policy.constraints.constraints.accessDuration.max}</Td>
<Td> <Td>
{conditionsCount} condition{conditionsCount !== 1 ? "s" : ""} {conditionsCount} condition{conditionsCount !== 1 ? "s" : ""}
</Td> </Td>
@@ -197,11 +201,11 @@ export const PoliciesTable = ({ handlePopUpOpen }: Props) => {
</Tr> </Tr>
{isExpanded && ( {isExpanded && (
<Tr className="bg-mineshaft-800"> <Tr className="bg-mineshaft-800">
<Td colSpan={5} className="p-0"> <Td colSpan={7} className="p-0">
<div className="flex max-h-80 w-full gap-2 overflow-auto overflow-x-hidden"> <div className="flex max-h-80 w-full gap-2 gap-4 overflow-auto overflow-x-hidden p-4">
<div className="flex-1 p-4"> <div className="flex-1">
<div className="mb-2 text-sm font-medium text-mineshaft-300"> <div className="mb-2 text-sm font-medium text-mineshaft-300">
Approval Contraints Approval Conditions
</div> </div>
{policy.conditions.conditions.map((step, index) => ( {policy.conditions.conditions.map((step, index) => (
<Fragment key={`${policy.id}--${index + 1}`}> <Fragment key={`${policy.id}--${index + 1}`}>
@@ -235,7 +239,7 @@ export const PoliciesTable = ({ handlePopUpOpen }: Props) => {
</Fragment> </Fragment>
))} ))}
</div> </div>
<div className="flex-2 p-4"> <div className="flex-2">
<div className="mb-2 text-sm font-medium text-mineshaft-300"> <div className="mb-2 text-sm font-medium text-mineshaft-300">
Approval Sequence Approval Sequence
</div> </div>
@@ -26,8 +26,8 @@ type Props = {
}; };
const FORM_STEPS: { name: string; key: string; fields: (keyof TPolicyForm)[] }[] = [ const FORM_STEPS: { name: string; key: string; fields: (keyof TPolicyForm)[] }[] = [
{ name: "Details", key: "details", fields: ["name", "maxRequestTtl"] }, { name: "Details", key: "details", fields: ["name", "maxRequestTtl", "constraints"] },
{ name: "Constraints", key: "constraints", fields: ["conditions", "constraints"] }, { name: "Conditions", key: "conditions", fields: ["conditions"] },
{ name: "Approvals", key: "approvals", fields: ["steps"] }, { name: "Approvals", key: "approvals", fields: ["steps"] },
{ name: "Review", key: "review", fields: [] } { name: "Review", key: "review", fields: [] }
]; ];
@@ -66,7 +66,7 @@ export const PolicyConstraintsStep = () => {
label="Account Paths" label="Account Paths"
isError={Boolean(error)} isError={Boolean(error)}
errorText={error?.message} errorText={error?.message}
helperText="Comma-separated account paths this condition applies to" helperText="Matches any of the comma-separated account paths this condition applies to"
> >
<Input <Input
value={pathField.value.join(", ")} value={pathField.value.join(", ")}
@@ -31,7 +31,7 @@ export const PolicyDetailsStep = () => {
<FormControl <FormControl
isError={Boolean(error)} isError={Boolean(error)}
errorText={error?.message} errorText={error?.message}
label={<TtlFormLabel label="Max Request TTL" />} label={<TtlFormLabel label="Max Approval Request TTL" />}
helperText="Maximum time-to-live for requests. Must be between 1 hour and 30 days. Leave empty for no limit." helperText="Maximum time-to-live for requests. Must be between 1 hour and 30 days. Leave empty for no limit."
> >
<Input {...field} value={field.value ?? ""} placeholder="1h" /> <Input {...field} value={field.value ?? ""} placeholder="1h" />
@@ -41,10 +41,10 @@ export const PolicyDetailsStep = () => {
<div className="border-t border-mineshaft-600 pt-4"> <div className="border-t border-mineshaft-600 pt-4">
<div className="mb-3"> <div className="mb-3">
<p className="pb-0.5 text-sm font-medium text-mineshaft-200"> <p className="pb-0.5 text-sm font-medium text-mineshaft-200">
Request Duration Constraints PAM Account Access Duration TTL
</p> </p>
<p className="text-xs text-mineshaft-400"> <p className="text-xs text-mineshaft-400">
Set minimum and maximum duration (in seconds) for access requests Set minimum and maximum duration (in seconds) for pam account access
</p> </p>
</div> </div>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
@@ -49,13 +49,15 @@ export const PolicyReviewStep = () => {
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<ReviewField label="Policy Name" value={name || "Not set"} /> <ReviewField label="Policy Name" value={name || "Not set"} />
<ReviewField label="Max Request TTL" value={maxRequestTtl || "No Limit"} /> <ReviewField label="Max Approval Request TTL" value={maxRequestTtl || "No Limit"} />
</div> </div>
</div> </div>
<div> <div>
<div className="mb-3 border-b border-mineshaft-600 pb-2"> <div className="mb-3 border-b border-mineshaft-600 pb-2">
<h3 className="text-sm font-medium text-mineshaft-200">Request Duration Constraints</h3> <h3 className="text-sm font-medium text-mineshaft-200">
PAM Account Access Duration TTL
</h3>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<ReviewField label="Minimum Duration" value={constraints.accessDuration.min} /> <ReviewField label="Minimum Duration" value={constraints.accessDuration.min} />
@@ -8,6 +8,7 @@ import {
faFilter, faFilter,
faFolderPlus, faFolderPlus,
faMagnifyingGlass, faMagnifyingGlass,
faPen,
faPlus, faPlus,
faSearch faSearch
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
@@ -67,6 +68,7 @@ import { PamAddFolderModal } from "./PamAddFolderModal";
import { PamDeleteAccountModal } from "./PamDeleteAccountModal"; import { PamDeleteAccountModal } from "./PamDeleteAccountModal";
import { PamDeleteFolderModal } from "./PamDeleteFolderModal"; import { PamDeleteFolderModal } from "./PamDeleteFolderModal";
import { PamFolderRow } from "./PamFolderRow"; import { PamFolderRow } from "./PamFolderRow";
import { PamRequestAccountAccessModal } from "./PamRequestAccountAccessModal";
import { PamUpdateAccountModal } from "./PamUpdateAccountModal"; import { PamUpdateAccountModal } from "./PamUpdateAccountModal";
import { PamUpdateFolderModal } from "./PamUpdateFolderModal"; import { PamUpdateFolderModal } from "./PamUpdateFolderModal";
import { useAccessAwsIamAccount } from "./useAccessAwsIamAccount"; import { useAccessAwsIamAccount } from "./useAccessAwsIamAccount";
@@ -90,6 +92,7 @@ export const PamAccountsTable = ({ projectId }: Props) => {
"deleteFolder", "deleteFolder",
"addAccount", "addAccount",
"accessAccount", "accessAccount",
"requestAccount",
"updateAccount", "updateAccount",
"deleteAccount" "deleteAccount"
] as const); ] as const);
@@ -323,6 +326,14 @@ export const PamAccountsTable = ({ projectId }: Props) => {
)} )}
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
<Button
variant="outline_bg"
leftIcon={<FontAwesomeIcon icon={faPen} />}
onClick={() => handlePopUpOpen("requestAccount")}
className="h-10 transition-colors"
>
Request Account
</Button>
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionPamAccountActions.Create} I={ProjectPermissionPamAccountActions.Create}
a={ProjectPermissionSub.PamAccounts} a={ProjectPermissionSub.PamAccounts}
@@ -490,6 +501,10 @@ export const PamAccountsTable = ({ projectId }: Props) => {
} }
projectId={projectId} projectId={projectId}
/> />
<PamRequestAccountAccessModal
isOpen={popUp.requestAccount.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("requestAccount", isOpen)}
/>
<PamDeleteAccountModal <PamDeleteAccountModal
isOpen={popUp.deleteAccount.isOpen} isOpen={popUp.deleteAccount.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("deleteAccount", isOpen)} onOpenChange={(isOpen) => handlePopUpToggle("deleteAccount", isOpen)}
@@ -0,0 +1,187 @@
import { useMemo } from "react";
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import ms from "ms";
import { z } from "zod";
import { TtlFormLabel } from "@app/components/features";
import { createNotification } from "@app/components/notifications";
import {
Button,
FormControl,
Input,
Modal,
ModalClose,
ModalContent,
TextArea
} from "@app/components/v2";
import { useProject } from "@app/context";
import { ApprovalPolicyType } from "@app/hooks/api/approvalPolicies";
import { useCreateApprovalRequest } from "@app/hooks/api/approvalRequests/mutations";
import { TPamAccount } from "@app/hooks/api/pam";
type Props = {
account?: TPamAccount;
accountPath?: string;
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
};
const formSchema = z.object({
accountPath: z.string().min(1, "Account path is required"),
accessDuration: z
.string()
.min(1, "Access duration is required")
.refine(
(value) => {
try {
const duration = ms(value);
return duration > 0;
} catch {
return false;
}
},
{ message: "Invalid duration format. Use formats like: 1h, 3d, 30m" }
),
justification: z.string().max(512).optional()
});
type FormData = z.infer<typeof formSchema>;
const Content = ({ onOpenChange, account, accountPath }: Props) => {
const { projectId } = useProject();
const { mutateAsync: createApprovalRequest, isPending: isSubmitting } =
useCreateApprovalRequest();
const fullAccountPath = useMemo(() => {
const accountName = account?.name ?? "";
if (accountPath) {
const path = accountPath.replace(/^\/+|\/+$/g, "");
return `${path}/${accountName}`;
}
return accountName;
}, [account, accountPath]);
const form = useForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: {
accountPath: fullAccountPath,
accessDuration: "4h",
justification: ""
}
});
const {
control,
handleSubmit,
formState: { isDirty }
} = form;
const onSubmit = async (formData: FormData) => {
try {
await createApprovalRequest({
policyType: ApprovalPolicyType.PamAccess,
projectId,
justification: formData.justification || null,
requestData: {
accountPath: formData.accountPath,
accessDuration: formData.accessDuration
}
});
createNotification({
text: "Access request submitted successfully",
type: "success"
});
onOpenChange(false);
} catch (error) {
console.error(error);
createNotification({
text: "Failed to submit access request",
type: "error"
});
}
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Controller
name="accountPath"
control={control}
render={({ field, fieldState: { error } }) => (
<FormControl
helperText="Account path including the account name. Supports glob patterns (e.g., /folder/**, /*/account-name)"
errorText={error?.message}
isError={Boolean(error?.message)}
label="Account Path"
>
<Input autoFocus placeholder="/folder/account-name" {...field} />
</FormControl>
)}
/>
<Controller
name="accessDuration"
control={control}
render={({ field, fieldState: { error } }) => (
<FormControl
label={<TtlFormLabel label="Access Duration" />}
helperText="Duration of access requested"
errorText={error?.message}
isError={Boolean(error?.message)}
>
<Input placeholder="4h" {...field} />
</FormControl>
)}
/>
<Controller
name="justification"
control={control}
render={({ field, fieldState: { error } }) => (
<FormControl
helperText="Provide a reason for requesting access"
errorText={error?.message}
isError={Boolean(error?.message)}
label="Justification"
isOptional
>
<TextArea placeholder="I need access to debug production issue..." {...field} />
</FormControl>
)}
/>
<div className="mt-6 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
colorSchema="secondary"
isLoading={isSubmitting}
isDisabled={isSubmitting || !isDirty}
>
Request Access
</Button>
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</ModalClose>
</div>
</form>
);
};
export const PamRequestAccountAccessModal = (props: Props) => {
const { isOpen, onOpenChange } = props;
return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent
className="max-w-2xl pb-2"
title="Request Account Access"
subTitle="Request access to this account path"
>
<Content {...props} />
</ModalContent>
</Modal>
);
};