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

View File

@@ -51,7 +51,7 @@ export type ApprovalRequestStep = {
export type PamAccessRequestData = {
accountPath: string;
accessDuration: number;
accessDuration: string;
};
export type TApprovalRequest = {

View File

@@ -80,7 +80,6 @@ export const ApprovalStepsSection = ({ request }: Props) => {
<div className="space-y-6">
{request.steps.map((step, index) => (
<div key={step.id} className="relative">
{/* Connector line */}
{index < request.steps.length - 1 && (
<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">
{step.approvers.map((approver) => (
<Badge variant="neutral">
{approver.type === ApproverType.Group ? <User /> : <Users />}
{approver.type === ApproverType.User ? <User /> : <Users />}
{getApproverLabel(approver.id, approver.type)}
</Badge>
))}

View File

@@ -98,7 +98,9 @@ export const PoliciesTable = ({ handlePopUpOpen }: Props) => {
<Tr>
<Th className="w-10" />
<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 className="w-5" />
</Tr>
@@ -107,7 +109,7 @@ export const PoliciesTable = ({ handlePopUpOpen }: Props) => {
{isPoliciesLoading && <TableSkeleton columns={5} innerKey="approval-policies" />}
{!isPoliciesLoading && policies.length === 0 && (
<Tr>
<Td colSpan={5}>
<Td colSpan={7}>
<EmptyState title="No policies found" icon={faUsers} />
</Td>
</Tr>
@@ -143,6 +145,8 @@ export const PoliciesTable = ({ handlePopUpOpen }: Props) => {
</Td>
<Td>{policy.name}</Td>
<Td>{maxTtl}</Td>
<Td>{policy.constraints.constraints.accessDuration.min}</Td>
<Td>{policy.constraints.constraints.accessDuration.max}</Td>
<Td>
{conditionsCount} condition{conditionsCount !== 1 ? "s" : ""}
</Td>
@@ -197,11 +201,11 @@ export const PoliciesTable = ({ handlePopUpOpen }: Props) => {
</Tr>
{isExpanded && (
<Tr className="bg-mineshaft-800">
<Td colSpan={5} className="p-0">
<div className="flex max-h-80 w-full gap-2 overflow-auto overflow-x-hidden">
<div className="flex-1 p-4">
<Td colSpan={7} className="p-0">
<div className="flex max-h-80 w-full gap-2 gap-4 overflow-auto overflow-x-hidden p-4">
<div className="flex-1">
<div className="mb-2 text-sm font-medium text-mineshaft-300">
Approval Contraints
Approval Conditions
</div>
{policy.conditions.conditions.map((step, index) => (
<Fragment key={`${policy.id}--${index + 1}`}>
@@ -235,7 +239,7 @@ export const PoliciesTable = ({ handlePopUpOpen }: Props) => {
</Fragment>
))}
</div>
<div className="flex-2 p-4">
<div className="flex-2">
<div className="mb-2 text-sm font-medium text-mineshaft-300">
Approval Sequence
</div>

View File

@@ -26,8 +26,8 @@ type Props = {
};
const FORM_STEPS: { name: string; key: string; fields: (keyof TPolicyForm)[] }[] = [
{ name: "Details", key: "details", fields: ["name", "maxRequestTtl"] },
{ name: "Constraints", key: "constraints", fields: ["conditions", "constraints"] },
{ name: "Details", key: "details", fields: ["name", "maxRequestTtl", "constraints"] },
{ name: "Conditions", key: "conditions", fields: ["conditions"] },
{ name: "Approvals", key: "approvals", fields: ["steps"] },
{ name: "Review", key: "review", fields: [] }
];

View File

@@ -66,7 +66,7 @@ export const PolicyConstraintsStep = () => {
label="Account Paths"
isError={Boolean(error)}
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
value={pathField.value.join(", ")}

View File

@@ -31,7 +31,7 @@ export const PolicyDetailsStep = () => {
<FormControl
isError={Boolean(error)}
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."
>
<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="mb-3">
<p className="pb-0.5 text-sm font-medium text-mineshaft-200">
Request Duration Constraints
PAM Account Access Duration TTL
</p>
<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>
</div>
<div className="grid grid-cols-2 gap-4">

View File

@@ -49,13 +49,15 @@ export const PolicyReviewStep = () => {
</div>
<div className="space-y-2">
<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 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 className="space-y-2">
<ReviewField label="Minimum Duration" value={constraints.accessDuration.min} />

View File

@@ -8,6 +8,7 @@ import {
faFilter,
faFolderPlus,
faMagnifyingGlass,
faPen,
faPlus,
faSearch
} from "@fortawesome/free-solid-svg-icons";
@@ -67,6 +68,7 @@ import { PamAddFolderModal } from "./PamAddFolderModal";
import { PamDeleteAccountModal } from "./PamDeleteAccountModal";
import { PamDeleteFolderModal } from "./PamDeleteFolderModal";
import { PamFolderRow } from "./PamFolderRow";
import { PamRequestAccountAccessModal } from "./PamRequestAccountAccessModal";
import { PamUpdateAccountModal } from "./PamUpdateAccountModal";
import { PamUpdateFolderModal } from "./PamUpdateFolderModal";
import { useAccessAwsIamAccount } from "./useAccessAwsIamAccount";
@@ -90,6 +92,7 @@ export const PamAccountsTable = ({ projectId }: Props) => {
"deleteFolder",
"addAccount",
"accessAccount",
"requestAccount",
"updateAccount",
"deleteAccount"
] as const);
@@ -323,6 +326,14 @@ export const PamAccountsTable = ({ projectId }: Props) => {
)}
</DropdownMenuContent>
</DropdownMenu>
<Button
variant="outline_bg"
leftIcon={<FontAwesomeIcon icon={faPen} />}
onClick={() => handlePopUpOpen("requestAccount")}
className="h-10 transition-colors"
>
Request Account
</Button>
<ProjectPermissionCan
I={ProjectPermissionPamAccountActions.Create}
a={ProjectPermissionSub.PamAccounts}
@@ -490,6 +501,10 @@ export const PamAccountsTable = ({ projectId }: Props) => {
}
projectId={projectId}
/>
<PamRequestAccountAccessModal
isOpen={popUp.requestAccount.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("requestAccount", isOpen)}
/>
<PamDeleteAccountModal
isOpen={popUp.deleteAccount.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("deleteAccount", isOpen)}

View File

@@ -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>
);
};