feat(secret-approval): resolved infinite query bug and added support for closing, re-opening request, stale req ui

This commit is contained in:
Akhil Mohan
2023-10-05 20:34:50 +05:30
parent 63588b4e44
commit 04c7d49477
15 changed files with 500 additions and 233 deletions
@@ -35,7 +35,7 @@ export const getSecretApprovalRequests = async (req: Request, res: Response) =>
.skip(offset) .skip(offset)
.populate("policy") .populate("policy")
.lean(); .lean();
if (!approvalRequests.length) return res.send({ requests: [] }); if (!approvalRequests.length) return res.send({ approvals: [] });
const unqiueEnvs = environment ?? { const unqiueEnvs = environment ?? {
$in: [...new Set(approvalRequests.map(({ environment }) => environment))] $in: [...new Set(approvalRequests.map(({ environment }) => environment))]
@@ -64,7 +64,7 @@ export const getSecretApprovalRequestDetails = async (req: Request, res: Respons
params: { id } params: { id }
} = await validateRequest(reqValidator.getSecretApprovalRequestDetails, req); } = await validateRequest(reqValidator.getSecretApprovalRequestDetails, req);
const secretApprovalRequest = await SecretApprovalRequest.findById(id) const secretApprovalRequest = await SecretApprovalRequest.findById(id)
.populate("policy") .populate<{ policy: ISecretApprovalPolicy }>("policy")
.populate({ .populate({
path: "commits.secretVersion", path: "commits.secretVersion",
populate: { populate: {
@@ -84,7 +84,9 @@ export const getSecretApprovalRequestDetails = async (req: Request, res: Respons
if ( if (
membership.role !== "admin" && membership.role !== "admin" &&
secretApprovalRequest.committer !== membership.id && secretApprovalRequest.committer !== membership.id &&
secretApprovalRequest.reviewers.find(({ member }) => member === membership.id) !secretApprovalRequest.policy.approvers.find(
(approverId) => approverId.toString() === membership._id.toString()
)
) { ) {
throw UnauthorizedRequestError({ message: "User has no access" }); throw UnauthorizedRequestError({ message: "User has no access" });
} }
@@ -94,11 +96,11 @@ export const getSecretApprovalRequestDetails = async (req: Request, res: Respons
}); });
}; };
export const updateSecretApprovalRequestStatus = async (req: Request, res: Response) => { export const updateSecretApprovalReviewStatus = async (req: Request, res: Response) => {
const { const {
body: { status }, body: { status },
params: { id } params: { id }
} = await validateRequest(reqValidator.updateSecretApprovalRequestStatus, req); } = await validateRequest(reqValidator.updateSecretApprovalReviewStatus, req);
const secretApprovalRequest = await SecretApprovalRequest.findById(id).populate<{ const secretApprovalRequest = await SecretApprovalRequest.findById(id).populate<{
policy: ISecretApprovalPolicy; policy: ISecretApprovalPolicy;
}>("policy"); }>("policy");
@@ -132,7 +134,7 @@ export const updateSecretApprovalRequestStatus = async (req: Request, res: Respo
export const mergeSecretApprovalRequest = async (req: Request, res: Response) => { export const mergeSecretApprovalRequest = async (req: Request, res: Response) => {
const { const {
body: { id } params: { id }
} = await validateRequest(reqValidator.mergeSecretApprovalRequest, req); } = await validateRequest(reqValidator.mergeSecretApprovalRequest, req);
const secretApprovalRequest = await SecretApprovalRequest.findById(id).populate<{ const secretApprovalRequest = await SecretApprovalRequest.findById(id).populate<{
@@ -149,7 +151,7 @@ export const mergeSecretApprovalRequest = async (req: Request, res: Response) =>
if ( if (
membership.role !== "admin" && membership.role !== "admin" &&
secretApprovalRequest.committer !== membership.id && secretApprovalRequest.committer !== membership.id &&
!secretApprovalRequest.policy.approvers.find((approverId) => approverId === membership.id) !secretApprovalRequest.policy.approvers.find((approverId) => approverId.equals(membership.id))
) { ) {
throw UnauthorizedRequestError({ message: "User has no access" }); throw UnauthorizedRequestError({ message: "User has no access" });
} }
@@ -166,6 +168,51 @@ export const mergeSecretApprovalRequest = async (req: Request, res: Response) =>
if (!hasMinApproval) throw BadRequestError({ message: "Doesn't have minimum approvals needed" }); if (!hasMinApproval) throw BadRequestError({ message: "Doesn't have minimum approvals needed" });
const approval = await performSecretApprovalRequestMerge(id, req.authData); const approval = await performSecretApprovalRequestMerge(
id,
req.authData,
membership._id.toString()
);
return res.send({ approval }); return res.send({ approval });
}; };
export const updateSecretApprovalRequestStatus = async (req: Request, res: Response) => {
const {
body: { status },
params: { id }
} = await validateRequest(reqValidator.updateSecretApprovalRequestStatus, req);
const secretApprovalRequest = await SecretApprovalRequest.findById(id).populate<{
policy: ISecretApprovalPolicy;
}>("policy");
if (!secretApprovalRequest)
throw BadRequestError({ message: "Secret approval request not found" });
const { membership } = await getUserProjectPermissions(
req.user._id,
secretApprovalRequest.workspace.toString()
);
if (
membership.role !== "admin" &&
secretApprovalRequest.committer !== membership.id &&
!secretApprovalRequest.policy.approvers.find((approverId) => approverId.equals(membership._id))
) {
throw UnauthorizedRequestError({ message: "User has no access" });
}
if (secretApprovalRequest.hasMerged)
throw BadRequestError({ message: "Approval request has been merged" });
if (secretApprovalRequest.status === "close" && status === "close")
throw BadRequestError({ message: "Approval request is already closed" });
if (secretApprovalRequest.status === "open" && status === "open")
throw BadRequestError({ message: "Approval request is already open" });
const updatedRequest = await SecretApprovalRequest.findByIdAndUpdate(
id,
{ status, statusChangeBy: membership._id },
{ new: true }
);
return res.send({ approval: updatedRequest });
};
@@ -54,6 +54,7 @@ export type ISecretCommits<T = Types.ObjectId, J = Types.ObjectId> = Array<
export interface ISecretApprovalRequest { export interface ISecretApprovalRequest {
_id: Types.ObjectId; _id: Types.ObjectId;
committer: Types.ObjectId; committer: Types.ObjectId;
statusChangeBy: Types.ObjectId;
reviewers: { reviewers: {
member: Types.ObjectId; member: Types.ObjectId;
status: ApprovalStatus; status: ApprovalStatus;
@@ -159,6 +160,7 @@ const secretApprovalRequestSchema = new Schema<ISecretApprovalRequest>(
hasMerged: { type: Boolean, default: false }, hasMerged: { type: Boolean, default: false },
status: { type: String, enum: ["close", "open"], default: "open" }, status: { type: String, enum: ["close", "open"], default: "open" },
committer: { type: Schema.Types.ObjectId, ref: "Membership" }, committer: { type: Schema.Types.ObjectId, ref: "Membership" },
statusChangeBy: { type: Schema.Types.ObjectId, ref: "Membership" },
commits: [ commits: [
{ {
secret: { type: Types.ObjectId, ref: "Secret" }, secret: { type: Types.ObjectId, ref: "Secret" },
+10 -2
View File
@@ -21,7 +21,7 @@ router.get(
); );
router.post( router.post(
"/merge", "/:id/merge",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT] acceptedAuthModes: [AuthMode.JWT]
}), }),
@@ -29,7 +29,15 @@ router.post(
); );
router.post( router.post(
"/:id", "/:id/review",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
secretApprovalRequestController.updateSecretApprovalReviewStatus
);
router.post(
"/:id/status",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT] acceptedAuthModes: [AuthMode.JWT]
}), }),
@@ -317,7 +317,11 @@ export const generateSecretApprovalRequest = async ({
}; };
// validation for a merge conditions happen in another function in controller // validation for a merge conditions happen in another function in controller
export const performSecretApprovalRequestMerge = async (id: string, authData: AuthData) => { export const performSecretApprovalRequestMerge = async (
id: string,
authData: AuthData,
userMembershipId: string
) => {
const secretApprovalRequest = await SecretApprovalRequest.findById(id) const secretApprovalRequest = await SecretApprovalRequest.findById(id)
.populate<{ commits: ISecretCommits<ISecret> }>({ .populate<{ commits: ISecretCommits<ISecret> }>({
path: "commits.secret", path: "commits.secret",
@@ -636,7 +640,8 @@ export const performSecretApprovalRequestMerge = async (id: string, authData: Au
{ {
conflicts, conflicts,
hasMerged: true, hasMerged: true,
status: "close" status: "close",
statusChangeBy: userMembershipId
}, },
{ new: true } { new: true }
); );
@@ -18,7 +18,7 @@ export const getSecretApprovalRequestDetails = z.object({
}) })
}); });
export const updateSecretApprovalRequestStatus = z.object({ export const updateSecretApprovalReviewStatus = z.object({
body: z.object({ body: z.object({
status: z.enum([ApprovalStatus.APPROVED, ApprovalStatus.REJECTED]) status: z.enum([ApprovalStatus.APPROVED, ApprovalStatus.REJECTED])
}), }),
@@ -28,7 +28,16 @@ export const updateSecretApprovalRequestStatus = z.object({
}); });
export const mergeSecretApprovalRequest = z.object({ export const mergeSecretApprovalRequest = z.object({
body: z.object({ params: z.object({
id: z.string().trim() id: z.string().trim()
}) })
}); });
export const updateSecretApprovalRequestStatus = z.object({
params: z.object({
id: z.string().trim()
}),
body: z.object({
status: z.enum(["open", "close"])
})
});
@@ -11,6 +11,9 @@ type Props = {
isLoading?: boolean; isLoading?: boolean;
}; };
// refactor(akhilmhdh): both color and size variants are together need to split it
// colorSchema should handle all color class names
// variant should handle how the button padding and other types should be set
const buttonVariants = cva( const buttonVariants = cva(
[ [
"button", "button",
@@ -106,6 +109,12 @@ const buttonVariants = cva(
variant: "outline", variant: "outline",
className: "text-red hover:bg-red hover:text-black" className: "text-red hover:bg-red hover:text-black"
}, },
{
colorSchema: "danger",
variant: "outline_bg",
className:
"bg-mineshaft-600 border border-red-500 hover:bg-red/[0.1] hover:border-red/40 text-red-500"
},
{ {
colorSchema: "primary", colorSchema: "primary",
variant: "plain", variant: "plain",
@@ -1,5 +1,6 @@
export { export {
usePerformSecretApprovalRequestMerge, usePerformSecretApprovalRequestMerge,
useUpdateSecretApprovalRequestStatus useUpdateSecretApprovalRequestStatus,
useUpdateSecretApprovalReviewStatus
} from "./mutation"; } from "./mutation";
export { useGetSecretApprovalRequestDetails, useGetSecretApprovalRequests } from "./queries"; export { useGetSecretApprovalRequestDetails, useGetSecretApprovalRequests } from "./queries";
@@ -3,14 +3,36 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request"; import { apiRequest } from "@app/config/request";
import { secretApprovalRequestKeys } from "./queries"; import { secretApprovalRequestKeys } from "./queries";
import { TPerformSecretApprovalRequestMerge, TUpdateSecretApprovalRequestStatusDTO } from "./types"; import {
TPerformSecretApprovalRequestMerge,
TUpdateSecretApprovalRequestStatusDTO,
TUpdateSecretApprovalReviewStatusDTO
} from "./types";
export const useUpdateSecretApprovalReviewStatus = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, TUpdateSecretApprovalReviewStatusDTO>({
mutationFn: async ({ id, status }) => {
const { data } = await apiRequest.post(`/api/v1/secret-approval-requests/${id}/review`, {
status
});
return data;
},
onSuccess: (_, { id }) => {
queryClient.invalidateQueries(secretApprovalRequestKeys.detail({ id }));
}
});
};
export const useUpdateSecretApprovalRequestStatus = () => { export const useUpdateSecretApprovalRequestStatus = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation<{}, {}, TUpdateSecretApprovalRequestStatusDTO>({ return useMutation<{}, {}, TUpdateSecretApprovalRequestStatusDTO>({
mutationFn: async ({ id, status }) => { mutationFn: async ({ id, status }) => {
const { data } = await apiRequest.post(`/api/v1/secret-approval-requests/${id}`, { status }); const { data } = await apiRequest.post(`/api/v1/secret-approval-requests/${id}/status`, {
status
});
return data; return data;
}, },
onSuccess: (_, { id }) => { onSuccess: (_, { id }) => {
@@ -24,11 +46,12 @@ export const usePerformSecretApprovalRequestMerge = () => {
return useMutation<{}, {}, TPerformSecretApprovalRequestMerge>({ return useMutation<{}, {}, TPerformSecretApprovalRequestMerge>({
mutationFn: async ({ id }) => { mutationFn: async ({ id }) => {
const { data } = await apiRequest.post("/api/v1/secret-approval-requests/merge", { id }); const { data } = await apiRequest.post(`/api/v1/secret-approval-requests/${id}/merge`);
return data; return data;
}, },
onSuccess: (_, { id }) => { onSuccess: (_, { id, workspaceId }) => {
queryClient.invalidateQueries(secretApprovalRequestKeys.detail({ id })); queryClient.invalidateQueries(secretApprovalRequestKeys.detail({ id }));
queryClient.invalidateQueries(secretApprovalRequestKeys.list({ workspaceId }));
} }
}); });
}; };
@@ -58,6 +58,7 @@ export type TSecretApprovalRequest<
hasMerged: boolean; hasMerged: boolean;
status: "open" | "close"; status: "open" | "close";
policy: TSecretApprovalPolicy; policy: TSecretApprovalPolicy;
statusChangeBy: string;
commits: { commits: {
// if there is no secret means it was creation // if there is no secret means it was creation
secret?: { version: number }; secret?: { version: number };
@@ -82,11 +83,17 @@ export type TGetSecretApprovalRequestDetails = {
decryptKey: UserWsKeyPair; decryptKey: UserWsKeyPair;
}; };
export type TUpdateSecretApprovalRequestStatusDTO = { export type TUpdateSecretApprovalReviewStatusDTO = {
status: ApprovalStatus; status: ApprovalStatus;
id: string; id: string;
}; };
export type TUpdateSecretApprovalRequestStatusDTO = {
status: "open" | "close";
id: string;
};
export type TPerformSecretApprovalRequestMerge = { export type TPerformSecretApprovalRequestMerge = {
id: string; id: string;
workspaceId: string;
}; };
+2 -2
View File
@@ -482,11 +482,11 @@ export const AppLayout = ({ children }: LayoutProps) => {
<a> <a>
<MenuItem <MenuItem
isSelected={ isSelected={
router.asPath === `/project/${currentWorkspace?._id}/allowlist` router.asPath === `/project/${currentWorkspace?._id}/approval`
} }
icon="system-outline-126-verified" icon="system-outline-126-verified"
> >
Admin Panel Secret Approval
</MenuItem> </MenuItem>
</a> </a>
</Link> </Link>
@@ -39,7 +39,8 @@ export const SecretApprovalRequest = () => {
data: secretApprovalRequests, data: secretApprovalRequests,
isFetchingNextPage: isFetchingNextApprovalRequest, isFetchingNextPage: isFetchingNextApprovalRequest,
fetchNextPage: fetchNextApprovalRequest, fetchNextPage: fetchNextApprovalRequest,
hasNextPage: hasNextApprovalPage hasNextPage: hasNextApprovalPage,
refetch
} = useGetSecretApprovalRequests({ } = useGetSecretApprovalRequests({
workspaceId, workspaceId,
status: statusFilter, status: statusFilter,
@@ -51,9 +52,13 @@ export const SecretApprovalRequest = () => {
(prev, curr) => ({ ...prev, [curr._id]: curr }), (prev, curr) => ({ ...prev, [curr._id]: curr }),
{} {}
); );
const isSecretApprovalScreen = Boolean(selectedApproval); const isSecretApprovalScreen = Boolean(selectedApproval);
const handleGoBackSecretRequestDetail = () => {
setSelectedApproval(null);
refetch({ refetchPage: (_page, index) => index === 0 });
};
return ( return (
<AnimatePresence exitBeforeEnter> <AnimatePresence exitBeforeEnter>
{isSecretApprovalScreen ? ( {isSecretApprovalScreen ? (
@@ -68,7 +73,7 @@ export const SecretApprovalRequest = () => {
workspaceId={workspaceId} workspaceId={workspaceId}
members={membersGroupById} members={membersGroupById}
approvalRequestId={selectedApproval?._id || ""} approvalRequestId={selectedApproval?._id || ""}
onGoBack={() => setSelectedApproval(null)} onGoBack={handleGoBackSecretRequestDetail}
committer={membersGroupById?.[selectedApproval?.committer || ""]} committer={membersGroupById?.[selectedApproval?.committer || ""]}
/> />
</motion.div> </motion.div>
@@ -192,16 +197,18 @@ export const SecretApprovalRequest = () => {
</Fragment> </Fragment>
))} ))}
</div> </div>
<Button {hasNextApprovalPage && (
className="mt-4 text-sm" <Button
isFullWidth className="mt-4 text-sm"
variant="star" isFullWidth
isLoading={isFetchingNextApprovalRequest} variant="star"
isDisabled={isFetchingNextApprovalRequest || !hasNextApprovalPage} isLoading={isFetchingNextApprovalRequest}
onClick={() => fetchNextApprovalRequest()} isDisabled={isFetchingNextApprovalRequest || !hasNextApprovalPage}
> onClick={() => fetchNextApprovalRequest()}
{hasNextApprovalPage ? "Load More" : "End of history"} >
</Button> {hasNextApprovalPage ? "Load More" : "End of history"}
</Button>
)}
</motion.div> </motion.div>
)} )}
</AnimatePresence> </AnimatePresence>
@@ -1,47 +1,160 @@
import { faCheck, faClose } from "@fortawesome/free-solid-svg-icons"; import {
faCheck,
faClose,
faLockOpen,
faSquareCheck,
faSquareXmark,
faUserLock
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { Button } from "@app/components/v2"; import { Button } from "@app/components/v2";
import {
usePerformSecretApprovalRequestMerge,
useUpdateSecretApprovalRequestStatus
} from "@app/hooks/api";
type Props = { type Props = {
approvalRequestId: string;
hasMerged?: boolean; hasMerged?: boolean;
status: "close" | "open";
isMergable?: boolean; isMergable?: boolean;
isMerging?: boolean; status: "close" | "open";
onMerge: () => void; approvals: number;
onClose?: () => void; statusChangeByEmail: string;
workspaceId: string;
}; };
export const SecretApprovalRequestAction = ({ export const SecretApprovalRequestAction = ({
approvalRequestId,
hasMerged, hasMerged,
status, status,
isMergable, isMergable,
onMerge, approvals,
isMerging, statusChangeByEmail,
onClose workspaceId
}: Props) => { }: Props) => {
const { createNotification } = useNotificationContext();
const { mutateAsync: performSecretApprovalMerge, isLoading: isMerging } =
usePerformSecretApprovalRequestMerge();
const { mutateAsync: updateSecretStatusChange, isLoading: isStatusChanging } =
useUpdateSecretApprovalRequestStatus();
const handleSecretApprovalRequestMerge = async () => {
try {
await performSecretApprovalMerge({
id: approvalRequestId,
workspaceId
});
createNotification({
type: "success",
text: "Successfully merged the request"
});
} catch (err) {
console.log(err);
createNotification({
type: "error",
text: "Failed to update the request status"
});
}
};
const handleSecretApprovalStatusChange = async (reqState: "open" | "close") => {
try {
await updateSecretStatusChange({
id: approvalRequestId,
status: reqState
});
createNotification({
type: "success",
text: "Successfully updated the request"
});
} catch (err) {
console.log(err);
createNotification({
type: "error",
text: "Failed to update the request status"
});
}
};
if (!hasMerged && status === "open") { if (!hasMerged && status === "open") {
return ( return (
<> <div className="flex justify-between items-center w-full">
<Button <div className="flex space-x-4 items-start">
leftIcon={<FontAwesomeIcon icon={faCheck} />} <FontAwesomeIcon
isDisabled={!isMergable} icon={isMergable ? faSquareCheck : faSquareXmark}
isLoading={isMerging} className={twMerge("text-2xl pt-1", isMergable ? "text-primary" : "text-red-600")}
onClick={onMerge} />
> <span className="flex flex-col">
Merge {isMergable ? "Good to merge" : "Review required"}
</Button> <span className="inline-block text-xs text-bunker-200">
<Button At least {approvals} approving review required
onClick={onClose} {Boolean(statusChangeByEmail) && `. Reopened by ${statusChangeByEmail}`}
variant="outline_bg" </span>
leftIcon={<FontAwesomeIcon icon={faClose} />} </span>
> </div>
Close request <div className="flex items-center space-x-6">
</Button> <Button
</> leftIcon={<FontAwesomeIcon icon={faCheck} />}
isDisabled={!isMergable}
isLoading={isMerging}
onClick={handleSecretApprovalRequestMerge}
>
Merge
</Button>
<Button
onClick={() => handleSecretApprovalStatusChange("close")}
isLoading={isStatusChanging}
variant="outline_bg"
colorSchema="danger"
leftIcon={<FontAwesomeIcon icon={faClose} />}
>
Close request
</Button>
</div>
</div>
); );
} }
if (hasMerged && status === "close") return <span>This approval request has been merged</span>; if (hasMerged && status === "close")
return (
<div className="flex justify-between items-center w-full">
<div className="flex space-x-4 items-start">
<FontAwesomeIcon icon={faCheck} className="text-2xl text-primary pt-1" />
<span className="flex flex-col">
Change request merged
<span className="inline-block text-xs text-bunker-200">
Merged by {statusChangeByEmail}
</span>
</span>
</div>
</div>
);
return <span>This approval request has been closed</span>; return (
<div className="flex justify-between items-center w-full">
<div className="flex space-x-4 items-start">
<FontAwesomeIcon icon={faUserLock} className="text-2xl text-primary pt-1" />
<span className="flex flex-col">
Change request has been closed
<span className="inline-block text-xs text-bunker-200">
Closed by {statusChangeByEmail}
</span>
</span>
</div>
<div className="flex items-center space-x-6">
<Button
onClick={() => handleSecretApprovalStatusChange("open")}
isLoading={isStatusChanging}
variant="outline_bg"
leftIcon={<FontAwesomeIcon icon={faLockOpen} />}
>
Reopen request
</Button>
</div>
</div>
);
}; };
@@ -0,0 +1,154 @@
import { faFilePen } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
SecretInput,
Table,
TableContainer,
Tag,
TBody,
Td,
Th,
THead,
Tr
} from "@app/components/v2";
import { CommitType, DecryptedSecret, TSecretApprovalSecChange, WsTag } from "@app/hooks/api/types";
export type Props = {
op: CommitType;
secretVersion?: DecryptedSecret;
newVersion?: Omit<TSecretApprovalSecChange, "tags"> & { tags?: WsTag[] };
presentSecretVersionNumber: number;
};
const generateItemTitle = (op: CommitType) => {
let text = { label: "", color: "" };
if (op === CommitType.CREATE) text = { label: "create", color: "#16a34a" };
else if (op === CommitType.UPDATE) text = { label: "change", color: "#ea580c" };
else text = { label: "deletion", color: "#b91c1c" };
return (
<span>
Request for <span style={{ color: text.color }}>secret {text.label}</span>
</span>
);
};
export const SecretApprovalRequestChangeItem = ({
op,
secretVersion,
newVersion,
presentSecretVersionNumber
}: Props) => {
// meaning request has changed
const isStale = (secretVersion?.version || 1) < presentSecretVersionNumber;
return (
<div className="bg-bunker-500 rounded-lg pt-2 pb-4 px-4">
<div className="py-3 px-1 flex items-center">
<div className="flex-grow">{generateItemTitle(op)}</div>
{isStale && (
<div className="flex items-center">
<FontAwesomeIcon icon={faFilePen} className="text-primary-600 text-sm" />
<span className="text-xs ml-2">Secret has been changed(stale)</span>
</div>
)}
</div>
<TableContainer>
<Table>
<THead>
<Tr>
{op === CommitType.UPDATE && <Th className="w-12" />}
<Th className="min-table-row">Secret</Th>
<Th>Value</Th>
<Th className="min-table-row">Comment</Th>
<Th className="min-table-row">Tags</Th>
</Tr>
</THead>
{op === CommitType.UPDATE ? (
<TBody>
<Tr>
<Td className="text-red-600">OLD</Td>
<Td>{secretVersion?.key}</Td>
<Td>
<SecretInput isReadOnly value={secretVersion?.value} />
</Td>
<Td>{secretVersion?.comment}</Td>
<Td>
{secretVersion?.tags?.map(({ name, _id: tagId, tagColor }) => (
<Tag
className="flex items-center space-x-2 w-min"
key={`${secretVersion._id}-${tagId}`}
>
<div
className="w-3 h-3 rounded-full"
style={{ backgroundColor: tagColor || "#bec2c8" }}
/>
<div className="text-sm">{name}</div>
</Tag>
))}
</Td>
</Tr>
<Tr>
<Td className="text-green-600">NEW</Td>
<Td>{newVersion?.secretKey}</Td>
<Td>
<SecretInput isReadOnly value={newVersion?.secretValue} />
</Td>
<Td>{newVersion?.secretComment}</Td>
<Td>
{newVersion?.tags?.map(({ name, _id: tagId, tagColor }) => (
<Tag
className="flex items-center space-x-2 w-min"
key={`${newVersion._id}-${tagId}`}
>
<div
className="w-3 h-3 rounded-full"
style={{ backgroundColor: tagColor || "#bec2c8" }}
/>
<div className="text-sm">{name}</div>
</Tag>
))}
</Td>
</Tr>
</TBody>
) : (
<TBody>
<Tr>
<Td>{op === CommitType.CREATE ? newVersion?.secretKey : secretVersion?.key}</Td>
<Td>
<SecretInput
isReadOnly
value={
op === CommitType.CREATE ? newVersion?.secretValue : secretVersion?.value
}
/>
</Td>
<Td>
{op === CommitType.CREATE ? newVersion?.secretComment : secretVersion?.comment}
</Td>
<Td>
{(op === CommitType.CREATE ? newVersion?.tags : secretVersion?.tags)?.map(
({ name, _id: tagId, tagColor }) => (
<Tag
className="flex items-center space-x-2 w-min"
key={`${
op === CommitType.CREATE ? newVersion?._id : secretVersion?._id
}-${tagId}`}
>
<div
className="w-3 h-3 rounded-full"
style={{ backgroundColor: tagColor || "#bec2c8" }}
/>
<div className="text-sm">{name}</div>
</Tag>
)
)}
</Td>
</Tr>
</TBody>
)}
</Table>
</TableContainer>
</div>
);
};
@@ -9,33 +9,18 @@ import {
} 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 { import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
Button, import { Button, ContentLoader, EmptyState, IconButton, Tooltip } from "@app/components/v2";
ContentLoader,
IconButton,
SecretInput,
Table,
TableContainer,
Tag,
TBody,
Td,
Th,
THead,
Tooltip,
Tr
} from "@app/components/v2";
import { useUser } from "@app/context"; import { useUser } from "@app/context";
import { import {
useGetSecretApprovalRequestDetails, useGetSecretApprovalRequestDetails,
useGetUserWsKey, useGetUserWsKey,
usePerformSecretApprovalRequestMerge, useUpdateSecretApprovalReviewStatus
useUpdateSecretApprovalRequestStatus
} from "@app/hooks/api"; } from "@app/hooks/api";
import { ApprovalStatus, CommitType, TWorkspaceUser } from "@app/hooks/api/types"; import { ApprovalStatus, CommitType, TWorkspaceUser } from "@app/hooks/api/types";
import { useNotificationContext } from "~/components/context/Notifications/NotificationProvider";
import { SecretApprovalRequestAction } from "./SecretApprovalRequestAction"; import { SecretApprovalRequestAction } from "./SecretApprovalRequestAction";
import { SecretApprovalRequestChangeItem } from "./SecretApprovalRequestChangeItem";
export const generateCommitText = (commits: { op: CommitType }[] = []) => { export const generateCommitText = (commits: { op: CommitType }[] = []) => {
const score: Record<string, number> = {}; const score: Record<string, number> = {};
@@ -112,9 +97,7 @@ export const SecretApprovalRequestChanges = ({
mutateAsync: updateSecretApprovalRequestStatus, mutateAsync: updateSecretApprovalRequestStatus,
isLoading: isUpdatingRequestStatus, isLoading: isUpdatingRequestStatus,
variables variables
} = useUpdateSecretApprovalRequestStatus(); } = useUpdateSecretApprovalReviewStatus();
const { mutateAsync: performSecretApprovalMerge, isLoading: isMerging } =
usePerformSecretApprovalRequestMerge();
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;
@@ -155,31 +138,18 @@ export const SecretApprovalRequestChanges = ({
} }
}; };
const handleSecretApprovalRequestMerge = async () => {
try {
await performSecretApprovalMerge({
id: approvalRequestId
});
createNotification({
type: "success",
text: "Successfully merged the request"
});
} catch (err) {
console.log(err);
createNotification({
type: "error",
text: "Failed to update the request status"
});
}
};
if (isSecretApprovalRequestLoading) { if (isSecretApprovalRequestLoading) {
<div> <div>
<ContentLoader /> <ContentLoader />
</div>; </div>;
} }
if (!isSecretApprovalRequestSuccess) return <div>Failed</div>; if (!isSecretApprovalRequestSuccess)
return (
<div>
<EmptyState title="Failed to load approvals" />
</div>
);
const isMergable = const isMergable =
secretApprovalRequestDetails?.policy?.approvals <= secretApprovalRequestDetails?.policy?.approvals <=
@@ -212,141 +182,54 @@ export const SecretApprovalRequestChanges = ({
</span> </span>
</div> </div>
</div> </div>
<Button {!hasMerged && secretApprovalRequestDetails.status === "open" && (
size="xs" <>
leftIcon={hasApproved && <FontAwesomeIcon icon={faCheck} />} <Button
onClick={() => handleSecretApprovalStatusUpdate(ApprovalStatus.APPROVED)} size="xs"
isLoading={isApproving} leftIcon={hasApproved && <FontAwesomeIcon icon={faCheck} />}
isDisabled={isApproving || hasApproved} onClick={() => handleSecretApprovalStatusUpdate(ApprovalStatus.APPROVED)}
> isLoading={isApproving}
{hasApproved ? "Approved" : "Approve"} isDisabled={isApproving || hasApproved}
</Button> >
<Button {hasApproved ? "Approved" : "Approve"}
size="xs" </Button>
colorSchema="danger" <Button
leftIcon={hasRejected && <FontAwesomeIcon icon={faCheck} />} size="xs"
onClick={() => handleSecretApprovalStatusUpdate(ApprovalStatus.REJECTED)} colorSchema="danger"
isLoading={isRejecting} leftIcon={hasRejected && <FontAwesomeIcon icon={faCheck} />}
isDisabled={isRejecting || hasRejected} onClick={() => handleSecretApprovalStatusUpdate(ApprovalStatus.REJECTED)}
> isLoading={isRejecting}
{hasRejected ? "Rejected" : "Reject"} isDisabled={isRejecting || hasRejected}
</Button> >
{hasRejected ? "Rejected" : "Reject"}
</Button>
</>
)}
</div> </div>
<div className="flex flex-col space-y-4"> <div className="flex flex-col space-y-4">
{secretApprovalRequestDetails.commits.map(({ op, secretVersion, newVersion }, index) => ( {secretApprovalRequestDetails.commits.map(
<div key={`commit-change-secret-${index + 1}`}> ({ op, secretVersion, secret, newVersion }, index) => (
<TableContainer> <SecretApprovalRequestChangeItem
<Table> op={op}
<THead> secretVersion={secretVersion}
<Tr> presentSecretVersionNumber={secret?.version || 0}
{op === CommitType.UPDATE && <Th className="w-12" />} newVersion={newVersion}
<Th className="min-table-row">Secret</Th> key={`${op}-${index + 1}-${secretVersion?._id}`}
<Th>Value</Th> />
<Th className="min-table-row">Comment</Th> )
<Th className="min-table-row">Tags</Th> )}
</Tr>
</THead>
{op === CommitType.UPDATE ? (
<TBody>
<Tr>
<Td className="text-red-600">OLD</Td>
<Td>{secretVersion?.key}</Td>
<Td>
<SecretInput isReadOnly value={secretVersion?.value} />
</Td>
<Td>{secretVersion?.comment}</Td>
<Td>
{secretVersion?.tags?.map(({ name, _id: tagId, tagColor }) => (
<Tag
className="flex items-center space-x-2 w-min"
key={`${secretVersion._id}-${tagId}`}
>
<div
className="w-3 h-3 rounded-full"
style={{ backgroundColor: tagColor || "#bec2c8" }}
/>
<div className="text-sm">{name}</div>
</Tag>
))}
</Td>
</Tr>
<Tr>
<Td className="text-green-600">NEW</Td>
<Td>{newVersion?.secretKey}</Td>
<Td>
<SecretInput isReadOnly value={newVersion?.secretValue} />
</Td>
<Td>{newVersion?.secretComment}</Td>
<Td>
{newVersion?.tags?.map(({ name, _id: tagId, tagColor }) => (
<Tag
className="flex items-center space-x-2 w-min"
key={`${newVersion._id}-${tagId}`}
>
<div
className="w-3 h-3 rounded-full"
style={{ backgroundColor: tagColor || "#bec2c8" }}
/>
<div className="text-sm">{name}</div>
</Tag>
))}
</Td>
</Tr>
</TBody>
) : (
<TBody>
<Tr>
<Td>
{op === CommitType.CREATE ? newVersion?.secretKey : secretVersion?.key}
</Td>
<Td>
<SecretInput
isReadOnly
value={
op === CommitType.CREATE
? newVersion?.secretValue
: secretVersion?.value
}
/>
</Td>
<Td>
{op === CommitType.CREATE
? newVersion?.secretComment
: secretVersion?.comment}
</Td>
<Td>
{(op === CommitType.CREATE ? newVersion?.tags : secretVersion?.tags)?.map(
({ name, _id: tagId, tagColor }) => (
<Tag
className="flex items-center space-x-2 w-min"
key={`${
op === CommitType.CREATE ? newVersion?._id : secretVersion?._id
}-${tagId}`}
>
<div
className="w-3 h-3 rounded-full"
style={{ backgroundColor: tagColor || "#bec2c8" }}
/>
<div className="text-sm">{name}</div>
</Tag>
)
)}
</Td>
</Tr>
</TBody>
)}
</Table>
</TableContainer>
</div>
))}
</div> </div>
<div className="flex items-center px-4 py-6 rounded-lg space-x-6 bg-mineshaft-800 mt-8"> <div className="flex items-center px-5 py-6 rounded-lg space-x-6 bg-mineshaft-800 mt-8">
<SecretApprovalRequestAction <SecretApprovalRequestAction
approvalRequestId={secretApprovalRequestDetails._id}
hasMerged={hasMerged} hasMerged={hasMerged}
approvals={secretApprovalRequestDetails.policy.approvals || 0}
status={secretApprovalRequestDetails.status} status={secretApprovalRequestDetails.status}
isMerging={isMerging}
isMergable={isMergable} isMergable={isMergable}
onMerge={handleSecretApprovalRequestMerge} statusChangeByEmail={
members[secretApprovalRequestDetails?.statusChangeBy || ""]?.user?.email || ""
}
workspaceId={workspaceId}
/> />
</div> </div>
</div> </div>
@@ -183,7 +183,6 @@ export const SecretListView = ({
modSecret: Omit<DecryptedSecret, "tags"> & { tags: { _id: string }[] }, modSecret: Omit<DecryptedSecret, "tags"> & { tags: { _id: string }[] },
cb?: () => void cb?: () => void
) => { ) => {
console.log(orgSecret, modSecret);
const { key: oldKey } = orgSecret; const { key: oldKey } = orgSecret;
const { key, value, overrideAction, idOverride, valueOverride, tags, comment } = modSecret; const { key, value, overrideAction, idOverride, valueOverride, tags, comment } = modSecret;
const hasKeyChanged = oldKey !== key; const hasKeyChanged = oldKey !== key;
@@ -219,7 +218,7 @@ export const SecretListView = ({
newKey: hasKeyChanged ? key : undefined, newKey: hasKeyChanged ? key : undefined,
skipMultilineEncoding: modSecret.skipMultilineEncoding skipMultilineEncoding: modSecret.skipMultilineEncoding
}); });
if (isProtectedBranch) cb?.(); if (isProtectedBranch) cb();
} }
queryClient.invalidateQueries( queryClient.invalidateQueries(