feat: resolved changes made in backend

This commit is contained in:
=
2025-12-07 16:58:38 +05:30
parent 273a899500
commit 5d580f9d4b
21 changed files with 234 additions and 198 deletions

View File

@@ -366,10 +366,6 @@ export const ROUTE_PATHS = Object.freeze({
PamSessionByIDPage: setRoute(
"/organizations/$orgId/projects/pam/$projectId/sessions/$sessionId",
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/pam/$projectId/_pam-layout/sessions/$sessionId"
),
ApprovalRequestDetailPage: setRoute(
"/organizations/$orgId/projects/pam/$projectId/approval-requests/$approvalRequestId",
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/pam/$projectId/_pam-layout/approval-requests/$approvalRequestId"
)
},
Public: {

View File

@@ -0,0 +1,10 @@
export { useRevokeApprovalGrant } from "./mutations";
export { approvalGrantQuery } from "./queries";
export {
ApprovalGrantStatus,
type PamAccessGrantAttributes,
type TApprovalGrant,
type TGetApprovalGrantByIdDTO,
type TListApprovalGrantsDTO,
type TRevokeApprovalGrantDTO
} from "./types";

View File

@@ -0,0 +1,23 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { approvalGrantQuery } from "./queries";
import { TApprovalGrant, TRevokeApprovalGrantDTO } from "./types";
export const useRevokeApprovalGrant = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ policyType, grantId, revocationReason }: TRevokeApprovalGrantDTO) => {
const { data } = await apiRequest.post<{ grant: TApprovalGrant }>(
`/api/v1/approval-policies/${policyType}/grants/${grantId}/revoke`,
{ revocationReason }
);
return data.grant;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: approvalGrantQuery.allKey() });
}
});
};

View File

@@ -0,0 +1,37 @@
import { queryOptions } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { TApprovalGrant, TGetApprovalGrantByIdDTO, TListApprovalGrantsDTO } from "./types";
export const approvalGrantQuery = {
allKey: () => ["approval-grants"] as const,
getByIdKey: (params: TGetApprovalGrantByIdDTO) =>
[...approvalGrantQuery.allKey(), "by-id", params] as const,
listKey: (params: TListApprovalGrantsDTO) =>
[...approvalGrantQuery.allKey(), "list", params] as const,
getById: (params: TGetApprovalGrantByIdDTO) =>
queryOptions({
queryKey: approvalGrantQuery.getByIdKey(params),
queryFn: async () => {
const { data } = await apiRequest.get<{ grant: TApprovalGrant }>(
`/api/v1/approval-policies/${params.policyType}/grants/${params.grantId}`
);
return data.grant;
}
}),
list: (params: TListApprovalGrantsDTO) =>
queryOptions({
queryKey: approvalGrantQuery.listKey(params),
queryFn: async () => {
const { data } = await apiRequest.get<{
grants: TApprovalGrant[];
}>(`/api/v1/approval-policies/${params.policyType}/grants`, {
params: {
projectId: params.projectId
}
});
return data.grants;
}
})
};

View File

@@ -0,0 +1,46 @@
import { ApprovalPolicyType } from "../approvalPolicies";
export enum ApprovalGrantStatus {
Active = "active",
Expired = "expired",
Revoked = "revoked"
}
// PAM Access Grant Attributes
export type PamAccessGrantAttributes = {
accountPath: string;
accessDuration: string;
};
// Base Grant Type
export type TApprovalGrant = {
id: string;
projectId: string;
requestId: string | null;
granteeUserId: string | null;
revokedByUserId: string | null;
revocationReason: string | null;
status: ApprovalGrantStatus;
type: ApprovalPolicyType;
attributes: PamAccessGrantAttributes;
createdAt: string;
expiresAt: string | null;
revokedAt: string | null;
};
// DTOs
export type TListApprovalGrantsDTO = {
policyType: ApprovalPolicyType;
projectId: string;
};
export type TGetApprovalGrantByIdDTO = {
policyType: ApprovalPolicyType;
grantId: string;
};
export type TRevokeApprovalGrantDTO = {
policyType: ApprovalPolicyType;
grantId: string;
revocationReason?: string;
};

View File

@@ -18,14 +18,13 @@ export type ApprovalPolicyStep = {
};
export type PamAccessPolicyConditions = {
resourceIds: string[];
accountPaths: string[];
}[];
export type PamAccessPolicyConstraints = {
requestDurationSeconds: {
min: number;
max: number;
accessDuration: {
min: string;
max: string;
};
};
@@ -33,7 +32,7 @@ export type TApprovalPolicy = {
id: string;
projectId: string;
name: string;
maxRequestTtlSeconds?: number | null;
maxRequestTtl?: string | null;
type: ApprovalPolicyType;
conditions: {
version: number;
@@ -52,7 +51,7 @@ export type TCreateApprovalPolicyDTO = {
policyType: ApprovalPolicyType;
projectId: string;
name: string;
maxRequestTtlSeconds?: number | null;
maxRequestTtl?: string | null;
conditions: PamAccessPolicyConditions;
constraints: PamAccessPolicyConstraints;
steps: ApprovalPolicyStep[];
@@ -62,7 +61,7 @@ export type TUpdateApprovalPolicyDTO = {
policyType: ApprovalPolicyType;
policyId: string;
name?: string;
maxRequestTtlSeconds?: number | null;
maxRequestTtl?: string | null;
conditions?: PamAccessPolicyConditions;
constraints?: PamAccessPolicyConstraints;
steps?: ApprovalPolicyStep[];

View File

@@ -1,5 +1,6 @@
export {
useApproveApprovalRequest,
useCancelApprovalRequest,
useCreateApprovalRequest,
useRejectApprovalRequest
} from "./mutations";

View File

@@ -6,6 +6,7 @@ import { approvalRequestQuery } from "./queries";
import {
TApprovalRequest,
TApproveApprovalRequestDTO,
TCancelApprovalRequestDTO,
TCreateApprovalRequestDTO,
TRejectApprovalRequestDTO
} from "./types";
@@ -57,3 +58,18 @@ export const useRejectApprovalRequest = () => {
}
});
};
export const useCancelApprovalRequest = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ policyType, requestId }: TCancelApprovalRequestDTO) => {
const { data } = await apiRequest.post<{ request: TApprovalRequest }>(
`/api/v1/approval-policies/${policyType}/requests/${requestId}/cancel`
);
return data.request;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: approvalRequestQuery.allKey() });
}
});
};

View File

@@ -49,9 +49,8 @@ export type ApprovalRequestStep = {
};
export type PamAccessRequestData = {
resourceId: string;
accountPath: string;
requestDurationSeconds: number;
accessDuration: number;
};
export type TApprovalRequest = {
@@ -78,7 +77,7 @@ export type TCreateApprovalRequestDTO = {
policyType: ApprovalPolicyType;
projectId: string;
justification?: string | null;
expiresAt?: Date | null;
requestDuration?: string | null;
requestData: PamAccessRequestData;
};
@@ -103,3 +102,8 @@ export type TRejectApprovalRequestDTO = {
requestId: string;
comment?: string;
};
export type TCancelApprovalRequestDTO = {
policyType: ApprovalPolicyType;
requestId: string;
};

View File

@@ -1,6 +1,9 @@
export * from "./accessApproval";
export * from "./admin";
export * from "./apiKeys";
export * from "./approvalGrants";
export * from "./approvalPolicies";
export * from "./approvalRequests";
export * from "./assumePrivileges";
export * from "./auditLogs";
export * from "./auditLogStreams";

View File

@@ -2,7 +2,7 @@ import { Helmet } from "react-helmet";
import { faBan, faChevronLeft } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useQuery } from "@tanstack/react-query";
import { Link, useNavigate, useParams } from "@tanstack/react-router";
import { Link, useParams } from "@tanstack/react-router";
import { ContentLoader, EmptyState, PageHeader } from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes";
@@ -61,7 +61,7 @@ const PageContent = () => {
<PageHeader
scope={ProjectType.PAM}
title="Approval Request"
description={`Request to access account ${request.requestData.requestData.accountPath} for ${request.requestData.requestData.requestDurationSeconds} by ${request.requesterName || "Unknown"}`}
description={`Request to access account ${request.requestData.requestData.accountPath} for ${request.requestData.requestData.accessDuration} by ${request.requesterName || "Unknown"}`}
/>
<div className="flex justify-center gap-4">
<div className="flex w-96 flex-col gap-4">

View File

@@ -5,7 +5,6 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications";
import {
Button,
FormControl,
FormLabel,
Popover,
PopoverContent,

View File

@@ -23,18 +23,8 @@ const getStatusBadgeColor = (status: ApprovalRequestStatus) => {
}
};
const formatDuration = (seconds: number) => {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
if (hours > 0) {
return `${hours}h${minutes > 0 ? ` ${minutes}m` : ""}`;
}
return `${minutes}m`;
};
export const RequestDetailsSection = ({ request }: Props) => {
const { accountPath, requestDurationSeconds } = request.requestData.requestData;
const { accountPath, accessDuration } = request.requestData.requestData;
return (
<div className="flex w-full flex-col gap-3 rounded-lg border border-mineshaft-600 bg-mineshaft-900 px-4 py-3">
@@ -53,9 +43,7 @@ export const RequestDetailsSection = ({ request }: Props) => {
</GenericFieldLabel>
<GenericFieldLabel label="Requester Email">{request.requesterEmail}</GenericFieldLabel>
<GenericFieldLabel label="Account Path">{accountPath}</GenericFieldLabel>
<GenericFieldLabel label="Duration">
{formatDuration(requestDurationSeconds)}
</GenericFieldLabel>
<GenericFieldLabel label="Duration">{accessDuration}</GenericFieldLabel>
{request.justification && (
<GenericFieldLabel label="Justification">
<p className="rounded-sm bg-mineshaft-600 p-2 text-xs break-words">

View File

@@ -63,16 +63,6 @@ const getStatusBadgeColor = (status: ApprovalRequestStatus) => {
}
};
const formatDuration = (seconds: number) => {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
if (hours > 0) {
return `${hours}h${minutes > 0 ? ` ${minutes}m` : ""}`;
}
return `${minutes}m`;
};
const checkIfUserNeedsToApprove = (
request: TApprovalRequest,
userId: string,
@@ -131,7 +121,6 @@ export const RequestsTable = () => {
request.requesterName?.toLowerCase().includes(search.toLowerCase()) ||
request.requesterEmail?.toLowerCase().includes(search.toLowerCase()) ||
request.justification?.toLowerCase().includes(search.toLowerCase()) ||
request.requestData.requestData.resourceId.toLowerCase().includes(search.toLowerCase()) ||
request.requestData.requestData.accountPath.toLowerCase().includes(search.toLowerCase())
);
}
@@ -272,7 +261,7 @@ export const RequestsTable = () => {
{!isRequestsLoading &&
paginatedRequests.map((request) => {
const needsApproval = checkIfUserNeedsToApprove(request, userId, userGroups);
const { accountPath, requestDurationSeconds } = request.requestData.requestData;
const { accountPath, accessDuration } = request.requestData.requestData;
return (
<Tr
@@ -297,14 +286,12 @@ export const RequestsTable = () => {
</div>
</Td>
<Td>
<span className="text-sm text-mineshaft-200">
{formatDuration(requestDurationSeconds)}
</span>
<span className="text-sm text-mineshaft-200">{accessDuration}</span>
</Td>
<Td>
<div className="flex items-center gap-2">
<Badge variant={getStatusBadgeColor(request.status)}>
{request.status}
<Badge className="capitalize" variant={getStatusBadgeColor(request.status)}>
{request.status.split("-").join(" ")}
</Badge>
{needsApproval && (
<div className="flex items-center gap-1 rounded bg-primary/20 px-2 py-0.5 text-xs text-primary">

View File

@@ -37,6 +37,8 @@ import {
ApproverType
} from "@app/hooks/api/approvalPolicies";
import { UsePopUpState } from "@app/hooks/usePopUp";
import { Badge } from "@app/components/v3";
import { User, Users } from "lucide-react";
type Props = {
handlePopUpOpen: (
@@ -113,9 +115,7 @@ export const PoliciesTable = ({ handlePopUpOpen }: Props) => {
{!isPoliciesLoading &&
policies.map((policy) => {
const isExpanded = expandedRows.has(policy.id);
const maxTtl = policy.maxRequestTtlSeconds
? `${Math.floor(policy.maxRequestTtlSeconds / 3600)}h`
: "No limit";
const maxTtl = policy.maxRequestTtl ? policy.maxRequestTtl : "No limit";
const conditionsCount = policy.conditions.conditions.length;
return (
@@ -198,8 +198,8 @@ export const PoliciesTable = ({ handlePopUpOpen }: Props) => {
{isExpanded && (
<Tr className="bg-mineshaft-800">
<Td colSpan={5} className="p-0">
<div className="max-h-80 overflow-auto overflow-x-hidden">
<div className="p-4">
<div className="flex max-h-80 w-full gap-2 overflow-auto overflow-x-hidden">
<div className="flex-1 p-4">
<div className="mb-2 text-sm font-medium text-mineshaft-300">
Approval Contraints
</div>
@@ -211,27 +211,6 @@ export const PoliciesTable = ({ handlePopUpOpen }: Props) => {
)}
>
<div className="space-y-2">
<div>
<span className="text-sm font-medium text-mineshaft-300">
Resources:
</span>
<p className="text-sm text-mineshaft-100">
{step.resourceIds.join(", ")}
</p>
</div>
<div className="flex items-center justify-center">
<div
style={{ height: "1px" }}
className="w-1/5 bg-mineshaft-500"
/>
<span className="px-2 text-xs font-medium text-mineshaft-400">
AND
</span>
<div
style={{ height: "1px" }}
className="w-1/5 bg-mineshaft-500"
/>
</div>
<div>
<span className="text-sm font-medium text-mineshaft-300">
Account Paths:
@@ -256,7 +235,7 @@ export const PoliciesTable = ({ handlePopUpOpen }: Props) => {
</Fragment>
))}
</div>
<div className="p-4">
<div className="flex-2 p-4">
<div className="mb-2 text-sm font-medium text-mineshaft-300">
Approval Sequence
</div>
@@ -264,11 +243,11 @@ export const PoliciesTable = ({ handlePopUpOpen }: Props) => {
<div
key={`${policy.id}-step-${index + 1}`}
className={twMerge(
"mb-3 rounded border border-mineshaft-600 bg-mineshaft-900 p-3",
"mb-3 rounded border border-mineshaft-600 bg-mineshaft-900",
index === policy.steps.length - 1 && "mb-0"
)}
>
<div className="mb-2 flex items-center justify-between">
<div className="mb-2 flex items-center justify-between bg-mineshaft-700 p-3">
<div className="text-sm font-medium text-mineshaft-200">
Step {index + 1}
{step.name && (
@@ -282,21 +261,19 @@ export const PoliciesTable = ({ handlePopUpOpen }: Props) => {
{step.requiredApprovals !== 1 ? "s" : ""}
</div>
</div>
<div className="flex flex-wrap gap-2">
<div className="flex flex-wrap gap-2 p-3">
{step.approvers.map((approver, approverIndex) => (
<div
<Badge
variant="neutral"
key={`${policy.id}-step-${index + 1}-approver-${approverIndex + 1}`}
className="rounded bg-mineshaft-700 px-2 py-1 text-xs text-mineshaft-300"
>
<FontAwesomeIcon
icon={faUsers}
className="mr-1.5 text-mineshaft-400"
/>
<span className="capitalize">{approver.type}:</span>{" "}
<span className="text-mineshaft-200">
{getApproverLabel(approver.id, approver.type)}
</span>
</div>
{approver.type === ApproverType.Group ? (
<User />
) : (
<Users />
)}
{getApproverLabel(approver.id, approver.type)}
</Badge>
))}
</div>
</div>

View File

@@ -26,7 +26,7 @@ type Props = {
};
const FORM_STEPS: { name: string; key: string; fields: (keyof TPolicyForm)[] }[] = [
{ name: "Details", key: "details", fields: ["name", "maxRequestTtlSeconds"] },
{ name: "Details", key: "details", fields: ["name", "maxRequestTtl"] },
{ name: "Constraints", key: "constraints", fields: ["conditions", "constraints"] },
{ name: "Approvals", key: "approvals", fields: ["steps"] },
{ name: "Review", key: "review", fields: [] }
@@ -45,12 +45,12 @@ export const PolicyModal = ({ popUp, handlePopUpToggle }: Props) => {
resolver: zodResolver(PolicyFormSchema),
defaultValues: {
name: "",
maxRequestTtlSeconds: null,
conditions: [{ resourceIds: [], accountPaths: [] }],
maxRequestTtl: null,
conditions: [{ accountPaths: [] }],
constraints: {
requestDurationSeconds: {
min: 30,
max: 604800
accessDuration: {
min: "30s",
max: "7d"
}
},
steps: [
@@ -74,7 +74,7 @@ export const PolicyModal = ({ popUp, handlePopUpToggle }: Props) => {
if (policyData?.policy) {
reset({
name: policyData.policy.name,
maxRequestTtlSeconds: policyData.policy.maxRequestTtlSeconds,
maxRequestTtl: policyData.policy.maxRequestTtl,
conditions: policyData.policy.conditions.conditions,
constraints: policyData.policy.constraints.constraints,
steps: policyData.policy.steps.map((step) => ({
@@ -85,12 +85,12 @@ export const PolicyModal = ({ popUp, handlePopUpToggle }: Props) => {
} else {
reset({
name: "",
maxRequestTtlSeconds: null,
conditions: [{ resourceIds: [], accountPaths: [] }],
maxRequestTtl: null,
conditions: [{ accountPaths: [] }],
constraints: {
requestDurationSeconds: {
min: 30,
max: 604800
accessDuration: {
min: "30s",
max: "7d"
}
},
steps: [

View File

@@ -1,21 +1,41 @@
import ms from "ms";
import { z } from "zod";
import { ApproverType } from "@app/hooks/api/approvalPolicies";
// 30 to 7 days
const DurationSchema = (
min = 30,
max = 604800,
msg = "Duration must be between 30 seconds and 7 days"
) =>
z.string().refine(
(val) => {
const duration = ms(val) / 1000;
// 30 seconds to 7 days
return duration >= min && duration <= max;
},
{ message: msg }
);
export const PolicyFormSchema = z.object({
name: z.string().min(1, "Policy name is required").max(128),
maxRequestTtlSeconds: z.number().min(3600).max(2592000).nullable().optional(),
maxRequestTtl: DurationSchema(
3600,
2592000,
"Duration must be between 1 hour and 30 days"
).nullish(),
conditions: z
.object({
resourceIds: z.array(z.string().uuid()),
accountPaths: z.array(z.string().min(1))
})
.array()
.min(1, "At least one condition is required"),
constraints: z.object({
requestDurationSeconds: z.object({
min: z.number().min(0).max(604800),
max: z.number().min(1).max(604800)
accessDuration: z.object({
min: DurationSchema(),
max: DurationSchema()
})
}),
steps: z

View File

@@ -33,7 +33,7 @@ export const PolicyConstraintsStep = () => {
variant="outline_bg"
size="xs"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => appendCondition({ resourceIds: [], accountPaths: [] })}
onClick={() => appendCondition({ accountPaths: [] })}
>
Add Condition
</Button>
@@ -58,35 +58,6 @@ export const PolicyConstraintsStep = () => {
)}
</div>
<div className="space-y-3">
<Controller
control={control}
name={`conditions.${index}.resourceIds`}
render={({ field: resourceField, fieldState: { error } }) => (
<FormControl
label="Resource IDs"
isError={Boolean(error)}
errorText={error?.message}
helperText="Comma-separated UUIDs of resources this condition applies to"
>
<Input
value={resourceField.value.join(", ")}
onChange={(e) => {
const ids = e.target.value
.split(",")
.map((id) => id.trim())
.filter(Boolean);
resourceField.onChange(ids);
}}
placeholder="e.g., 550e8400-e29b-41d4-a716-446655440000, ..."
/>
</FormControl>
)}
/>
<div className="flex items-center justify-center">
<div style={{ height: "1px" }} className="w-1/5 bg-mineshaft-500" />
<span className="px-2 text-xs font-medium text-mineshaft-400">AND</span>
<div style={{ height: "1px" }} className="w-1/5 bg-mineshaft-500" />
</div>
<Controller
control={control}
name={`conditions.${index}.accountPaths`}
@@ -106,7 +77,7 @@ export const PolicyConstraintsStep = () => {
.filter(Boolean);
pathField.onChange(paths);
}}
placeholder="e.g., /admin/*, /users/john"
placeholder="e.g., /admin/**, /users/john, /**"
/>
</FormControl>
)}

View File

@@ -1,5 +1,6 @@
import { Controller, useFormContext } from "react-hook-form";
import { TtlFormLabel } from "@app/components/features";
import { FormControl, Input } from "@app/components/v2";
import { TPolicyForm } from "../PolicySchema";
@@ -25,24 +26,15 @@ export const PolicyDetailsStep = () => {
/>
<Controller
control={control}
name="maxRequestTtlSeconds"
name="maxRequestTtl"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Max Request TTL (seconds)"
isError={Boolean(error)}
errorText={error?.message}
helperText="Maximum time-to-live for requests. Must be between 1 hour (3600s) and 30 days (2592000s). Leave empty for no limit."
label={<TtlFormLabel label="Max Request TTL" />}
helperText="Maximum time-to-live for requests. Must be between 1 hour and 30 days. Leave empty for no limit."
>
<Input
{...field}
type="number"
value={field.value ?? ""}
onChange={(e) => {
const val = e.target.value;
field.onChange(val === "" ? null : parseInt(val, 10));
}}
placeholder="e.g., 86400 (24 hours)"
/>
<Input {...field} value={field.value ?? ""} placeholder="1h" />
</FormControl>
)}
/>
@@ -58,41 +50,29 @@ export const PolicyDetailsStep = () => {
<div className="grid grid-cols-2 gap-4">
<Controller
control={control}
name="constraints.requestDurationSeconds.min"
name="constraints.accessDuration.min"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Minimum Hours"
label={<TtlFormLabel label="Minimum TTL" />}
isError={Boolean(error)}
errorText={error?.message}
helperText="30-604800 hours"
helperText="Must be between 30s and 7 days"
>
<Input
{...field}
type="number"
min={0}
max={168}
onChange={(e) => field.onChange(parseInt(e.target.value, 10))}
/>
<Input {...field} />
</FormControl>
)}
/>
<Controller
control={control}
name="constraints.requestDurationSeconds.max"
name="constraints.accessDuration.max"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Maximum Hours"
label={<TtlFormLabel label="Maximum TTL" />}
isError={Boolean(error)}
errorText={error?.message}
helperText="30-604800 hours (7 days)"
helperText="Must be between 30s and 7 days"
>
<Input
{...field}
type="number"
min={1}
max={168}
onChange={(e) => field.onChange(parseInt(e.target.value, 10))}
/>
<Input {...field} />
</FormControl>
)}
/>

View File

@@ -24,17 +24,7 @@ export const PolicyReviewStep = () => {
const { data: members = [] } = useGetWorkspaceUsers(projectId);
const { data: groups = [] } = useListWorkspaceGroups(projectId);
const { name, maxRequestTtlSeconds, conditions, constraints, steps } = watch();
const formatTtl = (seconds: number | null | undefined) => {
if (!seconds) return "No limit";
const hours = Math.floor(seconds / 3600);
const days = Math.floor(hours / 24);
if (days > 0) {
return `${days} day${days !== 1 ? "s" : ""} (${seconds}s)`;
}
return `${hours} hour${hours !== 1 ? "s" : ""} (${seconds}s)`;
};
const { name, maxRequestTtl, conditions, constraints, steps } = watch();
const getApproverLabel = (approverId: string, approverType: ApproverType) => {
if (approverType === ApproverType.User) {
@@ -59,7 +49,7 @@ export const PolicyReviewStep = () => {
</div>
<div className="space-y-2">
<ReviewField label="Policy Name" value={name || "Not set"} />
<ReviewField label="Max Request TTL" value={formatTtl(maxRequestTtlSeconds)} />
<ReviewField label="Max Request TTL" value={maxRequestTtl || "No Limit"} />
</div>
</div>
@@ -68,14 +58,8 @@ export const PolicyReviewStep = () => {
<h3 className="text-sm font-medium text-mineshaft-200">Request Duration Constraints</h3>
</div>
<div className="space-y-2">
<ReviewField
label="Minimum Duration"
value={`${constraints.requestDurationSeconds.min} second${constraints.requestDurationSeconds.min !== 1 ? "s" : ""}`}
/>
<ReviewField
label="Maximum Duration"
value={`${constraints.requestDurationSeconds.max} second${constraints.requestDurationSeconds.max !== 1 ? "s" : ""}`}
/>
<ReviewField label="Minimum Duration" value={constraints.accessDuration.min} />
<ReviewField label="Maximum Duration" value={constraints.accessDuration.max} />
</div>
</div>
@@ -95,14 +79,6 @@ export const PolicyReviewStep = () => {
Condition {index + 1}
</div>
<div className="space-y-2">
<div>
<span className="text-xs text-mineshaft-400">Resource IDs: </span>
<span className="text-xs text-mineshaft-200">
{condition.resourceIds.length > 0
? condition.resourceIds.join(", ")
: "None specified"}
</span>
</div>
<div>
<span className="text-xs text-mineshaft-400">Account Paths: </span>
<span className="text-xs text-mineshaft-200">

View File

@@ -0,0 +1,3 @@
export const RequestGrantTab = () => {
return <div>Grant list</div>;
};