mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 10:27:26 +00:00
Merge pull request #3654 from Infisical/approvals-redesign
improve change requests design
This commit is contained in:
@@ -44,8 +44,7 @@ export const SecretApprovalsPage = () => {
|
|||||||
<div className="container mx-auto h-full w-full max-w-7xl bg-bunker-800 text-white">
|
<div className="container mx-auto h-full w-full max-w-7xl bg-bunker-800 text-white">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title="Approval Workflows"
|
title="Approval Workflows"
|
||||||
description="Create approval policies for any modifications to secrets in sensitive environments and folders.
|
description="Create approval policies for any modifications to secrets in sensitive environments and folders."
|
||||||
"
|
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
href="https://infisical.com/docs/documentation/platform/pr-workflows"
|
href="https://infisical.com/docs/documentation/platform/pr-workflows"
|
||||||
|
|||||||
+67
-54
@@ -4,10 +4,9 @@ import {
|
|||||||
faClose,
|
faClose,
|
||||||
faLandMineOn,
|
faLandMineOn,
|
||||||
faLockOpen,
|
faLockOpen,
|
||||||
faSquareCheck,
|
|
||||||
faSquareXmark,
|
|
||||||
faTriangleExclamation,
|
faTriangleExclamation,
|
||||||
faUserLock
|
faUserLock,
|
||||||
|
faXmark
|
||||||
} 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 { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
@@ -112,62 +111,76 @@ export const SecretApprovalRequestAction = ({
|
|||||||
|
|
||||||
if (!hasMerged && status === "open") {
|
if (!hasMerged && status === "open") {
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full items-start justify-between transition-all">
|
<div className="flex w-full flex-col items-start justify-between py-4 transition-all">
|
||||||
<div className="flex items-start space-x-4">
|
<div className="flex items-center space-x-4 px-4">
|
||||||
<FontAwesomeIcon
|
<div
|
||||||
icon={isMergable ? faSquareCheck : faSquareXmark}
|
className={`flex items-center justify-center rounded-full ${isMergable ? "h-10 w-10 bg-green" : "h-11 w-11 bg-red-600"}`}
|
||||||
className={twMerge("pt-1 text-2xl", isMergable ? "text-primary" : "text-red-600")}
|
>
|
||||||
/>
|
<FontAwesomeIcon
|
||||||
|
icon={isMergable ? faCheck : faXmark}
|
||||||
|
className={isMergable ? "text-lg text-black" : "text-2xl text-white"}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<span className="flex flex-col">
|
<span className="flex flex-col">
|
||||||
{isMergable ? "Good to merge" : "Review required"}
|
<p className={`text-md font-medium ${isMergable && "text-lg"}`}>
|
||||||
<span className="inline-block text-xs text-bunker-200">
|
{isMergable ? "Good to merge" : "Merging is blocked"}
|
||||||
At least {approvals} approving review required
|
</p>
|
||||||
{Boolean(statusChangeByEmail) && `. Reopened by ${statusChangeByEmail}`}
|
{!isMergable && (
|
||||||
</span>
|
<span className="inline-block text-xs text-bunker-200">
|
||||||
{isSoftEnforcement && !isMergable && canBypassApprovalPermission && (
|
At least {approvals} approving review{`${approvals > 1 ? "s" : ""}`} required by
|
||||||
<div className="mt-2 flex flex-col space-y-2">
|
eligible reviewers.
|
||||||
<Checkbox
|
{Boolean(statusChangeByEmail) && `. Reopened by ${statusChangeByEmail}`}
|
||||||
onCheckedChange={(checked) => setByPassApproval(checked === true)}
|
</span>
|
||||||
isChecked={byPassApproval}
|
|
||||||
id="byPassApproval"
|
|
||||||
checkIndicatorBg="text-white"
|
|
||||||
className={twMerge(
|
|
||||||
"mr-2",
|
|
||||||
byPassApproval ? "border-red bg-red hover:bg-red-600" : ""
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<span className="text-xs text-red">
|
|
||||||
Merge without waiting for approval (bypass secret change policy)
|
|
||||||
</span>
|
|
||||||
</Checkbox>
|
|
||||||
{byPassApproval && (
|
|
||||||
<FormControl
|
|
||||||
label="Reason for bypass"
|
|
||||||
className="mt-2"
|
|
||||||
isRequired
|
|
||||||
tooltipText="Enter a reason for bypassing the secret change policy"
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
value={bypassReason}
|
|
||||||
onChange={(e) => setBypassReason(e.target.value)}
|
|
||||||
placeholder="Enter reason for bypass (min 10 chars)"
|
|
||||||
leftIcon={<FontAwesomeIcon icon={faTriangleExclamation} />}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-2">
|
<div
|
||||||
|
className={`mt-4 w-full border-mineshaft-600 px-5 ${isMergable ? "border-t pb-2" : "border-y pb-4"}`}
|
||||||
|
>
|
||||||
|
{isSoftEnforcement && !isMergable && canBypassApprovalPermission && (
|
||||||
|
<div className="mt-2 flex flex-col space-y-2 pt-2">
|
||||||
|
<Checkbox
|
||||||
|
onCheckedChange={(checked) => setByPassApproval(checked === true)}
|
||||||
|
isChecked={byPassApproval}
|
||||||
|
id="byPassApproval"
|
||||||
|
checkIndicatorBg="text-white"
|
||||||
|
className={twMerge(
|
||||||
|
"mr-2",
|
||||||
|
byPassApproval ? "border-red bg-red hover:bg-red-600" : ""
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span className="text-sm">
|
||||||
|
Merge without waiting for approval (bypass secret change policy)
|
||||||
|
</span>
|
||||||
|
</Checkbox>
|
||||||
|
{byPassApproval && (
|
||||||
|
<FormControl
|
||||||
|
label="Reason for bypass"
|
||||||
|
className="mt-2"
|
||||||
|
isRequired
|
||||||
|
tooltipText="Enter a reason for bypassing the secret change policy"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
value={bypassReason}
|
||||||
|
onChange={(e) => setBypassReason(e.target.value)}
|
||||||
|
placeholder="Enter reason for bypass (min 10 chars)"
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faTriangleExclamation} />}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 flex w-full items-center justify-end space-x-2 px-4">
|
||||||
{canApprove || isSoftEnforcement ? (
|
{canApprove || isSoftEnforcement ? (
|
||||||
<>
|
<div className="flex items-center space-x-4">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => handleSecretApprovalStatusChange("close")}
|
onClick={() => handleSecretApprovalStatusChange("close")}
|
||||||
isLoading={isStatusChanging}
|
isLoading={isStatusChanging}
|
||||||
variant="outline_bg"
|
variant="outline_bg"
|
||||||
colorSchema="secondary"
|
colorSchema="primary"
|
||||||
leftIcon={<FontAwesomeIcon icon={faClose} />}
|
leftIcon={<FontAwesomeIcon icon={faClose} />}
|
||||||
|
className="hover:border-red/60 hover:bg-red/10"
|
||||||
>
|
>
|
||||||
Close request
|
Close request
|
||||||
</Button>
|
</Button>
|
||||||
@@ -186,7 +199,7 @@ export const SecretApprovalRequestAction = ({
|
|||||||
>
|
>
|
||||||
Merge
|
Merge
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div>Only approvers can merge</div>
|
<div>Only approvers can merge</div>
|
||||||
)}
|
)}
|
||||||
@@ -197,13 +210,13 @@ export const SecretApprovalRequestAction = ({
|
|||||||
|
|
||||||
if (hasMerged && status === "close")
|
if (hasMerged && status === "close")
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full items-center justify-between">
|
<div className="flex w-full items-center justify-between rounded-md border border-primary/60 bg-primary/10">
|
||||||
<div className="flex items-start space-x-4">
|
<div className="flex items-start space-x-4 p-4">
|
||||||
<FontAwesomeIcon icon={faCheck} className="pt-1 text-2xl text-primary" />
|
<FontAwesomeIcon icon={faCheck} className="pt-1 text-2xl text-primary" />
|
||||||
<span className="flex flex-col">
|
<span className="flex flex-col">
|
||||||
Secret approval merged
|
Change request merged
|
||||||
<span className="inline-block text-xs text-bunker-200">
|
<span className="inline-block text-xs text-bunker-200">
|
||||||
Merged by {statusChangeByEmail}
|
Merged by {statusChangeByEmail}.
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+195
-163
@@ -1,18 +1,18 @@
|
|||||||
import { faExclamationTriangle, faInfo, faKey } from "@fortawesome/free-solid-svg-icons";
|
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
||||||
|
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
||||||
|
/* eslint-disable no-nested-ternary */
|
||||||
|
import { useState } from "react";
|
||||||
|
import {
|
||||||
|
faCircleXmark,
|
||||||
|
faExclamationTriangle,
|
||||||
|
faEye,
|
||||||
|
faEyeSlash,
|
||||||
|
faInfo,
|
||||||
|
faKey
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
import {
|
import { Tag, Tooltip } from "@app/components/v2";
|
||||||
SecretInput,
|
|
||||||
Table,
|
|
||||||
TableContainer,
|
|
||||||
Tag,
|
|
||||||
TBody,
|
|
||||||
Td,
|
|
||||||
Th,
|
|
||||||
THead,
|
|
||||||
Tooltip,
|
|
||||||
Tr
|
|
||||||
} 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 = {
|
||||||
@@ -29,19 +29,19 @@ export type Props = {
|
|||||||
|
|
||||||
const generateItemTitle = (op: CommitType) => {
|
const generateItemTitle = (op: CommitType) => {
|
||||||
let text = { label: "", color: "" };
|
let text = { label: "", color: "" };
|
||||||
if (op === CommitType.CREATE) text = { label: "create", color: "#16a34a" };
|
if (op === CommitType.CREATE) text = { label: "create", color: "#60DD00" };
|
||||||
else if (op === CommitType.UPDATE) text = { label: "change", color: "#ea580c" };
|
else if (op === CommitType.UPDATE) text = { label: "change", color: "#F8EB30" };
|
||||||
else text = { label: "deletion", color: "#b91c1c" };
|
else text = { label: "deletion", color: "#F83030" };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span>
|
<div className="text-md pb-2 font-medium">
|
||||||
Request for <span style={{ color: text.color }}>secret {text.label}</span>
|
Request for <span style={{ color: text.color }}>secret {text.label}</span>
|
||||||
</span>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const generateConflictText = (op: CommitType) => {
|
const generateConflictText = (op: CommitType) => {
|
||||||
if (op === CommitType.CREATE) return <div>Secret already exist</div>;
|
if (op === CommitType.CREATE) return <div>Secret already exists</div>;
|
||||||
if (op === CommitType.UPDATE) return <div>Secret not found</div>;
|
if (op === CommitType.UPDATE) return <div>Secret not found</div>;
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
@@ -59,10 +59,12 @@ export const SecretApprovalRequestChangeItem = ({
|
|||||||
const itemConflict =
|
const itemConflict =
|
||||||
hasMerged && conflicts.find((el) => el.op === op && el.secretId === newVersion?.id);
|
hasMerged && conflicts.find((el) => el.op === op && el.secretId === newVersion?.id);
|
||||||
const hasConflict = Boolean(itemConflict);
|
const hasConflict = Boolean(itemConflict);
|
||||||
|
const [isOldSecretValueVisible, setIsOldSecretValueVisible] = useState(false);
|
||||||
|
const [isNewSecretValueVisible, setIsNewSecretValueVisible] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rounded-lg bg-bunker-500 px-4 pb-4 pt-2">
|
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 px-4 pb-4 pt-2">
|
||||||
<div className="flex items-center px-1 py-3">
|
<div className="flex items-center px-1 py-1">
|
||||||
<div className="flex-grow">{generateItemTitle(op)}</div>
|
<div className="flex-grow">{generateItemTitle(op)}</div>
|
||||||
{!hasMerged && isStale && (
|
{!hasMerged && isStale && (
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
@@ -79,48 +81,86 @@ export const SecretApprovalRequestChangeItem = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<TableContainer>
|
<div>
|
||||||
<Table>
|
<div className="flex flex-col space-x-0 space-y-4 xl:flex-row xl:space-x-4 xl:space-y-0">
|
||||||
<THead>
|
{op === CommitType.UPDATE || op === CommitType.DELETE ? (
|
||||||
<Tr>
|
<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">
|
||||||
{op === CommitType.UPDATE && <Th className="w-12 shrink-0" />}
|
<div className="mb-4 flex flex-row justify-between">
|
||||||
<Th className="w-48 shrink-0">Secret</Th>
|
<span className="text-md font-medium">Legacy Secret</span>
|
||||||
<Th className="min-w-0 flex-1">Value</Th>
|
<div className="rounded-full bg-red px-2 pb-[0.14rem] pt-[0.2rem] text-xs font-medium">
|
||||||
<Th className="w-24 shrink-0">Comment</Th>
|
<FontAwesomeIcon icon={faCircleXmark} className="pr-1 text-white" />
|
||||||
<Th className="w-24 shrink-0">Tags</Th>
|
Deprecated
|
||||||
<Th className="w-40 shrink-0">Metadata</Th>
|
</div>
|
||||||
</Tr>
|
</div>
|
||||||
</THead>
|
<div className="mb-2">
|
||||||
{op === CommitType.UPDATE ? (
|
<div className="text-sm font-medium text-mineshaft-300">Key</div>
|
||||||
<TBody>
|
<div className="text-sm">{secretVersion?.secretKey} </div>
|
||||||
<Tr>
|
</div>
|
||||||
<Td className="text-red-600">OLD</Td>
|
<div className="mb-2">
|
||||||
<Td>{secretVersion?.secretKey}</Td>
|
<div className="text-sm font-medium text-mineshaft-300">Value</div>
|
||||||
<Td>
|
<div className="text-sm">
|
||||||
{newVersion?.isRotatedSecret ? (
|
{newVersion?.isRotatedSecret ? (
|
||||||
<span className="text-mineshaft-400">
|
<span className="text-mineshaft-400">
|
||||||
Rotated Secret value will not be affected
|
Rotated Secret value will not be affected
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<SecretInput isReadOnly value={secretVersion?.secretValue} />
|
<div
|
||||||
)}
|
onClick={() => setIsOldSecretValueVisible(!isOldSecretValueVisible)}
|
||||||
</Td>
|
className="flex flex-row items-center justify-between rounded-md border border-mineshaft-500 bg-mineshaft-900 pl-2"
|
||||||
<Td>{secretVersion?.secretComment}</Td>
|
|
||||||
<Td className="flex flex-wrap gap-2">
|
|
||||||
{secretVersion?.tags?.map(({ slug, id: tagId, color }) => (
|
|
||||||
<Tag
|
|
||||||
className="flex w-min items-center space-x-2"
|
|
||||||
key={`${secretVersion.id}-${tagId}`}
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="h-3 w-3 rounded-full"
|
className={`flex font-mono ${isOldSecretValueVisible || !secretVersion?.secretValue ? "text-md py-[0.55rem]" : "text-lg"}`}
|
||||||
style={{ backgroundColor: color || "#bec2c8" }}
|
>
|
||||||
/>
|
{isOldSecretValueVisible
|
||||||
<div className="text-sm">{slug}</div>
|
? secretVersion?.secretValue || "EMPTY"
|
||||||
</Tag>
|
: secretVersion?.secretValue
|
||||||
))}
|
? secretVersion?.secretValue?.split("").map(() => "•")
|
||||||
</Td>
|
: "EMPTY"}{" "}
|
||||||
<Td>
|
</div>
|
||||||
|
{secretVersion?.secretValue && (
|
||||||
|
<div className="flex h-10 w-10 items-center justify-center">
|
||||||
|
<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 className="mb-2">
|
||||||
|
<div className="text-sm font-medium text-mineshaft-300">Comment</div>
|
||||||
|
<div className="text-sm">
|
||||||
|
{secretVersion?.secretComment || (
|
||||||
|
<span className="text-sm text-mineshaft-300">-</span>
|
||||||
|
)}{" "}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mb-2">
|
||||||
|
<div className="text-sm font-medium text-mineshaft-300">Tags</div>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{(secretVersion?.tags?.length ?? 0) ? (
|
||||||
|
secretVersion?.tags?.map(({ slug, id: tagId, color }) => (
|
||||||
|
<Tag
|
||||||
|
className="flex w-min items-center space-x-2"
|
||||||
|
key={`${secretVersion.id}-${tagId}`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="h-3 w-3 rounded-full"
|
||||||
|
style={{ backgroundColor: color || "#bec2c8" }}
|
||||||
|
/>
|
||||||
|
<div className="text-sm">{slug}</div>
|
||||||
|
</Tag>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<span className="text-sm text-mineshaft-300">-</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mb-2">
|
||||||
|
<div className="text-sm font-medium text-mineshaft-300">Metadata</div>
|
||||||
|
<div>
|
||||||
{secretVersion?.secretMetadata?.length ? (
|
{secretVersion?.secretMetadata?.length ? (
|
||||||
<div className="mt-1 flex flex-wrap gap-2 text-sm text-mineshaft-300">
|
<div className="mt-1 flex flex-wrap gap-2 text-sm text-mineshaft-300">
|
||||||
{secretVersion.secretMetadata?.map((el) => (
|
{secretVersion.secretMetadata?.map((el) => (
|
||||||
@@ -146,93 +186,77 @@ export const SecretApprovalRequestChangeItem = ({
|
|||||||
) : (
|
) : (
|
||||||
<p className="text-sm text-mineshaft-300">-</p>
|
<p className="text-sm text-mineshaft-300">-</p>
|
||||||
)}
|
)}
|
||||||
</Td>
|
</div>
|
||||||
</Tr>
|
</div>
|
||||||
<Tr>
|
</div>
|
||||||
<Td className="text-green-600">NEW</Td>
|
) : (
|
||||||
<Td>{newVersion?.secretKey}</Td>
|
<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">
|
||||||
<Td>
|
{" "}
|
||||||
|
Secret not existent in the previous version.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{op === CommitType.UPDATE || op === CommitType.CREATE ? (
|
||||||
|
<div className="flex w-full cursor-default flex-col rounded-md border border-green-600/60 bg-green-600/10 p-4 xl:w-1/2">
|
||||||
|
<div className="mb-4 flex flex-row justify-between">
|
||||||
|
<span className="text-md font-medium">New Secret</span>
|
||||||
|
<div className="rounded-full bg-green-600 px-2 pb-[0.14rem] pt-[0.2rem] text-xs font-medium">
|
||||||
|
<FontAwesomeIcon icon={faCircleXmark} className="pr-1 text-white" />
|
||||||
|
Current
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mb-2">
|
||||||
|
<div className="text-sm font-medium text-mineshaft-300">Key</div>
|
||||||
|
<div className="text-sm">{newVersion?.secretKey} </div>
|
||||||
|
</div>
|
||||||
|
<div className="mb-2">
|
||||||
|
<div className="text-sm font-medium text-mineshaft-300">Value</div>
|
||||||
|
<div className="text-sm">
|
||||||
{newVersion?.isRotatedSecret ? (
|
{newVersion?.isRotatedSecret ? (
|
||||||
<span className="text-mineshaft-400">
|
<span className="text-mineshaft-400">
|
||||||
Rotated Secret value will not be affected
|
Rotated Secret value will not be affected
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<SecretInput isReadOnly value={newVersion?.secretValue} />
|
<div
|
||||||
)}
|
onClick={() => setIsNewSecretValueVisible(!isNewSecretValueVisible)}
|
||||||
</Td>
|
className="flex flex-row items-center justify-between rounded-md border border-mineshaft-500 bg-mineshaft-900 pl-2"
|
||||||
<Td>{newVersion?.secretComment}</Td>
|
|
||||||
<Td className="flex flex-wrap gap-2">
|
|
||||||
{newVersion?.tags?.map(({ slug, id: tagId, color }) => (
|
|
||||||
<Tag
|
|
||||||
className="flex w-min items-center space-x-2"
|
|
||||||
key={`${newVersion.id}-${tagId}`}
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="h-3 w-3 rounded-full"
|
className={`flex font-mono ${isNewSecretValueVisible || !newVersion?.secretValue ? "text-md py-[0.55rem]" : "text-lg"}`}
|
||||||
style={{ backgroundColor: color || "#bec2c8" }}
|
>
|
||||||
/>
|
{isNewSecretValueVisible
|
||||||
<div className="text-sm">{slug}</div>
|
? newVersion?.secretValue || "EMPTY"
|
||||||
</Tag>
|
: newVersion?.secretValue
|
||||||
))}
|
? newVersion?.secretValue?.split("").map(() => "•")
|
||||||
</Td>
|
: "EMPTY"}{" "}
|
||||||
<Td>
|
</div>
|
||||||
{newVersion?.secretMetadata?.length ? (
|
{newVersion?.secretValue && (
|
||||||
<div className="mt-1 flex flex-wrap gap-2 text-sm text-mineshaft-300">
|
<div className="flex h-10 w-10 items-center justify-center">
|
||||||
{newVersion.secretMetadata?.map((el) => (
|
<FontAwesomeIcon
|
||||||
<div key={el.key} className="flex items-center">
|
icon={isNewSecretValueVisible ? faEyeSlash : faEye}
|
||||||
<Tag
|
className="cursor-pointer rounded-md border border-mineshaft-500 bg-mineshaft-800 p-1.5 text-mineshaft-300 hover:bg-mineshaft-700"
|
||||||
size="xs"
|
/>
|
||||||
className="mr-0 flex items-center rounded-r-none border border-mineshaft-500"
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faKey} size="xs" className="mr-1" />
|
|
||||||
<div>{el.key}</div>
|
|
||||||
</Tag>
|
|
||||||
<Tag
|
|
||||||
size="xs"
|
|
||||||
className="flex items-center rounded-l-none border border-mineshaft-500 bg-mineshaft-900 pl-1"
|
|
||||||
>
|
|
||||||
<div className="max-w-[150px] overflow-hidden text-ellipsis whitespace-nowrap">
|
|
||||||
{el.value}
|
|
||||||
</div>
|
|
||||||
</Tag>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
|
||||||
<p className="text-sm text-mineshaft-300">-</p>
|
|
||||||
)}
|
)}
|
||||||
</Td>
|
</div>
|
||||||
</Tr>
|
</div>
|
||||||
</TBody>
|
<div className="mb-2">
|
||||||
) : (
|
<div className="text-sm font-medium text-mineshaft-300">Comment</div>
|
||||||
<TBody>
|
<div className="text-sm">
|
||||||
<Tr>
|
{newVersion?.secretComment || (
|
||||||
<Td>
|
<span className="text-sm text-mineshaft-300">-</span>
|
||||||
{op === CommitType.CREATE ? newVersion?.secretKey : secretVersion?.secretKey}
|
)}{" "}
|
||||||
</Td>
|
</div>
|
||||||
<Td>
|
</div>
|
||||||
<SecretInput
|
<div className="mb-2">
|
||||||
isReadOnly
|
<div className="text-sm font-medium text-mineshaft-300">Tags</div>
|
||||||
value={
|
<div className="flex flex-wrap gap-2">
|
||||||
op === CommitType.CREATE
|
{(newVersion?.tags?.length ?? 0) ? (
|
||||||
? newVersion?.secretValue
|
newVersion?.tags?.map(({ slug, id: tagId, color }) => (
|
||||||
: secretVersion?.secretValue
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Td>
|
|
||||||
<Td>
|
|
||||||
{op === CommitType.CREATE
|
|
||||||
? newVersion?.secretComment
|
|
||||||
: secretVersion?.secretComment}
|
|
||||||
</Td>
|
|
||||||
<Td>
|
|
||||||
{(op === CommitType.CREATE ? newVersion?.tags : secretVersion?.tags)?.map(
|
|
||||||
({ slug, id: tagId, color }) => (
|
|
||||||
<Tag
|
<Tag
|
||||||
className="flex w-min items-center space-x-2"
|
className="flex w-min items-center space-x-2"
|
||||||
key={`${
|
key={`${newVersion.id}-${tagId}`}
|
||||||
op === CommitType.CREATE ? newVersion?.id : secretVersion?.id
|
|
||||||
}-${tagId}`}
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="h-3 w-3 rounded-full"
|
className="h-3 w-3 rounded-full"
|
||||||
@@ -240,41 +264,49 @@ export const SecretApprovalRequestChangeItem = ({
|
|||||||
/>
|
/>
|
||||||
<div className="text-sm">{slug}</div>
|
<div className="text-sm">{slug}</div>
|
||||||
</Tag>
|
</Tag>
|
||||||
)
|
))
|
||||||
)}
|
|
||||||
</Td>
|
|
||||||
<Td>
|
|
||||||
{newVersion?.secretMetadata?.length ? (
|
|
||||||
<div className="mt-1 flex flex-wrap gap-2 text-sm text-mineshaft-300">
|
|
||||||
{newVersion.secretMetadata?.map((el) => (
|
|
||||||
<div key={el.key} className="flex items-center">
|
|
||||||
<Tag
|
|
||||||
size="xs"
|
|
||||||
className="mr-0 flex items-center rounded-r-none border border-mineshaft-500"
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faKey} size="xs" className="mr-1" />
|
|
||||||
<div>{el.key}</div>
|
|
||||||
</Tag>
|
|
||||||
<Tag
|
|
||||||
size="xs"
|
|
||||||
className="flex items-center rounded-l-none border border-mineshaft-500 bg-mineshaft-900 pl-1"
|
|
||||||
>
|
|
||||||
<div className="max-w-[150px] overflow-hidden text-ellipsis whitespace-nowrap">
|
|
||||||
{el.value}
|
|
||||||
</div>
|
|
||||||
</Tag>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
<p className="text-sm text-mineshaft-300">-</p>
|
<span className="text-sm text-mineshaft-300">-</span>
|
||||||
)}
|
)}
|
||||||
</Td>
|
</div>
|
||||||
</Tr>
|
</div>
|
||||||
</TBody>
|
<div className="mb-2">
|
||||||
|
<div className="text-sm font-medium text-mineshaft-300">Metadata</div>
|
||||||
|
{newVersion?.secretMetadata?.length ? (
|
||||||
|
<div className="mt-1 flex flex-wrap gap-2 text-sm text-mineshaft-300">
|
||||||
|
{newVersion.secretMetadata?.map((el) => (
|
||||||
|
<div key={el.key} className="flex items-center">
|
||||||
|
<Tag
|
||||||
|
size="xs"
|
||||||
|
className="mr-0 flex items-center rounded-r-none border border-mineshaft-500"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faKey} size="xs" className="mr-1" />
|
||||||
|
<div>{el.key}</div>
|
||||||
|
</Tag>
|
||||||
|
<Tag
|
||||||
|
size="xs"
|
||||||
|
className="flex items-center rounded-l-none border border-mineshaft-500 bg-mineshaft-900 pl-1"
|
||||||
|
>
|
||||||
|
<div className="max-w-[150px] overflow-hidden text-ellipsis whitespace-nowrap">
|
||||||
|
{el.value}
|
||||||
|
</div>
|
||||||
|
</Tag>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="text-sm text-mineshaft-300">-</p>
|
||||||
|
)}
|
||||||
|
</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">
|
||||||
|
{" "}
|
||||||
|
Secret not existent in the new version.
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</Table>
|
</div>
|
||||||
</TableContainer>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+38
-36
@@ -13,6 +13,7 @@ import {
|
|||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { RadioGroup, RadioGroupIndicator, RadioGroupItem } from "@radix-ui/react-radio-group";
|
import { RadioGroup, RadioGroupIndicator, RadioGroupItem } from "@radix-ui/react-radio-group";
|
||||||
|
import { format } from "date-fns";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
|
|
||||||
@@ -51,7 +52,7 @@ export const generateCommitText = (commits: { op: CommitType }[] = []) => {
|
|||||||
text.push(
|
text.push(
|
||||||
<span key="created-commit">
|
<span key="created-commit">
|
||||||
{score[CommitType.CREATE]} secret{score[CommitType.CREATE] !== 1 && "s"}
|
{score[CommitType.CREATE]} secret{score[CommitType.CREATE] !== 1 && "s"}
|
||||||
<span style={{ color: "#16a34a" }}> created</span>
|
<span style={{ color: "#60DD00" }}> created</span>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
if (score[CommitType.UPDATE])
|
if (score[CommitType.UPDATE])
|
||||||
@@ -59,7 +60,7 @@ export const generateCommitText = (commits: { op: CommitType }[] = []) => {
|
|||||||
<span key="updated-commit">
|
<span key="updated-commit">
|
||||||
{Boolean(text.length) && ","}
|
{Boolean(text.length) && ","}
|
||||||
{score[CommitType.UPDATE]} secret{score[CommitType.UPDATE] !== 1 && "s"}
|
{score[CommitType.UPDATE]} secret{score[CommitType.UPDATE] !== 1 && "s"}
|
||||||
<span style={{ color: "#ea580c" }} className="text-orange-600">
|
<span style={{ color: "#F8EB30" }} className="text-orange-600">
|
||||||
{" "}
|
{" "}
|
||||||
updated
|
updated
|
||||||
</span>
|
</span>
|
||||||
@@ -70,7 +71,7 @@ export const generateCommitText = (commits: { op: CommitType }[] = []) => {
|
|||||||
<span className="deleted-commit">
|
<span className="deleted-commit">
|
||||||
{Boolean(text.length) && "and"}
|
{Boolean(text.length) && "and"}
|
||||||
{score[CommitType.DELETE]} secret{score[CommitType.UPDATE] !== 1 && "s"}
|
{score[CommitType.DELETE]} secret{score[CommitType.UPDATE] !== 1 && "s"}
|
||||||
<span style={{ color: "#b91c1c" }}> deleted</span>
|
<span style={{ color: "#F83030" }}> deleted</span>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -221,29 +222,32 @@ export const SecretApprovalRequestChanges = ({
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-grow flex-col">
|
<div className="flex flex-grow flex-col">
|
||||||
<div className="mb-1 text-lg">
|
<div className="text-lg">
|
||||||
{generateCommitText(secretApprovalRequestDetails.commits)}
|
{generateCommitText(secretApprovalRequestDetails.commits)}
|
||||||
{secretApprovalRequestDetails.isReplicated && (
|
{secretApprovalRequestDetails.isReplicated && (
|
||||||
<span className="text-sm text-bunker-300"> (replication)</span>
|
<span className="text-sm text-bunker-300"> (replication)</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center text-sm text-bunker-300">
|
<div className="text-sm text-bunker-300">
|
||||||
{secretApprovalRequestDetails?.committerUser?.firstName || ""}
|
<p className="inline">
|
||||||
{secretApprovalRequestDetails?.committerUser?.lastName || ""} (
|
{secretApprovalRequestDetails?.committerUser?.firstName || ""}
|
||||||
{secretApprovalRequestDetails?.committerUser?.email}) wants to change{" "}
|
{secretApprovalRequestDetails?.committerUser?.lastName || ""} (
|
||||||
{secretApprovalRequestDetails.commits.length} secret values in
|
{secretApprovalRequestDetails?.committerUser?.email}) wants to change{" "}
|
||||||
<span className="mx-1 rounded bg-primary-600/60 px-1 text-primary-300">
|
{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}
|
{secretApprovalRequestDetails.environment}
|
||||||
</span>
|
</p>
|
||||||
<div className="flex w-min items-center rounded border border-mineshaft-500 pl-1 pr-2">
|
<div className="inline-flex w-min items-center rounded border border-mineshaft-500 pl-1 pr-2">
|
||||||
<div className="border-r border-mineshaft-500 pr-1">
|
<p className="cursor-default border-r border-mineshaft-500 pr-1">
|
||||||
<FontAwesomeIcon icon={faFolder} className="text-primary" size="sm" />
|
<FontAwesomeIcon icon={faFolder} className="text-primary" size="sm" />
|
||||||
</div>
|
</p>
|
||||||
<Tooltip content={formatReservedPaths(secretApprovalRequestDetails.secretPath)}>
|
<p
|
||||||
<div className="truncate pb-0.5 pl-2 text-sm" style={{ maxWidth: "10rem" }}>
|
className="cursor-default truncate pb-0.5 pl-2 text-sm"
|
||||||
{formatReservedPaths(secretApprovalRequestDetails.secretPath)}
|
style={{ maxWidth: "10rem" }}
|
||||||
</div>
|
>
|
||||||
</Tooltip>
|
{formatReservedPaths(secretApprovalRequestDetails.secretPath)}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -255,17 +259,14 @@ export const SecretApprovalRequestChanges = ({
|
|||||||
onOpenChange={(isOpen) => handlePopUpToggle("reviewChanges", isOpen)}
|
onOpenChange={(isOpen) => handlePopUpToggle("reviewChanges", isOpen)}
|
||||||
>
|
>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button
|
<Button rightIcon={<FontAwesomeIcon className="ml-2" icon={faAngleDown} />}>
|
||||||
variant="outline_bg"
|
|
||||||
rightIcon={<FontAwesomeIcon className="ml-2" icon={faAngleDown} />}
|
|
||||||
>
|
|
||||||
Review
|
Review
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end" asChild className="mt-3">
|
<DropdownMenuContent align="end" asChild className="mt-3">
|
||||||
<form onSubmit={handleSubmit(handleSubmitReview)}>
|
<form onSubmit={handleSubmit(handleSubmitReview)}>
|
||||||
<div className="flex w-[400px] flex-col space-y-2 p-5">
|
<div className="flex w-[500px] flex-col space-y-2 p-5">
|
||||||
<div className="text-lg font-medium">Finish your review</div>
|
<div className="text-md font-medium">Finish your review</div>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="comment"
|
name="comment"
|
||||||
@@ -275,7 +276,7 @@ export const SecretApprovalRequestChanges = ({
|
|||||||
{...field}
|
{...field}
|
||||||
placeholder="Leave a comment..."
|
placeholder="Leave a comment..."
|
||||||
reSize="none"
|
reSize="none"
|
||||||
className="text-md mt-2 h-48 border border-mineshaft-600 bg-bunker-800"
|
className="text-md mt-2 h-40 border border-mineshaft-600 bg-bunker-800"
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
@@ -320,7 +321,7 @@ export const SecretApprovalRequestChanges = ({
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<RadioGroupItem
|
<RadioGroupItem
|
||||||
id="reject"
|
id="reject"
|
||||||
className="h-4 w-4 rounded-full border border-gray-300 text-primary focus:ring-2 focus:ring-mineshaft-500"
|
className="h-4 w-4 rounded-full border border-gray-300 text-red focus:ring-2 focus:ring-mineshaft-500"
|
||||||
value={ApprovalStatus.REJECTED}
|
value={ApprovalStatus.REJECTED}
|
||||||
aria-labelledby="reject-label"
|
aria-labelledby="reject-label"
|
||||||
>
|
>
|
||||||
@@ -376,14 +377,14 @@ export const SecretApprovalRequestChanges = ({
|
|||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-4 flex flex-col items-center rounded-lg">
|
<div className="my-4 flex flex-col items-center rounded-lg border border-mineshaft-600">
|
||||||
{secretApprovalRequestDetails?.policy?.approvers
|
{secretApprovalRequestDetails?.policy?.approvers
|
||||||
.filter((requiredApprover) => reviewedUsers?.[requiredApprover.userId])
|
.filter((requiredApprover) => reviewedUsers?.[requiredApprover.userId])
|
||||||
.map((requiredApprover) => {
|
.map((requiredApprover) => {
|
||||||
const reviewer = reviewedUsers?.[requiredApprover.userId];
|
const reviewer = reviewedUsers?.[requiredApprover.userId];
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="mb-4 flex w-full flex-col rounded-md bg-mineshaft-800 p-6"
|
className="flex w-full flex-col rounded-md bg-mineshaft-800 p-4"
|
||||||
key={`required-approver-${requiredApprover.userId}`}
|
key={`required-approver-${requiredApprover.userId}`}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
@@ -396,10 +397,11 @@ export const SecretApprovalRequestChanges = ({
|
|||||||
>
|
>
|
||||||
{reviewer?.status === ApprovalStatus.APPROVED ? "approved" : "rejected"}
|
{reviewer?.status === ApprovalStatus.APPROVED ? "approved" : "rejected"}
|
||||||
</span>{" "}
|
</span>{" "}
|
||||||
the request.
|
the request on{" "}
|
||||||
|
{format(new Date(secretApprovalRequestDetails.createdAt), "PPpp zzz")}.
|
||||||
</div>
|
</div>
|
||||||
{reviewer?.comment && (
|
{reviewer?.comment && (
|
||||||
<FormControl label="Comment" className="mb-0 mt-2">
|
<FormControl label="Comment" className="mb-0 mt-4">
|
||||||
<TextArea value={reviewer.comment} isDisabled reSize="none">
|
<TextArea value={reviewer.comment} isDisabled reSize="none">
|
||||||
{reviewer?.comment && reviewer.comment}
|
{reviewer?.comment && reviewer.comment}
|
||||||
</TextArea>
|
</TextArea>
|
||||||
@@ -409,7 +411,7 @@ export const SecretApprovalRequestChanges = ({
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-6 rounded-lg bg-mineshaft-800 px-5 py-6">
|
<div className="mt-2 flex items-center space-x-6 rounded-lg border border-mineshaft-600 bg-mineshaft-800">
|
||||||
<SecretApprovalRequestAction
|
<SecretApprovalRequestAction
|
||||||
canApprove={canApprove}
|
canApprove={canApprove}
|
||||||
approvalRequestId={secretApprovalRequestDetails.id}
|
approvalRequestId={secretApprovalRequestDetails.id}
|
||||||
@@ -423,7 +425,7 @@ export const SecretApprovalRequestChanges = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="sticky top-0 w-1/5 pt-4" style={{ minWidth: "240px" }}>
|
<div className="sticky top-0 w-1/5 cursor-default pt-4" style={{ minWidth: "240px" }}>
|
||||||
<div className="text-sm text-bunker-300">Reviewers</div>
|
<div className="text-sm text-bunker-300">Reviewers</div>
|
||||||
<div className="mt-2 flex flex-col space-y-2 text-sm">
|
<div className="mt-2 flex flex-col space-y-2 text-sm">
|
||||||
{secretApprovalRequestDetails?.policy?.approvers
|
{secretApprovalRequestDetails?.policy?.approvers
|
||||||
@@ -435,10 +437,10 @@ export const SecretApprovalRequestChanges = ({
|
|||||||
const reviewer = reviewedUsers?.[requiredApprover.userId];
|
const reviewer = reviewedUsers?.[requiredApprover.userId];
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="flex flex-nowrap items-center space-x-2 rounded bg-mineshaft-800 px-2 py-1"
|
className="flex flex-nowrap items-center justify-between space-x-2 rounded border border-mineshaft-600 bg-mineshaft-800 px-2 py-1"
|
||||||
key={`required-approver-${requiredApprover.userId}`}
|
key={`required-approver-${requiredApprover.userId}`}
|
||||||
>
|
>
|
||||||
<div className="flex-grow text-sm">
|
<div className="flex text-sm">
|
||||||
<Tooltip
|
<Tooltip
|
||||||
content={`${requiredApprover.firstName || ""} ${
|
content={`${requiredApprover.firstName || ""} ${
|
||||||
requiredApprover.lastName || ""
|
requiredApprover.lastName || ""
|
||||||
@@ -458,7 +460,7 @@ export const SecretApprovalRequestChanges = ({
|
|||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
<Tooltip content={reviewer?.status || ApprovalStatus.PENDING}>
|
<Tooltip content={`Status: ${reviewer?.status || ApprovalStatus.PENDING}`}>
|
||||||
{getReviewedStatusSymbol(reviewer?.status)}
|
{getReviewedStatusSymbol(reviewer?.status)}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user