mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-16 05:13:43 +00:00
feature secret rotations v2
This commit is contained in:
@@ -14,7 +14,8 @@ import {
|
||||
faFolderPlus,
|
||||
faKey,
|
||||
faList,
|
||||
faPlus
|
||||
faPlus,
|
||||
faRotate
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Link, useNavigate, useRouter, useSearch } from "@tanstack/react-router";
|
||||
@@ -23,6 +24,11 @@ import { twMerge } from "tailwind-merge";
|
||||
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { CreateSecretRotationV2Modal } from "@app/components/secret-rotations-v2";
|
||||
import { DeleteSecretRotationV2Modal } from "@app/components/secret-rotations-v2/DeleteSecretRotationV2Modal";
|
||||
import { EditSecretRotationV2Modal } from "@app/components/secret-rotations-v2/EditSecretRotationV2Modal";
|
||||
import { RotateSecretRotationV2Modal } from "@app/components/secret-rotations-v2/RotateSecretRotationV2Modal";
|
||||
import { ViewSecretRotationV2GeneratedCredentialsModal } from "@app/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials";
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
@@ -57,6 +63,7 @@ import {
|
||||
useSubscription,
|
||||
useWorkspace
|
||||
} from "@app/context";
|
||||
import { ProjectPermissionSecretRotationActions } from "@app/context/ProjectPermissionContext/types";
|
||||
import { useDebounce, usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
|
||||
import {
|
||||
useCreateFolder,
|
||||
@@ -71,9 +78,16 @@ import { DashboardSecretsOrderBy } from "@app/hooks/api/dashboard/types";
|
||||
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||
import { useUpdateFolderBatch } from "@app/hooks/api/secretFolders/queries";
|
||||
import { TUpdateFolderBatchDTO } from "@app/hooks/api/secretFolders/types";
|
||||
import { TSecretRotationV2 } from "@app/hooks/api/secretRotationsV2";
|
||||
import { SecretType, SecretV3RawSanitized, TSecretFolder } from "@app/hooks/api/types";
|
||||
import { ProjectType, ProjectVersion } from "@app/hooks/api/workspace/types";
|
||||
import { useDynamicSecretOverview, useFolderOverview, useSecretOverview } from "@app/hooks/utils";
|
||||
import {
|
||||
useDynamicSecretOverview,
|
||||
useFolderOverview,
|
||||
useSecretOverview,
|
||||
useSecretRotationOverview
|
||||
} from "@app/hooks/utils";
|
||||
import { SecretOverviewSecretRotationRow } from "@app/pages/secret-manager/OverviewPage/components/SecretOverviewSecretRotationRow";
|
||||
|
||||
import { CreateDynamicSecretForm } from "../SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm";
|
||||
import { FolderForm } from "../SecretDashboardPage/components/ActionBar/FolderForm";
|
||||
@@ -99,7 +113,8 @@ enum RowType {
|
||||
Folder = "folder",
|
||||
DynamicSecret = "dynamic",
|
||||
Secret = "secret",
|
||||
Import = "import"
|
||||
Import = "import",
|
||||
SecretRotation = "rotation"
|
||||
}
|
||||
|
||||
type Filter = {
|
||||
@@ -110,7 +125,8 @@ const DEFAULT_FILTER_STATE = {
|
||||
[RowType.Folder]: true,
|
||||
[RowType.DynamicSecret]: true,
|
||||
[RowType.Secret]: true,
|
||||
[RowType.Import]: true
|
||||
[RowType.Import]: true,
|
||||
[RowType.SecretRotation]: true
|
||||
};
|
||||
|
||||
export const OverviewPage = () => {
|
||||
@@ -195,6 +211,15 @@ export const OverviewPage = () => {
|
||||
})
|
||||
)
|
||||
);
|
||||
const userAvailableSecretRotationEnvs = userAvailableEnvs.filter((env) =>
|
||||
permission.can(
|
||||
ProjectPermissionSecretRotationActions.Create,
|
||||
subject(ProjectPermissionSub.SecretRotation, {
|
||||
environment: env.slug,
|
||||
secretPath
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
const [visibleEnvs, setVisibleEnvs] = useState(userAvailableEnvs);
|
||||
|
||||
@@ -224,6 +249,7 @@ export const OverviewPage = () => {
|
||||
includeDynamicSecrets: filter.dynamic,
|
||||
includeSecrets: filter.secret,
|
||||
includeImports: filter.import,
|
||||
includeSecretRotations: filter.rotation,
|
||||
search: debouncedSearchFilter,
|
||||
limit,
|
||||
offset
|
||||
@@ -235,15 +261,18 @@ export const OverviewPage = () => {
|
||||
secrets,
|
||||
folders,
|
||||
dynamicSecrets,
|
||||
secretRotations,
|
||||
totalFolderCount,
|
||||
totalSecretCount,
|
||||
totalDynamicSecretCount,
|
||||
totalSecretRotationCount,
|
||||
totalImportCount,
|
||||
totalCount = 0,
|
||||
totalUniqueFoldersInPage,
|
||||
totalUniqueSecretsInPage,
|
||||
totalUniqueSecretImportsInPage,
|
||||
totalUniqueDynamicSecretsInPage
|
||||
totalUniqueDynamicSecretsInPage,
|
||||
totalUniqueSecretRotationsInPage
|
||||
} = overview ?? {};
|
||||
|
||||
const secretImportsShaped = secretImports
|
||||
@@ -273,6 +302,13 @@ export const OverviewPage = () => {
|
||||
const { dynamicSecretNames, isDynamicSecretPresentInEnv } =
|
||||
useDynamicSecretOverview(dynamicSecrets);
|
||||
|
||||
const {
|
||||
secretRotationNames,
|
||||
isSecretRotationPresentInEnv,
|
||||
getSecretRotationByName,
|
||||
getSecretRotationStatusesByName
|
||||
} = useSecretRotationOverview(secretRotations);
|
||||
|
||||
const { secKeys, getEnvSecretKeyCount } = useSecretOverview(
|
||||
secrets?.concat(secretImportsShaped) || []
|
||||
);
|
||||
@@ -301,6 +337,11 @@ export const OverviewPage = () => {
|
||||
"misc",
|
||||
"updateFolder",
|
||||
"addDynamicSecret",
|
||||
"addSecretRotation",
|
||||
"editSecretRotation",
|
||||
"rotateSecretRotation",
|
||||
"viewSecretRotationGeneratedCredentials",
|
||||
"deleteSecretRotation",
|
||||
"upgradePlan"
|
||||
] as const);
|
||||
|
||||
@@ -841,6 +882,21 @@ export const OverviewPage = () => {
|
||||
<span>Dynamic Secrets</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleToggleRowType(RowType.SecretRotation);
|
||||
}}
|
||||
icon={
|
||||
filter[RowType.SecretRotation] && <FontAwesomeIcon icon={faCheckCircle} />
|
||||
}
|
||||
iconPos="right"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<FontAwesomeIcon icon={faRotate} className="text-mineshaft-400" />
|
||||
<span>Secret Rotations</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
@@ -952,6 +1008,29 @@ export const OverviewPage = () => {
|
||||
Add Dynamic Secret
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
content={
|
||||
userAvailableSecretRotationEnvs.length === 0 ? "Access restricted" : ""
|
||||
}
|
||||
>
|
||||
<Button
|
||||
leftIcon={<FontAwesomeIcon icon={faRotate} className="pr-2" />}
|
||||
onClick={() => {
|
||||
if (subscription?.secretRotation) {
|
||||
handlePopUpOpen("addSecretRotation");
|
||||
handlePopUpClose("misc");
|
||||
return;
|
||||
}
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
isDisabled={userAvailableSecretRotationEnvs.length === 0}
|
||||
variant="outline_bg"
|
||||
className="h-10 text-left"
|
||||
isFullWidth
|
||||
>
|
||||
Add Secret Rotation
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
@@ -1141,6 +1220,29 @@ export const OverviewPage = () => {
|
||||
key={`overview-${dynamicSecretName}-${index + 1}`}
|
||||
/>
|
||||
))}
|
||||
{secretRotationNames.map((secretRotationName, index) => (
|
||||
<SecretOverviewSecretRotationRow
|
||||
secretRotationName={secretRotationName}
|
||||
isSecretRotationInEnv={isSecretRotationPresentInEnv}
|
||||
environments={visibleEnvs}
|
||||
getSecretRotationByName={getSecretRotationByName}
|
||||
getSecretRotationStatusesByName={getSecretRotationStatusesByName}
|
||||
key={`overview-${secretRotationName}-${index + 1}`}
|
||||
scrollOffset={scrollOffset}
|
||||
onEdit={(secretRotation) =>
|
||||
handlePopUpOpen("editSecretRotation", secretRotation)
|
||||
}
|
||||
onRotate={(secretRotation) =>
|
||||
handlePopUpOpen("rotateSecretRotation", secretRotation)
|
||||
}
|
||||
onViewGeneratedCredentials={(secretRotation) =>
|
||||
handlePopUpOpen("viewSecretRotationGeneratedCredentials", secretRotation)
|
||||
}
|
||||
onDelete={(secretRotation) =>
|
||||
handlePopUpOpen("deleteSecretRotation", secretRotation)
|
||||
}
|
||||
/>
|
||||
))}
|
||||
{secKeys.map((key, index) => (
|
||||
<SecretOverviewTableRow
|
||||
isSelected={Boolean(selectedEntries.secret[key])}
|
||||
@@ -1165,7 +1267,8 @@ export const OverviewPage = () => {
|
||||
(totalUniqueFoldersInPage || 0) -
|
||||
(totalUniqueDynamicSecretsInPage || 0) -
|
||||
(totalUniqueSecretsInPage || 0) -
|
||||
(totalUniqueSecretImportsInPage || 0),
|
||||
(totalUniqueSecretImportsInPage || 0) -
|
||||
(totalUniqueSecretRotationsInPage || 0),
|
||||
0
|
||||
)}
|
||||
/>
|
||||
@@ -1206,6 +1309,7 @@ export const OverviewPage = () => {
|
||||
secretCount={totalSecretCount}
|
||||
folderCount={totalFolderCount}
|
||||
importCount={totalImportCount}
|
||||
secretRotationCount={totalSecretRotationCount}
|
||||
/>
|
||||
}
|
||||
className="rounded-b-md border-t border-solid border-t-mineshaft-600"
|
||||
@@ -1277,6 +1381,34 @@ export const OverviewPage = () => {
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<CreateSecretRotationV2Modal
|
||||
secretPath={secretPath}
|
||||
environments={userAvailableSecretRotationEnvs}
|
||||
isOpen={popUp.addSecretRotation.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("addSecretRotation", isOpen)}
|
||||
/>
|
||||
<EditSecretRotationV2Modal
|
||||
isOpen={popUp.editSecretRotation.isOpen}
|
||||
secretRotation={popUp.editSecretRotation.data as TSecretRotationV2}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("editSecretRotation", isOpen)}
|
||||
/>
|
||||
<RotateSecretRotationV2Modal
|
||||
isOpen={popUp.rotateSecretRotation.isOpen}
|
||||
secretRotation={popUp.rotateSecretRotation.data as TSecretRotationV2}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("rotateSecretRotation", isOpen)}
|
||||
/>
|
||||
<ViewSecretRotationV2GeneratedCredentialsModal
|
||||
isOpen={popUp.viewSecretRotationGeneratedCredentials.isOpen}
|
||||
secretRotation={popUp.viewSecretRotationGeneratedCredentials.data as TSecretRotationV2}
|
||||
onOpenChange={(isOpen) =>
|
||||
handlePopUpToggle("viewSecretRotationGeneratedCredentials", isOpen)
|
||||
}
|
||||
/>
|
||||
<DeleteSecretRotationV2Modal
|
||||
isOpen={popUp.deleteSecretRotation.isOpen}
|
||||
secretRotation={popUp.deleteSecretRotation.data as TSecretRotationV2}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("deleteSecretRotation", isOpen)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
+318
@@ -0,0 +1,318 @@
|
||||
import { subject } from "@casl/ability";
|
||||
import {
|
||||
faAsterisk,
|
||||
faCheck,
|
||||
faClose,
|
||||
faEdit,
|
||||
faEye,
|
||||
faEyeSlash,
|
||||
faInfoCircle,
|
||||
faRotate,
|
||||
faXmark
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { SecretRotationV2StatusBadge } from "@app/components/secret-rotations-v2/SecretRotationV2StatusBadge";
|
||||
import { Badge, IconButton, TableContainer, Tag, Td, Tooltip, Tr } from "@app/components/v2";
|
||||
import { Blur } from "@app/components/v2/Blur";
|
||||
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
|
||||
import {
|
||||
ProjectPermissionSecretRotationActions,
|
||||
ProjectPermissionSub
|
||||
} from "@app/context/ProjectPermissionContext/types";
|
||||
import { SECRET_ROTATION_MAP } from "@app/helpers/secretRotationsV2";
|
||||
import { useToggle } from "@app/hooks";
|
||||
import { SecretRotationStatus, TSecretRotationV2 } from "@app/hooks/api/secretRotationsV2";
|
||||
import { getExpandedRowStyle } from "@app/pages/secret-manager/OverviewPage/components/utils";
|
||||
|
||||
type Props = {
|
||||
secretRotationName: string;
|
||||
environments: { name: string; slug: string }[];
|
||||
isSecretRotationInEnv: (name: string, env: string) => boolean;
|
||||
getSecretRotationByName: (slug: string, name: string) => TSecretRotationV2 | undefined;
|
||||
getSecretRotationStatusesByName: (name: string) => (SecretRotationStatus | null)[] | undefined;
|
||||
scrollOffset: number;
|
||||
onEdit: (secretRotation: TSecretRotationV2) => void;
|
||||
onRotate: (secretRotation: TSecretRotationV2) => void;
|
||||
onViewGeneratedCredentials: (secretRotation: TSecretRotationV2) => void;
|
||||
onDelete: (secretRotation: TSecretRotationV2) => void;
|
||||
};
|
||||
|
||||
export const SecretOverviewSecretRotationRow = ({
|
||||
secretRotationName,
|
||||
environments = [],
|
||||
isSecretRotationInEnv,
|
||||
scrollOffset,
|
||||
getSecretRotationByName,
|
||||
getSecretRotationStatusesByName,
|
||||
onEdit,
|
||||
onRotate,
|
||||
onViewGeneratedCredentials,
|
||||
onDelete
|
||||
}: Props) => {
|
||||
const [isExpanded, setIsExpanded] = useToggle(false);
|
||||
const [isSecretVisible, setIsSecretVisible] = useToggle();
|
||||
|
||||
const totalCols = environments.length + 1; // secret key row
|
||||
|
||||
const statuses = getSecretRotationStatusesByName(secretRotationName);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tr isHoverable isSelectable onClick={setIsExpanded.toggle} className="group">
|
||||
<Td className="sticky left-0 z-10 border-0 bg-mineshaft-800 bg-clip-padding p-0 group-hover:bg-mineshaft-700">
|
||||
<div className="flex w-full items-center space-x-5 border-r border-mineshaft-600 py-2.5 pl-5 pr-2">
|
||||
<div className="text-mineshaft-400">
|
||||
<FontAwesomeIcon icon={faRotate} />
|
||||
</div>
|
||||
<div className="flex-1">{secretRotationName}</div>
|
||||
{statuses?.some((status) => status === SecretRotationStatus.Failed) && (
|
||||
<Tooltip className="max-w-sm" content="One or more secrets failed to rotate.">
|
||||
<div>
|
||||
<Badge
|
||||
variant="danger"
|
||||
className="flex h-5 w-min items-center gap-1 whitespace-nowrap"
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
<span>Rotation Failed</span>
|
||||
</Badge>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</Td>
|
||||
{environments.map(({ slug }, i) => {
|
||||
const isPresent = isSecretRotationInEnv(secretRotationName, slug);
|
||||
|
||||
return (
|
||||
<Td
|
||||
key={`sec-overview-${slug}-${i + 1}-folder`}
|
||||
className={twMerge(
|
||||
"border-r border-mineshaft-600 py-3 group-hover:bg-mineshaft-700",
|
||||
isPresent ? "text-green-600" : "text-red-600"
|
||||
)}
|
||||
>
|
||||
<div className="flex justify-center">
|
||||
<FontAwesomeIcon
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
icon={isPresent ? faCheck : faXmark}
|
||||
/>
|
||||
</div>
|
||||
</Td>
|
||||
);
|
||||
})}
|
||||
</Tr>
|
||||
{isExpanded &&
|
||||
environments.map(({ name: envName, slug }) => {
|
||||
const secretRotation = getSecretRotationByName(slug, secretRotationName);
|
||||
|
||||
if (!secretRotation) return null;
|
||||
|
||||
const { type, secrets, environment, folder, description } = secretRotation;
|
||||
|
||||
const { name: rotationType, image } = SECRET_ROTATION_MAP[type];
|
||||
|
||||
return (
|
||||
<Tr key={`secret-rotation-${slug}-${secretRotationName}`}>
|
||||
<Td
|
||||
colSpan={totalCols}
|
||||
className={`bg-bunker-600 px-0 py-0 ${isExpanded && "border-b-2 border-mineshaft-500"}`}
|
||||
>
|
||||
<div style={getExpandedRowStyle(scrollOffset)} className="ml-2 p-2">
|
||||
<TableContainer>
|
||||
<table className="secret-table">
|
||||
<thead className="!border-b">
|
||||
<tr className="h-10">
|
||||
<th
|
||||
style={{ padding: "0.5rem 1rem" }}
|
||||
className="min-table-row min-w-[30vw] !border-r-0"
|
||||
>
|
||||
<div className="flex w-full flex-wrap items-center">
|
||||
<span>{envName}</span>
|
||||
<Tag className="mx-2.5 flex items-center gap-1 px-1.5 py-0 text-xs normal-case">
|
||||
<img
|
||||
src={`/images/integrations/${image}`}
|
||||
style={{
|
||||
height: "11px",
|
||||
width: "11px"
|
||||
}}
|
||||
alt={`${rotationType} logo`}
|
||||
/>
|
||||
{rotationType}
|
||||
</Tag>
|
||||
{description && (
|
||||
<Tooltip content={description}>
|
||||
<FontAwesomeIcon
|
||||
icon={faInfoCircle}
|
||||
className="text-mineshaft-400"
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</th>
|
||||
<div className="absolute right-1 top-0.5 ml-auto mr-1 mt-1 w-min">
|
||||
<div className="flex items-center gap-2">
|
||||
<SecretRotationV2StatusBadge secretRotation={secretRotation} />
|
||||
<Tooltip content={isSecretVisible ? "Hide Values" : "Reveal Values"}>
|
||||
<IconButton
|
||||
ariaLabel={isSecretVisible ? "Hide Values" : "Reveal Values"}
|
||||
variant="plain"
|
||||
size="md"
|
||||
onClick={() => setIsSecretVisible.toggle()}
|
||||
>
|
||||
<FontAwesomeIcon icon={isSecretVisible ? faEyeSlash : faEye} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionSecretRotationActions.ReadGeneratedCredentials}
|
||||
a={subject(ProjectPermissionSub.SecretRotation, {
|
||||
environment: environment.slug,
|
||||
secretPath: folder.path
|
||||
})}
|
||||
renderTooltip
|
||||
allowedLabel="View Generated Credentials"
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
ariaLabel="View generated credentials"
|
||||
variant="plain"
|
||||
size="sm"
|
||||
isDisabled={!isAllowed}
|
||||
onClick={() => onViewGeneratedCredentials(secretRotation)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faAsterisk} />
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionSecretRotationActions.RotateSecrets}
|
||||
a={subject(ProjectPermissionSub.SecretRotation, {
|
||||
environment: environment.slug,
|
||||
secretPath: folder.path
|
||||
})}
|
||||
renderTooltip
|
||||
allowedLabel="Rotate Secrets"
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
ariaLabel="Rotate secrets"
|
||||
variant="plain"
|
||||
size="sm"
|
||||
isDisabled={!isAllowed}
|
||||
onClick={() => onRotate(secretRotation)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faRotate} />
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionSecretRotationActions.Edit}
|
||||
a={subject(ProjectPermissionSub.SecretRotation, {
|
||||
environment: environment.slug,
|
||||
secretPath: folder.path
|
||||
})}
|
||||
renderTooltip
|
||||
allowedLabel="Edit"
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
ariaLabel="Edit rotation"
|
||||
variant="plain"
|
||||
size="md"
|
||||
isDisabled={!isAllowed}
|
||||
onClick={() => onEdit(secretRotation)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faEdit} />
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionSecretRotationActions.Delete}
|
||||
a={subject(ProjectPermissionSub.SecretRotation, {
|
||||
environment: environment.slug,
|
||||
secretPath: folder.path
|
||||
})}
|
||||
renderTooltip
|
||||
allowedLabel="Delete"
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
ariaLabel="Delete rotation"
|
||||
variant="plain"
|
||||
colorSchema="danger"
|
||||
size="md"
|
||||
onClick={() => onDelete(secretRotation)}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
<FontAwesomeIcon icon={faClose} size="lg" />
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
</div>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="border-t-2 border-mineshaft-600">
|
||||
{secrets.map((secret, index) => {
|
||||
return (
|
||||
<Tooltip
|
||||
className="max-w-sm"
|
||||
content={
|
||||
secret
|
||||
? undefined
|
||||
: "You do not have permission to view this secret."
|
||||
}
|
||||
>
|
||||
<tr
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
key={`rotation-secret-${secretRotation.id}-${index}`}
|
||||
className="!last:border-b-0 h-full hover:bg-mineshaft-700"
|
||||
>
|
||||
<td
|
||||
className="flex h-full items-center"
|
||||
style={{ padding: "0.5rem 1rem" }}
|
||||
>
|
||||
<span className={twMerge(!secret && "blur")}>
|
||||
{secret?.key ?? "********"}
|
||||
</span>
|
||||
</td>
|
||||
<td
|
||||
className="col-span-2 h-full w-full"
|
||||
style={{ padding: "0.5rem 1rem" }}
|
||||
>
|
||||
{/* eslint-disable-next-line no-nested-ternary */}
|
||||
{!secret ? (
|
||||
<div className="h-full pl-4 blur">********</div>
|
||||
) : secret.secretValueHidden ? (
|
||||
<Blur
|
||||
className="py-0"
|
||||
tooltipText="You do not have permission to read the value of this secret."
|
||||
/>
|
||||
) : (
|
||||
<InfisicalSecretInput
|
||||
isReadOnly
|
||||
value={secret.value}
|
||||
isVisible={isSecretVisible}
|
||||
secretPath={secretRotation.folder.path}
|
||||
environment={secretRotation.environment.slug}
|
||||
onChange={() => {}}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</TableContainer>
|
||||
</div>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
+1
@@ -0,0 +1 @@
|
||||
export { SecretOverviewSecretRotationRow } from "./SecretOverviewSecretRotationRow";
|
||||
+7
-5
@@ -53,6 +53,7 @@ type Props = {
|
||||
secretId?: string
|
||||
) => Promise<void>;
|
||||
onSecretDelete: (env: string, key: string, secretId?: string) => Promise<void>;
|
||||
isRotatedSecret?: boolean;
|
||||
};
|
||||
|
||||
export const SecretEditRow = ({
|
||||
@@ -68,7 +69,8 @@ export const SecretEditRow = ({
|
||||
environment,
|
||||
secretPath,
|
||||
isVisible,
|
||||
secretId
|
||||
secretId,
|
||||
isRotatedSecret
|
||||
}: Props) => {
|
||||
const {
|
||||
handleSubmit,
|
||||
@@ -148,7 +150,7 @@ export const SecretEditRow = ({
|
||||
isOpen={isModalOpen}
|
||||
onClose={toggleModal}
|
||||
title="Do you want to delete the selected secret?"
|
||||
deleteKey="delete"
|
||||
deleteKey={secretName}
|
||||
onDeleteApproved={handleDeleteSecret}
|
||||
/>
|
||||
|
||||
@@ -163,7 +165,7 @@ export const SecretEditRow = ({
|
||||
render={({ field }) => (
|
||||
<InfisicalSecretInput
|
||||
{...field}
|
||||
isReadOnly={isImportedSecret}
|
||||
isReadOnly={isImportedSecret || isRotatedSecret}
|
||||
value={field.value as string}
|
||||
key="secret-input"
|
||||
isVisible={isVisible}
|
||||
@@ -287,13 +289,13 @@ export const SecretEditRow = ({
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<div className="opacity-0 group-hover:opacity-100">
|
||||
<Tooltip content="Delete">
|
||||
<Tooltip content={isRotatedSecret ? "Cannot Delete Rotated Secret" : "Delete"}>
|
||||
<IconButton
|
||||
variant="plain"
|
||||
ariaLabel="delete-value"
|
||||
className="h-full"
|
||||
onClick={toggleModal}
|
||||
isDisabled={isDeleting || !isAllowed}
|
||||
isDisabled={isDeleting || !isAllowed || isRotatedSecret}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
|
||||
+15
-8
@@ -2,10 +2,12 @@ import { faCircle } from "@fortawesome/free-regular-svg-icons";
|
||||
import {
|
||||
faAngleDown,
|
||||
faCheck,
|
||||
faCodeBranch,
|
||||
faEye,
|
||||
faEyeSlash,
|
||||
faFileImport,
|
||||
faKey,
|
||||
faRotate,
|
||||
faXmark
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
@@ -15,6 +17,7 @@ import { Button, Checkbox, TableContainer, Td, Tooltip, Tr } from "@app/componen
|
||||
import { useToggle } from "@app/hooks";
|
||||
import { SecretType, SecretV3RawSanitized } from "@app/hooks/api/secrets/types";
|
||||
import { WorkspaceEnv } from "@app/hooks/api/types";
|
||||
import { getExpandedRowStyle } from "@app/pages/secret-manager/OverviewPage/components/utils";
|
||||
|
||||
import { SecretEditRow } from "./SecretEditRow";
|
||||
import SecretRenameRow from "./SecretRenameRow";
|
||||
@@ -149,14 +152,7 @@ export const SecretOverviewTableRow = ({
|
||||
isFormExpanded && "border-b-2 border-mineshaft-500"
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className="ml-2 p-2"
|
||||
style={{
|
||||
marginLeft: scrollOffset,
|
||||
width: "calc(100vw - 350px)", // 350px accounts for sidebar and margin
|
||||
maxWidth: "1270px" // largest width of table on ultra wide
|
||||
}}
|
||||
>
|
||||
<div className="ml-2 p-2" style={getExpandedRowStyle(scrollOffset)}>
|
||||
<SecretRenameRow
|
||||
secretKey={secretKey}
|
||||
environments={environments}
|
||||
@@ -214,6 +210,16 @@ export const SecretOverviewTableRow = ({
|
||||
<FontAwesomeIcon icon={faFileImport} />
|
||||
</Tooltip>
|
||||
)}
|
||||
{secret?.isRotatedSecret && (
|
||||
<Tooltip content="Rotated Secret">
|
||||
<FontAwesomeIcon icon={faRotate} />
|
||||
</Tooltip>
|
||||
)}
|
||||
{secret?.valueOverride && (
|
||||
<Tooltip content="Personal Override">
|
||||
<FontAwesomeIcon icon={faCodeBranch} />
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="col-span-2 h-8 w-full">
|
||||
@@ -237,6 +243,7 @@ export const SecretOverviewTableRow = ({
|
||||
onSecretCreate={onSecretCreate}
|
||||
onSecretUpdate={onSecretUpdate}
|
||||
environment={slug}
|
||||
isRotatedSecret={secret?.isRotatedSecret}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
+34
-5
@@ -6,7 +6,8 @@ import {
|
||||
faFingerprint,
|
||||
faFolder,
|
||||
faKey,
|
||||
faMagnifyingGlass
|
||||
faMagnifyingGlass,
|
||||
faRotate
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
@@ -34,6 +35,7 @@ import { useDebounce } from "@app/hooks";
|
||||
import { useGetProjectSecretsQuickSearch } from "@app/hooks/api/dashboard";
|
||||
import { WsTag } from "@app/hooks/api/tags/types";
|
||||
import { WorkspaceEnv } from "@app/hooks/api/workspace/types";
|
||||
import { QuickSearchSecretRotationItem } from "@app/pages/secret-manager/OverviewPage/components/SecretSearchInput/components/QuickSearchSecretRotationItem";
|
||||
import { RowType } from "@app/pages/secret-manager/SecretDashboardPage/SecretMainPage.types";
|
||||
|
||||
import { QuickSearchDynamicSecretItem } from "./QuickSearchDynamicSecretItem";
|
||||
@@ -51,7 +53,11 @@ export type QuickSearchModalProps = {
|
||||
onOpenChange: (isOpen: boolean) => void;
|
||||
};
|
||||
|
||||
type ResourceType = RowType.Secret | RowType.DynamicSecret | RowType.Folder;
|
||||
type ResourceType =
|
||||
| RowType.Secret
|
||||
| RowType.DynamicSecret
|
||||
| RowType.Folder
|
||||
| RowType.SecretRotation;
|
||||
|
||||
const Content = ({
|
||||
environments,
|
||||
@@ -67,7 +73,8 @@ const Content = ({
|
||||
const [showFilter, setShowFilter] = useState<Record<ResourceType, boolean>>({
|
||||
[RowType.Secret]: true,
|
||||
[RowType.Folder]: true,
|
||||
[RowType.DynamicSecret]: true
|
||||
[RowType.DynamicSecret]: true,
|
||||
[RowType.SecretRotation]: true
|
||||
});
|
||||
const isEnabled = Boolean(search.trim()) || Boolean(Object.values(filterTags).length);
|
||||
const { data, isPending } = useGetProjectSecretsQuickSearch(
|
||||
@@ -81,12 +88,13 @@ const Content = ({
|
||||
{ enabled: isEnabled }
|
||||
);
|
||||
|
||||
const { folders = {}, secrets = {}, dynamicSecrets = {} } = data ?? {};
|
||||
const { folders = {}, secrets = {}, dynamicSecrets = {}, secretRotations = {} } = data ?? {};
|
||||
|
||||
const isEmpty =
|
||||
(!showFilter[RowType.Folder] || Object.values(folders).length === 0) &&
|
||||
(!showFilter[RowType.Secret] || Object.values(secrets).length === 0) &&
|
||||
(!showFilter[RowType.DynamicSecret] || Object.values(dynamicSecrets).length === 0);
|
||||
(!showFilter[RowType.DynamicSecret] || Object.values(dynamicSecrets).length === 0) &&
|
||||
(!showFilter[RowType.SecretRotation] || Object.values(secretRotations).length === 0);
|
||||
|
||||
const handleToggleTag = (tag: string) => {
|
||||
setFilterTags((prev) => {
|
||||
@@ -161,6 +169,19 @@ const Content = ({
|
||||
<span>Dynamic Secrets</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleToggleShowType(RowType.SecretRotation);
|
||||
}}
|
||||
icon={showFilter[RowType.SecretRotation] && <FontAwesomeIcon icon={faCheckCircle} />}
|
||||
iconPos="right"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<FontAwesomeIcon icon={faRotate} className="text-mineshaft-400" />
|
||||
<span>Secret Rotations</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
@@ -240,6 +261,14 @@ const Content = ({
|
||||
key={key}
|
||||
/>
|
||||
))}
|
||||
{showFilter[RowType.SecretRotation] &&
|
||||
Object.entries(secretRotations).map(([key, secretRotationGroup]) => (
|
||||
<QuickSearchSecretRotationItem
|
||||
key={key}
|
||||
onClose={onClose}
|
||||
secretRotationGroup={secretRotationGroup}
|
||||
/>
|
||||
))}
|
||||
{showFilter[RowType.Secret] &&
|
||||
Object.entries(secrets).map(([key, secretGroup]) => (
|
||||
<QuickSearchSecretItem
|
||||
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
import { faChevronRight, faFolder, faRotate } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
|
||||
import { Td, Tooltip, Tr } from "@app/components/v2";
|
||||
import { reverseTruncate } from "@app/helpers/reverseTruncate";
|
||||
import { TDashboardProjectSecretsQuickSearch } from "@app/hooks/api/dashboard/types";
|
||||
|
||||
type Props = {
|
||||
secretRotationGroup: TDashboardProjectSecretsQuickSearch["secretRotations"][string];
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export const QuickSearchSecretRotationItem = ({ secretRotationGroup, onClose }: Props) => {
|
||||
const navigate = useNavigate({
|
||||
from: "/secret-manager/$projectId/overview"
|
||||
});
|
||||
|
||||
const [groupSecretRotation] = secretRotationGroup;
|
||||
|
||||
const handleNavigate = () => {
|
||||
navigate({
|
||||
search: (prev) => ({
|
||||
...prev,
|
||||
secretPath: groupSecretRotation.folder.path,
|
||||
search: groupSecretRotation.name
|
||||
})
|
||||
});
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Tr
|
||||
className="hover cursor-pointer bg-mineshaft-700 hover:bg-mineshaft-600"
|
||||
onClick={handleNavigate}
|
||||
>
|
||||
<Td className="w-full">
|
||||
<div className="inline-flex max-w-[20rem] flex-col">
|
||||
<span className="truncate">
|
||||
<FontAwesomeIcon className="mr-2 self-center text-mineshaft-400" icon={faRotate} />
|
||||
{groupSecretRotation.name}
|
||||
</span>
|
||||
<span className="text-xs text-mineshaft-400">
|
||||
<FontAwesomeIcon size="xs" className="mr-0.5 text-yellow-700" icon={faFolder} />{" "}
|
||||
<Tooltip className="max-w-7xl" content={groupSecretRotation.folder.path}>
|
||||
<span>{reverseTruncate(groupSecretRotation.folder.path)}</span>
|
||||
</Tooltip>
|
||||
</span>
|
||||
</div>
|
||||
</Td>
|
||||
<Td />
|
||||
<Td>
|
||||
<FontAwesomeIcon icon={faChevronRight} />
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
};
|
||||
+26
-2
@@ -1,4 +1,10 @@
|
||||
import { faFileImport, faFingerprint, faFolder, faKey } from "@fortawesome/free-solid-svg-icons";
|
||||
import {
|
||||
faFileImport,
|
||||
faFingerprint,
|
||||
faFolder,
|
||||
faKey,
|
||||
faRotate
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { Tooltip } from "@app/components/v2";
|
||||
@@ -8,13 +14,15 @@ type Props = {
|
||||
importCount?: number;
|
||||
secretCount?: number;
|
||||
dynamicSecretCount?: number;
|
||||
secretRotationCount?: number;
|
||||
};
|
||||
|
||||
export const SecretTableResourceCount = ({
|
||||
folderCount = 0,
|
||||
dynamicSecretCount = 0,
|
||||
secretCount = 0,
|
||||
importCount = 0
|
||||
importCount = 0,
|
||||
secretRotationCount = 0
|
||||
}: Props) => {
|
||||
return (
|
||||
<div className="flex items-center gap-2 divide-x divide-mineshaft-500 text-sm text-mineshaft-400">
|
||||
@@ -66,6 +74,22 @@ export const SecretTableResourceCount = ({
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
{secretRotationCount > 0 && (
|
||||
<Tooltip
|
||||
className="max-w-sm"
|
||||
content={
|
||||
<p className="whitespace-nowrap text-center">
|
||||
Total secret rotation count{" "}
|
||||
<span className="text-center text-mineshaft-400">(matching filters)</span>
|
||||
</p>
|
||||
}
|
||||
>
|
||||
<div className="flex items-center gap-2 pl-2">
|
||||
<FontAwesomeIcon icon={faRotate} className="text-mineshaft-400" />
|
||||
<span>{secretRotationCount}</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
{secretCount > 0 && (
|
||||
<Tooltip
|
||||
className="max-w-sm"
|
||||
|
||||
+9
-1
@@ -46,6 +46,9 @@ export const SelectionPanel = ({ secretPath, resetSelectedEntries, selectedEntri
|
||||
|
||||
const selectedFolderCount = Object.keys(selectedEntries.folder).length;
|
||||
const selectedKeysCount = Object.keys(selectedEntries.secret).length;
|
||||
const isRotatedSecretSelected = Object.values(selectedEntries.secret).some((record) =>
|
||||
Object.values(record).some((secret) => secret.isRotatedSecret)
|
||||
);
|
||||
const selectedCount = selectedFolderCount + selectedKeysCount;
|
||||
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
@@ -121,7 +124,7 @@ export const SelectionPanel = ({ secretPath, resetSelectedEntries, selectedEntri
|
||||
})
|
||||
);
|
||||
|
||||
if (entry && canDeleteSecret) {
|
||||
if (entry && canDeleteSecret && !entry.isRotatedSecret) {
|
||||
return [
|
||||
...accum,
|
||||
{
|
||||
@@ -186,6 +189,11 @@ export const SelectionPanel = ({ secretPath, resetSelectedEntries, selectedEntri
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<div className="ml-1 flex-grow px-2 text-sm">{selectedCount} Selected</div>
|
||||
{isRotatedSecretSelected && (
|
||||
<span className="text-sm text-mineshaft-400">
|
||||
Rotated Secrets will not be affected by action.
|
||||
</span>
|
||||
)}
|
||||
{shouldShowDelete && (
|
||||
<>
|
||||
<Tooltip content={areFoldersSelected ? "Moving folders is not supported" : undefined}>
|
||||
|
||||
+4
-1
@@ -156,7 +156,10 @@ const Content = ({
|
||||
);
|
||||
|
||||
Object.values(secrets).forEach((secretRecord) =>
|
||||
Object.entries(secretRecord).map(([env, secret]) => secretsByEnv[env].push(secret))
|
||||
Object.entries(secretRecord).forEach(([env, secret]) => {
|
||||
if (secret.isRotatedSecret) return;
|
||||
secretsByEnv[env].push(secret);
|
||||
})
|
||||
);
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
export const getExpandedRowStyle = (scrollOffset: number) => ({
|
||||
marginLeft: scrollOffset,
|
||||
width: "calc(100vw - 355px)", // 350px accounts for sidebar and margin
|
||||
maxWidth: "1270px" // largest width of table on ultra-wide
|
||||
});
|
||||
Reference in New Issue
Block a user