mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-15 19:16:22 +00:00
Merge branch 'main' into feat/point-in-time-revamp
This commit is contained in:
+14
-2
@@ -32,6 +32,11 @@ import {
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import {
|
||||
getUserTablePreference,
|
||||
PreferenceKey,
|
||||
setUserTablePreference
|
||||
} from "@app/helpers/userTablePreferences";
|
||||
import { usePagination, useResetPageHelper } from "@app/hooks";
|
||||
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||
import { useSyncIntegration } from "@app/hooks/api/integrations/queries";
|
||||
@@ -110,7 +115,14 @@ export const IntegrationsTable = ({
|
||||
orderBy,
|
||||
setOrderDirection,
|
||||
setOrderBy
|
||||
} = usePagination<IntegrationsOrderBy>(IntegrationsOrderBy.App, { initPerPage: 20 });
|
||||
} = usePagination<IntegrationsOrderBy>(IntegrationsOrderBy.App, {
|
||||
initPerPage: getUserTablePreference("integrationsTable", PreferenceKey.PerPage, 20)
|
||||
});
|
||||
|
||||
const handlePerPageChange = (newPerPage: number) => {
|
||||
setPerPage(newPerPage);
|
||||
setUserTablePreference("integrationsTable", PreferenceKey.PerPage, newPerPage);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (integrations?.some((integration) => integration.isSynced === false))
|
||||
@@ -437,7 +449,7 @@ export const IntegrationsTable = ({
|
||||
page={page}
|
||||
perPage={perPage}
|
||||
onChangePage={setPage}
|
||||
onChangePerPage={setPerPage}
|
||||
onChangePerPage={handlePerPageChange}
|
||||
/>
|
||||
)}
|
||||
{!isLoading && !filteredIntegrations?.length && (
|
||||
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
import { TOnePassSync } from "@app/hooks/api/secretSyncs/types/1password-sync";
|
||||
|
||||
import { getSecretSyncDestinationColValues } from "../helpers";
|
||||
import { SecretSyncTableCell } from "../SecretSyncTableCell";
|
||||
|
||||
type Props = {
|
||||
secretSync: TOnePassSync;
|
||||
};
|
||||
|
||||
export const OnePassSyncDestinationCol = ({ secretSync }: Props) => {
|
||||
const { primaryText, secondaryText } = getSecretSyncDestinationColValues(secretSync);
|
||||
|
||||
return <SecretSyncTableCell primaryText={primaryText} secondaryText={secondaryText} />;
|
||||
};
|
||||
+3
@@ -1,5 +1,6 @@
|
||||
import { SecretSync, TSecretSync } from "@app/hooks/api/secretSyncs";
|
||||
|
||||
import { OnePassSyncDestinationCol } from "./1PasswordSyncDestinationCol";
|
||||
import { AwsParameterStoreSyncDestinationCol } from "./AwsParameterStoreSyncDestinationCol";
|
||||
import { AwsSecretsManagerSyncDestinationCol } from "./AwsSecretsManagerSyncDestinationCol";
|
||||
import { AzureAppConfigurationDestinationSyncCol } from "./AzureAppConfigurationDestinationSyncCol";
|
||||
@@ -52,6 +53,8 @@ export const SecretSyncDestinationCol = ({ secretSync }: Props) => {
|
||||
return <TeamCitySyncDestinationCol secretSync={secretSync} />;
|
||||
case SecretSync.OCIVault:
|
||||
return <OCIVaultSyncDestinationCol secretSync={secretSync} />;
|
||||
case SecretSync.OnePass:
|
||||
return <OnePassSyncDestinationCol secretSync={secretSync} />;
|
||||
default:
|
||||
throw new Error(
|
||||
`Unhandled Secret Sync Destination Col: ${(secretSync as TSecretSync).destination}`
|
||||
|
||||
+14
-2
@@ -38,6 +38,11 @@ import {
|
||||
} from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { SECRET_SYNC_MAP } from "@app/helpers/secretSyncs";
|
||||
import {
|
||||
getUserTablePreference,
|
||||
PreferenceKey,
|
||||
setUserTablePreference
|
||||
} from "@app/helpers/userTablePreferences";
|
||||
import { usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
|
||||
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||
import {
|
||||
@@ -119,7 +124,14 @@ export const SecretSyncsTable = ({ secretSyncs }: Props) => {
|
||||
orderBy,
|
||||
setOrderDirection,
|
||||
setOrderBy
|
||||
} = usePagination<SecretSyncsOrderBy>(SecretSyncsOrderBy.Name, { initPerPage: 20 });
|
||||
} = usePagination<SecretSyncsOrderBy>(SecretSyncsOrderBy.Name, {
|
||||
initPerPage: getUserTablePreference("secretSyncTable", PreferenceKey.PerPage, 20)
|
||||
});
|
||||
|
||||
const handlePerPageChange = (newPerPage: number) => {
|
||||
setPerPage(newPerPage);
|
||||
setUserTablePreference("secretSyncTable", PreferenceKey.PerPage, newPerPage);
|
||||
};
|
||||
|
||||
const filteredSecretSyncs = useMemo(
|
||||
() =>
|
||||
@@ -465,7 +477,7 @@ export const SecretSyncsTable = ({ secretSyncs }: Props) => {
|
||||
page={page}
|
||||
perPage={perPage}
|
||||
onChangePage={setPage}
|
||||
onChangePerPage={setPerPage}
|
||||
onChangePerPage={handlePerPageChange}
|
||||
/>
|
||||
)}
|
||||
{!filteredSecretSyncs?.length && (
|
||||
|
||||
+4
@@ -106,6 +106,10 @@ export const getSecretSyncDestinationColValues = (secretSync: TSecretSync) => {
|
||||
primaryText = destinationConfig.compartmentOcid;
|
||||
secondaryText = destinationConfig.vaultOcid;
|
||||
break;
|
||||
case SecretSync.OnePass:
|
||||
primaryText = destinationConfig.vaultId;
|
||||
secondaryText = "Vault ID";
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unhandled Destination Col Values ${destination}`);
|
||||
}
|
||||
|
||||
@@ -64,6 +64,11 @@ import {
|
||||
useWorkspace
|
||||
} from "@app/context";
|
||||
import { ProjectPermissionSecretRotationActions } from "@app/context/ProjectPermissionContext/types";
|
||||
import {
|
||||
getUserTablePreference,
|
||||
PreferenceKey,
|
||||
setUserTablePreference
|
||||
} from "@app/helpers/userTablePreferences";
|
||||
import { useDebounce, usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
|
||||
import {
|
||||
useCreateFolder,
|
||||
@@ -180,7 +185,14 @@ export const OverviewPage = () => {
|
||||
page,
|
||||
setPerPage,
|
||||
orderBy
|
||||
} = usePagination<DashboardSecretsOrderBy>(DashboardSecretsOrderBy.Name);
|
||||
} = usePagination<DashboardSecretsOrderBy>(DashboardSecretsOrderBy.Name, {
|
||||
initPerPage: getUserTablePreference("secretOverviewTable", PreferenceKey.PerPage, 100)
|
||||
});
|
||||
|
||||
const handlePerPageChange = (newPerPage: number) => {
|
||||
setPerPage(newPerPage);
|
||||
setUserTablePreference("secretOverviewTable", PreferenceKey.PerPage, newPerPage);
|
||||
};
|
||||
|
||||
const resetSelectedEntries = useCallback(() => {
|
||||
setSelectedEntries({
|
||||
@@ -1416,7 +1428,7 @@ export const OverviewPage = () => {
|
||||
page={page}
|
||||
perPage={perPage}
|
||||
onChangePage={(newPage) => setPage(newPage)}
|
||||
onChangePerPage={(newPerPage) => setPerPage(newPerPage)}
|
||||
onChangePerPage={handlePerPageChange}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
+1
@@ -232,6 +232,7 @@ export const SecretEditRow = ({
|
||||
environment={environment}
|
||||
isImport={isImportedSecret}
|
||||
defaultValue={secretValueHidden ? "" : undefined}
|
||||
canEditButNotView={secretValueHidden && !isOverride}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -44,8 +44,7 @@ export const SecretApprovalsPage = () => {
|
||||
<div className="container mx-auto h-full w-full max-w-7xl bg-bunker-800 text-white">
|
||||
<PageHeader
|
||||
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
|
||||
href="https://infisical.com/docs/documentation/platform/pr-workflows"
|
||||
|
||||
+109
-91
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable no-nested-ternary */
|
||||
/* eslint-disable react/jsx-no-useless-fragment */
|
||||
import { useMemo, useState } from "react";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import {
|
||||
faCheck,
|
||||
faCheckCircle,
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
} from "@app/components/v2";
|
||||
import { Badge } from "@app/components/v2/Badge";
|
||||
import {
|
||||
ProjectPermissionApprovalActions,
|
||||
ProjectPermissionMemberActions,
|
||||
ProjectPermissionSub,
|
||||
useProjectPermission,
|
||||
@@ -83,8 +84,9 @@ export const AccessApprovalRequest = ({
|
||||
}) => {
|
||||
const [selectedRequest, setSelectedRequest] = useState<
|
||||
| (TAccessApprovalRequest & {
|
||||
user: TWorkspaceUser["user"] | null;
|
||||
user: { firstName?: string; lastName?: string; email?: string } | null;
|
||||
isRequestedByCurrentUser: boolean;
|
||||
isSelfApproveAllowed: boolean;
|
||||
isApprover: boolean;
|
||||
})
|
||||
| null
|
||||
@@ -100,6 +102,11 @@ export const AccessApprovalRequest = ({
|
||||
const { subscription } = useSubscription();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
|
||||
const canBypassApprovalPermission = permission.can(
|
||||
ProjectPermissionApprovalActions.AllowAccessBypass,
|
||||
ProjectPermissionSub.SecretApproval
|
||||
);
|
||||
|
||||
const { data: members } = useGetWorkspaceUsers(projectId, true);
|
||||
const membersGroupById = members?.reduce<Record<string, TWorkspaceUser>>(
|
||||
(prev, curr) => ({ ...prev, [curr.user.id]: curr }),
|
||||
@@ -118,7 +125,7 @@ export const AccessApprovalRequest = ({
|
||||
projectSlug
|
||||
});
|
||||
|
||||
const { data: requests } = useGetAccessApprovalRequests({
|
||||
const { data: requests, refetch: refetchRequests } = useGetAccessApprovalRequests({
|
||||
projectSlug,
|
||||
authorProjectMembershipId: requestedByFilter,
|
||||
envSlug: envFilter
|
||||
@@ -143,56 +150,105 @@ export const AccessApprovalRequest = ({
|
||||
return requests;
|
||||
}, [requests, statusFilter, requestedByFilter, envFilter]);
|
||||
|
||||
const generateRequestDetails = (request: TAccessApprovalRequest) => {
|
||||
const isReviewedByUser = request.reviewers.findIndex(({ member }) => member === user.id) !== -1;
|
||||
const isRejectedByAnyone = request.reviewers.some(
|
||||
({ status }) => status === ApprovalStatus.REJECTED
|
||||
);
|
||||
const isApprover = request.policy.approvers.indexOf(user.id || "") !== -1;
|
||||
const isAccepted = request.isApproved;
|
||||
const isSoftEnforcement = request.policy.enforcementLevel === EnforcementLevel.Soft;
|
||||
const isRequestedByCurrentUser = request.requestedByUserId === user.id;
|
||||
const isSelfApproveAllowed = request.policy.allowedSelfApprovals;
|
||||
const userReviewStatus = request.reviewers.find(({ member }) => member === user.id)?.status;
|
||||
const generateRequestDetails = useCallback(
|
||||
(request: TAccessApprovalRequest) => {
|
||||
const isReviewedByUser =
|
||||
request.reviewers.findIndex(({ member }) => member === user.id) !== -1;
|
||||
const isRejectedByAnyone = request.reviewers.some(
|
||||
({ status }) => status === ApprovalStatus.REJECTED
|
||||
);
|
||||
const isApprover = request.policy.approvers.indexOf(user.id || "") !== -1;
|
||||
const isAccepted = request.isApproved;
|
||||
const isSoftEnforcement = request.policy.enforcementLevel === EnforcementLevel.Soft;
|
||||
const isRequestedByCurrentUser = request.requestedByUserId === user.id;
|
||||
const isSelfApproveAllowed = request.policy.allowedSelfApprovals;
|
||||
const userReviewStatus = request.reviewers.find(({ member }) => member === user.id)?.status;
|
||||
|
||||
let displayData: { label: string; type: "primary" | "danger" | "success" } = {
|
||||
label: "",
|
||||
type: "primary"
|
||||
};
|
||||
|
||||
const isExpired =
|
||||
request.privilege &&
|
||||
request.isApproved &&
|
||||
new Date() > new Date(request.privilege.temporaryAccessEndTime || ("" as string));
|
||||
|
||||
if (isExpired) displayData = { label: "Access Expired", type: "danger" };
|
||||
else if (isAccepted) displayData = { label: "Access Granted", type: "success" };
|
||||
else if (isRejectedByAnyone) displayData = { label: "Rejected", type: "danger" };
|
||||
else if (userReviewStatus === ApprovalStatus.APPROVED) {
|
||||
displayData = {
|
||||
label: `Pending ${request.policy.approvals - request.reviewers.length} review${
|
||||
request.policy.approvals - request.reviewers.length > 1 ? "s" : ""
|
||||
}`,
|
||||
type: "primary"
|
||||
};
|
||||
} else if (!isReviewedByUser)
|
||||
displayData = {
|
||||
label: "Review Required",
|
||||
let displayData: { label: string; type: "primary" | "danger" | "success" } = {
|
||||
label: "",
|
||||
type: "primary"
|
||||
};
|
||||
|
||||
return {
|
||||
displayData,
|
||||
isReviewedByUser,
|
||||
isRejectedByAnyone,
|
||||
isApprover,
|
||||
userReviewStatus,
|
||||
isAccepted,
|
||||
isSoftEnforcement,
|
||||
isRequestedByCurrentUser,
|
||||
isSelfApproveAllowed
|
||||
};
|
||||
};
|
||||
const isExpired =
|
||||
request.privilege &&
|
||||
request.isApproved &&
|
||||
new Date() > new Date(request.privilege.temporaryAccessEndTime || ("" as string));
|
||||
|
||||
if (isExpired) displayData = { label: "Access Expired", type: "danger" };
|
||||
else if (isAccepted) displayData = { label: "Access Granted", type: "success" };
|
||||
else if (isRejectedByAnyone) displayData = { label: "Rejected", type: "danger" };
|
||||
else if (userReviewStatus === ApprovalStatus.APPROVED) {
|
||||
displayData = {
|
||||
label: `Pending ${request.policy.approvals - request.reviewers.length} review${
|
||||
request.policy.approvals - request.reviewers.length > 1 ? "s" : ""
|
||||
}`,
|
||||
type: "primary"
|
||||
};
|
||||
} else if (!isReviewedByUser)
|
||||
displayData = {
|
||||
label: "Review Required",
|
||||
type: "primary"
|
||||
};
|
||||
|
||||
return {
|
||||
displayData,
|
||||
isReviewedByUser,
|
||||
isRejectedByAnyone,
|
||||
isApprover,
|
||||
userReviewStatus,
|
||||
isAccepted,
|
||||
isSoftEnforcement,
|
||||
isRequestedByCurrentUser,
|
||||
isSelfApproveAllowed
|
||||
};
|
||||
},
|
||||
[user]
|
||||
);
|
||||
|
||||
const handleSelectRequest = useCallback(
|
||||
(request: TAccessApprovalRequest) => {
|
||||
const details = generateRequestDetails(request);
|
||||
|
||||
// Whether the request has already been approved / rejected / reviewed
|
||||
const isInactive =
|
||||
details.isAccepted || details.isReviewedByUser || details.isRejectedByAnyone;
|
||||
|
||||
// Whether the current user can bypass policy
|
||||
const canBypass =
|
||||
details.isSoftEnforcement &&
|
||||
details.isRequestedByCurrentUser &&
|
||||
canBypassApprovalPermission;
|
||||
|
||||
// Whether the current user can approve
|
||||
const canApprove =
|
||||
details.isApprover && (!details.isRequestedByCurrentUser || details.isSelfApproveAllowed);
|
||||
|
||||
if (isInactive || (!canApprove && !canBypass)) return;
|
||||
|
||||
if (membersGroupById?.[request.requestedByUserId].user || details.isRequestedByCurrentUser) {
|
||||
setSelectedRequest({
|
||||
...request,
|
||||
user:
|
||||
details.isRequestedByCurrentUser || !membersGroupById?.[request.requestedByUserId].user
|
||||
? user
|
||||
: membersGroupById?.[request.requestedByUserId].user,
|
||||
isRequestedByCurrentUser: details.isRequestedByCurrentUser,
|
||||
isSelfApproveAllowed: details.isSelfApproveAllowed,
|
||||
isApprover: details.isApprover
|
||||
});
|
||||
}
|
||||
|
||||
handlePopUpOpen("reviewRequest");
|
||||
},
|
||||
[
|
||||
generateRequestDetails,
|
||||
canBypassApprovalPermission,
|
||||
membersGroupById,
|
||||
user,
|
||||
setSelectedRequest,
|
||||
handlePopUpOpen
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -344,50 +400,10 @@ export const AccessApprovalRequest = ({
|
||||
className="flex w-full cursor-pointer px-8 py-4 hover:bg-mineshaft-700 aria-disabled:opacity-80"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => {
|
||||
if (
|
||||
((!details.isApprover ||
|
||||
details.isReviewedByUser ||
|
||||
details.isRejectedByAnyone ||
|
||||
details.isAccepted) &&
|
||||
!(
|
||||
details.isSoftEnforcement &&
|
||||
details.isRequestedByCurrentUser &&
|
||||
!details.isAccepted
|
||||
)) ||
|
||||
(request.requestedByUserId === user.id && !details.isSelfApproveAllowed)
|
||||
)
|
||||
return;
|
||||
if (membersGroupById?.[request.requestedByUserId].user) {
|
||||
setSelectedRequest({
|
||||
...request,
|
||||
user: membersGroupById?.[request.requestedByUserId].user,
|
||||
isRequestedByCurrentUser: details.isRequestedByCurrentUser,
|
||||
isApprover: details.isApprover
|
||||
});
|
||||
}
|
||||
|
||||
handlePopUpOpen("reviewRequest");
|
||||
}}
|
||||
onClick={() => handleSelectRequest(request)}
|
||||
onKeyDown={(evt) => {
|
||||
if (
|
||||
!details.isApprover ||
|
||||
details.isAccepted ||
|
||||
details.isReviewedByUser ||
|
||||
details.isRejectedByAnyone
|
||||
)
|
||||
return;
|
||||
if (evt.key === "Enter") {
|
||||
if (membersGroupById?.[request.requestedByUserId].user) {
|
||||
setSelectedRequest({
|
||||
...request,
|
||||
user: membersGroupById?.[request.requestedByUserId].user,
|
||||
isRequestedByCurrentUser: details.isRequestedByCurrentUser,
|
||||
isApprover: details.isApprover
|
||||
});
|
||||
}
|
||||
|
||||
handlePopUpOpen("reviewRequest");
|
||||
handleSelectRequest(request);
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -453,7 +469,9 @@ export const AccessApprovalRequest = ({
|
||||
onOpenChange={() => {
|
||||
handlePopUpClose("reviewRequest");
|
||||
setSelectedRequest(null);
|
||||
refetchRequests();
|
||||
}}
|
||||
canBypassApprovalPermission={canBypassApprovalPermission}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
+108
-50
@@ -1,14 +1,16 @@
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { faTriangleExclamation } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import ms from "ms";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, Checkbox, Modal, ModalContent } from "@app/components/v2";
|
||||
import { Button, Checkbox, FormControl, Input, Modal, ModalContent } from "@app/components/v2";
|
||||
import { Badge } from "@app/components/v2/Badge";
|
||||
import { ProjectPermissionActions } from "@app/context";
|
||||
import { useReviewAccessRequest } from "@app/hooks/api";
|
||||
import { TAccessApprovalRequest } from "@app/hooks/api/accessApproval/types";
|
||||
import { EnforcementLevel } from "@app/hooks/api/policies/enums";
|
||||
import { TWorkspaceUser } from "@app/hooks/api/types";
|
||||
|
||||
export const ReviewAccessRequestModal = ({
|
||||
isOpen,
|
||||
@@ -16,21 +18,26 @@ export const ReviewAccessRequestModal = ({
|
||||
request,
|
||||
projectSlug,
|
||||
selectedRequester,
|
||||
selectedEnvSlug
|
||||
selectedEnvSlug,
|
||||
canBypassApprovalPermission
|
||||
}: {
|
||||
isOpen: boolean;
|
||||
onOpenChange: (isOpen: boolean) => void;
|
||||
request: TAccessApprovalRequest & {
|
||||
user: TWorkspaceUser["user"] | null;
|
||||
user: { firstName?: string; lastName?: string; email?: string } | null;
|
||||
isRequestedByCurrentUser: boolean;
|
||||
isSelfApproveAllowed: boolean;
|
||||
isApprover: boolean;
|
||||
};
|
||||
projectSlug: string;
|
||||
selectedRequester: string | undefined;
|
||||
selectedEnvSlug: string | undefined;
|
||||
canBypassApprovalPermission: boolean;
|
||||
}) => {
|
||||
const [isLoading, setIsLoading] = useState<"approved" | "rejected" | null>(null);
|
||||
const [byPassApproval, setByPassApproval] = useState(false);
|
||||
const [bypassApproval, setBypassApproval] = useState(false);
|
||||
const [bypassReason, setBypassReason] = useState("");
|
||||
|
||||
const isSoftEnforcement = request.policy.enforcementLevel === EnforcementLevel.Soft;
|
||||
|
||||
const accessDetails = {
|
||||
@@ -80,31 +87,52 @@ export const ReviewAccessRequestModal = ({
|
||||
|
||||
const reviewAccessRequest = useReviewAccessRequest();
|
||||
|
||||
const handleReview = useCallback(async (status: "approved" | "rejected") => {
|
||||
setIsLoading(status);
|
||||
try {
|
||||
await reviewAccessRequest.mutateAsync({
|
||||
requestId: request.id,
|
||||
status,
|
||||
projectSlug,
|
||||
envSlug: selectedEnvSlug,
|
||||
requestedBy: selectedRequester
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
const handleReview = useCallback(
|
||||
async (status: "approved" | "rejected") => {
|
||||
if (bypassApproval && bypassReason.length < 10) {
|
||||
createNotification({
|
||||
title: "Failed to bypass approval",
|
||||
text: "Reason must be 10 characters or longer",
|
||||
type: "error"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(status);
|
||||
try {
|
||||
await reviewAccessRequest.mutateAsync({
|
||||
requestId: request.id,
|
||||
status,
|
||||
projectSlug,
|
||||
envSlug: selectedEnvSlug,
|
||||
requestedBy: selectedRequester,
|
||||
bypassReason: bypassApproval ? bypassReason : undefined
|
||||
});
|
||||
|
||||
createNotification({
|
||||
title: `Request ${status}`,
|
||||
text: `The request has been ${status}`,
|
||||
type: status === "approved" ? "success" : "info"
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
setIsLoading(null);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(null);
|
||||
return;
|
||||
}
|
||||
|
||||
createNotification({
|
||||
title: `Request ${status}`,
|
||||
text: `The request has been ${status}`,
|
||||
type: status === "approved" ? "success" : "info"
|
||||
});
|
||||
|
||||
setIsLoading(null);
|
||||
onOpenChange(false);
|
||||
}, []);
|
||||
onOpenChange(false);
|
||||
},
|
||||
[
|
||||
bypassApproval,
|
||||
bypassReason,
|
||||
reviewAccessRequest,
|
||||
request,
|
||||
selectedEnvSlug,
|
||||
selectedRequester,
|
||||
onOpenChange
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||
@@ -115,12 +143,17 @@ export const ReviewAccessRequestModal = ({
|
||||
>
|
||||
<div className="text-sm">
|
||||
<span>
|
||||
<span className="font-bold">
|
||||
{request.user?.firstName} {request.user?.lastName} ({request.user?.email})
|
||||
</span>{" "}
|
||||
{request.user &&
|
||||
(request.user.firstName || request.user.lastName) &&
|
||||
request.user.email ? (
|
||||
<span className="font-bold">
|
||||
{request.user?.firstName} {request.user?.lastName} ({request.user?.email})
|
||||
</span>
|
||||
) : (
|
||||
<span>A user</span>
|
||||
)}{" "}
|
||||
is requesting access to the following resource:
|
||||
</span>
|
||||
|
||||
<div className="mb-2 mt-4 border-l border-blue-500 bg-blue-500/20 px-3 py-2 text-mineshaft-200">
|
||||
<div className="mb-1 lowercase">
|
||||
<span className="font-bold capitalize">Requested path: </span>
|
||||
@@ -144,12 +177,16 @@ export const ReviewAccessRequestModal = ({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-x-2">
|
||||
<Button
|
||||
isLoading={isLoading === "approved"}
|
||||
isDisabled={
|
||||
!!isLoading || (!request.isApprover && !byPassApproval && isSoftEnforcement)
|
||||
!!isLoading ||
|
||||
(!(
|
||||
request.isApprover &&
|
||||
(!request.isRequestedByCurrentUser || request.isSelfApproveAllowed)
|
||||
) &&
|
||||
!bypassApproval)
|
||||
}
|
||||
onClick={() => handleReview("approved")}
|
||||
className="mt-4"
|
||||
@@ -168,21 +205,42 @@ export const ReviewAccessRequestModal = ({
|
||||
Reject Request
|
||||
</Button>
|
||||
</div>
|
||||
{isSoftEnforcement && request.isRequestedByCurrentUser && !request.isApprover && (
|
||||
<div className="mt-4">
|
||||
<Checkbox
|
||||
onCheckedChange={(checked) => setByPassApproval(checked === true)}
|
||||
isChecked={byPassApproval}
|
||||
id="byPassApproval"
|
||||
checkIndicatorBg="text-white"
|
||||
className={byPassApproval ? "border-red bg-red hover:bg-red-600" : ""}
|
||||
>
|
||||
<span className="text-sm text-red">
|
||||
Approve without waiting for requirements to be met (bypass policy protection)
|
||||
</span>
|
||||
</Checkbox>
|
||||
</div>
|
||||
)}
|
||||
{isSoftEnforcement &&
|
||||
request.isRequestedByCurrentUser &&
|
||||
!(request.isApprover && request.isSelfApproveAllowed) &&
|
||||
canBypassApprovalPermission && (
|
||||
<div className="mt-2 flex flex-col space-y-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-xs text-red">
|
||||
Approve without waiting for requirements to be met (bypass policy protection)
|
||||
</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.currentTarget.value)}
|
||||
placeholder="Enter reason for bypass (min 10 chars)"
|
||||
leftIcon={<FontAwesomeIcon icon={faTriangleExclamation} />}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
|
||||
+10
-6
@@ -29,13 +29,13 @@ import {
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub,
|
||||
TProjectPermission,
|
||||
useProjectPermission,
|
||||
useSubscription,
|
||||
useWorkspace
|
||||
} from "@app/context";
|
||||
import { ProjectPermissionApprovalActions } from "@app/context/ProjectPermissionContext/types";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import {
|
||||
useDeleteAccessApprovalPolicy,
|
||||
@@ -61,8 +61,10 @@ const useApprovalPolicies = (permission: TProjectPermission, currentWorkspace?:
|
||||
projectSlug: currentWorkspace?.slug as string,
|
||||
options: {
|
||||
enabled:
|
||||
permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval) &&
|
||||
!!currentWorkspace?.slug
|
||||
permission.can(
|
||||
ProjectPermissionApprovalActions.Read,
|
||||
ProjectPermissionSub.SecretApproval
|
||||
) && !!currentWorkspace?.slug
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -71,8 +73,10 @@ const useApprovalPolicies = (permission: TProjectPermission, currentWorkspace?:
|
||||
workspaceId: currentWorkspace?.id as string,
|
||||
options: {
|
||||
enabled:
|
||||
permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval) &&
|
||||
!!currentWorkspace?.id
|
||||
permission.can(
|
||||
ProjectPermissionApprovalActions.Read,
|
||||
ProjectPermissionSub.SecretApproval
|
||||
) && !!currentWorkspace?.id
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -160,7 +164,7 @@ export const ApprovalPolicyList = ({ workspaceId }: IProps) => {
|
||||
</div>
|
||||
<div>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Create}
|
||||
I={ProjectPermissionApprovalActions.Create}
|
||||
a={ProjectPermissionSub.SecretApproval}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
|
||||
+45
-69
@@ -126,8 +126,6 @@ export const AccessPolicyForm = ({
|
||||
|
||||
const policyName = policyDetails[watch("policyType")]?.name || "Policy";
|
||||
|
||||
const approversRequired = watch("approvals") || 1;
|
||||
|
||||
const handleCreatePolicy = async ({
|
||||
environment,
|
||||
groupApprovers,
|
||||
@@ -303,73 +301,6 @@ export const AccessPolicyForm = ({
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="enforcementLevel"
|
||||
defaultValue={EnforcementLevel.Hard}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Enforcement Level"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
tooltipText={
|
||||
<>
|
||||
<p>
|
||||
Determines the level of enforcement for required approvers of a request:
|
||||
</p>
|
||||
<p className="mt-2">
|
||||
<span className="font-bold">Hard</span> enforcement requires at least{" "}
|
||||
<span className="font-bold"> {approversRequired}</span> approver(s) to
|
||||
approve the request.`
|
||||
</p>
|
||||
<p className="mt-2">
|
||||
<span className="font-bold">Soft</span> enforcement At least{" "}
|
||||
<span className="font-bold">{approversRequired}</span> approver(s) must
|
||||
approve the request; however, the requester can bypass approval
|
||||
requirements in emergencies.
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Select
|
||||
value={field.value}
|
||||
onValueChange={(val) => field.onChange(val as EnforcementLevel)}
|
||||
className="w-full border border-mineshaft-500"
|
||||
>
|
||||
{Object.values(EnforcementLevel).map((level) => {
|
||||
return (
|
||||
<SelectItem value={level} key={`enforcement-level-${level}`}>
|
||||
<span className="capitalize">{level}</span>
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
name="environment"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Environment"
|
||||
isRequired
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<FilterableSelect
|
||||
isDisabled={isEditMode}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder="Select environment..."
|
||||
options={environments}
|
||||
getOptionValue={(option) => option.slug}
|
||||
getOptionLabel={(option) => option.name}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="secretPath"
|
||||
@@ -386,6 +317,28 @@ export const AccessPolicyForm = ({
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="environment"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Environment"
|
||||
isRequired
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<FilterableSelect
|
||||
isDisabled={isEditMode}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder="Select environment..."
|
||||
options={environments}
|
||||
getOptionValue={(option) => option.slug}
|
||||
getOptionLabel={(option) => option.name}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div className="mb-2">
|
||||
<p>Approvers</p>
|
||||
<p className="font-inter text-xs text-mineshaft-300 opacity-90">
|
||||
@@ -465,6 +418,29 @@ export const AccessPolicyForm = ({
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="enforcementLevel"
|
||||
defaultValue={EnforcementLevel.Hard}
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Bypass Approvals"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Switch
|
||||
id="bypass-approvals"
|
||||
thumbClassName="bg-mineshaft-800"
|
||||
isChecked={value === EnforcementLevel.Soft}
|
||||
onCheckedChange={(v) =>
|
||||
onChange(v ? EnforcementLevel.Soft : EnforcementLevel.Hard)
|
||||
}
|
||||
>
|
||||
Allow request creators to bypass policy in break-glass situations
|
||||
</Switch>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div className="mt-8 flex items-center space-x-4">
|
||||
<Button type="submit" isLoading={isSubmitting} isDisabled={isSubmitting}>
|
||||
Save
|
||||
|
||||
+4
-3
@@ -14,7 +14,8 @@ import {
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { Badge } from "@app/components/v2/Badge";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||
import { ProjectPermissionSub } from "@app/context";
|
||||
import { ProjectPermissionApprovalActions } from "@app/context/ProjectPermissionContext/types";
|
||||
import { getMemberLabel } from "@app/helpers/members";
|
||||
import { policyDetails } from "@app/helpers/policies";
|
||||
import { Approver } from "@app/hooks/api/accessApproval/types";
|
||||
@@ -117,7 +118,7 @@ export const ApprovalPolicyRow = ({
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="center" className="min-w-[100%] p-1">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
I={ProjectPermissionApprovalActions.Edit}
|
||||
a={ProjectPermissionSub.SecretApproval}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
@@ -136,7 +137,7 @@ export const ApprovalPolicyRow = ({
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
I={ProjectPermissionApprovalActions.Delete}
|
||||
a={ProjectPermissionSub.SecretApproval}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
|
||||
+78
-54
@@ -4,16 +4,20 @@ import {
|
||||
faClose,
|
||||
faLandMineOn,
|
||||
faLockOpen,
|
||||
faSquareCheck,
|
||||
faSquareXmark,
|
||||
faTriangleExclamation,
|
||||
faUserLock
|
||||
faUserLock,
|
||||
faXmark
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, Checkbox, FormControl, Input } from "@app/components/v2";
|
||||
import {
|
||||
ProjectPermissionApprovalActions,
|
||||
ProjectPermissionSub,
|
||||
useProjectPermission
|
||||
} from "@app/context";
|
||||
import {
|
||||
usePerformSecretApprovalRequestMerge,
|
||||
useUpdateSecretApprovalRequestStatus
|
||||
@@ -49,6 +53,12 @@ export const SecretApprovalRequestAction = ({
|
||||
const { mutateAsync: updateSecretStatusChange, isPending: isStatusChanging } =
|
||||
useUpdateSecretApprovalRequestStatus();
|
||||
|
||||
const { permission } = useProjectPermission();
|
||||
const canBypassApprovalPermission = permission.can(
|
||||
ProjectPermissionApprovalActions.AllowChangeBypass,
|
||||
ProjectPermissionSub.SecretApproval
|
||||
);
|
||||
|
||||
const [byPassApproval, setByPassApproval] = useState(false);
|
||||
const [bypassReason, setBypassReason] = useState("");
|
||||
|
||||
@@ -101,62 +111,76 @@ export const SecretApprovalRequestAction = ({
|
||||
|
||||
if (!hasMerged && status === "open") {
|
||||
return (
|
||||
<div className="flex w-full items-start justify-between transition-all">
|
||||
<div className="flex items-start space-x-4">
|
||||
<FontAwesomeIcon
|
||||
icon={isMergable ? faSquareCheck : faSquareXmark}
|
||||
className={twMerge("pt-1 text-2xl", isMergable ? "text-primary" : "text-red-600")}
|
||||
/>
|
||||
<div className="flex w-full flex-col items-start justify-between py-4 transition-all">
|
||||
<div className="flex items-center space-x-4 px-4">
|
||||
<div
|
||||
className={`flex items-center justify-center rounded-full ${isMergable ? "h-10 w-10 bg-green" : "h-11 w-11 bg-red-600"}`}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={isMergable ? faCheck : faXmark}
|
||||
className={isMergable ? "text-lg text-black" : "text-2xl text-white"}
|
||||
/>
|
||||
</div>
|
||||
<span className="flex flex-col">
|
||||
{isMergable ? "Good to merge" : "Review required"}
|
||||
<span className="inline-block text-xs text-bunker-200">
|
||||
At least {approvals} approving review required
|
||||
{Boolean(statusChangeByEmail) && `. Reopened by ${statusChangeByEmail}`}
|
||||
</span>
|
||||
{isSoftEnforcement && !isMergable && (
|
||||
<div className="mt-2 flex flex-col space-y-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-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>
|
||||
<p className={`text-md font-medium ${isMergable && "text-lg"}`}>
|
||||
{isMergable ? "Good to merge" : "Merging is blocked"}
|
||||
</p>
|
||||
{!isMergable && (
|
||||
<span className="inline-block text-xs text-bunker-200">
|
||||
At least {approvals} approving review{`${approvals > 1 ? "s" : ""}`} required by
|
||||
eligible reviewers.
|
||||
{Boolean(statusChangeByEmail) && `. Reopened by ${statusChangeByEmail}`}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</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 ? (
|
||||
<>
|
||||
<div className="flex items-center space-x-4">
|
||||
<Button
|
||||
onClick={() => handleSecretApprovalStatusChange("close")}
|
||||
isLoading={isStatusChanging}
|
||||
variant="outline_bg"
|
||||
colorSchema="secondary"
|
||||
colorSchema="primary"
|
||||
leftIcon={<FontAwesomeIcon icon={faClose} />}
|
||||
className="hover:border-red/60 hover:bg-red/10"
|
||||
>
|
||||
Close request
|
||||
</Button>
|
||||
@@ -175,7 +199,7 @@ export const SecretApprovalRequestAction = ({
|
||||
>
|
||||
Merge
|
||||
</Button>
|
||||
</>
|
||||
</div>
|
||||
) : (
|
||||
<div>Only approvers can merge</div>
|
||||
)}
|
||||
@@ -186,13 +210,13 @@ export const SecretApprovalRequestAction = ({
|
||||
|
||||
if (hasMerged && status === "close")
|
||||
return (
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div className="flex items-start space-x-4">
|
||||
<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 p-4">
|
||||
<FontAwesomeIcon icon={faCheck} className="pt-1 text-2xl text-primary" />
|
||||
<span className="flex flex-col">
|
||||
Secret approval merged
|
||||
Change request merged
|
||||
<span className="inline-block text-xs text-bunker-200">
|
||||
Merged by {statusChangeByEmail}
|
||||
Merged by {statusChangeByEmail}.
|
||||
</span>
|
||||
</span>
|
||||
</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 {
|
||||
SecretInput,
|
||||
Table,
|
||||
TableContainer,
|
||||
Tag,
|
||||
TBody,
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { Tag, Tooltip } from "@app/components/v2";
|
||||
import { CommitType, SecretV3Raw, TSecretApprovalSecChange, WsTag } from "@app/hooks/api/types";
|
||||
|
||||
export type Props = {
|
||||
@@ -29,19 +29,19 @@ export type Props = {
|
||||
|
||||
const generateItemTitle = (op: CommitType) => {
|
||||
let text = { label: "", color: "" };
|
||||
if (op === CommitType.CREATE) text = { label: "create", color: "#16a34a" };
|
||||
else if (op === CommitType.UPDATE) text = { label: "change", color: "#ea580c" };
|
||||
else text = { label: "deletion", color: "#b91c1c" };
|
||||
if (op === CommitType.CREATE) text = { label: "create", color: "#60DD00" };
|
||||
else if (op === CommitType.UPDATE) text = { label: "change", color: "#F8EB30" };
|
||||
else text = { label: "deletion", color: "#F83030" };
|
||||
|
||||
return (
|
||||
<span>
|
||||
<div className="text-md pb-2 font-medium">
|
||||
Request for <span style={{ color: text.color }}>secret {text.label}</span>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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>;
|
||||
return null;
|
||||
};
|
||||
@@ -59,10 +59,12 @@ export const SecretApprovalRequestChangeItem = ({
|
||||
const itemConflict =
|
||||
hasMerged && conflicts.find((el) => el.op === op && el.secretId === newVersion?.id);
|
||||
const hasConflict = Boolean(itemConflict);
|
||||
const [isOldSecretValueVisible, setIsOldSecretValueVisible] = useState(false);
|
||||
const [isNewSecretValueVisible, setIsNewSecretValueVisible] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="rounded-lg bg-bunker-500 px-4 pb-4 pt-2">
|
||||
<div className="flex items-center px-1 py-3">
|
||||
<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-1">
|
||||
<div className="flex-grow">{generateItemTitle(op)}</div>
|
||||
{!hasMerged && isStale && (
|
||||
<div className="flex items-center">
|
||||
@@ -79,48 +81,86 @@ export const SecretApprovalRequestChangeItem = ({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
{op === CommitType.UPDATE && <Th className="w-12 shrink-0" />}
|
||||
<Th className="w-48 shrink-0">Secret</Th>
|
||||
<Th className="min-w-0 flex-1">Value</Th>
|
||||
<Th className="w-24 shrink-0">Comment</Th>
|
||||
<Th className="w-24 shrink-0">Tags</Th>
|
||||
<Th className="w-40 shrink-0">Metadata</Th>
|
||||
</Tr>
|
||||
</THead>
|
||||
{op === CommitType.UPDATE ? (
|
||||
<TBody>
|
||||
<Tr>
|
||||
<Td className="text-red-600">OLD</Td>
|
||||
<Td>{secretVersion?.secretKey}</Td>
|
||||
<Td>
|
||||
<div>
|
||||
<div className="flex flex-col space-x-0 space-y-4 xl:flex-row xl:space-x-4 xl:space-y-0">
|
||||
{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="mb-4 flex flex-row justify-between">
|
||||
<span className="text-md font-medium">Legacy Secret</span>
|
||||
<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" />
|
||||
Deprecated
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-2">
|
||||
<div className="text-sm font-medium text-mineshaft-300">Key</div>
|
||||
<div className="text-sm">{secretVersion?.secretKey} </div>
|
||||
</div>
|
||||
<div className="mb-2">
|
||||
<div className="text-sm font-medium text-mineshaft-300">Value</div>
|
||||
<div className="text-sm">
|
||||
{newVersion?.isRotatedSecret ? (
|
||||
<span className="text-mineshaft-400">
|
||||
Rotated Secret value will not be affected
|
||||
</span>
|
||||
) : (
|
||||
<SecretInput isReadOnly value={secretVersion?.secretValue} />
|
||||
)}
|
||||
</Td>
|
||||
<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
|
||||
onClick={() => setIsOldSecretValueVisible(!isOldSecretValueVisible)}
|
||||
className="flex flex-row items-center justify-between rounded-md border border-mineshaft-500 bg-mineshaft-900 pl-2"
|
||||
>
|
||||
<div
|
||||
className="h-3 w-3 rounded-full"
|
||||
style={{ backgroundColor: color || "#bec2c8" }}
|
||||
/>
|
||||
<div className="text-sm">{slug}</div>
|
||||
</Tag>
|
||||
))}
|
||||
</Td>
|
||||
<Td>
|
||||
className={`flex font-mono ${isOldSecretValueVisible || !secretVersion?.secretValue ? "text-md py-[0.55rem]" : "text-lg"}`}
|
||||
>
|
||||
{isOldSecretValueVisible
|
||||
? secretVersion?.secretValue || "EMPTY"
|
||||
: secretVersion?.secretValue
|
||||
? secretVersion?.secretValue?.split("").map(() => "•")
|
||||
: "EMPTY"}{" "}
|
||||
</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 ? (
|
||||
<div className="mt-1 flex flex-wrap gap-2 text-sm text-mineshaft-300">
|
||||
{secretVersion.secretMetadata?.map((el) => (
|
||||
@@ -146,93 +186,77 @@ export const SecretApprovalRequestChangeItem = ({
|
||||
) : (
|
||||
<p className="text-sm text-mineshaft-300">-</p>
|
||||
)}
|
||||
</Td>
|
||||
</Tr>
|
||||
<Tr>
|
||||
<Td className="text-green-600">NEW</Td>
|
||||
<Td>{newVersion?.secretKey}</Td>
|
||||
<Td>
|
||||
</div>
|
||||
</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 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 ? (
|
||||
<span className="text-mineshaft-400">
|
||||
Rotated Secret value will not be affected
|
||||
</span>
|
||||
) : (
|
||||
<SecretInput isReadOnly value={newVersion?.secretValue} />
|
||||
)}
|
||||
</Td>
|
||||
<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
|
||||
onClick={() => setIsNewSecretValueVisible(!isNewSecretValueVisible)}
|
||||
className="flex flex-row items-center justify-between rounded-md border border-mineshaft-500 bg-mineshaft-900 pl-2"
|
||||
>
|
||||
<div
|
||||
className="h-3 w-3 rounded-full"
|
||||
style={{ backgroundColor: color || "#bec2c8" }}
|
||||
/>
|
||||
<div className="text-sm">{slug}</div>
|
||||
</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>
|
||||
className={`flex font-mono ${isNewSecretValueVisible || !newVersion?.secretValue ? "text-md py-[0.55rem]" : "text-lg"}`}
|
||||
>
|
||||
{isNewSecretValueVisible
|
||||
? newVersion?.secretValue || "EMPTY"
|
||||
: newVersion?.secretValue
|
||||
? newVersion?.secretValue?.split("").map(() => "•")
|
||||
: "EMPTY"}{" "}
|
||||
</div>
|
||||
{newVersion?.secretValue && (
|
||||
<div className="flex h-10 w-10 items-center justify-center">
|
||||
<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>
|
||||
) : (
|
||||
<p className="text-sm text-mineshaft-300">-</p>
|
||||
)}
|
||||
</Td>
|
||||
</Tr>
|
||||
</TBody>
|
||||
) : (
|
||||
<TBody>
|
||||
<Tr>
|
||||
<Td>
|
||||
{op === CommitType.CREATE ? newVersion?.secretKey : secretVersion?.secretKey}
|
||||
</Td>
|
||||
<Td>
|
||||
<SecretInput
|
||||
isReadOnly
|
||||
value={
|
||||
op === CommitType.CREATE
|
||||
? newVersion?.secretValue
|
||||
: 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 }) => (
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-2">
|
||||
<div className="text-sm font-medium text-mineshaft-300">Comment</div>
|
||||
<div className="text-sm">
|
||||
{newVersion?.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">
|
||||
{(newVersion?.tags?.length ?? 0) ? (
|
||||
newVersion?.tags?.map(({ slug, id: tagId, color }) => (
|
||||
<Tag
|
||||
className="flex w-min items-center space-x-2"
|
||||
key={`${
|
||||
op === CommitType.CREATE ? newVersion?.id : secretVersion?.id
|
||||
}-${tagId}`}
|
||||
key={`${newVersion.id}-${tagId}`}
|
||||
>
|
||||
<div
|
||||
className="h-3 w-3 rounded-full"
|
||||
@@ -240,41 +264,49 @@ export const SecretApprovalRequestChangeItem = ({
|
||||
/>
|
||||
<div className="text-sm">{slug}</div>
|
||||
</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>
|
||||
</Tr>
|
||||
</TBody>
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
</TableContainer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
+38
-36
@@ -13,6 +13,7 @@ import {
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { RadioGroup, RadioGroupIndicator, RadioGroupItem } from "@radix-ui/react-radio-group";
|
||||
import { format } from "date-fns";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import z from "zod";
|
||||
|
||||
@@ -51,7 +52,7 @@ export const generateCommitText = (commits: { op: CommitType }[] = []) => {
|
||||
text.push(
|
||||
<span key="created-commit">
|
||||
{score[CommitType.CREATE]} secret{score[CommitType.CREATE] !== 1 && "s"}
|
||||
<span style={{ color: "#16a34a" }}> created</span>
|
||||
<span style={{ color: "#60DD00" }}> created</span>
|
||||
</span>
|
||||
);
|
||||
if (score[CommitType.UPDATE])
|
||||
@@ -59,7 +60,7 @@ export const generateCommitText = (commits: { op: CommitType }[] = []) => {
|
||||
<span key="updated-commit">
|
||||
{Boolean(text.length) && ","}
|
||||
{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
|
||||
</span>
|
||||
@@ -70,7 +71,7 @@ export const generateCommitText = (commits: { op: CommitType }[] = []) => {
|
||||
<span className="deleted-commit">
|
||||
{Boolean(text.length) && "and"}
|
||||
{score[CommitType.DELETE]} secret{score[CommitType.UPDATE] !== 1 && "s"}
|
||||
<span style={{ color: "#b91c1c" }}> deleted</span>
|
||||
<span style={{ color: "#F83030" }}> deleted</span>
|
||||
</span>
|
||||
);
|
||||
|
||||
@@ -221,29 +222,32 @@ export const SecretApprovalRequestChanges = ({
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-grow flex-col">
|
||||
<div className="mb-1 text-lg">
|
||||
<div className="text-lg">
|
||||
{generateCommitText(secretApprovalRequestDetails.commits)}
|
||||
{secretApprovalRequestDetails.isReplicated && (
|
||||
<span className="text-sm text-bunker-300"> (replication)</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center text-sm text-bunker-300">
|
||||
{secretApprovalRequestDetails?.committerUser?.firstName || ""}
|
||||
{secretApprovalRequestDetails?.committerUser?.lastName || ""} (
|
||||
{secretApprovalRequestDetails?.committerUser?.email}) wants to change{" "}
|
||||
{secretApprovalRequestDetails.commits.length} secret values in
|
||||
<span className="mx-1 rounded bg-primary-600/60 px-1 text-primary-300">
|
||||
<div className="text-sm text-bunker-300">
|
||||
<p className="inline">
|
||||
{secretApprovalRequestDetails?.committerUser?.firstName || ""}
|
||||
{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}
|
||||
</span>
|
||||
<div className="flex w-min items-center rounded border border-mineshaft-500 pl-1 pr-2">
|
||||
<div className="border-r border-mineshaft-500 pr-1">
|
||||
</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" />
|
||||
</div>
|
||||
<Tooltip content={formatReservedPaths(secretApprovalRequestDetails.secretPath)}>
|
||||
<div className="truncate pb-0.5 pl-2 text-sm" style={{ maxWidth: "10rem" }}>
|
||||
{formatReservedPaths(secretApprovalRequestDetails.secretPath)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</p>
|
||||
<p
|
||||
className="cursor-default truncate pb-0.5 pl-2 text-sm"
|
||||
style={{ maxWidth: "10rem" }}
|
||||
>
|
||||
{formatReservedPaths(secretApprovalRequestDetails.secretPath)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -255,17 +259,14 @@ export const SecretApprovalRequestChanges = ({
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("reviewChanges", isOpen)}
|
||||
>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
rightIcon={<FontAwesomeIcon className="ml-2" icon={faAngleDown} />}
|
||||
>
|
||||
<Button rightIcon={<FontAwesomeIcon className="ml-2" icon={faAngleDown} />}>
|
||||
Review
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" asChild className="mt-3">
|
||||
<form onSubmit={handleSubmit(handleSubmitReview)}>
|
||||
<div className="flex w-[400px] flex-col space-y-2 p-5">
|
||||
<div className="text-lg font-medium">Finish your review</div>
|
||||
<div className="flex w-[500px] flex-col space-y-2 p-5">
|
||||
<div className="text-md font-medium">Finish your review</div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="comment"
|
||||
@@ -275,7 +276,7 @@ export const SecretApprovalRequestChanges = ({
|
||||
{...field}
|
||||
placeholder="Leave a comment..."
|
||||
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>
|
||||
)}
|
||||
@@ -320,7 +321,7 @@ export const SecretApprovalRequestChanges = ({
|
||||
<div className="flex items-center gap-2">
|
||||
<RadioGroupItem
|
||||
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}
|
||||
aria-labelledby="reject-label"
|
||||
>
|
||||
@@ -376,14 +377,14 @@ export const SecretApprovalRequestChanges = ({
|
||||
)
|
||||
)}
|
||||
</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
|
||||
.filter((requiredApprover) => reviewedUsers?.[requiredApprover.userId])
|
||||
.map((requiredApprover) => {
|
||||
const reviewer = reviewedUsers?.[requiredApprover.userId];
|
||||
return (
|
||||
<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}`}
|
||||
>
|
||||
<div>
|
||||
@@ -396,10 +397,11 @@ export const SecretApprovalRequestChanges = ({
|
||||
>
|
||||
{reviewer?.status === ApprovalStatus.APPROVED ? "approved" : "rejected"}
|
||||
</span>{" "}
|
||||
the request.
|
||||
the request on{" "}
|
||||
{format(new Date(secretApprovalRequestDetails.createdAt), "PPpp zzz")}.
|
||||
</div>
|
||||
{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">
|
||||
{reviewer?.comment && reviewer.comment}
|
||||
</TextArea>
|
||||
@@ -409,7 +411,7 @@ export const SecretApprovalRequestChanges = ({
|
||||
);
|
||||
})}
|
||||
</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
|
||||
canApprove={canApprove}
|
||||
approvalRequestId={secretApprovalRequestDetails.id}
|
||||
@@ -423,7 +425,7 @@ export const SecretApprovalRequestChanges = ({
|
||||
/>
|
||||
</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="mt-2 flex flex-col space-y-2 text-sm">
|
||||
{secretApprovalRequestDetails?.policy?.approvers
|
||||
@@ -435,10 +437,10 @@ export const SecretApprovalRequestChanges = ({
|
||||
const reviewer = reviewedUsers?.[requiredApprover.userId];
|
||||
return (
|
||||
<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}`}
|
||||
>
|
||||
<div className="flex-grow text-sm">
|
||||
<div className="flex text-sm">
|
||||
<Tooltip
|
||||
content={`${requiredApprover.firstName || ""} ${
|
||||
requiredApprover.lastName || ""
|
||||
@@ -458,7 +460,7 @@ export const SecretApprovalRequestChanges = ({
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip content={reviewer?.status || ApprovalStatus.PENDING}>
|
||||
<Tooltip content={`Status: ${reviewer?.status || ApprovalStatus.PENDING}`}>
|
||||
{getReviewedStatusSymbol(reviewer?.status)}
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
@@ -29,6 +29,11 @@ import {
|
||||
ProjectPermissionSecretActions,
|
||||
ProjectPermissionSecretRotationActions
|
||||
} from "@app/context/ProjectPermissionContext/types";
|
||||
import {
|
||||
getUserTablePreference,
|
||||
PreferenceKey,
|
||||
setUserTablePreference
|
||||
} from "@app/helpers/userTablePreferences";
|
||||
import { useDebounce, usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
|
||||
import {
|
||||
useGetImportedSecretsSingleEnv,
|
||||
@@ -99,7 +104,14 @@ const Page = () => {
|
||||
page,
|
||||
setPerPage,
|
||||
orderBy
|
||||
} = usePagination<DashboardSecretsOrderBy>(DashboardSecretsOrderBy.Name);
|
||||
} = usePagination<DashboardSecretsOrderBy>(DashboardSecretsOrderBy.Name, {
|
||||
initPerPage: getUserTablePreference("secretDashboardTable", PreferenceKey.PerPage, 100)
|
||||
});
|
||||
|
||||
const handlePerPageChange = (newPerPage: number) => {
|
||||
setPerPage(newPerPage);
|
||||
setUserTablePreference("secretDashboardTable", PreferenceKey.PerPage, newPerPage);
|
||||
};
|
||||
|
||||
const [snapshotId, setSnapshotId] = useState<string | null>(null);
|
||||
const isRollbackMode = Boolean(snapshotId);
|
||||
@@ -604,7 +616,7 @@ const Page = () => {
|
||||
page={page}
|
||||
perPage={perPage}
|
||||
onChangePage={(newPage) => setPage(newPage)}
|
||||
onChangePerPage={(newPerPage) => setPerPage(newPerPage)}
|
||||
onChangePerPage={handlePerPageChange}
|
||||
/>
|
||||
)}
|
||||
<Modal
|
||||
|
||||
+4
-3
@@ -58,7 +58,7 @@ import {
|
||||
} from "./SecretListView.utils";
|
||||
import { CollapsibleSecretImports } from "./CollapsibleSecretImports";
|
||||
|
||||
const hiddenValue = "******";
|
||||
export const HIDDEN_SECRET_VALUE = "******";
|
||||
|
||||
type Props = {
|
||||
secret: SecretV3RawSanitized;
|
||||
@@ -122,7 +122,7 @@ export const SecretItem = memo(
|
||||
|
||||
const getDefaultValue = () => {
|
||||
if (secret.secretValueHidden) {
|
||||
return canEditSecretValue ? hiddenValue : "";
|
||||
return canEditSecretValue ? HIDDEN_SECRET_VALUE : "";
|
||||
}
|
||||
return secret.valueOverride || secret.value || "";
|
||||
};
|
||||
@@ -366,10 +366,11 @@ export const SecretItem = memo(
|
||||
isReadOnly={isReadOnly || isRotatedSecret}
|
||||
key="secret-value"
|
||||
isVisible={isVisible && !secretValueHidden}
|
||||
canEditButNotView={secretValueHidden && !isOverriden}
|
||||
environment={environment}
|
||||
secretPath={secretPath}
|
||||
{...field}
|
||||
defaultValue={secretValueHidden ? hiddenValue : undefined}
|
||||
defaultValue={secretValueHidden ? HIDDEN_SECRET_VALUE : undefined}
|
||||
containerClassName="py-1.5 rounded-md transition-all"
|
||||
/>
|
||||
)}
|
||||
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
import { GenericFieldLabel } from "@app/components/secret-syncs";
|
||||
import { TOnePassSync } from "@app/hooks/api/secretSyncs/types/1password-sync";
|
||||
|
||||
type Props = {
|
||||
secretSync: TOnePassSync;
|
||||
};
|
||||
|
||||
export const OnePassSyncDestinationSection = ({ secretSync }: Props) => {
|
||||
const {
|
||||
destinationConfig: { vaultId }
|
||||
} = secretSync;
|
||||
|
||||
return <GenericFieldLabel label="Vault ID">{vaultId}</GenericFieldLabel>;
|
||||
};
|
||||
+4
@@ -10,6 +10,7 @@ import { ProjectPermissionSecretSyncActions } from "@app/context/ProjectPermissi
|
||||
import { APP_CONNECTION_MAP } from "@app/helpers/appConnections";
|
||||
import { SecretSync, TSecretSync } from "@app/hooks/api/secretSyncs";
|
||||
|
||||
import { OnePassSyncDestinationSection } from "./1PasswordSyncDestinationSection";
|
||||
import { AwsParameterStoreSyncDestinationSection } from "./AwsParameterStoreSyncDestinationSection";
|
||||
import { AwsSecretsManagerSyncDestinationSection } from "./AwsSecretsManagerSyncDestinationSection";
|
||||
import { AzureAppConfigurationSyncDestinationSection } from "./AzureAppConfigurationSyncDestinationSection";
|
||||
@@ -85,6 +86,9 @@ export const SecretSyncDestinationSection = ({ secretSync, onEditDestination }:
|
||||
case SecretSync.OCIVault:
|
||||
DestinationComponents = <OCIVaultSyncDestinationSection secretSync={secretSync} />;
|
||||
break;
|
||||
case SecretSync.OnePass:
|
||||
DestinationComponents = <OnePassSyncDestinationSection secretSync={secretSync} />;
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unhandled Destination Section components: ${destination}`);
|
||||
}
|
||||
|
||||
+1
@@ -50,6 +50,7 @@ export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) =
|
||||
case SecretSync.HCVault:
|
||||
case SecretSync.TeamCity:
|
||||
case SecretSync.OCIVault:
|
||||
case SecretSync.OnePass:
|
||||
AdditionalSyncOptionsComponent = null;
|
||||
break;
|
||||
default:
|
||||
|
||||
+15
-3
@@ -25,9 +25,14 @@ import {
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import {
|
||||
getUserTablePreference,
|
||||
PreferenceKey,
|
||||
setUserTablePreference
|
||||
} from "@app/helpers/userTablePreferences";
|
||||
import { usePagination, useResetPageHelper } from "@app/hooks";
|
||||
import { useGetWsTags } from "@app/hooks/api";
|
||||
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||
import { useGetWsTags } from "@app/hooks/api/tags";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
type Props = {
|
||||
@@ -61,7 +66,14 @@ export const SecretTagsTable = ({ handlePopUpOpen }: Props) => {
|
||||
offset,
|
||||
orderDirection,
|
||||
toggleOrderDirection
|
||||
} = usePagination(TagsOrderBy.Slug, { initPerPage: 10 });
|
||||
} = usePagination(TagsOrderBy.Slug, {
|
||||
initPerPage: getUserTablePreference("secretTagsTable", PreferenceKey.PerPage, 20)
|
||||
});
|
||||
|
||||
const handlePerPageChange = (newPerPage: number) => {
|
||||
setPerPage(newPerPage);
|
||||
setUserTablePreference("secretTagsTable", PreferenceKey.PerPage, newPerPage);
|
||||
};
|
||||
|
||||
const filteredTags = useMemo(
|
||||
() =>
|
||||
@@ -151,7 +163,7 @@ export const SecretTagsTable = ({ handlePopUpOpen }: Props) => {
|
||||
page={page}
|
||||
perPage={perPage}
|
||||
onChangePage={setPage}
|
||||
onChangePerPage={setPerPage}
|
||||
onChangePerPage={handlePerPageChange}
|
||||
/>
|
||||
)}
|
||||
{!isPending && !filteredTags?.length && (
|
||||
|
||||
Reference in New Issue
Block a user