mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 06:28:11 +00:00
Merge pull request #5009 from Infisical/ga-improvements
improvement(global-approvals): basic UI improvements
This commit is contained in:
@@ -119,8 +119,8 @@ const PageContent = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<div className="flex justify-center gap-4">
|
<div className="flex flex-col justify-center gap-4 lg:flex-row">
|
||||||
<div className="flex w-96 flex-col gap-4">
|
<div className="flex flex-col gap-4 lg:w-96">
|
||||||
<RequestDetailsSection request={request} />
|
<RequestDetailsSection request={request} />
|
||||||
<RequestActionsSection request={request} />
|
<RequestActionsSection request={request} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+1
-1
@@ -75,7 +75,7 @@ export const ApprovalStepsSection = ({ request }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<div className="flex w-full flex-col gap-3 rounded-lg border border-mineshaft-600 bg-mineshaft-900 px-4 py-3">
|
<div className="flex w-full flex-col gap-3 rounded-lg border border-mineshaft-600 bg-mineshaft-900 px-4 py-3">
|
||||||
<div className="mb-2 flex items-center justify-between border-b border-mineshaft-500 pb-2">
|
<div className="mb-2 flex items-center justify-between border-b border-mineshaft-500 pb-2">
|
||||||
<h3 className="text-lg font-medium text-mineshaft-100">Approval Workflow</h3>
|
<h3 className="text-lg font-medium text-mineshaft-100">Approval Sequence</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{request.steps.map((step, index) => (
|
{request.steps.map((step, index) => (
|
||||||
|
|||||||
+1
-1
@@ -127,7 +127,7 @@ export const RequestActionsSection = ({ request }: Props) => {
|
|||||||
Review
|
Review
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent align="start" title="Finish your review" className="w-96">
|
<PopoverContent align="start" title="Finish your review" className="w-96 pt-4">
|
||||||
<FormLabel label="Finish your review" />
|
<FormLabel label="Finish your review" />
|
||||||
<TextArea
|
<TextArea
|
||||||
value={comment}
|
value={comment}
|
||||||
|
|||||||
+1
-1
@@ -43,7 +43,7 @@ export const RequestDetailsSection = ({ request }: Props) => {
|
|||||||
</GenericFieldLabel>
|
</GenericFieldLabel>
|
||||||
<GenericFieldLabel label="Requester Email">{request.requesterEmail}</GenericFieldLabel>
|
<GenericFieldLabel label="Requester Email">{request.requesterEmail}</GenericFieldLabel>
|
||||||
<GenericFieldLabel label="Account Path">{accountPath}</GenericFieldLabel>
|
<GenericFieldLabel label="Account Path">{accountPath}</GenericFieldLabel>
|
||||||
<GenericFieldLabel label="Duration">{accessDuration}</GenericFieldLabel>
|
<GenericFieldLabel label="Access Duration">{accessDuration}</GenericFieldLabel>
|
||||||
{request.justification && (
|
{request.justification && (
|
||||||
<GenericFieldLabel label="Justification">
|
<GenericFieldLabel label="Justification">
|
||||||
<p className="rounded-sm bg-mineshaft-600 p-2 text-xs break-words">
|
<p className="rounded-sm bg-mineshaft-600 p-2 text-xs break-words">
|
||||||
|
|||||||
+8
-8
@@ -152,8 +152,11 @@ export const ApprovalRequestTab = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
<div className="mb-4 flex items-center justify-between">
|
<div className="mb-4">
|
||||||
<p className="text-xl font-medium text-mineshaft-100">Approval Requests</p>
|
<div className="flex items-center gap-x-2">
|
||||||
|
<p className="text-xl font-medium text-mineshaft-100">Access Requests</p>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-bunker-300">View pending access requests to PAM accounts</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
@@ -262,7 +265,7 @@ export const ApprovalRequestTab = () => {
|
|||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||||
placeholder="Search approval requests..."
|
placeholder="Search access requests..."
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<TableContainer className="mt-4">
|
<TableContainer className="mt-4">
|
||||||
@@ -271,7 +274,7 @@ export const ApprovalRequestTab = () => {
|
|||||||
<Tr>
|
<Tr>
|
||||||
<Th>Requester</Th>
|
<Th>Requester</Th>
|
||||||
<Th>Account Path</Th>
|
<Th>Account Path</Th>
|
||||||
<Th>Duration</Th>
|
<Th>Access Duration</Th>
|
||||||
<Th>Status</Th>
|
<Th>Status</Th>
|
||||||
<Th>Requested</Th>
|
<Th>Requested</Th>
|
||||||
</Tr>
|
</Tr>
|
||||||
@@ -286,10 +289,7 @@ export const ApprovalRequestTab = () => {
|
|||||||
return (
|
return (
|
||||||
<Tr
|
<Tr
|
||||||
key={request.id}
|
key={request.id}
|
||||||
className={twMerge(
|
className="cursor-pointer transition-colors hover:bg-mineshaft-700"
|
||||||
"cursor-pointer transition-colors hover:bg-mineshaft-700",
|
|
||||||
needsApproval && "bg-primary/5 hover:bg-primary/10"
|
|
||||||
)}
|
|
||||||
onClick={() => handleRowClick(request.id)}
|
onClick={() => handleRowClick(request.id)}
|
||||||
>
|
>
|
||||||
<Td>
|
<Td>
|
||||||
|
|||||||
@@ -38,10 +38,14 @@ export const PolicyTab = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
<div className="mb-4 flex items-center justify-between">
|
<div className="mb-4 flex items-center">
|
||||||
<div className="flex items-center gap-x-2">
|
<div className="flex-1">
|
||||||
<p className="text-xl font-medium text-mineshaft-100">Approval Policies</p>
|
<div className="flex items-center gap-x-2">
|
||||||
|
<p className="text-xl font-medium text-mineshaft-100">Access Policies</p>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-bunker-300">View and manage PAM access policies</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="outline_bg"
|
variant="outline_bg"
|
||||||
type="submit"
|
type="submit"
|
||||||
@@ -51,6 +55,7 @@ export const PolicyTab = () => {
|
|||||||
Create Policy
|
Create Policy
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<PoliciesTable handlePopUpOpen={handlePopUpOpen} />
|
<PoliciesTable handlePopUpOpen={handlePopUpOpen} />
|
||||||
<PolicyModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
<PolicyModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||||
<DeleteActionModal
|
<DeleteActionModal
|
||||||
|
|||||||
+2
-7
@@ -98,9 +98,7 @@ 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 Approval Request TTL</Th>
|
<Th>Max. Access Duration</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>
|
||||||
@@ -117,7 +115,6 @@ export const PoliciesTable = ({ handlePopUpOpen }: Props) => {
|
|||||||
{!isPoliciesLoading &&
|
{!isPoliciesLoading &&
|
||||||
policies.map((policy) => {
|
policies.map((policy) => {
|
||||||
const isExpanded = expandedRows.has(policy.id);
|
const isExpanded = expandedRows.has(policy.id);
|
||||||
const maxTtl = policy.maxRequestTtl ? policy.maxRequestTtl : "No limit";
|
|
||||||
const conditionsCount = policy.conditions.conditions.length;
|
const conditionsCount = policy.conditions.conditions.length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -144,8 +141,6 @@ export const PoliciesTable = ({ handlePopUpOpen }: Props) => {
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
</Td>
|
</Td>
|
||||||
<Td>{policy.name}</Td>
|
<Td>{policy.name}</Td>
|
||||||
<Td>{maxTtl}</Td>
|
|
||||||
<Td>{policy.constraints.constraints.accessDuration.min}</Td>
|
|
||||||
<Td>{policy.constraints.constraints.accessDuration.max}</Td>
|
<Td>{policy.constraints.constraints.accessDuration.max}</Td>
|
||||||
<Td>
|
<Td>
|
||||||
{conditionsCount} condition{conditionsCount !== 1 ? "s" : ""}
|
{conditionsCount} condition{conditionsCount !== 1 ? "s" : ""}
|
||||||
@@ -202,7 +197,7 @@ export const PoliciesTable = ({ handlePopUpOpen }: Props) => {
|
|||||||
{isExpanded && (
|
{isExpanded && (
|
||||||
<Tr className="bg-mineshaft-800">
|
<Tr className="bg-mineshaft-800">
|
||||||
<Td colSpan={7} className="p-0">
|
<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 max-h-80 w-full gap-4 overflow-auto overflow-x-hidden p-4">
|
||||||
<div className="flex-1">
|
<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 Conditions
|
Approval Conditions
|
||||||
|
|||||||
+11
-11
@@ -15,7 +15,6 @@ import {
|
|||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
import { PolicyApprovalSteps } from "./PolicySteps/PolicyApprovalSteps";
|
import { PolicyApprovalSteps } from "./PolicySteps/PolicyApprovalSteps";
|
||||||
import { PolicyConstraintsStep } from "./PolicySteps/PolicyConstraintsStep";
|
|
||||||
import { PolicyDetailsStep } from "./PolicySteps/PolicyDetailsStep";
|
import { PolicyDetailsStep } from "./PolicySteps/PolicyDetailsStep";
|
||||||
import { PolicyReviewStep } from "./PolicySteps/PolicyReviewStep";
|
import { PolicyReviewStep } from "./PolicySteps/PolicyReviewStep";
|
||||||
import { PolicyFormSchema, TPolicyForm } from "./PolicySchema";
|
import { PolicyFormSchema, TPolicyForm } from "./PolicySchema";
|
||||||
@@ -26,9 +25,12 @@ 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", "constraints"] },
|
{
|
||||||
{ name: "Conditions", key: "conditions", fields: ["conditions"] },
|
name: "Configuration",
|
||||||
{ name: "Approvals", key: "approvals", fields: ["steps"] },
|
key: "configuration",
|
||||||
|
fields: ["name", "constraints", "conditions"]
|
||||||
|
},
|
||||||
|
{ name: "Approval Sequence", key: "approvals", fields: ["steps"] },
|
||||||
{ name: "Review", key: "review", fields: [] }
|
{ name: "Review", key: "review", fields: [] }
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -61,8 +63,7 @@ export const PolicyModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
approvers: []
|
approvers: []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
}
|
||||||
mode: "onChange"
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const { handleSubmit, trigger, reset } = formMethods;
|
const { handleSubmit, trigger, reset } = formMethods;
|
||||||
@@ -184,7 +185,8 @@ export const PolicyModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<Modal isOpen={isOpen} onOpenChange={(open) => handlePopUpToggle("policy", open)}>
|
<Modal isOpen={isOpen} onOpenChange={(open) => handlePopUpToggle("policy", open)}>
|
||||||
<ModalContent
|
<ModalContent
|
||||||
title={policyData?.policyId ? "Edit Policy" : "Create Policy"}
|
title={policyData?.policyId ? "Edit Access Policy" : "Create Access Policy"}
|
||||||
|
subTitle="Configure a policy dictating account access with constraints"
|
||||||
className="max-w-3xl"
|
className="max-w-3xl"
|
||||||
>
|
>
|
||||||
<FormProvider {...formMethods}>
|
<FormProvider {...formMethods}>
|
||||||
@@ -215,9 +217,6 @@ export const PolicyModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
<Tab.Panel>
|
<Tab.Panel>
|
||||||
<PolicyDetailsStep />
|
<PolicyDetailsStep />
|
||||||
</Tab.Panel>
|
</Tab.Panel>
|
||||||
<Tab.Panel>
|
|
||||||
<PolicyConstraintsStep />
|
|
||||||
</Tab.Panel>
|
|
||||||
<Tab.Panel>
|
<Tab.Panel>
|
||||||
<PolicyApprovalSteps />
|
<PolicyApprovalSteps />
|
||||||
</Tab.Panel>
|
</Tab.Panel>
|
||||||
@@ -237,7 +236,8 @@ export const PolicyModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
isLoading={isCreating || isUpdating}
|
isLoading={isCreating || isUpdating}
|
||||||
isDisabled={isCreating || isUpdating}
|
isDisabled={isCreating || isUpdating}
|
||||||
>
|
>
|
||||||
{policyData?.policyId ? "Update" : "Create"} {isFinalStep ? "" : "Next"}
|
{isFinalStep && (policyData?.policyId ? "Update" : "Create")}
|
||||||
|
{!isFinalStep && "Next"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
+13
-2
@@ -28,7 +28,13 @@ export const PolicyFormSchema = z.object({
|
|||||||
).nullish(),
|
).nullish(),
|
||||||
conditions: z
|
conditions: z
|
||||||
.object({
|
.object({
|
||||||
accountPaths: z.array(z.string().min(1))
|
accountPaths: z
|
||||||
|
.string()
|
||||||
|
.array()
|
||||||
|
.min(1, "Must have at least one account path")
|
||||||
|
.refine((val) => val.every((path) => path.length > 0), {
|
||||||
|
message: "All account paths must be non-empty"
|
||||||
|
})
|
||||||
})
|
})
|
||||||
.array()
|
.array()
|
||||||
.min(1, "At least one condition is required"),
|
.min(1, "At least one condition is required"),
|
||||||
@@ -40,7 +46,12 @@ export const PolicyFormSchema = z.object({
|
|||||||
}),
|
}),
|
||||||
steps: z
|
steps: z
|
||||||
.object({
|
.object({
|
||||||
name: z.string().max(128).nullable().optional(),
|
name: z
|
||||||
|
.string()
|
||||||
|
.max(128)
|
||||||
|
.nullable()
|
||||||
|
.optional()
|
||||||
|
.transform((name) => name || null),
|
||||||
requiredApprovals: z.number().min(1).max(100),
|
requiredApprovals: z.number().min(1).max(100),
|
||||||
notifyApprovers: z.boolean().optional(),
|
notifyApprovers: z.boolean().optional(),
|
||||||
approvers: z
|
approvers: z
|
||||||
|
|||||||
+1
-1
@@ -78,7 +78,7 @@ export const PolicyApprovalSteps = () => {
|
|||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{stepFields.map((field, index) => (
|
{stepFields.map((field, index) => (
|
||||||
<div key={field.id} className="rounded border border-mineshaft-600 bg-mineshaft-800 p-4">
|
<div key={field.id} className="rounded border border-mineshaft-600 bg-mineshaft-700 p-4">
|
||||||
<div className="mb-3 flex items-center justify-between">
|
<div className="mb-3 flex items-center justify-between">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="flex h-6 w-6 items-center justify-center rounded-full bg-primary/20 text-xs font-semibold text-primary">
|
<span className="flex h-6 w-6 items-center justify-center rounded-full bg-primary/20 text-xs font-semibold text-primary">
|
||||||
|
|||||||
-102
@@ -1,102 +0,0 @@
|
|||||||
import { Controller, useFieldArray, useFormContext } from "react-hook-form";
|
|
||||||
import { faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
||||||
|
|
||||||
import { Button, FormControl, IconButton, Input } from "@app/components/v2";
|
|
||||||
|
|
||||||
import { TPolicyForm } from "../PolicySchema";
|
|
||||||
|
|
||||||
export const PolicyConstraintsStep = () => {
|
|
||||||
const { control } = useFormContext<TPolicyForm>();
|
|
||||||
|
|
||||||
const {
|
|
||||||
fields: conditionFields,
|
|
||||||
append: appendCondition,
|
|
||||||
remove: removeCondition
|
|
||||||
} = useFieldArray({
|
|
||||||
control,
|
|
||||||
name: "conditions"
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div>
|
|
||||||
<div className="mb-3 flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<p className="pb-0.5 text-sm font-medium text-mineshaft-200">Conditions</p>
|
|
||||||
<p className="text-xs text-mineshaft-400">
|
|
||||||
Define which resources and account paths this policy applies to
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline_bg"
|
|
||||||
size="xs"
|
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
|
||||||
onClick={() => appendCondition({ accountPaths: [] })}
|
|
||||||
>
|
|
||||||
Add Condition
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{conditionFields.map((field, index) => (
|
|
||||||
<div key={field.id}>
|
|
||||||
<div className="rounded border border-mineshaft-600 bg-mineshaft-800 p-4">
|
|
||||||
<div className="mb-3 flex items-center justify-between">
|
|
||||||
<span className="text-sm font-medium text-mineshaft-300">
|
|
||||||
Condition {index + 1}
|
|
||||||
</span>
|
|
||||||
{conditionFields.length > 1 && (
|
|
||||||
<IconButton
|
|
||||||
ariaLabel="Remove condition"
|
|
||||||
variant="plain"
|
|
||||||
size="xs"
|
|
||||||
onClick={() => removeCondition(index)}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faTrash} className="text-red-500" />
|
|
||||||
</IconButton>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="space-y-3">
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name={`conditions.${index}.accountPaths`}
|
|
||||||
render={({ field: pathField, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="Account Paths"
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
helperText="Matches any of the comma-separated account paths this condition applies to"
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
value={pathField.value.join(", ")}
|
|
||||||
onChange={(e) => {
|
|
||||||
const paths = e.target.value
|
|
||||||
.split(",")
|
|
||||||
.map((path) => path.trim())
|
|
||||||
.filter(Boolean);
|
|
||||||
pathField.onChange(paths);
|
|
||||||
}}
|
|
||||||
placeholder="e.g., /admin/**, /users/john, /**"
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{index < conditionFields.length - 1 && (
|
|
||||||
<div className="flex items-center">
|
|
||||||
<div className="flex flex-col items-center">
|
|
||||||
<div className="h-3 w-px bg-mineshaft-500" />
|
|
||||||
<span className="px-2 text-xs font-medium text-mineshaft-400">OR</span>
|
|
||||||
<div className="h-3 w-px bg-mineshaft-500" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
+54
-68
@@ -9,75 +9,61 @@ export const PolicyDetailsStep = () => {
|
|||||||
const { control } = useFormContext<TPolicyForm>();
|
const { control } = useFormContext<TPolicyForm>();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="flex flex-col gap-4">
|
||||||
<Controller
|
<div className="flex w-full gap-4">
|
||||||
control={control}
|
<Controller
|
||||||
name="name"
|
control={control}
|
||||||
render={({ field, fieldState: { error } }) => (
|
name="name"
|
||||||
<FormControl
|
render={({ field, fieldState: { error } }) => (
|
||||||
label="Policy Name"
|
<FormControl
|
||||||
isRequired
|
label="Policy Name"
|
||||||
isError={Boolean(error)}
|
className="mb-0 flex-1"
|
||||||
errorText={error?.message}
|
isRequired
|
||||||
>
|
isError={Boolean(error)}
|
||||||
<Input {...field} placeholder="Enter policy name" />
|
errorText={error?.message}
|
||||||
</FormControl>
|
>
|
||||||
)}
|
<Input {...field} placeholder="Enter policy name" />
|
||||||
/>
|
</FormControl>
|
||||||
<Controller
|
)}
|
||||||
control={control}
|
/>
|
||||||
name="maxRequestTtl"
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
<Controller
|
||||||
<FormControl
|
control={control}
|
||||||
isError={Boolean(error)}
|
name="constraints.accessDuration.max"
|
||||||
errorText={error?.message}
|
render={({ field, fieldState: { error } }) => (
|
||||||
label={<TtlFormLabel label="Max Approval Request TTL" />}
|
<FormControl
|
||||||
helperText="Maximum time-to-live for requests. Must be between 1 hour and 30 days. Leave empty for no limit."
|
label={<TtlFormLabel label="Max. Access Duration" />}
|
||||||
>
|
className="mb-0"
|
||||||
<Input {...field} value={field.value ?? ""} placeholder="1h" />
|
isError={Boolean(error)}
|
||||||
</FormControl>
|
errorText={error?.message}
|
||||||
)}
|
>
|
||||||
/>
|
<Input {...field} />
|
||||||
<div className="border-t border-mineshaft-600 pt-4">
|
</FormControl>
|
||||||
<div className="mb-3">
|
)}
|
||||||
<p className="pb-0.5 text-sm font-medium text-mineshaft-200">
|
/>
|
||||||
PAM Account Access Duration TTL
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-mineshaft-400">
|
|
||||||
Set minimum and maximum duration (in seconds) for pam account access
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="constraints.accessDuration.min"
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label={<TtlFormLabel label="Minimum TTL" />}
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
helperText="Must be between 30s and 7 days"
|
|
||||||
>
|
|
||||||
<Input {...field} />
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="constraints.accessDuration.max"
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label={<TtlFormLabel label="Maximum TTL" />}
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
helperText="Must be between 30s and 7 days"
|
|
||||||
>
|
|
||||||
<Input {...field} />
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="conditions.0.accountPaths"
|
||||||
|
render={({ field: pathField, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
isRequired
|
||||||
|
label="Account Paths"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
helperText="Policy matches any of these comma-separated account paths"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
value={pathField.value.join(",")}
|
||||||
|
onChange={(e) => {
|
||||||
|
pathField.onChange(e.target.value.split(",").map((path) => path.trim()));
|
||||||
|
}}
|
||||||
|
placeholder="e.g., /admin/**, /users/john, /**"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+14
-52
@@ -24,7 +24,7 @@ export const PolicyReviewStep = () => {
|
|||||||
const { data: members = [] } = useGetWorkspaceUsers(projectId);
|
const { data: members = [] } = useGetWorkspaceUsers(projectId);
|
||||||
const { data: groups = [] } = useListWorkspaceGroups(projectId);
|
const { data: groups = [] } = useListWorkspaceGroups(projectId);
|
||||||
|
|
||||||
const { name, maxRequestTtl, conditions, constraints, steps } = watch();
|
const { name, conditions, constraints, steps } = watch();
|
||||||
|
|
||||||
const getApproverLabel = (approverId: string, approverType: ApproverType) => {
|
const getApproverLabel = (approverId: string, approverType: ApproverType) => {
|
||||||
if (approverType === ApproverType.User) {
|
if (approverType === ApproverType.User) {
|
||||||
@@ -45,61 +45,23 @@ export const PolicyReviewStep = () => {
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<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">Policy Details</h3>
|
<h3 className="text-sm font-medium text-mineshaft-200">Policy Configuration</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<ReviewField label="Policy Name" value={name || "Not set"} />
|
<ReviewField label="Policy Name" value={name || "Not set"} />
|
||||||
<ReviewField label="Max Approval Request TTL" value={maxRequestTtl || "No Limit"} />
|
<ReviewField label="Max. Access Duration" value={constraints.accessDuration.max} />
|
||||||
|
<ReviewField
|
||||||
|
label="Account Paths"
|
||||||
|
value={
|
||||||
|
conditions[0].accountPaths.length ? conditions[0].accountPaths.join(",") : "Not set"
|
||||||
|
}
|
||||||
|
/>
|
||||||
</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">
|
<h3 className="text-sm font-medium text-mineshaft-200">Approval Sequence</h3>
|
||||||
PAM Account Access Duration TTL
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<ReviewField label="Minimum Duration" value={constraints.accessDuration.min} />
|
|
||||||
<ReviewField label="Maximum Duration" value={constraints.accessDuration.max} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<div className="mb-3 border-b border-mineshaft-600 pb-2">
|
|
||||||
<h3 className="text-sm font-medium text-mineshaft-200">
|
|
||||||
Conditions ({conditions.length})
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-3">
|
|
||||||
{conditions.map((condition, index) => (
|
|
||||||
<div
|
|
||||||
key={`condition-${index + 1}`}
|
|
||||||
className="rounded border border-mineshaft-600 bg-mineshaft-800/50 p-3"
|
|
||||||
>
|
|
||||||
<div className="mb-2 text-xs font-medium text-mineshaft-300">
|
|
||||||
Condition {index + 1}
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div>
|
|
||||||
<span className="text-xs text-mineshaft-400">Account Paths: </span>
|
|
||||||
<span className="text-xs text-mineshaft-200">
|
|
||||||
{condition.accountPaths.length > 0
|
|
||||||
? condition.accountPaths.join(", ")
|
|
||||||
: "None specified"}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<div className="mb-3 border-b border-mineshaft-600 pb-2">
|
|
||||||
<h3 className="text-sm font-medium text-mineshaft-200">
|
|
||||||
Approval Workflow ({steps.length} step{steps.length !== 1 ? "s" : ""})
|
|
||||||
</h3>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{steps.map((step, index) => {
|
{steps.map((step, index) => {
|
||||||
@@ -109,7 +71,7 @@ export const PolicyReviewStep = () => {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={`step-${index + 1}`}
|
key={`step-${index + 1}`}
|
||||||
className="rounded border border-mineshaft-600 bg-mineshaft-800/50 p-3"
|
className="rounded border border-mineshaft-600 bg-mineshaft-700 p-3"
|
||||||
>
|
>
|
||||||
<div className="mb-3 flex items-center justify-between">
|
<div className="mb-3 flex items-center justify-between">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -187,8 +149,8 @@ export const PolicyReviewStep = () => {
|
|||||||
{/* Summary Notice */}
|
{/* Summary Notice */}
|
||||||
<div className="rounded-md border border-primary/30 bg-primary/5 p-3">
|
<div className="rounded-md border border-primary/30 bg-primary/5 p-3">
|
||||||
<p className="text-xs text-mineshaft-300">
|
<p className="text-xs text-mineshaft-300">
|
||||||
Please review all the details above. Click "Create" to save this policy or
|
Please review all the details above. Submit to save this policy or go back to make
|
||||||
"Back" to make changes.
|
changes.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
-1
@@ -1,4 +1,3 @@
|
|||||||
export { PolicyApprovalSteps } from "./PolicyApprovalSteps";
|
export { PolicyApprovalSteps } from "./PolicyApprovalSteps";
|
||||||
export { PolicyConstraintsStep } from "./PolicyConstraintsStep";
|
|
||||||
export { PolicyDetailsStep } from "./PolicyDetailsStep";
|
export { PolicyDetailsStep } from "./PolicyDetailsStep";
|
||||||
export { PolicyReviewStep } from "./PolicyReviewStep";
|
export { PolicyReviewStep } from "./PolicyReviewStep";
|
||||||
|
|||||||
@@ -180,8 +180,11 @@ export const RequestGrantTab = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
<div className="mb-4 flex items-center justify-between">
|
<div className="mb-4">
|
||||||
<p className="text-xl font-medium text-mineshaft-100">Access Grants</p>
|
<div className="flex items-center gap-x-2">
|
||||||
|
<p className="text-xl font-medium text-mineshaft-100">Access Grants</p>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-bunker-300">View and revoke access grants to PAM accounts</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
@@ -269,7 +272,7 @@ export const RequestGrantTab = () => {
|
|||||||
<Tr>
|
<Tr>
|
||||||
<Th>User</Th>
|
<Th>User</Th>
|
||||||
<Th>Account Path</Th>
|
<Th>Account Path</Th>
|
||||||
<Th>Duration</Th>
|
<Th>Access Duration</Th>
|
||||||
<Th>Status</Th>
|
<Th>Status</Th>
|
||||||
<Th>Granted</Th>
|
<Th>Granted</Th>
|
||||||
<Th>Expires</Th>
|
<Th>Expires</Th>
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ import {
|
|||||||
faArrowDown,
|
faArrowDown,
|
||||||
faArrowUp,
|
faArrowUp,
|
||||||
faCheckCircle,
|
faCheckCircle,
|
||||||
|
faClipboardCheck,
|
||||||
faFilter,
|
faFilter,
|
||||||
faFolderPlus,
|
faFolderPlus,
|
||||||
faMagnifyingGlass,
|
faMagnifyingGlass,
|
||||||
faPen,
|
|
||||||
faPlus,
|
faPlus,
|
||||||
faSearch
|
faSearch
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
@@ -328,11 +328,11 @@ export const PamAccountsTable = ({ projectId }: Props) => {
|
|||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
<Button
|
<Button
|
||||||
variant="outline_bg"
|
variant="outline_bg"
|
||||||
leftIcon={<FontAwesomeIcon icon={faPen} />}
|
leftIcon={<FontAwesomeIcon icon={faClipboardCheck} />}
|
||||||
onClick={() => handlePopUpOpen("requestAccount")}
|
onClick={() => handlePopUpOpen("requestAccount")}
|
||||||
className="h-10 transition-colors"
|
className="h-10 transition-colors"
|
||||||
>
|
>
|
||||||
Request Account
|
Request Access
|
||||||
</Button>
|
</Button>
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionPamAccountActions.Create}
|
I={ProjectPermissionPamAccountActions.Create}
|
||||||
|
|||||||
@@ -111,6 +111,7 @@ const Content = ({ onOpenChange, account, accountPath }: Props) => {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field, fieldState: { error } }) => (
|
render={({ field, fieldState: { error } }) => (
|
||||||
<FormControl
|
<FormControl
|
||||||
|
isRequired
|
||||||
helperText="Account path including the account name. Supports glob patterns (e.g., /folder/**, /*/account-name)"
|
helperText="Account path including the account name. Supports glob patterns (e.g., /folder/**, /*/account-name)"
|
||||||
errorText={error?.message}
|
errorText={error?.message}
|
||||||
isError={Boolean(error?.message)}
|
isError={Boolean(error?.message)}
|
||||||
@@ -126,7 +127,6 @@ const Content = ({ onOpenChange, account, accountPath }: Props) => {
|
|||||||
render={({ field, fieldState: { error } }) => (
|
render={({ field, fieldState: { error } }) => (
|
||||||
<FormControl
|
<FormControl
|
||||||
label={<TtlFormLabel label="Access Duration" />}
|
label={<TtlFormLabel label="Access Duration" />}
|
||||||
helperText="Duration of access requested"
|
|
||||||
errorText={error?.message}
|
errorText={error?.message}
|
||||||
isError={Boolean(error?.message)}
|
isError={Boolean(error?.message)}
|
||||||
>
|
>
|
||||||
@@ -139,7 +139,6 @@ const Content = ({ onOpenChange, account, accountPath }: Props) => {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field, fieldState: { error } }) => (
|
render={({ field, fieldState: { error } }) => (
|
||||||
<FormControl
|
<FormControl
|
||||||
helperText="Provide a reason for requesting access"
|
|
||||||
errorText={error?.message}
|
errorText={error?.message}
|
||||||
isError={Boolean(error?.message)}
|
isError={Boolean(error?.message)}
|
||||||
label="Justification"
|
label="Justification"
|
||||||
@@ -178,7 +177,7 @@ export const PamRequestAccountAccessModal = (props: Props) => {
|
|||||||
<ModalContent
|
<ModalContent
|
||||||
className="max-w-2xl pb-2"
|
className="max-w-2xl pb-2"
|
||||||
title="Request Account Access"
|
title="Request Account Access"
|
||||||
subTitle="Request access to this account path"
|
subTitle="Request access to an account path"
|
||||||
>
|
>
|
||||||
<Content {...props} />
|
<Content {...props} />
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|||||||
Reference in New Issue
Block a user