mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
feat: updated ui for replication approval
This commit is contained in:
@@ -8,6 +8,11 @@ export const formatReservedPaths = (secretPath: string) => {
|
|||||||
return secretPath;
|
return secretPath;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const parsePathFromReplicatedPath = (secretPath: string) => {
|
||||||
|
const i = secretPath.indexOf(ReservedFolders.SecretReplication);
|
||||||
|
return secretPath.slice(0, i);
|
||||||
|
};
|
||||||
|
|
||||||
export const camelCaseToSpaces = (input: string) => {
|
export const camelCaseToSpaces = (input: string) => {
|
||||||
return input.replace(/([a-z])([A-Z])/g, "$1 $2");
|
return input.replace(/([a-z])([A-Z])/g, "$1 $2");
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -224,8 +224,7 @@ export const SecretApprovalRequest = () => {
|
|||||||
createdAt,
|
createdAt,
|
||||||
reviewers,
|
reviewers,
|
||||||
status,
|
status,
|
||||||
committerUser,
|
committerUser
|
||||||
isReplicated: isReplication
|
|
||||||
} = secretApproval;
|
} = secretApproval;
|
||||||
const isReviewed = reviewers.some(
|
const isReviewed = reviewers.some(
|
||||||
({ status: reviewStatus, userId }) =>
|
({ status: reviewStatus, userId }) =>
|
||||||
@@ -244,13 +243,15 @@ export const SecretApprovalRequest = () => {
|
|||||||
>
|
>
|
||||||
<div className="mb-1">
|
<div className="mb-1">
|
||||||
<FontAwesomeIcon icon={faCodeBranch} className="mr-2" />
|
<FontAwesomeIcon icon={faCodeBranch} className="mr-2" />
|
||||||
{generateCommitText(commits)}
|
{secretApproval.isReplicated
|
||||||
|
? `${commits.length} secret pending import`
|
||||||
|
: generateCommitText(commits)}
|
||||||
<span className="text-xs text-bunker-300"> #{secretApproval.slug}</span>
|
<span className="text-xs text-bunker-300"> #{secretApproval.slug}</span>
|
||||||
</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{" "}
|
||||||
{committerUser?.firstName || ""} {committerUser?.lastName || ""} (
|
{committerUser?.firstName || ""} {committerUser?.lastName || ""} (
|
||||||
{committerUser?.email}){isReplication && " via replication"}
|
{committerUser?.email})
|
||||||
{!isReviewed && status === "open" && " - Review required"}
|
{!isReviewed && status === "open" && " - Review required"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ 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 { Tag, Tooltip } from "@app/components/v2";
|
import { SecretInput, Tag, Tooltip } from "@app/components/v2";
|
||||||
import { CommitType, SecretV3Raw, TSecretApprovalSecChange, WsTag } from "@app/hooks/api/types";
|
import { CommitType, SecretV3Raw, TSecretApprovalSecChange, WsTag } from "@app/hooks/api/types";
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
@@ -86,10 +86,10 @@ export const SecretApprovalRequestChangeItem = ({
|
|||||||
{op === CommitType.UPDATE || op === CommitType.DELETE ? (
|
{op === CommitType.UPDATE || op === CommitType.DELETE ? (
|
||||||
<div className="flex w-full cursor-default flex-col rounded-md border border-red-600/60 bg-red-600/10 p-4 xl:w-1/2">
|
<div className="flex w-full cursor-default flex-col rounded-md border border-red-600/60 bg-red-600/10 p-4 xl:w-1/2">
|
||||||
<div className="mb-4 flex flex-row justify-between">
|
<div className="mb-4 flex flex-row justify-between">
|
||||||
<span className="text-md font-medium">Legacy Secret</span>
|
<span className="text-md font-medium">Previous Secret</span>
|
||||||
<div className="rounded-full bg-red px-2 pb-[0.14rem] pt-[0.2rem] text-xs font-medium">
|
<div className="rounded-full bg-red px-2 pb-[0.14rem] pt-[0.2rem] text-xs font-medium">
|
||||||
<FontAwesomeIcon icon={faCircleXmark} className="pr-1 text-white" />
|
<FontAwesomeIcon icon={faCircleXmark} className="pr-1 text-white" />
|
||||||
Deprecated
|
Previous
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
@@ -104,27 +104,22 @@ export const SecretApprovalRequestChangeItem = ({
|
|||||||
Rotated Secret value will not be affected
|
Rotated Secret value will not be affected
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div className="relative">
|
||||||
onClick={() => setIsOldSecretValueVisible(!isOldSecretValueVisible)}
|
<SecretInput
|
||||||
className="relative flex max-w-[100vh] flex-row items-center justify-between rounded-md border border-mineshaft-500 bg-mineshaft-900 px-2"
|
isReadOnly
|
||||||
>
|
isVisible={isOldSecretValueVisible}
|
||||||
|
value={secretVersion?.secretValue}
|
||||||
|
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-bunker-700 px-2 py-1.5"
|
||||||
|
/>
|
||||||
<div
|
<div
|
||||||
className={`flex break-all font-mono ${isOldSecretValueVisible || !secretVersion?.secretValue ? "text-md py-[0.55rem]" : "text-lg"}`}
|
className="absolute right-1 top-1"
|
||||||
|
onClick={() => setIsOldSecretValueVisible(!isOldSecretValueVisible)}
|
||||||
>
|
>
|
||||||
{isOldSecretValueVisible
|
<FontAwesomeIcon
|
||||||
? secretVersion?.secretValue || "EMPTY"
|
icon={isOldSecretValueVisible ? faEyeSlash : faEye}
|
||||||
: secretVersion?.secretValue
|
className="cursor-pointer rounded-md border border-mineshaft-500 bg-mineshaft-800 p-1.5 text-mineshaft-300 hover:bg-mineshaft-700"
|
||||||
? secretVersion?.secretValue?.split("").map(() => "•")
|
/>
|
||||||
: "EMPTY"}{" "}
|
|
||||||
</div>
|
</div>
|
||||||
{secretVersion?.secretValue && (
|
|
||||||
<div className="flex h-10 w-10 items-center justify-center pl-1">
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon={isOldSecretValueVisible ? faEyeSlash : faEye}
|
|
||||||
className="cursor-pointer rounded-md border border-mineshaft-500 bg-mineshaft-800 p-1.5 text-mineshaft-300 hover:bg-mineshaft-700"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -191,8 +186,7 @@ export const SecretApprovalRequestChangeItem = ({
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-md flex w-full items-center justify-center rounded-md border border-mineshaft-600 bg-mineshaft-800 text-mineshaft-300 xl:w-1/2">
|
<div className="text-md flex w-full items-center justify-center rounded-md border border-mineshaft-600 bg-mineshaft-800 text-mineshaft-300 xl:w-1/2">
|
||||||
{" "}
|
Secret did not exist in the previous version.
|
||||||
Secret not existent in the previous version.
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{op === CommitType.UPDATE || op === CommitType.CREATE ? (
|
{op === CommitType.UPDATE || op === CommitType.CREATE ? (
|
||||||
@@ -216,27 +210,22 @@ export const SecretApprovalRequestChangeItem = ({
|
|||||||
Rotated Secret value will not be affected
|
Rotated Secret value will not be affected
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div className="relative">
|
||||||
onClick={() => setIsNewSecretValueVisible(!isNewSecretValueVisible)}
|
<SecretInput
|
||||||
className="relative flex max-w-[100vh] flex-row items-center justify-between rounded-md border border-mineshaft-500 bg-mineshaft-900 px-2"
|
isReadOnly
|
||||||
>
|
isVisible={isNewSecretValueVisible}
|
||||||
|
value={newVersion?.secretValue}
|
||||||
|
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-bunker-700 px-2 py-1.5"
|
||||||
|
/>
|
||||||
<div
|
<div
|
||||||
className={`flex break-all font-mono ${isNewSecretValueVisible || !newVersion?.secretValue ? "text-md py-[0.55rem]" : "text-lg"}`}
|
className="absolute right-1 top-1"
|
||||||
|
onClick={() => setIsNewSecretValueVisible(!isNewSecretValueVisible)}
|
||||||
>
|
>
|
||||||
{isNewSecretValueVisible
|
<FontAwesomeIcon
|
||||||
? newVersion?.secretValue || "EMPTY"
|
icon={isNewSecretValueVisible ? faEyeSlash : faEye}
|
||||||
: newVersion?.secretValue
|
className="cursor-pointer rounded-md border border-mineshaft-500 bg-mineshaft-800 p-1.5 text-mineshaft-300 hover:bg-mineshaft-700"
|
||||||
? newVersion?.secretValue?.split("").map(() => "•")
|
/>
|
||||||
: "EMPTY"}{" "}
|
|
||||||
</div>
|
</div>
|
||||||
{newVersion?.secretValue && (
|
|
||||||
<div className="flex h-10 w-10 items-center justify-center pl-1">
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon={isNewSecretValueVisible ? faEyeSlash : faEye}
|
|
||||||
className="cursor-pointer rounded-md border border-mineshaft-500 bg-mineshaft-800 p-1.5 text-mineshaft-300 hover:bg-mineshaft-700"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -302,7 +291,7 @@ export const SecretApprovalRequestChangeItem = ({
|
|||||||
) : (
|
) : (
|
||||||
<div className="text-md flex w-full items-center justify-center rounded-md border border-mineshaft-600 bg-mineshaft-800 text-mineshaft-300 xl:w-1/2">
|
<div className="text-md flex w-full items-center justify-center rounded-md border border-mineshaft-600 bg-mineshaft-800 text-mineshaft-300 xl:w-1/2">
|
||||||
{" "}
|
{" "}
|
||||||
Secret not existent in the new version.
|
Secret did not exist in the previous version.
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -30,19 +30,24 @@ import {
|
|||||||
TextArea,
|
TextArea,
|
||||||
Tooltip
|
Tooltip
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { useUser } from "@app/context";
|
import { useUser, useWorkspace } from "@app/context";
|
||||||
import { usePopUp } from "@app/hooks";
|
import { usePopUp } from "@app/hooks";
|
||||||
import {
|
import {
|
||||||
useGetSecretApprovalRequestDetails,
|
useGetSecretApprovalRequestDetails,
|
||||||
|
useGetSecretImports,
|
||||||
useUpdateSecretApprovalReviewStatus
|
useUpdateSecretApprovalReviewStatus
|
||||||
} from "@app/hooks/api";
|
} from "@app/hooks/api";
|
||||||
import { ApprovalStatus, CommitType } from "@app/hooks/api/types";
|
import { ApprovalStatus, CommitType } from "@app/hooks/api/types";
|
||||||
import { formatReservedPaths } from "@app/lib/fn/string";
|
import { formatReservedPaths, parsePathFromReplicatedPath } from "@app/lib/fn/string";
|
||||||
|
|
||||||
import { SecretApprovalRequestAction } from "./SecretApprovalRequestAction";
|
import { SecretApprovalRequestAction } from "./SecretApprovalRequestAction";
|
||||||
import { SecretApprovalRequestChangeItem } from "./SecretApprovalRequestChangeItem";
|
import { SecretApprovalRequestChangeItem } from "./SecretApprovalRequestChangeItem";
|
||||||
|
|
||||||
export const generateCommitText = (commits: { op: CommitType }[] = []) => {
|
export const generateCommitText = (commits: { op: CommitType }[] = [], isReplicated = false) => {
|
||||||
|
if (isReplicated) {
|
||||||
|
return <span>{commits.length} secret pending import</span>;
|
||||||
|
}
|
||||||
|
|
||||||
const score: Record<string, number> = {};
|
const score: Record<string, number> = {};
|
||||||
commits.forEach(({ op }) => {
|
commits.forEach(({ op }) => {
|
||||||
score[op] = (score?.[op] || 0) + 1;
|
score[op] = (score?.[op] || 0) + 1;
|
||||||
@@ -74,7 +79,6 @@ export const generateCommitText = (commits: { op: CommitType }[] = []) => {
|
|||||||
<span style={{ color: "#F83030" }}> deleted</span>
|
<span style={{ color: "#F83030" }}> deleted</span>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|
||||||
return text;
|
return text;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -105,6 +109,7 @@ export const SecretApprovalRequestChanges = ({
|
|||||||
workspaceId
|
workspaceId
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { user: userSession } = useUser();
|
const { user: userSession } = useUser();
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
const {
|
const {
|
||||||
data: secretApprovalRequestDetails,
|
data: secretApprovalRequestDetails,
|
||||||
isSuccess: isSecretApprovalRequestSuccess,
|
isSuccess: isSecretApprovalRequestSuccess,
|
||||||
@@ -112,6 +117,20 @@ export const SecretApprovalRequestChanges = ({
|
|||||||
} = useGetSecretApprovalRequestDetails({
|
} = useGetSecretApprovalRequestDetails({
|
||||||
id: approvalRequestId
|
id: approvalRequestId
|
||||||
});
|
});
|
||||||
|
const approvalSecretPath = parsePathFromReplicatedPath(
|
||||||
|
secretApprovalRequestDetails?.secretPath || ""
|
||||||
|
);
|
||||||
|
const { data: secretImports } = useGetSecretImports({
|
||||||
|
environment: secretApprovalRequestDetails?.environment || "",
|
||||||
|
projectId: currentWorkspace.id,
|
||||||
|
path: approvalSecretPath
|
||||||
|
});
|
||||||
|
|
||||||
|
const replicatedImport = secretApprovalRequestDetails?.isReplicated
|
||||||
|
? secretImports?.find(
|
||||||
|
(el) => secretApprovalRequestDetails?.secretPath?.includes(el.id) && el.isReplication
|
||||||
|
)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
mutateAsync: updateSecretApprovalRequestStatus,
|
mutateAsync: updateSecretApprovalRequestStatus,
|
||||||
@@ -226,34 +245,16 @@ export const SecretApprovalRequestChanges = ({
|
|||||||
: secretApprovalRequestDetails.status}
|
: secretApprovalRequestDetails.status}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-grow flex-col">
|
<div className="flex-grow flex-col">
|
||||||
<div className="text-lg">
|
<div className="text-xl">
|
||||||
{generateCommitText(secretApprovalRequestDetails.commits)}
|
{generateCommitText(
|
||||||
{secretApprovalRequestDetails.isReplicated && (
|
secretApprovalRequestDetails.commits,
|
||||||
<span className="text-sm text-bunker-300"> (replication)</span>
|
secretApprovalRequestDetails.isReplicated
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-bunker-300">
|
<div className="flex items-center space-x-2 text-xs text-gray-400">
|
||||||
<p className="inline">
|
By {secretApprovalRequestDetails?.committerUser?.firstName} (
|
||||||
{secretApprovalRequestDetails?.committerUser?.firstName || ""}
|
{secretApprovalRequestDetails?.committerUser?.email})
|
||||||
{secretApprovalRequestDetails?.committerUser?.lastName || ""} (
|
|
||||||
{secretApprovalRequestDetails?.committerUser?.email}) wants to change{" "}
|
|
||||||
{secretApprovalRequestDetails.commits.length} secret values in
|
|
||||||
</p>
|
|
||||||
<p className="mx-1 inline rounded bg-primary-600/40 px-1 py-1 text-primary-300">
|
|
||||||
{secretApprovalRequestDetails.environment}
|
|
||||||
</p>
|
|
||||||
<div className="inline-flex w-min items-center rounded border border-mineshaft-500 pl-1 pr-2">
|
|
||||||
<p className="cursor-default border-r border-mineshaft-500 pr-1">
|
|
||||||
<FontAwesomeIcon icon={faFolder} className="text-primary" size="sm" />
|
|
||||||
</p>
|
|
||||||
<p
|
|
||||||
className="cursor-default truncate pb-0.5 pl-2 text-sm"
|
|
||||||
style={{ maxWidth: "10rem" }}
|
|
||||||
>
|
|
||||||
{formatReservedPaths(secretApprovalRequestDetails.secretPath)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{!hasMerged &&
|
{!hasMerged &&
|
||||||
@@ -367,10 +368,87 @@ export const SecretApprovalRequestChanges = ({
|
|||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="mb-4 flex flex-col rounded-r border-l-2 border-l-primary bg-mineshaft-300/5 px-4 py-2.5">
|
||||||
|
<div>
|
||||||
|
{secretApprovalRequestDetails.isReplicated ? (
|
||||||
|
<div className="text-sm text-bunker-300">
|
||||||
|
A secret import in
|
||||||
|
<p
|
||||||
|
className="mx-1 inline rounded bg-primary-600/40 text-primary-300"
|
||||||
|
style={{ padding: "2px 4px" }}
|
||||||
|
>
|
||||||
|
{secretApprovalRequestDetails?.environment}
|
||||||
|
</p>
|
||||||
|
<div className="mr-2 inline-flex w-min items-center rounded border border-mineshaft-500 pl-1 pr-2">
|
||||||
|
<p className="cursor-default border-r border-mineshaft-500 pr-1">
|
||||||
|
<FontAwesomeIcon icon={faFolder} className="text-primary" size="sm" />
|
||||||
|
</p>
|
||||||
|
<Tooltip content={approvalSecretPath}>
|
||||||
|
<p
|
||||||
|
className="cursor-default truncate pb-0.5 pl-2 text-sm"
|
||||||
|
style={{ maxWidth: "15rem" }}
|
||||||
|
>
|
||||||
|
{approvalSecretPath}
|
||||||
|
</p>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
has pending changes to be accepted from its source at{" "}
|
||||||
|
<p
|
||||||
|
className="mx-1 inline rounded bg-primary-600/40 text-primary-300"
|
||||||
|
style={{ padding: "2px 4px" }}
|
||||||
|
>
|
||||||
|
{replicatedImport?.importEnv?.slug}
|
||||||
|
</p>
|
||||||
|
<div className="inline-flex w-min items-center rounded border border-mineshaft-500 pl-1 pr-2">
|
||||||
|
<p className="cursor-default border-r border-mineshaft-500 pr-1">
|
||||||
|
<FontAwesomeIcon icon={faFolder} className="text-primary" size="sm" />
|
||||||
|
</p>
|
||||||
|
<Tooltip content={replicatedImport?.importPath}>
|
||||||
|
<p
|
||||||
|
className="cursor-default truncate pb-0.5 pl-2 text-sm"
|
||||||
|
style={{ maxWidth: "15rem" }}
|
||||||
|
>
|
||||||
|
{replicatedImport?.importPath}
|
||||||
|
</p>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
. Approving these changes will add them to that import.
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-sm text-bunker-300">
|
||||||
|
<p className="inline">Secret(s) in</p>
|
||||||
|
<p
|
||||||
|
className="mx-1 inline rounded bg-primary-600/40 text-primary-300"
|
||||||
|
style={{ padding: "2px 4px" }}
|
||||||
|
>
|
||||||
|
{secretApprovalRequestDetails?.environment}
|
||||||
|
</p>
|
||||||
|
<div className="mr-1 inline-flex w-min items-center rounded border border-mineshaft-500 pl-1 pr-2">
|
||||||
|
<p className="cursor-default border-r border-mineshaft-500 pr-1">
|
||||||
|
<FontAwesomeIcon icon={faFolder} className="text-primary" size="sm" />
|
||||||
|
</p>
|
||||||
|
<Tooltip content={formatReservedPaths(secretApprovalRequestDetails.secretPath)}>
|
||||||
|
<p
|
||||||
|
className="cursor-default truncate pb-0.5 pl-2 text-sm"
|
||||||
|
style={{ maxWidth: "20rem" }}
|
||||||
|
>
|
||||||
|
{formatReservedPaths(secretApprovalRequestDetails.secretPath)}
|
||||||
|
</p>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
<p className="inline">
|
||||||
|
have pending changes. Approving these changes will add them to that environment
|
||||||
|
and path.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div className="flex flex-col space-y-4">
|
<div className="flex flex-col space-y-4">
|
||||||
{secretApprovalRequestDetails.commits.map(
|
{secretApprovalRequestDetails.commits.map(
|
||||||
({ op, secretVersion, secret, ...newVersion }, index) => (
|
({ op, secretVersion, secret, ...newVersion }, index) => (
|
||||||
<SecretApprovalRequestChangeItem
|
<SecretApprovalRequestChangeItem
|
||||||
|
isReplicated={secretApprovalRequestDetails?.isReplicated}
|
||||||
op={op}
|
op={op}
|
||||||
conflicts={secretApprovalRequestDetails.conflicts}
|
conflicts={secretApprovalRequestDetails.conflicts}
|
||||||
hasMerged={hasMerged}
|
hasMerged={hasMerged}
|
||||||
|
|||||||
Reference in New Issue
Block a user