mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 20:27:12 +00:00
feat(secret-approval): conflict ui, request count and secret path in request detail
This commit is contained in:
@@ -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({
|
||||||
|
|||||||
@@ -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
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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={
|
||||||
|
|||||||
+15
-5
@@ -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>
|
||||||
|
|||||||
+2
-1
@@ -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",
|
||||||
|
|||||||
+26
-5
@@ -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>
|
||||||
|
|||||||
+11
-3
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user