mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
feat: add secret approval review comment
This commit is contained in:
@@ -0,0 +1,19 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (!(await knex.schema.hasColumn(TableName.SecretApprovalRequestReviewer, "comment"))) {
|
||||||
|
await knex.schema.alterTable(TableName.SecretApprovalRequestReviewer, (t) => {
|
||||||
|
t.string("comment");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasColumn(TableName.SecretApprovalRequestReviewer, "comment")) {
|
||||||
|
await knex.schema.alterTable(TableName.SecretApprovalRequestReviewer, (t) => {
|
||||||
|
t.dropColumn("comment");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,7 +13,8 @@ export const SecretApprovalRequestsReviewersSchema = z.object({
|
|||||||
requestId: z.string().uuid(),
|
requestId: z.string().uuid(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
reviewerUserId: z.string().uuid()
|
reviewerUserId: z.string().uuid(),
|
||||||
|
comment: z.string().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TSecretApprovalRequestsReviewers = z.infer<typeof SecretApprovalRequestsReviewersSchema>;
|
export type TSecretApprovalRequestsReviewers = z.infer<typeof SecretApprovalRequestsReviewersSchema>;
|
||||||
|
|||||||
@@ -159,7 +159,8 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
|||||||
id: z.string()
|
id: z.string()
|
||||||
}),
|
}),
|
||||||
body: z.object({
|
body: z.object({
|
||||||
status: z.enum([ApprovalStatus.APPROVED, ApprovalStatus.REJECTED])
|
status: z.enum([ApprovalStatus.APPROVED, ApprovalStatus.REJECTED]),
|
||||||
|
comment: z.string().optional()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
@@ -175,8 +176,25 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
|||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
approvalId: req.params.id,
|
approvalId: req.params.id,
|
||||||
status: req.body.status
|
status: req.body.status,
|
||||||
|
comment: req.body.comment
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
projectId: review.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.SECRET_APPROVAL_REQUEST_REVIEWED,
|
||||||
|
metadata: {
|
||||||
|
secretApprovalRequestId: review.requestId,
|
||||||
|
reviewedBy: review.reviewerUserId,
|
||||||
|
status: review.status as ApprovalStatus,
|
||||||
|
comment: review.comment || ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return { review };
|
return { review };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -267,7 +285,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
|||||||
environment: z.string(),
|
environment: z.string(),
|
||||||
statusChangedByUser: approvalRequestUser.optional(),
|
statusChangedByUser: approvalRequestUser.optional(),
|
||||||
committerUser: approvalRequestUser,
|
committerUser: approvalRequestUser,
|
||||||
reviewers: approvalRequestUser.extend({ status: z.string() }).array(),
|
reviewers: approvalRequestUser.extend({ status: z.string(), comment: z.string().optional() }).array(),
|
||||||
secretPath: z.string(),
|
secretPath: z.string(),
|
||||||
commits: secretRawSchema
|
commits: secretRawSchema
|
||||||
.omit({ _id: true, environment: true, workspace: true, type: true, version: true })
|
.omit({ _id: true, environment: true, workspace: true, type: true, version: true })
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
} from "@app/services/secret-sync/secret-sync-types";
|
} from "@app/services/secret-sync/secret-sync-types";
|
||||||
|
|
||||||
import { KmipPermission } from "../kmip/kmip-enum";
|
import { KmipPermission } from "../kmip/kmip-enum";
|
||||||
|
import { ApprovalStatus } from "../secret-approval-request/secret-approval-request-types";
|
||||||
|
|
||||||
export type TListProjectAuditLogDTO = {
|
export type TListProjectAuditLogDTO = {
|
||||||
filter: {
|
filter: {
|
||||||
@@ -165,6 +166,7 @@ export enum EventType {
|
|||||||
SECRET_APPROVAL_REQUEST = "secret-approval-request",
|
SECRET_APPROVAL_REQUEST = "secret-approval-request",
|
||||||
SECRET_APPROVAL_CLOSED = "secret-approval-closed",
|
SECRET_APPROVAL_CLOSED = "secret-approval-closed",
|
||||||
SECRET_APPROVAL_REOPENED = "secret-approval-reopened",
|
SECRET_APPROVAL_REOPENED = "secret-approval-reopened",
|
||||||
|
SECRET_APPROVAL_REQUEST_REVIEWED = "secret-approval-request-reviewed",
|
||||||
SIGN_SSH_KEY = "sign-ssh-key",
|
SIGN_SSH_KEY = "sign-ssh-key",
|
||||||
ISSUE_SSH_CREDS = "issue-ssh-creds",
|
ISSUE_SSH_CREDS = "issue-ssh-creds",
|
||||||
CREATE_SSH_CA = "create-ssh-certificate-authority",
|
CREATE_SSH_CA = "create-ssh-certificate-authority",
|
||||||
@@ -1314,6 +1316,16 @@ interface SecretApprovalRequest {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface SecretApprovalRequestReviewed {
|
||||||
|
type: EventType.SECRET_APPROVAL_REQUEST_REVIEWED;
|
||||||
|
metadata: {
|
||||||
|
secretApprovalRequestId: string;
|
||||||
|
reviewedBy: string;
|
||||||
|
status: ApprovalStatus;
|
||||||
|
comment: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
interface SignSshKey {
|
interface SignSshKey {
|
||||||
type: EventType.SIGN_SSH_KEY;
|
type: EventType.SIGN_SSH_KEY;
|
||||||
metadata: {
|
metadata: {
|
||||||
@@ -2482,4 +2494,5 @@ export type Event =
|
|||||||
| KmipOperationRevokeEvent
|
| KmipOperationRevokeEvent
|
||||||
| KmipOperationLocateEvent
|
| KmipOperationLocateEvent
|
||||||
| KmipOperationRegisterEvent
|
| KmipOperationRegisterEvent
|
||||||
| CreateSecretRequestEvent;
|
| CreateSecretRequestEvent
|
||||||
|
| SecretApprovalRequestReviewed;
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
tx.ref("lastName").withSchema("committerUser").as("committerUserLastName"),
|
tx.ref("lastName").withSchema("committerUser").as("committerUserLastName"),
|
||||||
tx.ref("reviewerUserId").withSchema(TableName.SecretApprovalRequestReviewer),
|
tx.ref("reviewerUserId").withSchema(TableName.SecretApprovalRequestReviewer),
|
||||||
tx.ref("status").withSchema(TableName.SecretApprovalRequestReviewer).as("reviewerStatus"),
|
tx.ref("status").withSchema(TableName.SecretApprovalRequestReviewer).as("reviewerStatus"),
|
||||||
|
tx.ref("comment").withSchema(TableName.SecretApprovalRequestReviewer).as("reviewerComment"),
|
||||||
tx.ref("email").withSchema("secretApprovalReviewerUser").as("reviewerEmail"),
|
tx.ref("email").withSchema("secretApprovalReviewerUser").as("reviewerEmail"),
|
||||||
tx.ref("username").withSchema("secretApprovalReviewerUser").as("reviewerUsername"),
|
tx.ref("username").withSchema("secretApprovalReviewerUser").as("reviewerUsername"),
|
||||||
tx.ref("firstName").withSchema("secretApprovalReviewerUser").as("reviewerFirstName"),
|
tx.ref("firstName").withSchema("secretApprovalReviewerUser").as("reviewerFirstName"),
|
||||||
@@ -162,8 +163,10 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
reviewerEmail: email,
|
reviewerEmail: email,
|
||||||
reviewerLastName: lastName,
|
reviewerLastName: lastName,
|
||||||
reviewerUsername: username,
|
reviewerUsername: username,
|
||||||
reviewerFirstName: firstName
|
reviewerFirstName: firstName,
|
||||||
}) => (userId ? { userId, status, email, firstName, lastName, username } : undefined)
|
reviewerComment: comment
|
||||||
|
}) =>
|
||||||
|
userId ? { userId, status, email, firstName, lastName, username, comment: comment ?? "" } : undefined
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "approverUserId",
|
key: "approverUserId",
|
||||||
|
|||||||
@@ -320,6 +320,7 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
approvalId,
|
approvalId,
|
||||||
actor,
|
actor,
|
||||||
status,
|
status,
|
||||||
|
comment,
|
||||||
actorId,
|
actorId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId
|
actorOrgId
|
||||||
@@ -372,15 +373,18 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
return secretApprovalRequestReviewerDAL.create(
|
return secretApprovalRequestReviewerDAL.create(
|
||||||
{
|
{
|
||||||
status,
|
status,
|
||||||
|
comment,
|
||||||
requestId: secretApprovalRequest.id,
|
requestId: secretApprovalRequest.id,
|
||||||
reviewerUserId: actorId
|
reviewerUserId: actorId
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return secretApprovalRequestReviewerDAL.updateById(review.id, { status }, tx);
|
|
||||||
|
return secretApprovalRequestReviewerDAL.updateById(review.id, { status, comment }, tx);
|
||||||
});
|
});
|
||||||
return reviewStatus;
|
|
||||||
|
return { ...reviewStatus, projectId: secretApprovalRequest.projectId };
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateApprovalStatus = async ({
|
const updateApprovalStatus = async ({
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ export type TStatusChangeDTO = {
|
|||||||
export type TReviewRequestDTO = {
|
export type TReviewRequestDTO = {
|
||||||
approvalId: string;
|
approvalId: string;
|
||||||
status: ApprovalStatus;
|
status: ApprovalStatus;
|
||||||
|
comment?: string;
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
export type TApprovalRequestCountDTO = TProjectPermission;
|
export type TApprovalRequestCountDTO = TProjectPermission;
|
||||||
|
|||||||
@@ -122,6 +122,7 @@ export const eventToNameMap: { [K in EventType]: string } = {
|
|||||||
"OIDC group membership mapping assigned user to groups",
|
"OIDC group membership mapping assigned user to groups",
|
||||||
[EventType.OIDC_GROUP_MEMBERSHIP_MAPPING_REMOVE_USER]:
|
[EventType.OIDC_GROUP_MEMBERSHIP_MAPPING_REMOVE_USER]:
|
||||||
"OIDC group membership mapping removed user from groups",
|
"OIDC group membership mapping removed user from groups",
|
||||||
|
[EventType.SECRET_APPROVAL_REQUEST_REVIEWED]: "Review Secret Approval Request",
|
||||||
[EventType.CREATE_KMIP_CLIENT]: "Create KMIP client",
|
[EventType.CREATE_KMIP_CLIENT]: "Create KMIP client",
|
||||||
[EventType.UPDATE_KMIP_CLIENT]: "Update KMIP client",
|
[EventType.UPDATE_KMIP_CLIENT]: "Update KMIP client",
|
||||||
[EventType.DELETE_KMIP_CLIENT]: "Delete KMIP client",
|
[EventType.DELETE_KMIP_CLIENT]: "Delete KMIP client",
|
||||||
|
|||||||
@@ -150,5 +150,6 @@ export enum EventType {
|
|||||||
KMIP_OPERATION_ACTIVATE = "kmip-operation-activate",
|
KMIP_OPERATION_ACTIVATE = "kmip-operation-activate",
|
||||||
KMIP_OPERATION_REVOKE = "kmip-operation-revoke",
|
KMIP_OPERATION_REVOKE = "kmip-operation-revoke",
|
||||||
KMIP_OPERATION_LOCATE = "kmip-operation-locate",
|
KMIP_OPERATION_LOCATE = "kmip-operation-locate",
|
||||||
KMIP_OPERATION_REGISTER = "kmip-operation-register"
|
KMIP_OPERATION_REGISTER = "kmip-operation-register",
|
||||||
|
SECRET_APPROVAL_REQUEST_REVIEWED = "secret-approval-request-reviewed"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,9 +13,10 @@ export const useUpdateSecretApprovalReviewStatus = () => {
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation<object, object, TUpdateSecretApprovalReviewStatusDTO>({
|
return useMutation<object, object, TUpdateSecretApprovalReviewStatusDTO>({
|
||||||
mutationFn: async ({ id, status }) => {
|
mutationFn: async ({ id, status, comment }) => {
|
||||||
const { data } = await apiRequest.post(`/api/v1/secret-approval-requests/${id}/review`, {
|
const { data } = await apiRequest.post(`/api/v1/secret-approval-requests/${id}/review`, {
|
||||||
status
|
status,
|
||||||
|
comment
|
||||||
});
|
});
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ export type TSecretApprovalRequest = {
|
|||||||
reviewers: {
|
reviewers: {
|
||||||
userId: string;
|
userId: string;
|
||||||
status: ApprovalStatus;
|
status: ApprovalStatus;
|
||||||
|
comment: string;
|
||||||
email: string;
|
email: string;
|
||||||
firstName: string;
|
firstName: string;
|
||||||
lastName: string;
|
lastName: string;
|
||||||
@@ -114,6 +115,7 @@ export type TGetSecretApprovalRequestDetails = {
|
|||||||
|
|
||||||
export type TUpdateSecretApprovalReviewStatusDTO = {
|
export type TUpdateSecretApprovalReviewStatusDTO = {
|
||||||
status: ApprovalStatus;
|
status: ApprovalStatus;
|
||||||
|
comment?: string;
|
||||||
id: string;
|
id: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,36 @@
|
|||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import {
|
import {
|
||||||
|
faAngleDown,
|
||||||
faArrowLeft,
|
faArrowLeft,
|
||||||
faCheck,
|
|
||||||
faCheckCircle,
|
faCheckCircle,
|
||||||
faCircle,
|
faCircle,
|
||||||
faCodeBranch,
|
faCodeBranch,
|
||||||
|
faComment,
|
||||||
faFolder,
|
faFolder,
|
||||||
faXmarkCircle
|
faXmarkCircle
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { RadioGroup, RadioGroupIndicator, RadioGroupItem } from "@radix-ui/react-radio-group";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
import z from "zod";
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { Button, ContentLoader, EmptyState, IconButton, Tooltip } from "@app/components/v2";
|
import {
|
||||||
|
Button,
|
||||||
|
ContentLoader,
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
EmptyState,
|
||||||
|
FormControl,
|
||||||
|
IconButton,
|
||||||
|
TextArea,
|
||||||
|
Tooltip
|
||||||
|
} from "@app/components/v2";
|
||||||
import { useUser } from "@app/context";
|
import { useUser } from "@app/context";
|
||||||
|
import { usePopUp } from "@app/hooks";
|
||||||
import {
|
import {
|
||||||
useGetSecretApprovalRequestDetails,
|
useGetSecretApprovalRequestDetails,
|
||||||
useUpdateSecretApprovalReviewStatus
|
useUpdateSecretApprovalReviewStatus
|
||||||
@@ -74,6 +91,13 @@ type Props = {
|
|||||||
onGoBack: () => void;
|
onGoBack: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const reviewFormSchema = z.object({
|
||||||
|
comment: z.string().trim().optional().default(""),
|
||||||
|
status: z.nativeEnum(ApprovalStatus)
|
||||||
|
});
|
||||||
|
|
||||||
|
type TReviewFormSchema = z.infer<typeof reviewFormSchema>;
|
||||||
|
|
||||||
export const SecretApprovalRequestChanges = ({
|
export const SecretApprovalRequestChanges = ({
|
||||||
approvalRequestId,
|
approvalRequestId,
|
||||||
onGoBack,
|
onGoBack,
|
||||||
@@ -94,6 +118,16 @@ export const SecretApprovalRequestChanges = ({
|
|||||||
variables
|
variables
|
||||||
} = useUpdateSecretApprovalReviewStatus();
|
} = useUpdateSecretApprovalReviewStatus();
|
||||||
|
|
||||||
|
const { popUp, handlePopUpToggle } = usePopUp(["reviewChanges"] as const);
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
handleSubmit,
|
||||||
|
reset,
|
||||||
|
formState: { isSubmitting }
|
||||||
|
} = useForm<TReviewFormSchema>({
|
||||||
|
resolver: zodResolver(reviewFormSchema)
|
||||||
|
});
|
||||||
|
|
||||||
const isApproving = variables?.status === ApprovalStatus.APPROVED && isUpdatingRequestStatus;
|
const isApproving = variables?.status === ApprovalStatus.APPROVED && isUpdatingRequestStatus;
|
||||||
const isRejecting = variables?.status === ApprovalStatus.REJECTED && isUpdatingRequestStatus;
|
const isRejecting = variables?.status === ApprovalStatus.REJECTED && isUpdatingRequestStatus;
|
||||||
|
|
||||||
@@ -101,23 +135,23 @@ export const SecretApprovalRequestChanges = ({
|
|||||||
const canApprove = secretApprovalRequestDetails?.policy?.approvers?.some(
|
const canApprove = secretApprovalRequestDetails?.policy?.approvers?.some(
|
||||||
({ userId }) => userId === userSession.id
|
({ userId }) => userId === userSession.id
|
||||||
);
|
);
|
||||||
|
|
||||||
const reviewedUsers = secretApprovalRequestDetails?.reviewers?.reduce<
|
const reviewedUsers = secretApprovalRequestDetails?.reviewers?.reduce<
|
||||||
Record<string, ApprovalStatus>
|
Record<string, { status: ApprovalStatus; comment: string }>
|
||||||
>(
|
>(
|
||||||
(prev, curr) => ({
|
(prev, curr) => ({
|
||||||
...prev,
|
...prev,
|
||||||
[curr.userId]: curr.status
|
[curr.userId]: { status: curr.status, comment: curr.comment }
|
||||||
}),
|
}),
|
||||||
{}
|
{}
|
||||||
);
|
);
|
||||||
const hasApproved = reviewedUsers?.[userSession.id] === ApprovalStatus.APPROVED;
|
|
||||||
const hasRejected = reviewedUsers?.[userSession.id] === ApprovalStatus.REJECTED;
|
|
||||||
|
|
||||||
const handleSecretApprovalStatusUpdate = async (status: ApprovalStatus) => {
|
const handleSecretApprovalStatusUpdate = async (status: ApprovalStatus, comment: string) => {
|
||||||
try {
|
try {
|
||||||
await updateSecretApprovalRequestStatus({
|
await updateSecretApprovalRequestStatus({
|
||||||
id: approvalRequestId,
|
id: approvalRequestId,
|
||||||
status
|
status,
|
||||||
|
comment
|
||||||
});
|
});
|
||||||
createNotification({
|
createNotification({
|
||||||
type: "success",
|
type: "success",
|
||||||
@@ -130,6 +164,16 @@ export const SecretApprovalRequestChanges = ({
|
|||||||
text: "Failed to update the request status"
|
text: "Failed to update the request status"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handlePopUpToggle("reviewChanges", false);
|
||||||
|
reset({
|
||||||
|
comment: "",
|
||||||
|
status: ApprovalStatus.APPROVED
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmitReview = (data: TReviewFormSchema) => {
|
||||||
|
handleSecretApprovalStatusUpdate(data.status, data.comment);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isSecretApprovalRequestLoading) {
|
if (isSecretApprovalRequestLoading) {
|
||||||
@@ -150,7 +194,7 @@ export const SecretApprovalRequestChanges = ({
|
|||||||
const isMergable =
|
const isMergable =
|
||||||
secretApprovalRequestDetails?.policy?.approvals <=
|
secretApprovalRequestDetails?.policy?.approvals <=
|
||||||
secretApprovalRequestDetails?.policy?.approvers?.filter(
|
secretApprovalRequestDetails?.policy?.approvers?.filter(
|
||||||
({ userId }) => reviewedUsers?.[userId] === ApprovalStatus.APPROVED
|
({ userId }) => reviewedUsers?.[userId]?.status === ApprovalStatus.APPROVED
|
||||||
).length;
|
).length;
|
||||||
const hasMerged = secretApprovalRequestDetails?.hasMerged;
|
const hasMerged = secretApprovalRequestDetails?.hasMerged;
|
||||||
|
|
||||||
@@ -202,27 +246,115 @@ export const SecretApprovalRequestChanges = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{!hasMerged && secretApprovalRequestDetails.status === "open" && (
|
{!hasMerged && secretApprovalRequestDetails.status === "open" && (
|
||||||
<>
|
<DropdownMenu
|
||||||
<Button
|
open={popUp.reviewChanges.isOpen}
|
||||||
size="xs"
|
onOpenChange={(isOpen) => handlePopUpToggle("reviewChanges", isOpen)}
|
||||||
leftIcon={hasApproved && <FontAwesomeIcon icon={faCheck} />}
|
>
|
||||||
onClick={() => handleSecretApprovalStatusUpdate(ApprovalStatus.APPROVED)}
|
<DropdownMenuTrigger asChild>
|
||||||
isLoading={isApproving}
|
<Button
|
||||||
isDisabled={isApproving || hasApproved || !canApprove}
|
variant="outline_bg"
|
||||||
>
|
rightIcon={<FontAwesomeIcon className="ml-2" icon={faAngleDown} />}
|
||||||
{hasApproved ? "Approved" : "Approve"}
|
>
|
||||||
</Button>
|
Review
|
||||||
<Button
|
</Button>
|
||||||
size="xs"
|
</DropdownMenuTrigger>
|
||||||
colorSchema="danger"
|
<DropdownMenuContent align="end" asChild className="mt-3">
|
||||||
leftIcon={hasRejected && <FontAwesomeIcon icon={faCheck} />}
|
<form onSubmit={handleSubmit(handleSubmitReview)}>
|
||||||
onClick={() => handleSecretApprovalStatusUpdate(ApprovalStatus.REJECTED)}
|
<div className="flex w-[400px] flex-col space-y-2 p-5">
|
||||||
isLoading={isRejecting}
|
<div className="font-medium">Finish your review</div>
|
||||||
isDisabled={isRejecting || hasRejected || !canApprove}
|
<Controller
|
||||||
>
|
control={control}
|
||||||
{hasRejected ? "Rejected" : "Reject"}
|
name="comment"
|
||||||
</Button>
|
render={({ field, fieldState: { error } }) => (
|
||||||
</>
|
<FormControl errorText={error?.message} isError={Boolean(error)}>
|
||||||
|
<TextArea
|
||||||
|
{...field}
|
||||||
|
placeholder="Leave a comment..."
|
||||||
|
reSize="none"
|
||||||
|
className="h-48 border border-mineshaft-600 bg-bunker-800"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="status"
|
||||||
|
defaultValue={ApprovalStatus.APPROVED}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl errorText={error?.message} isError={Boolean(error)}>
|
||||||
|
<RadioGroup
|
||||||
|
value={field.value}
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
className="mb-4 space-y-2"
|
||||||
|
aria-label="Status"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<RadioGroupItem
|
||||||
|
id="approve"
|
||||||
|
className="h-4 w-4 rounded-full border border-gray-300 text-primary focus:ring-2 focus:ring-mineshaft-500"
|
||||||
|
value={ApprovalStatus.APPROVED}
|
||||||
|
aria-labelledby="approve-label"
|
||||||
|
>
|
||||||
|
<RadioGroupIndicator className="flex h-full w-full items-center justify-center after:h-2 after:w-2 after:rounded-full after:bg-current" />
|
||||||
|
</RadioGroupItem>
|
||||||
|
<span
|
||||||
|
id="approve-label"
|
||||||
|
className="cursor-pointer"
|
||||||
|
onClick={() => field.onChange(ApprovalStatus.APPROVED)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter" || e.key === " ") {
|
||||||
|
e.preventDefault();
|
||||||
|
field.onChange(ApprovalStatus.APPROVED);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
tabIndex={0}
|
||||||
|
role="button"
|
||||||
|
>
|
||||||
|
Approve
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<RadioGroupItem
|
||||||
|
id="reject"
|
||||||
|
className="h-4 w-4 rounded-full border border-gray-300 text-primary focus:ring-2 focus:ring-mineshaft-500"
|
||||||
|
value={ApprovalStatus.REJECTED}
|
||||||
|
aria-labelledby="reject-label"
|
||||||
|
>
|
||||||
|
<RadioGroupIndicator className="flex h-full w-full items-center justify-center after:h-2 after:w-2 after:rounded-full after:bg-current" />
|
||||||
|
</RadioGroupItem>
|
||||||
|
<span
|
||||||
|
id="reject-label"
|
||||||
|
className="cursor-pointer"
|
||||||
|
onClick={() => field.onChange(ApprovalStatus.REJECTED)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter" || e.key === " ") {
|
||||||
|
e.preventDefault();
|
||||||
|
field.onChange(ApprovalStatus.REJECTED);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
tabIndex={0}
|
||||||
|
role="button"
|
||||||
|
>
|
||||||
|
Reject
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</RadioGroup>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
isLoading={isApproving || isRejecting || isSubmitting}
|
||||||
|
variant="outline_bg"
|
||||||
|
>
|
||||||
|
Submit Review
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col space-y-4">
|
<div className="flex flex-col space-y-4">
|
||||||
@@ -258,7 +390,7 @@ export const SecretApprovalRequestChanges = ({
|
|||||||
<div className="text-sm text-bunker-300">Reviewers</div>
|
<div className="text-sm text-bunker-300">Reviewers</div>
|
||||||
<div className="mt-2 flex flex-col space-y-2 text-sm">
|
<div className="mt-2 flex flex-col space-y-2 text-sm">
|
||||||
{secretApprovalRequestDetails?.policy?.approvers.map((requiredApprover) => {
|
{secretApprovalRequestDetails?.policy?.approvers.map((requiredApprover) => {
|
||||||
const status = reviewedUsers?.[requiredApprover.userId];
|
const reviewer = reviewedUsers?.[requiredApprover.userId];
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="flex flex-nowrap items-center space-x-2 rounded bg-mineshaft-800 px-2 py-1"
|
className="flex flex-nowrap items-center space-x-2 rounded bg-mineshaft-800 px-2 py-1"
|
||||||
@@ -275,8 +407,17 @@ export const SecretApprovalRequestChanges = ({
|
|||||||
<span className="text-red">*</span>
|
<span className="text-red">*</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Tooltip content={status || ApprovalStatus.PENDING}>
|
{reviewer?.comment && (
|
||||||
{getReviewedStatusSymbol(status)}
|
<Tooltip content={reviewer.comment}>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faComment}
|
||||||
|
size="xs"
|
||||||
|
className="mr-1 text-mineshaft-300"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
<Tooltip content={reviewer?.status || ApprovalStatus.PENDING}>
|
||||||
|
{getReviewedStatusSymbol(reviewer?.status)}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -290,7 +431,7 @@ export const SecretApprovalRequestChanges = ({
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
.map((reviewer) => {
|
.map((reviewer) => {
|
||||||
const status = reviewedUsers?.[reviewer.userId];
|
const status = reviewedUsers?.[reviewer.userId].status;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="flex flex-nowrap items-center space-x-2 rounded bg-mineshaft-800 px-2 py-1"
|
className="flex flex-nowrap items-center space-x-2 rounded bg-mineshaft-800 px-2 py-1"
|
||||||
@@ -303,6 +444,15 @@ export const SecretApprovalRequestChanges = ({
|
|||||||
<span className="text-red">*</span>
|
<span className="text-red">*</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
{reviewer.comment && (
|
||||||
|
<Tooltip content={reviewer.comment}>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faComment}
|
||||||
|
size="xs"
|
||||||
|
className="mr-1 text-mineshaft-300"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
<Tooltip content={status || ApprovalStatus.PENDING}>
|
<Tooltip content={status || ApprovalStatus.PENDING}>
|
||||||
{getReviewedStatusSymbol(status)}
|
{getReviewedStatusSymbol(status)}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
Reference in New Issue
Block a user