feat(secret-approval): conflict ui, request count and secret path in request detail

This commit is contained in:
Akhil Mohan
2023-10-05 20:45:16 +05:30
parent 80a3ea42ac
commit cb893f71ee
14 changed files with 227 additions and 42 deletions
@@ -12,6 +12,54 @@ import { Types } from "mongoose";
import { EEAuditLogService } from "../../ee/services"; import { EEAuditLogService } from "../../ee/services";
import { EventType } from "../../ee/models"; import { EventType } from "../../ee/models";
export const getSecretApprovalRequestCount = async (req: Request, res: Response) => {
const {
query: { workspaceId }
} = await validateRequest(reqValidator.getSecretApprovalRequestCount, req);
const { membership } = await getUserProjectPermissions(req.user._id, workspaceId);
const approvalRequestCount = await SecretApprovalRequest.aggregate([
{
$match: {
workspace: new Types.ObjectId(workspaceId)
}
},
{
$lookup: {
from: SecretApprovalPolicy.collection.name,
localField: "policy",
foreignField: "_id",
as: "policy"
}
},
{ $unwind: "$policy" },
...(membership.role !== "admin"
? [
{
$match: {
$or: [
{ committer: new Types.ObjectId(membership.id) },
{ "policy.approvers": new Types.ObjectId(membership.id) }
]
}
}
]
: []),
{
$group: {
_id: "$status",
count: { $sum: 1 }
}
}
]);
const openRequests = approvalRequestCount.find(({ _id }) => _id === "open");
const closedRequests = approvalRequestCount.find(({ _id }) => _id === "close");
return res.send({
approvals: { open: openRequests?.count || 0, closed: closedRequests?.count || 0 }
});
};
export const getSecretApprovalRequests = async (req: Request, res: Response) => { export const getSecretApprovalRequests = async (req: Request, res: Response) => {
const { const {
query: { status, committer, workspaceId, environment, limit, offset } query: { status, committer, workspaceId, environment, limit, offset }
@@ -95,7 +143,8 @@ export const getSecretApprovalRequestDetails = async (req: Request, res: Respons
} }
}) })
.populate("commits.secret", "version") .populate("commits.secret", "version")
.populate("commits.newVersion.tags"); .populate("commits.newVersion.tags")
.lean();
if (!secretApprovalRequest) if (!secretApprovalRequest)
throw BadRequestError({ message: "Secret approval request not found" }); throw BadRequestError({ message: "Secret approval request not found" });
@@ -114,8 +163,19 @@ export const getSecretApprovalRequestDetails = async (req: Request, res: Respons
throw UnauthorizedRequestError({ message: "User has no access" }); throw UnauthorizedRequestError({ message: "User has no access" });
} }
let secretPath = "/";
const approvalRootFolders = await Folder.findOne({
workspace: secretApprovalRequest.workspace,
environment: secretApprovalRequest.environment
}).lean();
if (approvalRootFolders) {
secretPath =
getFolderWithPathFromId(approvalRootFolders?.nodes, secretApprovalRequest.folderId)
?.folderPath || "/";
}
return res.send({ return res.send({
approval: secretApprovalRequest approval: { ...secretApprovalRequest, secretPath }
}); });
}; };
@@ -12,6 +12,14 @@ router.get(
secretApprovalRequestController.getSecretApprovalRequests secretApprovalRequestController.getSecretApprovalRequests
); );
router.get(
"/count",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
secretApprovalRequestController.getSecretApprovalRequestCount
);
router.get( router.get(
"/:id", "/:id",
requireAuth({ requireAuth({
+12 -7
View File
@@ -355,7 +355,7 @@ export const performSecretApprovalRequestMerge = async (
const environment = secretApprovalRequest.environment; const environment = secretApprovalRequest.environment;
const folderId = secretApprovalRequest.folderId; const folderId = secretApprovalRequest.folderId;
const postHogClient = await TelemetryService.getPostHogClient(); const postHogClient = await TelemetryService.getPostHogClient();
const conflicts: Array<{ id: string; op: CommitType }> = []; const conflicts: Array<{ secretId: string; op: CommitType }> = [];
const secretCreationCommits = secretApprovalRequest.commits.filter( const secretCreationCommits = secretApprovalRequest.commits.filter(
({ op }) => op === CommitType.CREATE ({ op }) => op === CommitType.CREATE
@@ -382,7 +382,7 @@ export const performSecretApprovalRequestMerge = async (
secretCreationCommits secretCreationCommits
.filter(({ newVersion }) => conflictGroupByBlindIndex[newVersion.secretBlindIndex || ""]) .filter(({ newVersion }) => conflictGroupByBlindIndex[newVersion.secretBlindIndex || ""])
.forEach((el) => { .forEach((el) => {
conflicts.push({ op: CommitType.CREATE, id: el.newVersion._id.toString() }); conflicts.push({ op: CommitType.CREATE, secretId: el.newVersion._id.toString() });
}); });
// create secret // create secret
@@ -482,15 +482,20 @@ export const performSecretApprovalRequestMerge = async (
); );
secretUpdationCommits secretUpdationCommits
.filter( .filter(
({ newVersion }) => ({ newVersion, secret }) =>
newVersion.secretBlindIndex && conflictGroupByBlindIndex[newVersion.secretBlindIndex] (newVersion.secretBlindIndex && conflictGroupByBlindIndex[newVersion.secretBlindIndex]) ||
!secret
) )
.forEach((el) => { .forEach((el) => {
conflicts.push({ op: CommitType.UPDATE, id: el.newVersion._id.toString() }); conflicts.push({ op: CommitType.UPDATE, secretId: el.newVersion._id.toString() });
}); });
const nonConflictSecrets = secretUpdationCommits.filter(({ newVersion }) => const nonConflictSecrets = secretUpdationCommits.filter(
newVersion?.secretBlindIndex ? !conflictGroupByBlindIndex[newVersion.secretBlindIndex] : true ({ newVersion, secret }) =>
Boolean(secret) &&
(newVersion?.secretBlindIndex
? !conflictGroupByBlindIndex[newVersion.secretBlindIndex]
: true)
); );
await Secret.bulkWrite( await Secret.bulkWrite(
// id and version are stripped off // id and version are stripped off
@@ -12,6 +12,12 @@ export const getSecretApprovalRequests = z.object({
}) })
}); });
export const getSecretApprovalRequestCount = z.object({
query: z.object({
workspaceId: z.string().trim()
})
});
export const getSecretApprovalRequestDetails = z.object({ export const getSecretApprovalRequestDetails = z.object({
params: z.object({ params: z.object({
id: z.string().trim() id: z.string().trim()
@@ -3,4 +3,8 @@ export {
useUpdateSecretApprovalRequestStatus, useUpdateSecretApprovalRequestStatus,
useUpdateSecretApprovalReviewStatus useUpdateSecretApprovalReviewStatus
} from "./mutation"; } from "./mutation";
export { useGetSecretApprovalRequestDetails, useGetSecretApprovalRequests } from "./queries"; export {
useGetSecretApprovalRequestCount,
useGetSecretApprovalRequestDetails,
useGetSecretApprovalRequests
} from "./queries";
@@ -35,8 +35,9 @@ export const useUpdateSecretApprovalRequestStatus = () => {
}); });
return data; return data;
}, },
onSuccess: (_, { id }) => { onSuccess: (_, { id, workspaceId }) => {
queryClient.invalidateQueries(secretApprovalRequestKeys.detail({ id })); queryClient.invalidateQueries(secretApprovalRequestKeys.detail({ id }));
queryClient.invalidateQueries(secretApprovalRequestKeys.count({ workspaceId }));
} }
}); });
}; };
@@ -52,6 +53,7 @@ export const usePerformSecretApprovalRequestMerge = () => {
onSuccess: (_, { id, workspaceId }) => { onSuccess: (_, { id, workspaceId }) => {
queryClient.invalidateQueries(secretApprovalRequestKeys.detail({ id })); queryClient.invalidateQueries(secretApprovalRequestKeys.detail({ id }));
queryClient.invalidateQueries(secretApprovalRequestKeys.list({ workspaceId })); queryClient.invalidateQueries(secretApprovalRequestKeys.list({ workspaceId }));
queryClient.invalidateQueries(secretApprovalRequestKeys.count({ workspaceId }));
} }
}); });
}; };
@@ -15,9 +15,11 @@ import { UserWsKeyPair } from "../keys/types";
import { decryptSecrets } from "../secrets/queries"; import { decryptSecrets } from "../secrets/queries";
import { DecryptedSecret } from "../secrets/types"; import { DecryptedSecret } from "../secrets/types";
import { import {
TGetSecretApprovalRequestCount,
TGetSecretApprovalRequestDetails, TGetSecretApprovalRequestDetails,
TGetSecretApprovalRequestList, TGetSecretApprovalRequestList,
TSecretApprovalRequest, TSecretApprovalRequest,
TSecretApprovalRequestCount,
TSecretApprovalSecChange, TSecretApprovalSecChange,
TSecretApprovalSecChangeData TSecretApprovalSecChangeData
} from "./types"; } from "./types";
@@ -36,7 +38,11 @@ export const secretApprovalRequestKeys = {
"secret-approval-requests" "secret-approval-requests"
] as const, ] as const,
detail: ({ id }: Omit<TGetSecretApprovalRequestDetails, "decryptKey">) => detail: ({ id }: Omit<TGetSecretApprovalRequestDetails, "decryptKey">) =>
[{ id }, "secret-approval-request-detail"] as const [{ id }, "secret-approval-request-detail"] as const,
count: ({ workspaceId }: TGetSecretApprovalRequestCount) => [
{ workspaceId },
"secret-approval-request-count"
]
}; };
export const decryptSecretApprovalSecret = ( export const decryptSecretApprovalSecret = (
@@ -187,3 +193,32 @@ export const useGetSecretApprovalRequestDetails = ({
}), }),
enabled: Boolean(id && decryptKey) && (options?.enabled ?? true) enabled: Boolean(id && decryptKey) && (options?.enabled ?? true)
}); });
const fetchSecretApprovalRequestCount = async ({ workspaceId }: TGetSecretApprovalRequestCount) => {
const { data } = await apiRequest.get<{ approvals: TSecretApprovalRequestCount }>(
"/api/v1/secret-approval-requests/count",
{ params: { workspaceId } }
);
return data.approvals;
};
export const useGetSecretApprovalRequestCount = ({
workspaceId,
options = {}
}: TGetSecretApprovalRequestCount & {
options?: Omit<
UseQueryOptions<
TSecretApprovalRequestCount,
unknown,
TSecretApprovalRequestCount,
ReturnType<typeof secretApprovalRequestKeys.count>
>,
"queryKey" | "queryFn"
>;
}) =>
useQuery({
queryKey: secretApprovalRequestKeys.count({ workspaceId }),
queryFn: () => fetchSecretApprovalRequestCount({ workspaceId }),
enabled: Boolean(workspaceId) && (options?.enabled ?? true)
});
@@ -17,7 +17,6 @@ export enum CommitType {
export type TSecretApprovalSecChangeData = { export type TSecretApprovalSecChangeData = {
_id: string; _id: string;
version: number;
secretKeyCiphertext: string; secretKeyCiphertext: string;
secretKeyIV: string; secretKeyIV: string;
secretKeyTag: string; secretKeyTag: string;
@@ -31,6 +30,7 @@ export type TSecretApprovalSecChangeData = {
algorithm: "aes-256-gcm"; algorithm: "aes-256-gcm";
keyEncoding: "utf8" | "base64"; keyEncoding: "utf8" | "base64";
tags?: WsTag[]; tags?: WsTag[];
version: number;
}; };
export type TSecretApprovalSecChange = { export type TSecretApprovalSecChange = {
@@ -57,10 +57,12 @@ export type TSecretApprovalRequest<
workspace: string; workspace: string;
environment: string; environment: string;
folderId: string; folderId: string;
secretPath: string;
hasMerged: boolean; hasMerged: boolean;
status: "open" | "close"; status: "open" | "close";
policy: TSecretApprovalPolicy; policy: TSecretApprovalPolicy;
statusChangeBy: string; statusChangeBy: string;
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
secret?: { version: number }; secret?: { version: number };
@@ -71,6 +73,11 @@ export type TSecretApprovalRequest<
}[]; }[];
}; };
export type TSecretApprovalRequestCount = {
open: number;
closed: number;
};
export type TGetSecretApprovalRequestList = { export type TGetSecretApprovalRequestList = {
workspaceId: string; workspaceId: string;
environment?: string; environment?: string;
@@ -80,6 +87,10 @@ export type TGetSecretApprovalRequestList = {
offset?: number; offset?: number;
}; };
export type TGetSecretApprovalRequestCount = {
workspaceId: string;
};
export type TGetSecretApprovalRequestDetails = { export type TGetSecretApprovalRequestDetails = {
id: string; id: string;
decryptKey: UserWsKeyPair; decryptKey: UserWsKeyPair;
@@ -93,6 +104,7 @@ export type TUpdateSecretApprovalReviewStatusDTO = {
export type TUpdateSecretApprovalRequestStatusDTO = { export type TUpdateSecretApprovalRequestStatusDTO = {
status: "open" | "close"; status: "open" | "close";
id: string; id: string;
workspaceId: string;
}; };
export type TPerformSecretApprovalRequestMerge = { export type TPerformSecretApprovalRequestMerge = {
@@ -8,6 +8,7 @@ import {
} from "@app/components/utilities/cryptography/crypto"; } from "@app/components/utilities/cryptography/crypto";
import { apiRequest } from "@app/config/request"; import { apiRequest } from "@app/config/request";
import { secretApprovalRequestKeys } from "../secretApprovalRequest/queries";
import { secretSnapshotKeys } from "../secretSnapshots/queries"; import { secretSnapshotKeys } from "../secretSnapshots/queries";
import { secretKeys } from "./queries"; import { secretKeys } from "./queries";
import { import {
@@ -114,6 +115,7 @@ export const useCreateSecretV3 = ({
queryClient.invalidateQueries( queryClient.invalidateQueries(
secretSnapshotKeys.count({ environment, workspaceId, directory: secretPath }) secretSnapshotKeys.count({ environment, workspaceId, directory: secretPath })
); );
queryClient.invalidateQueries(secretApprovalRequestKeys.count({ workspaceId }));
}, },
...options ...options
}); });
@@ -173,6 +175,7 @@ export const useUpdateSecretV3 = ({
queryClient.invalidateQueries( queryClient.invalidateQueries(
secretSnapshotKeys.count({ environment, workspaceId, directory: secretPath }) secretSnapshotKeys.count({ environment, workspaceId, directory: secretPath })
); );
queryClient.invalidateQueries(secretApprovalRequestKeys.count({ workspaceId }));
}, },
...options ...options
}); });
@@ -209,6 +212,7 @@ export const useDeleteSecretV3 = ({
queryClient.invalidateQueries( queryClient.invalidateQueries(
secretSnapshotKeys.count({ environment, workspaceId, directory: secretPath }) secretSnapshotKeys.count({ environment, workspaceId, directory: secretPath })
); );
queryClient.invalidateQueries(secretApprovalRequestKeys.count({ workspaceId }));
}, },
...options ...options
}); });
@@ -261,6 +265,7 @@ export const useCreateSecretBatch = ({
queryClient.invalidateQueries( queryClient.invalidateQueries(
secretSnapshotKeys.count({ environment, workspaceId, directory: secretPath }) secretSnapshotKeys.count({ environment, workspaceId, directory: secretPath })
); );
queryClient.invalidateQueries(secretApprovalRequestKeys.count({ workspaceId }));
}, },
...options ...options
}); });
@@ -313,6 +318,7 @@ export const useUpdateSecretBatch = ({
queryClient.invalidateQueries( queryClient.invalidateQueries(
secretSnapshotKeys.count({ environment, workspaceId, directory: secretPath }) secretSnapshotKeys.count({ environment, workspaceId, directory: secretPath })
); );
queryClient.invalidateQueries(secretApprovalRequestKeys.count({ workspaceId }));
}, },
...options ...options
}); });
@@ -349,6 +355,7 @@ export const useDeleteSecretBatch = ({
queryClient.invalidateQueries( queryClient.invalidateQueries(
secretSnapshotKeys.count({ environment, workspaceId, directory: secretPath }) secretSnapshotKeys.count({ environment, workspaceId, directory: secretPath })
); );
queryClient.invalidateQueries(secretApprovalRequestKeys.count({ workspaceId }));
}, },
...options ...options
}); });
+21 -15
View File
@@ -66,6 +66,7 @@ import {
useAddUserToWs, useAddUserToWs,
useCreateWorkspace, useCreateWorkspace,
useGetOrgTrialUrl, useGetOrgTrialUrl,
useGetSecretApprovalRequestCount,
useLogoutUser, useLogoutUser,
useUploadWsKey useUploadWsKey
} from "@app/hooks/api"; } from "@app/hooks/api";
@@ -114,7 +115,9 @@ export const AppLayout = ({ children }: LayoutProps) => {
const { orgs, currentOrg } = useOrganization(); const { orgs, currentOrg } = useOrganization();
const { user } = useUser(); const { user } = useUser();
const { subscription } = useSubscription(); const { subscription } = useSubscription();
const workspaceId = currentWorkspace?._id || "";
// const [ isLearningNoteOpen, setIsLearningNoteOpen ] = useState(true); // const [ isLearningNoteOpen, setIsLearningNoteOpen ] = useState(true);
const { data: secretApprovalReqCount } = useGetSecretApprovalRequestCount({ workspaceId });
const isAddingProjectsAllowed = subscription?.workspaceLimit const isAddingProjectsAllowed = subscription?.workspaceLimit
? subscription.workspacesUsed < subscription.workspaceLimit ? subscription.workspacesUsed < subscription.workspaceLimit
@@ -477,21 +480,24 @@ export const AppLayout = ({ children }: LayoutProps) => {
</MenuItem> </MenuItem>
</a> </a>
</Link> </Link>
{process.env.NEXT_PUBLIC_SECRET_APPROVAL === "true" && ( <Link href={`/project/${currentWorkspace?._id}/approval`} passHref>
<Link href={`/project/${currentWorkspace?._id}/approval`} passHref> <a className="relative">
<a> <MenuItem
<MenuItem isSelected={
isSelected={ router.asPath === `/project/${currentWorkspace?._id}/approval`
router.asPath === `/project/${currentWorkspace?._id}/approval` }
} icon="system-outline-189-domain-verification"
icon="system-outline-189-domain-verification" >
> Secret approval
Secret Approval {Boolean(secretApprovalReqCount?.open) && (
</MenuItem> <span className="text-xs p-0.5 rounded ml-2 bg-primary text-black">
</a> {secretApprovalReqCount?.open}
</Link> </span>
)} )}
{/* <Link href={`/project/${currentWorkspace?._id}/allowlist`} passHref> </MenuItem>
</a>
</Link>
<Link href={`/project/${currentWorkspace?._id}/allowlist`} passHref>
<a> <a>
<MenuItem <MenuItem
isSelected={ isSelected={
@@ -20,7 +20,11 @@ import {
Skeleton Skeleton
} from "@app/components/v2"; } from "@app/components/v2";
import { useUser, useWorkspace } from "@app/context"; import { useUser, useWorkspace } from "@app/context";
import { useGetSecretApprovalRequests, useGetWorkspaceUsers } from "@app/hooks/api"; import {
useGetSecretApprovalRequestCount,
useGetSecretApprovalRequests,
useGetWorkspaceUsers
} from "@app/hooks/api";
import { ApprovalStatus, TSecretApprovalRequest, TWorkspaceUser } from "@app/hooks/api/types"; import { ApprovalStatus, TSecretApprovalRequest, TWorkspaceUser } from "@app/hooks/api/types";
import { import {
@@ -51,6 +55,8 @@ export const SecretApprovalRequest = () => {
environment: envFilter, environment: envFilter,
committer: committerFilter committer: committerFilter
}); });
const { data: secretApprovalRequestCount, isSuccess: isSecretApprovalReqCountSuccess } =
useGetSecretApprovalRequestCount({ workspaceId });
const { user: presentUser } = useUser(); const { user: presentUser } = useUser();
const { data: members } = useGetWorkspaceUsers(workspaceId); const { data: members } = useGetWorkspaceUsers(workspaceId);
const membersGroupById = members?.reduce<Record<string, TWorkspaceUser>>( const membersGroupById = members?.reduce<Record<string, TWorkspaceUser>>(
@@ -103,13 +109,17 @@ export const SecretApprovalRequest = () => {
onKeyDown={(evt) => { onKeyDown={(evt) => {
if (evt.key === "Enter") setStatusFilter("open"); if (evt.key === "Enter") setStatusFilter("open");
}} }}
className={statusFilter === "close" ? "text-gray-500 hover:text-gray-400 duration-100" : ""} className={
statusFilter === "close" ? "text-gray-500 hover:text-gray-400 duration-100" : ""
}
> >
<FontAwesomeIcon icon={faCodeBranch} className="mr-2" /> <FontAwesomeIcon icon={faCodeBranch} className="mr-2" />
Open {isSecretApprovalReqCountSuccess && secretApprovalRequestCount?.open} Open
</div> </div>
<div <div
className={statusFilter === "open" ? "text-gray-500 hover:text-gray-400 duration-100" : ""} className={
statusFilter === "open" ? "text-gray-500 hover:text-gray-400 duration-100" : ""
}
role="button" role="button"
tabIndex={0} tabIndex={0}
onClick={() => setStatusFilter("close")} onClick={() => setStatusFilter("close")}
@@ -118,7 +128,7 @@ export const SecretApprovalRequest = () => {
}} }}
> >
<FontAwesomeIcon icon={faCheck} className="mr-2" /> <FontAwesomeIcon icon={faCheck} className="mr-2" />
Closed {isSecretApprovalReqCountSuccess && secretApprovalRequestCount.closed} Closed
</div> </div>
<div className="flex-grow flex justify-end space-x-8"> <div className="flex-grow flex justify-end space-x-8">
<DropdownMenu> <DropdownMenu>
@@ -65,7 +65,8 @@ export const SecretApprovalRequestAction = ({
try { try {
await updateSecretStatusChange({ await updateSecretStatusChange({
id: approvalRequestId, id: approvalRequestId,
status: reqState status: reqState,
workspaceId
}); });
createNotification({ createNotification({
type: "success", type: "success",
@@ -1,4 +1,4 @@
import { faFilePen } from "@fortawesome/free-solid-svg-icons"; import { faExclamationTriangle, faInfo } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { import {
@@ -10,6 +10,7 @@ import {
Td, Td,
Th, Th,
THead, THead,
Tooltip,
Tr Tr
} from "@app/components/v2"; } from "@app/components/v2";
import { CommitType, DecryptedSecret, TSecretApprovalSecChange, WsTag } from "@app/hooks/api/types"; import { CommitType, DecryptedSecret, TSecretApprovalSecChange, WsTag } from "@app/hooks/api/types";
@@ -20,6 +21,7 @@ export type Props = {
newVersion?: Omit<TSecretApprovalSecChange, "tags"> & { tags?: WsTag[] }; newVersion?: Omit<TSecretApprovalSecChange, "tags"> & { tags?: WsTag[] };
presentSecretVersionNumber: number; presentSecretVersionNumber: number;
hasMerged?: Boolean; hasMerged?: Boolean;
conflicts: Array<{ secretId: string; op: CommitType }>;
}; };
const generateItemTitle = (op: CommitType) => { const generateItemTitle = (op: CommitType) => {
@@ -35,25 +37,44 @@ const generateItemTitle = (op: CommitType) => {
); );
}; };
const generateConflictText = (op: CommitType) => {
if (op === CommitType.CREATE) return <div>Secret already exist</div>;
if (op === CommitType.UPDATE) return <div>Secret not found</div>;
return null;
};
export const SecretApprovalRequestChangeItem = ({ export const SecretApprovalRequestChangeItem = ({
op, op,
secretVersion, secretVersion,
newVersion, newVersion,
presentSecretVersionNumber, presentSecretVersionNumber,
hasMerged hasMerged,
conflicts
}: Props) => { }: Props) => {
// meaning request has changed // meaning request has changed
const isStale = (secretVersion?.version || 1) < presentSecretVersionNumber && !hasMerged; const isStale = (secretVersion?.version || 1) < presentSecretVersionNumber;
const itemConflict =
hasMerged && conflicts.find((el) => el.op === op && el.secretId === newVersion?._id);
const hasConflict = Boolean(itemConflict);
return ( return (
<div className="bg-bunker-500 rounded-lg pt-2 pb-4 px-4"> <div className="bg-bunker-500 rounded-lg pt-2 pb-4 px-4">
<div className="py-3 px-1 flex items-center"> <div className="py-3 px-1 flex items-center">
<div className="flex-grow">{generateItemTitle(op)}</div> <div className="flex-grow">{generateItemTitle(op)}</div>
{isStale && ( {!hasMerged && isStale && (
<div className="flex items-center"> <div className="flex items-center">
<FontAwesomeIcon icon={faFilePen} className="text-primary-600 text-sm" /> <FontAwesomeIcon icon={faInfo} className="text-primary-600 text-sm" />
<span className="text-xs ml-2">Secret has been changed(stale)</span> <span className="text-xs ml-2">Secret has been changed(stale)</span>
</div> </div>
)} )}
{hasMerged && hasConflict && (
<div className="flex items-center text-sm text-bunker-300 space-x-2">
<Tooltip content="Merge Conflict">
<FontAwesomeIcon icon={faExclamationTriangle} className="text-red-700" />
</Tooltip>
<div>{generateConflictText(op)}</div>
</div>
)}
</div> </div>
<TableContainer> <TableContainer>
<Table> <Table>
@@ -5,6 +5,7 @@ import {
faCheckCircle, faCheckCircle,
faCircle, faCircle,
faCodeBranch, faCodeBranch,
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";
@@ -175,13 +176,19 @@ export const SecretApprovalRequestChanges = ({
<div className="text-lg mb-1"> <div className="text-lg mb-1">
{generateCommitText(secretApprovalRequestDetails.commits)} {generateCommitText(secretApprovalRequestDetails.commits)}
</div> </div>
<div className="text-sm text-bunker-300"> <div className="text-sm text-bunker-300 flex items-center">
{committer?.user?.firstName} {committer?.user?.firstName}
{committer?.user?.lastName} ({committer?.user?.email}) wants to change{" "} {committer?.user?.lastName} ({committer?.user?.email}) wants to change{" "}
{secretApprovalRequestDetails.commits.length} secret values in{" "} {secretApprovalRequestDetails.commits.length} secret values in
<span className="text-blue-300 bg-blue-600/60 px-1"> <span className="text-primary-300 bg-primary-600/60 px-1 mx-1 rounded">
{secretApprovalRequestDetails.environment} {secretApprovalRequestDetails.environment}
</span> </span>
<div className="flex items-center border border-mineshaft-500 pl-1 pr-2 rounded w-min">
<div className="border-r border-mineshaft-500 pr-1">
<FontAwesomeIcon icon={faFolder} className="text-primary" size="sm" />
</div>
<div className="text-sm pl-2 pb-0.5">{secretApprovalRequestDetails.secretPath}</div>
</div>
</div> </div>
</div> </div>
{!hasMerged && secretApprovalRequestDetails.status === "open" && ( {!hasMerged && secretApprovalRequestDetails.status === "open" && (
@@ -213,6 +220,7 @@ export const SecretApprovalRequestChanges = ({
({ op, secretVersion, secret, newVersion }, index) => ( ({ op, secretVersion, secret, newVersion }, index) => (
<SecretApprovalRequestChangeItem <SecretApprovalRequestChangeItem
op={op} op={op}
conflicts={secretApprovalRequestDetails.conflicts}
hasMerged={hasMerged} hasMerged={hasMerged}
secretVersion={secretVersion} secretVersion={secretVersion}
presentSecretVersionNumber={secret?.version || 0} presentSecretVersionNumber={secret?.version || 0}