mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
feat: ui changes for secret approval group
This commit is contained in:
@@ -9,12 +9,12 @@ export const useCreateSecretApprovalPolicy = () => {
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation<{}, {}, TCreateSecretPolicyDTO>({
|
return useMutation<{}, {}, TCreateSecretPolicyDTO>({
|
||||||
mutationFn: async ({ environment, workspaceId, approvals, approvers, secretPath, name }) => {
|
mutationFn: async ({ environment, workspaceId, approvals, approverUserIds, secretPath, name }) => {
|
||||||
const { data } = await apiRequest.post("/api/v1/secret-approvals", {
|
const { data } = await apiRequest.post("/api/v1/secret-approvals", {
|
||||||
environment,
|
environment,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
approvals,
|
approvals,
|
||||||
approvers,
|
approverUserIds,
|
||||||
secretPath,
|
secretPath,
|
||||||
name
|
name
|
||||||
});
|
});
|
||||||
@@ -30,10 +30,10 @@ export const useUpdateSecretApprovalPolicy = () => {
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation<{}, {}, TUpdateSecretPolicyDTO>({
|
return useMutation<{}, {}, TUpdateSecretPolicyDTO>({
|
||||||
mutationFn: async ({ id, approvers, approvals, secretPath, name }) => {
|
mutationFn: async ({ id, approverUserIds, approvals, secretPath, name }) => {
|
||||||
const { data } = await apiRequest.patch(`/api/v1/secret-approvals/${id}`, {
|
const { data } = await apiRequest.patch(`/api/v1/secret-approvals/${id}`, {
|
||||||
approvals,
|
approvals,
|
||||||
approvers,
|
approverUserIds,
|
||||||
secretPath,
|
secretPath,
|
||||||
name
|
name
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ export type TSecretApprovalPolicy = {
|
|||||||
envId: string;
|
envId: string;
|
||||||
environment: WorkspaceEnv;
|
environment: WorkspaceEnv;
|
||||||
secretPath?: string;
|
secretPath?: string;
|
||||||
approvers: string[];
|
|
||||||
approvals: number;
|
approvals: number;
|
||||||
|
userApprovers: { userId: string }[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TGetSecretApprovalPoliciesDTO = {
|
export type TGetSecretApprovalPoliciesDTO = {
|
||||||
@@ -26,14 +26,14 @@ export type TCreateSecretPolicyDTO = {
|
|||||||
name?: string;
|
name?: string;
|
||||||
environment: string;
|
environment: string;
|
||||||
secretPath?: string | null;
|
secretPath?: string | null;
|
||||||
approvers?: string[];
|
approverUserIds?: string[];
|
||||||
approvals?: number;
|
approvals?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TUpdateSecretPolicyDTO = {
|
export type TUpdateSecretPolicyDTO = {
|
||||||
id: string;
|
id: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
approvers?: string[];
|
approverUserIds?: string[];
|
||||||
secretPath?: string | null;
|
secretPath?: string | null;
|
||||||
approvals?: number;
|
approvals?: number;
|
||||||
// for invalidating list
|
// for invalidating list
|
||||||
|
|||||||
@@ -47,10 +47,14 @@ export type TSecretApprovalRequest<J extends unknown = EncryptedSecret> = {
|
|||||||
isReplicated?: boolean;
|
isReplicated?: boolean;
|
||||||
slug: string;
|
slug: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
committerId: string;
|
committerUserId: string;
|
||||||
reviewers: {
|
reviewers: {
|
||||||
member: string;
|
userId: string;
|
||||||
status: ApprovalStatus;
|
status: ApprovalStatus;
|
||||||
|
email: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
username: string;
|
||||||
}[];
|
}[];
|
||||||
workspace: string;
|
workspace: string;
|
||||||
environment: string;
|
environment: string;
|
||||||
@@ -58,8 +62,30 @@ export type TSecretApprovalRequest<J extends unknown = EncryptedSecret> = {
|
|||||||
secretPath: string;
|
secretPath: string;
|
||||||
hasMerged: boolean;
|
hasMerged: boolean;
|
||||||
status: "open" | "close";
|
status: "open" | "close";
|
||||||
policy: TSecretApprovalPolicy;
|
policy: Omit<TSecretApprovalPolicy, "approvers"> & {
|
||||||
statusChangeBy: string;
|
approvers: {
|
||||||
|
userId: string;
|
||||||
|
email: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
username: string;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
statusChangedByUserId: string;
|
||||||
|
statusChangedByUser?: {
|
||||||
|
userId: string;
|
||||||
|
email: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
username: string;
|
||||||
|
};
|
||||||
|
committerUser: {
|
||||||
|
userId: string;
|
||||||
|
email: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
username: string;
|
||||||
|
};
|
||||||
conflicts: Array<{ secretId: string; op: CommitType.UPDATE }>;
|
conflicts: Array<{ secretId: string; op: CommitType.UPDATE }>;
|
||||||
commits: ({
|
commits: ({
|
||||||
// if there is no secret means it was creation
|
// if there is no secret means it was creation
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
DeleteActionModal,
|
DeleteActionModal,
|
||||||
EmptyState,
|
EmptyState,
|
||||||
|
Modal,
|
||||||
|
ModalContent,
|
||||||
Table,
|
Table,
|
||||||
TableContainer,
|
TableContainer,
|
||||||
TableSkeleton,
|
TableSkeleton,
|
||||||
@@ -145,13 +147,20 @@ export const SecretApprovalPolicyList = ({ workspaceId }: Props) => {
|
|||||||
</TBody>
|
</TBody>
|
||||||
</Table>
|
</Table>
|
||||||
</TableContainer>
|
</TableContainer>
|
||||||
<SecretPolicyForm
|
<Modal
|
||||||
workspaceId={workspaceId}
|
|
||||||
isOpen={popUp.secretPolicyForm.isOpen}
|
isOpen={popUp.secretPolicyForm.isOpen}
|
||||||
onToggle={(isOpen) => handlePopUpToggle("secretPolicyForm", isOpen)}
|
onOpenChange={(isOpen) => handlePopUpToggle("secretPolicyForm", isOpen)}
|
||||||
members={members}
|
>
|
||||||
editValues={popUp.secretPolicyForm.data as TSecretApprovalPolicy}
|
<ModalContent title={popUp.secretPolicyForm.data ? "Edit policy" : "Create policy"}>
|
||||||
/>
|
<SecretPolicyForm
|
||||||
|
workspaceId={workspaceId}
|
||||||
|
isOpen={popUp.secretPolicyForm.isOpen}
|
||||||
|
onToggle={(isOpen) => handlePopUpToggle("secretPolicyForm", isOpen)}
|
||||||
|
members={members}
|
||||||
|
editValues={popUp.secretPolicyForm.data as TSecretApprovalPolicy}
|
||||||
|
/>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
<DeleteActionModal
|
<DeleteActionModal
|
||||||
isOpen={popUp.deletePolicy.isOpen}
|
isOpen={popUp.deletePolicy.isOpen}
|
||||||
deleteKey="remove"
|
deleteKey="remove"
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export const SecretApprovalPolicyRow = ({
|
|||||||
{
|
{
|
||||||
workspaceId,
|
workspaceId,
|
||||||
id: policy.id,
|
id: policy.id,
|
||||||
approvers: selectedApprovers
|
approverUserIds: selectedApprovers
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
onSettled: () => {
|
onSettled: () => {
|
||||||
@@ -60,7 +60,7 @@ export const SecretApprovalPolicyRow = ({
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
setSelectedApprovers(policy.approvers);
|
setSelectedApprovers(policy.userApprovers.map(({ userId }) => userId));
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -73,7 +73,9 @@ export const SecretApprovalPolicyRow = ({
|
|||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
isReadOnly
|
isReadOnly
|
||||||
value={policy.approvers?.length ? `${policy.approvers.length} selected` : "None"}
|
value={
|
||||||
|
policy?.userApprovers.length ? `${policy.userApprovers.length} selected` : "None"
|
||||||
|
}
|
||||||
className="text-left"
|
className="text-left"
|
||||||
/>
|
/>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
@@ -84,17 +86,17 @@ export const SecretApprovalPolicyRow = ({
|
|||||||
<DropdownMenuLabel>
|
<DropdownMenuLabel>
|
||||||
Select members that are allowed to approve changes
|
Select members that are allowed to approve changes
|
||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
{members?.map(({ id, user }) => {
|
{members?.map(({ user }) => {
|
||||||
const isChecked = selectedApprovers.includes(id);
|
const isChecked = selectedApprovers.includes(user.id);
|
||||||
return (
|
return (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={(evt) => {
|
onClick={(evt) => {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
setSelectedApprovers((state) =>
|
setSelectedApprovers((state) =>
|
||||||
isChecked ? state.filter((el) => el !== id) : [...state, id]
|
isChecked ? state.filter((el) => el !== user.id) : [...state, user.id]
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
key={`create-policy-members-${id}`}
|
key={`create-policy-members-${user.id}`}
|
||||||
iconPos="right"
|
iconPos="right"
|
||||||
icon={isChecked && <FontAwesomeIcon icon={faCheckCircle} />}
|
icon={isChecked && <FontAwesomeIcon icon={faCheckCircle} />}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { useEffect } from "react";
|
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import { faCheckCircle } from "@fortawesome/free-solid-svg-icons";
|
import { faCheckCircle } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
@@ -15,8 +14,6 @@ import {
|
|||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
FormControl,
|
FormControl,
|
||||||
Input,
|
Input,
|
||||||
Modal,
|
|
||||||
ModalContent,
|
|
||||||
Select,
|
Select,
|
||||||
SelectItem
|
SelectItem
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
@@ -40,9 +37,9 @@ const formSchema = z
|
|||||||
name: z.string().optional(),
|
name: z.string().optional(),
|
||||||
secretPath: z.string().optional().nullable(),
|
secretPath: z.string().optional().nullable(),
|
||||||
approvals: z.number().min(1),
|
approvals: z.number().min(1),
|
||||||
approvers: z.string().array().min(1)
|
approverUserIds: z.string().array().min(1)
|
||||||
})
|
})
|
||||||
.refine((data) => data.approvals <= data.approvers.length, {
|
.refine((data) => data.approvals <= data.approverUserIds.length, {
|
||||||
path: ["approvals"],
|
path: ["approvals"],
|
||||||
message: "The number of approvals should be lower than the number of approvers."
|
message: "The number of approvals should be lower than the number of approvers."
|
||||||
});
|
});
|
||||||
@@ -50,7 +47,6 @@ const formSchema = z
|
|||||||
type TFormSchema = z.infer<typeof formSchema>;
|
type TFormSchema = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
export const SecretPolicyForm = ({
|
export const SecretPolicyForm = ({
|
||||||
isOpen,
|
|
||||||
onToggle,
|
onToggle,
|
||||||
members = [],
|
members = [],
|
||||||
workspaceId,
|
workspaceId,
|
||||||
@@ -59,20 +55,22 @@ export const SecretPolicyForm = ({
|
|||||||
const {
|
const {
|
||||||
control,
|
control,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
reset,
|
|
||||||
watch,
|
watch,
|
||||||
formState: { isSubmitting }
|
formState: { isSubmitting }
|
||||||
} = useForm<TFormSchema>({
|
} = useForm<TFormSchema>({
|
||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
values: editValues ? { ...editValues, environment: editValues.environment.slug } : undefined
|
values: editValues
|
||||||
|
? {
|
||||||
|
...editValues,
|
||||||
|
approverUserIds: editValues.userApprovers.map(({ userId }) => userId),
|
||||||
|
environment: editValues.environment.slug
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
});
|
});
|
||||||
const { currentWorkspace } = useWorkspace();
|
const { currentWorkspace } = useWorkspace();
|
||||||
const selectedEnvironment = watch("environment");
|
const selectedEnvironment = watch("environment");
|
||||||
|
|
||||||
const environments = currentWorkspace?.environments || [];
|
const environments = currentWorkspace?.environments || [];
|
||||||
useEffect(() => {
|
|
||||||
if (!isOpen) reset({});
|
|
||||||
}, [isOpen]);
|
|
||||||
|
|
||||||
const isEditMode = Boolean(editValues);
|
const isEditMode = Boolean(editValues);
|
||||||
|
|
||||||
@@ -131,8 +129,6 @@ export const SecretPolicyForm = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={isOpen} onOpenChange={onToggle}>
|
|
||||||
<ModalContent title={isEditMode ? "Edit policy" : "Create policy"}>
|
|
||||||
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
@@ -155,6 +151,7 @@ export const SecretPolicyForm = ({
|
|||||||
errorText={error?.message}
|
errorText={error?.message}
|
||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
|
isDisabled={isEditMode}
|
||||||
value={value}
|
value={value}
|
||||||
onValueChange={(val) => onChange(val)}
|
onValueChange={(val) => onChange(val)}
|
||||||
className="w-full border border-mineshaft-500"
|
className="w-full border border-mineshaft-500"
|
||||||
@@ -186,7 +183,7 @@ export const SecretPolicyForm = ({
|
|||||||
/>
|
/>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="approvers"
|
name="approverUserIds"
|
||||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
<FormControl
|
<FormControl
|
||||||
label="Approvers Required"
|
label="Approvers Required"
|
||||||
@@ -208,17 +205,19 @@ export const SecretPolicyForm = ({
|
|||||||
<DropdownMenuLabel>
|
<DropdownMenuLabel>
|
||||||
Select members that are allowed to approve changes
|
Select members that are allowed to approve changes
|
||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
{members.map(({ id, user }) => {
|
{members.map(({ user }) => {
|
||||||
const isChecked = value?.includes(id);
|
const isChecked = value?.includes(user.id);
|
||||||
return (
|
return (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={(evt) => {
|
onClick={(evt) => {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
onChange(
|
onChange(
|
||||||
isChecked ? value?.filter((el) => el !== id) : [...(value || []), id]
|
isChecked
|
||||||
|
? value?.filter((el) => el !== user.id)
|
||||||
|
: [...(value || []), user.id]
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
key={`create-policy-members-${id}`}
|
key={`create-policy-members-${user.id}`}
|
||||||
iconPos="right"
|
iconPos="right"
|
||||||
icon={isChecked && <FontAwesomeIcon icon={faCheckCircle} />}
|
icon={isChecked && <FontAwesomeIcon icon={faCheckCircle} />}
|
||||||
>
|
>
|
||||||
@@ -258,7 +257,6 @@ export const SecretPolicyForm = ({
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ import {
|
|||||||
useGetSecretApprovalRequests,
|
useGetSecretApprovalRequests,
|
||||||
useGetWorkspaceUsers
|
useGetWorkspaceUsers
|
||||||
} from "@app/hooks/api";
|
} from "@app/hooks/api";
|
||||||
import { ApprovalStatus, TSecretApprovalRequest, TWorkspaceUser } from "@app/hooks/api/types";
|
import { ApprovalStatus, TSecretApprovalRequest } from "@app/hooks/api/types";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
generateCommitText,
|
generateCommitText,
|
||||||
@@ -63,14 +63,9 @@ export const SecretApprovalRequest = () => {
|
|||||||
});
|
});
|
||||||
const { data: secretApprovalRequestCount, isSuccess: isSecretApprovalReqCountSuccess } =
|
const { data: secretApprovalRequestCount, isSuccess: isSecretApprovalReqCountSuccess } =
|
||||||
useGetSecretApprovalRequestCount({ workspaceId });
|
useGetSecretApprovalRequestCount({ workspaceId });
|
||||||
const { user: presentUser } = useUser();
|
const { user: userSession } = useUser();
|
||||||
const { permission } = useProjectPermission();
|
const { permission } = useProjectPermission();
|
||||||
const { data: members } = useGetWorkspaceUsers(workspaceId);
|
const { data: members } = useGetWorkspaceUsers(workspaceId);
|
||||||
const membersGroupById = members?.reduce<Record<string, TWorkspaceUser>>(
|
|
||||||
(prev, curr) => ({ ...prev, [curr.id]: curr }),
|
|
||||||
{}
|
|
||||||
);
|
|
||||||
const myMembershipId = members?.find(({ user }) => user.id === presentUser?.id)?.id;
|
|
||||||
const isSecretApprovalScreen = Boolean(selectedApproval);
|
const isSecretApprovalScreen = Boolean(selectedApproval);
|
||||||
|
|
||||||
const handleGoBackSecretRequestDetail = () => {
|
const handleGoBackSecretRequestDetail = () => {
|
||||||
@@ -93,10 +88,8 @@ export const SecretApprovalRequest = () => {
|
|||||||
>
|
>
|
||||||
<SecretApprovalRequestChanges
|
<SecretApprovalRequestChanges
|
||||||
workspaceId={workspaceId}
|
workspaceId={workspaceId}
|
||||||
members={membersGroupById}
|
|
||||||
approvalRequestId={selectedApproval?.id || ""}
|
approvalRequestId={selectedApproval?.id || ""}
|
||||||
onGoBack={handleGoBackSecretRequestDetail}
|
onGoBack={handleGoBackSecretRequestDetail}
|
||||||
committer={membersGroupById?.[selectedApproval?.committerId || ""]}
|
|
||||||
/>
|
/>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
) : (
|
) : (
|
||||||
@@ -182,10 +175,12 @@ export const SecretApprovalRequest = () => {
|
|||||||
{members?.map(({ user, id }) => (
|
{members?.map(({ user, id }) => (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setCommitterFilter((state) => (state === id ? undefined : id))
|
setCommitterFilter((state) => (state === user.id ? undefined : user.id))
|
||||||
}
|
}
|
||||||
key={`request-filter-member-${id}`}
|
key={`request-filter-member-${id}`}
|
||||||
icon={committerFilter === id && <FontAwesomeIcon icon={faCheckCircle} />}
|
icon={
|
||||||
|
committerFilter === user.id && <FontAwesomeIcon icon={faCheckCircle} />
|
||||||
|
}
|
||||||
iconPos="right"
|
iconPos="right"
|
||||||
>
|
>
|
||||||
{user.username}
|
{user.username}
|
||||||
@@ -208,19 +203,16 @@ export const SecretApprovalRequest = () => {
|
|||||||
const {
|
const {
|
||||||
id: reqId,
|
id: reqId,
|
||||||
commits,
|
commits,
|
||||||
committerId,
|
|
||||||
createdAt,
|
createdAt,
|
||||||
policy,
|
|
||||||
reviewers,
|
reviewers,
|
||||||
status,
|
status,
|
||||||
|
committerUser,
|
||||||
isReplicated: isReplication
|
isReplicated: isReplication
|
||||||
} = secretApproval;
|
} = secretApproval;
|
||||||
const isApprover = policy?.approvers?.indexOf(myMembershipId || "") !== -1;
|
const isReviewed = reviewers.some(
|
||||||
const isReviewed =
|
({ status: reviewStatus, userId }) =>
|
||||||
reviewers.findIndex(
|
userId === userSession.id && reviewStatus === ApprovalStatus.APPROVED
|
||||||
({ member, status: reviewStatus }) =>
|
);
|
||||||
member === myMembershipId && reviewStatus === ApprovalStatus.APPROVED
|
|
||||||
) !== -1;
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={reqId}
|
key={reqId}
|
||||||
@@ -239,11 +231,9 @@ export const SecretApprovalRequest = () => {
|
|||||||
</div>
|
</div>
|
||||||
<span className="text-xs text-gray-500">
|
<span className="text-xs text-gray-500">
|
||||||
Opened {formatDistance(new Date(createdAt), new Date())} ago by{" "}
|
Opened {formatDistance(new Date(createdAt), new Date())} ago by{" "}
|
||||||
{membersGroupById?.[committerId]?.user?.firstName}{" "}
|
{committerUser?.firstName || ""} {committerUser?.lastName || ""} (
|
||||||
{membersGroupById?.[committerId]?.user?.lastName} (
|
{committerUser?.email}){isReplication && " via replication"}
|
||||||
{membersGroupById?.[committerId]?.user?.email})
|
{!isReviewed && status === "open" && " - Review required"}
|
||||||
{isReplication && " via replication"}
|
|
||||||
{isApprover && !isReviewed && status === "open" && " - Review required"}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ type Props = {
|
|||||||
status: "close" | "open";
|
status: "close" | "open";
|
||||||
approvals: number;
|
approvals: number;
|
||||||
canApprove?: boolean;
|
canApprove?: boolean;
|
||||||
statusChangeByEmail: string;
|
statusChangeByEmail?: string;
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import {
|
|||||||
useGetUserWsKey,
|
useGetUserWsKey,
|
||||||
useUpdateSecretApprovalReviewStatus
|
useUpdateSecretApprovalReviewStatus
|
||||||
} from "@app/hooks/api";
|
} from "@app/hooks/api";
|
||||||
import { ApprovalStatus, CommitType, TWorkspaceUser } from "@app/hooks/api/types";
|
import { ApprovalStatus, CommitType } from "@app/hooks/api/types";
|
||||||
import { formatReservedPaths } from "@app/lib/fn/string";
|
import { formatReservedPaths } from "@app/lib/fn/string";
|
||||||
|
|
||||||
import { SecretApprovalRequestAction } from "./SecretApprovalRequestAction";
|
import { SecretApprovalRequestAction } from "./SecretApprovalRequestAction";
|
||||||
@@ -73,18 +73,14 @@ type Props = {
|
|||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
approvalRequestId: string;
|
approvalRequestId: string;
|
||||||
onGoBack: () => void;
|
onGoBack: () => void;
|
||||||
committer?: TWorkspaceUser;
|
|
||||||
members?: Record<string, TWorkspaceUser>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SecretApprovalRequestChanges = ({
|
export const SecretApprovalRequestChanges = ({
|
||||||
approvalRequestId,
|
approvalRequestId,
|
||||||
onGoBack,
|
onGoBack,
|
||||||
committer,
|
workspaceId
|
||||||
workspaceId,
|
|
||||||
members = {}
|
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { user } = useUser();
|
const { user: userSession } = useUser();
|
||||||
const { data: decryptFileKey } = useGetUserWsKey(workspaceId);
|
const { data: decryptFileKey } = useGetUserWsKey(workspaceId);
|
||||||
const {
|
const {
|
||||||
data: secretApprovalRequestDetails,
|
data: secretApprovalRequestDetails,
|
||||||
@@ -105,22 +101,20 @@ export const SecretApprovalRequestChanges = ({
|
|||||||
const isRejecting = variables?.status === ApprovalStatus.REJECTED && isUpdatingRequestStatus;
|
const isRejecting = variables?.status === ApprovalStatus.REJECTED && isUpdatingRequestStatus;
|
||||||
|
|
||||||
// membership of present user
|
// membership of present user
|
||||||
const myMembership = Object.values(members).find(
|
const canApprove = secretApprovalRequestDetails?.policy?.approvers?.some(
|
||||||
({ user: membershipUser }) => membershipUser.email === user.email
|
({ userId }) => userId === userSession.id
|
||||||
);
|
);
|
||||||
const myMembershipId = myMembership?.id || "";
|
const reviewedUsers = secretApprovalRequestDetails?.reviewers?.reduce<
|
||||||
const canApprove = secretApprovalRequestDetails?.policy?.approvers?.includes(myMembershipId);
|
|
||||||
const reviewedMembers = secretApprovalRequestDetails?.reviewers?.reduce<
|
|
||||||
Record<string, ApprovalStatus>
|
Record<string, ApprovalStatus>
|
||||||
>(
|
>(
|
||||||
(prev, curr) => ({
|
(prev, curr) => ({
|
||||||
...prev,
|
...prev,
|
||||||
[curr.member]: curr.status
|
[curr.userId]: curr.status
|
||||||
}),
|
}),
|
||||||
{}
|
{}
|
||||||
);
|
);
|
||||||
const hasApproved = reviewedMembers?.[myMembershipId] === ApprovalStatus.APPROVED;
|
const hasApproved = reviewedUsers?.[userSession.id] === ApprovalStatus.APPROVED;
|
||||||
const hasRejected = reviewedMembers?.[myMembershipId] === ApprovalStatus.REJECTED;
|
const hasRejected = reviewedUsers?.[userSession.id] === ApprovalStatus.REJECTED;
|
||||||
|
|
||||||
const handleSecretApprovalStatusUpdate = async (status: ApprovalStatus) => {
|
const handleSecretApprovalStatusUpdate = async (status: ApprovalStatus) => {
|
||||||
try {
|
try {
|
||||||
@@ -159,7 +153,7 @@ export const SecretApprovalRequestChanges = ({
|
|||||||
const isMergable =
|
const isMergable =
|
||||||
secretApprovalRequestDetails?.policy?.approvals <=
|
secretApprovalRequestDetails?.policy?.approvals <=
|
||||||
secretApprovalRequestDetails?.policy?.approvers?.filter(
|
secretApprovalRequestDetails?.policy?.approvers?.filter(
|
||||||
(approverId) => reviewedMembers?.[approverId] === ApprovalStatus.APPROVED
|
({ userId }) => reviewedUsers?.[userId] === ApprovalStatus.APPROVED
|
||||||
).length;
|
).length;
|
||||||
const hasMerged = secretApprovalRequestDetails?.hasMerged;
|
const hasMerged = secretApprovalRequestDetails?.hasMerged;
|
||||||
|
|
||||||
@@ -191,8 +185,9 @@ export const SecretApprovalRequestChanges = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center text-sm text-bunker-300">
|
<div className="flex items-center text-sm text-bunker-300">
|
||||||
{committer?.user?.firstName}
|
{secretApprovalRequestDetails?.committerUser?.firstName || ""}
|
||||||
{committer?.user?.lastName} ({committer?.user?.email}) wants to change{" "}
|
{secretApprovalRequestDetails?.committerUser?.lastName || ""} (
|
||||||
|
{secretApprovalRequestDetails?.committerUser?.email}) wants to change{" "}
|
||||||
{secretApprovalRequestDetails.commits.length} secret values in
|
{secretApprovalRequestDetails.commits.length} secret values in
|
||||||
<span className="mx-1 rounded bg-primary-600/60 px-1 text-primary-300">
|
<span className="mx-1 rounded bg-primary-600/60 px-1 text-primary-300">
|
||||||
{secretApprovalRequestDetails.environment}
|
{secretApprovalRequestDetails.environment}
|
||||||
@@ -256,9 +251,7 @@ export const SecretApprovalRequestChanges = ({
|
|||||||
approvals={secretApprovalRequestDetails.policy.approvals || 0}
|
approvals={secretApprovalRequestDetails.policy.approvals || 0}
|
||||||
status={secretApprovalRequestDetails.status}
|
status={secretApprovalRequestDetails.status}
|
||||||
isMergable={isMergable}
|
isMergable={isMergable}
|
||||||
statusChangeByEmail={
|
statusChangeByEmail={secretApprovalRequestDetails.statusChangedByUser?.email}
|
||||||
members[secretApprovalRequestDetails?.statusChangeBy || ""]?.user?.email || ""
|
|
||||||
}
|
|
||||||
workspaceId={workspaceId}
|
workspaceId={workspaceId}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -266,17 +259,19 @@ export const SecretApprovalRequestChanges = ({
|
|||||||
<div className="sticky top-0 w-1/5 pt-4" style={{ minWidth: "240px" }}>
|
<div className="sticky top-0 w-1/5 pt-4" style={{ minWidth: "240px" }}>
|
||||||
<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((requiredApproverId) => {
|
{secretApprovalRequestDetails?.policy?.approvers.map((requiredApprover) => {
|
||||||
const userDetails = members?.[requiredApproverId]?.user;
|
const status = reviewedUsers?.[requiredApprover.userId];
|
||||||
const status = reviewedMembers?.[requiredApproverId];
|
|
||||||
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"
|
||||||
key={`required-approver-${requiredApproverId}`}
|
key={`required-approver-${requiredApprover.userId}`}
|
||||||
>
|
>
|
||||||
<div className="flex-grow text-sm">
|
<div className="flex-grow text-sm">
|
||||||
<Tooltip content={`${userDetails.firstName} ${userDetails.lastName}`}>
|
<Tooltip
|
||||||
<span>{userDetails?.email} </span>
|
content={`${requiredApprover.firstName || ""} ${requiredApprover.lastName || ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span>{requiredApprover?.email} </span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<span className="text-red">*</span>
|
<span className="text-red">*</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -290,19 +285,21 @@ export const SecretApprovalRequestChanges = ({
|
|||||||
})}
|
})}
|
||||||
{secretApprovalRequestDetails?.reviewers
|
{secretApprovalRequestDetails?.reviewers
|
||||||
.filter(
|
.filter(
|
||||||
({ member }) => !secretApprovalRequestDetails?.policy?.approvers?.includes(member)
|
(reviewer) =>
|
||||||
|
!secretApprovalRequestDetails?.policy?.approvers?.some(
|
||||||
|
({ userId }) => userId === reviewer.userId
|
||||||
|
)
|
||||||
)
|
)
|
||||||
.map((reviewer) => {
|
.map((reviewer) => {
|
||||||
const userDetails = members?.[reviewer.member]?.user;
|
const status = reviewedUsers?.[reviewer.userId];
|
||||||
const status = reviewedMembers?.[reviewer.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"
|
||||||
key={`required-approver-${reviewer.member}`}
|
key={`required-approver-${reviewer.userId}`}
|
||||||
>
|
>
|
||||||
<div className="flex-grow text-sm">
|
<div className="flex-grow text-sm">
|
||||||
<Tooltip content={`${userDetails.firstName} ${userDetails.lastName}`}>
|
<Tooltip content={`${reviewer.firstName || ""} ${reviewer.lastName || ""}`}>
|
||||||
<span>{userDetails?.email} </span>
|
<span>{reviewer?.email} </span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<span className="text-red">*</span>
|
<span className="text-red">*</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user