mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-20 13:43:37 +00:00
Merge branch 'main' into feat/remindersImprovement
This commit is contained in:
@@ -34,7 +34,7 @@ export const CommitDetailsPage = () => {
|
||||
|
||||
const handleGoBackToHistory = () => {
|
||||
navigate({
|
||||
to: "/projects/$projectId/secret-manager/commits/$environment/$folderId",
|
||||
to: "/projects/secret-management/$projectId/commits/$environment/$folderId",
|
||||
params: {
|
||||
projectId: currentWorkspace.id,
|
||||
folderId,
|
||||
@@ -49,7 +49,7 @@ export const CommitDetailsPage = () => {
|
||||
|
||||
const handleGoToRollbackPreview = () => {
|
||||
navigate({
|
||||
to: "/projects/$projectId/secret-manager/commits/$environment/$folderId/$commitId/restore",
|
||||
to: "/projects/secret-management/$projectId/commits/$environment/$folderId/$commitId/restore",
|
||||
params: {
|
||||
projectId: currentWorkspace.id,
|
||||
folderId,
|
||||
|
||||
+125
-127
@@ -1,5 +1,10 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { faAngleDown } from "@fortawesome/free-solid-svg-icons";
|
||||
import {
|
||||
faAngleDown,
|
||||
faChevronLeft,
|
||||
faCodeCommit,
|
||||
faWarning
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { DropdownMenuItem } from "@radix-ui/react-dropdown-menu";
|
||||
import { useSearch } from "@tanstack/react-router";
|
||||
@@ -7,12 +12,14 @@ import { useSearch } from "@tanstack/react-router";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
Button,
|
||||
ContentLoader,
|
||||
DeleteActionModal,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuTrigger,
|
||||
IconButton,
|
||||
Spinner
|
||||
EmptyState,
|
||||
PageHeader
|
||||
} from "@app/components/v2";
|
||||
import { ROUTE_PATHS } from "@app/const/routes";
|
||||
import {
|
||||
@@ -108,25 +115,25 @@ export const CommitDetailsTab = ({
|
||||
// If no commit is selected or data is loading, show appropriate message
|
||||
if (!selectedCommitId) {
|
||||
return (
|
||||
<div className="flex h-64 items-center justify-center">
|
||||
<p className="text-gray-400">Select a commit to view details</p>
|
||||
</div>
|
||||
<EmptyState className="mt-40" title="Select a commit to view details." icon={faCodeCommit}>
|
||||
<Button className="mt-4" colorSchema="secondary" onClick={() => goBackToHistory()}>
|
||||
Back to Commits
|
||||
</Button>
|
||||
</EmptyState>
|
||||
);
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex h-64 items-center justify-center">
|
||||
<Spinner size="lg" />
|
||||
</div>
|
||||
);
|
||||
return <ContentLoader />;
|
||||
}
|
||||
|
||||
if (!commitDetails) {
|
||||
return (
|
||||
<div className="flex h-64 items-center justify-center">
|
||||
<p className="text-gray-400">No details found for this commit</p>
|
||||
</div>
|
||||
<EmptyState className="mt-40" title="No details found for this commit." icon={faCodeCommit}>
|
||||
<Button className="mt-4" colorSchema="secondary" onClick={() => goBackToHistory()}>
|
||||
Back to Commits
|
||||
</Button>
|
||||
</EmptyState>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -138,9 +145,11 @@ export const CommitDetailsTab = ({
|
||||
} catch (error) {
|
||||
console.error("Failed to parse commit details:", error);
|
||||
return (
|
||||
<div className="flex h-64 items-center justify-center">
|
||||
<p className="text-gray-400">Error parsing commit details</p>
|
||||
</div>
|
||||
<EmptyState className="mt-40" title="Error parsing commit details." icon={faWarning}>
|
||||
<Button className="mt-4" colorSchema="secondary" onClick={() => goBackToHistory()}>
|
||||
Back to Commits
|
||||
</Button>
|
||||
</EmptyState>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -223,13 +232,12 @@ export const CommitDetailsTab = ({
|
||||
// Render an item from the merged list
|
||||
const renderMergedItem = (item: MergedItem): JSX.Element => {
|
||||
return (
|
||||
<div key={item.id} className="mb-2">
|
||||
<SecretVersionDiffView
|
||||
item={item}
|
||||
isCollapsed={collapsedItems[item.id]}
|
||||
onToggleCollapse={(id) => toggleItemCollapsed(id)}
|
||||
/>
|
||||
</div>
|
||||
<SecretVersionDiffView
|
||||
key={item.id}
|
||||
item={item}
|
||||
isCollapsed={collapsedItems[item.id]}
|
||||
onToggleCollapse={(id) => toggleItemCollapsed(id)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -240,114 +248,104 @@ export const CommitDetailsTab = ({
|
||||
"Unknown";
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div>
|
||||
<div className="flex justify-between pb-2">
|
||||
<div className="w-5/6">
|
||||
<div>
|
||||
<div className="flex items-center">
|
||||
<h1 className="mr-4 truncate text-3xl font-semibold text-white">
|
||||
{parsedCommitDetails.changes?.message || "No message"}
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div className="font-small mb-4 mt-2 flex items-center text-sm">
|
||||
<p>
|
||||
<span> Commited by </span>
|
||||
<b>{actorDisplay}</b>
|
||||
<span> on </span>
|
||||
<b>
|
||||
{formatDisplayDate(
|
||||
parsedCommitDetails.changes?.createdAt || new Date().toISOString()
|
||||
)}
|
||||
</b>
|
||||
{parsedCommitDetails.changes?.isLatest && (
|
||||
<span className="ml-1 italic text-gray-400">(Latest)</span>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-start">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionCommitsActions.PerformRollback}
|
||||
a={ProjectPermissionSub.Commits}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
asChild
|
||||
disabled={!isAllowed}
|
||||
className={`${!isAllowed ? "cursor-not-allowed" : ""}`}
|
||||
<>
|
||||
<Button
|
||||
variant="link"
|
||||
type="submit"
|
||||
leftIcon={<FontAwesomeIcon icon={faChevronLeft} />}
|
||||
onClick={() => {
|
||||
goBackToHistory();
|
||||
}}
|
||||
>
|
||||
Commit History
|
||||
</Button>
|
||||
<PageHeader
|
||||
title={`${parsedCommitDetails.changes?.message}` || "No message"}
|
||||
description={
|
||||
<>
|
||||
Commited by {actorDisplay} on{" "}
|
||||
{formatDisplayDate(parsedCommitDetails.changes?.createdAt || new Date().toISOString())}
|
||||
{parsedCommitDetails.changes?.isLatest && (
|
||||
<span className="ml-1 text-mineshaft-400">(Latest)</span>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionCommitsActions.PerformRollback}
|
||||
a={ProjectPermissionSub.Commits}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
asChild
|
||||
disabled={!isAllowed}
|
||||
className={`${!isAllowed ? "cursor-not-allowed" : ""}`}
|
||||
>
|
||||
<Button
|
||||
rightIcon={<FontAwesomeIcon className="ml-2" icon={faAngleDown} />}
|
||||
variant="solid"
|
||||
className="h-min"
|
||||
colorSchema="secondary"
|
||||
>
|
||||
Restore Options
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="max-w-sm bg-bunker-500" sideOffset={2}>
|
||||
{!parsedCommitDetails.changes.isLatest && (
|
||||
<DropdownMenuItem
|
||||
className="group cursor-pointer border-b border-mineshaft-600 px-3 py-3 transition-colors hover:bg-mineshaft-700"
|
||||
onClick={() => goToRollbackPreview()}
|
||||
>
|
||||
<IconButton
|
||||
ariaLabel="commit-options"
|
||||
variant="outline_bg"
|
||||
className="h-10 rounded border border-mineshaft-600 bg-mineshaft-800 px-4 py-2 text-sm font-medium"
|
||||
>
|
||||
<p className="mr-2">Restore Options</p>
|
||||
<FontAwesomeIcon icon={faAngleDown} />
|
||||
</IconButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="end"
|
||||
sideOffset={2}
|
||||
className="animate-in fade-in-50 zoom-in-95 min-w-[240px] rounded-md bg-mineshaft-800 p-1 shadow-lg"
|
||||
style={{ marginTop: "0" }}
|
||||
>
|
||||
{!parsedCommitDetails.changes.isLatest && (
|
||||
<DropdownMenuItem
|
||||
className="group cursor-pointer rounded-md px-3 py-3 transition-colors hover:bg-mineshaft-700"
|
||||
onClick={() => goToRollbackPreview()}
|
||||
>
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-sm font-medium text-white">
|
||||
Roll back to this commit
|
||||
</span>
|
||||
<span className="max-w-[180px] whitespace-normal break-words text-xs leading-snug text-gray-400">
|
||||
Return this folder to its exact state at the time of this commit,
|
||||
discarding all other changes made after it
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
|
||||
<DropdownMenuItem
|
||||
className="group cursor-pointer rounded-md px-3 py-3 transition-colors hover:bg-mineshaft-700"
|
||||
onClick={() => handlePopUpOpen("revertChanges")}
|
||||
>
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-sm font-medium text-white">Revert changes</span>
|
||||
<span className="max-w-[180px] whitespace-normal break-words text-xs leading-snug text-gray-400">
|
||||
Will restore to the previous version of affected resources
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-sm font-medium text-white">
|
||||
Roll back to this commit
|
||||
</span>
|
||||
<span className="whitespace-normal break-words text-xs leading-snug text-gray-400">
|
||||
Return this folder to its exact state at the time of this commit,
|
||||
discarding all other changes made after it
|
||||
</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuItem
|
||||
className="group cursor-pointer px-3 py-3 transition-colors hover:bg-mineshaft-700"
|
||||
onClick={() => handlePopUpOpen("revertChanges")}
|
||||
>
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-sm font-medium text-white">Revert changes</span>
|
||||
<span className="whitespace-normal break-words text-xs leading-snug text-gray-400">
|
||||
Will restore to the previous version of affected resources
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</PageHeader>
|
||||
<div className="flex w-full flex-col rounded-lg border border-mineshaft-600 bg-mineshaft-900 pt-4">
|
||||
<div className="mx-4 flex items-center justify-between border-b border-mineshaft-400 pb-4">
|
||||
<h3 className="text-lg font-semibold text-mineshaft-100">Commit Changes</h3>
|
||||
</div>
|
||||
|
||||
<div className="py-2">
|
||||
<div className="overflow-hidden">
|
||||
<div className="space-y-2">
|
||||
{sortedChangedItems.length > 0 ? (
|
||||
sortedChangedItems.map((item) => renderMergedItem(item))
|
||||
) : (
|
||||
<div className="flex h-32 items-center justify-center rounded-lg border border-mineshaft-600 bg-mineshaft-800">
|
||||
<p className="text-gray-400">No changed items found</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col overflow-hidden px-4">
|
||||
<div className="thin-scrollbar overflow-y-auto py-4">
|
||||
{sortedChangedItems.length > 0 ? (
|
||||
sortedChangedItems.map((item) => renderMergedItem(item))
|
||||
) : (
|
||||
<EmptyState
|
||||
title="No changes found."
|
||||
className="h-full pb-0 pt-28"
|
||||
icon={faCodeCommit}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.revertChanges.isOpen}
|
||||
deleteKey="revert"
|
||||
@@ -357,6 +355,6 @@ export const CommitDetailsTab = ({
|
||||
onDeleteApproved={handleRevertChanges}
|
||||
buttonText="Yes, revert changes"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
+1
-1
@@ -102,7 +102,7 @@ export const RollbackPreviewTab = (): JSX.Element => {
|
||||
|
||||
const goBackToHistory = () => {
|
||||
navigate({
|
||||
to: "/projects/$projectId/secret-manager/commits/$environment/$folderId",
|
||||
to: "/projects/secret-management/$projectId/commits/$environment/$folderId",
|
||||
params: {
|
||||
projectId: currentWorkspace.id,
|
||||
folderId,
|
||||
|
||||
+4
-4
@@ -12,7 +12,7 @@ const RollbackPreviewTabQueryParamsSchema = z.object({
|
||||
});
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/commits/$environment/$folderId/$commitId/restore"
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/commits/$environment/$folderId/$commitId/restore"
|
||||
)({
|
||||
component: RollbackPreviewTab,
|
||||
validateSearch: zodValidator(RollbackPreviewTabQueryParamsSchema),
|
||||
@@ -33,7 +33,7 @@ export const Route = createFileRoute(
|
||||
links: context.project.environments.map((el) => ({
|
||||
label: el.name,
|
||||
link: linkOptions({
|
||||
to: "/projects/$projectId/secret-manager/secrets/$envSlug",
|
||||
to: "/projects/secret-management/$projectId/secrets/$envSlug",
|
||||
params: {
|
||||
projectId: params.projectId,
|
||||
envSlug: el.slug
|
||||
@@ -56,7 +56,7 @@ export const Route = createFileRoute(
|
||||
{
|
||||
label: "Commits",
|
||||
link: linkOptions({
|
||||
to: "/projects/$projectId/secret-manager/commits/$environment/$folderId",
|
||||
to: "/projects/secret-management/$projectId/commits/$environment/$folderId",
|
||||
params: {
|
||||
projectId: params.projectId,
|
||||
environment: params.environment,
|
||||
@@ -70,7 +70,7 @@ export const Route = createFileRoute(
|
||||
{
|
||||
label: params.commitId,
|
||||
link: linkOptions({
|
||||
to: "/projects/$projectId/secret-manager/commits/$environment/$folderId/$commitId",
|
||||
to: "/projects/secret-management/$projectId/commits/$environment/$folderId/$commitId",
|
||||
params: {
|
||||
projectId: params.projectId,
|
||||
environment: params.environment,
|
||||
|
||||
+61
-50
@@ -1,7 +1,10 @@
|
||||
/* eslint-disable no-nested-ternary */
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
import { faChevronDown, faChevronUp } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faChevronDown, faChevronUp, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { IconButton, Tooltip } from "@app/components/v2";
|
||||
|
||||
export interface Version {
|
||||
id?: string;
|
||||
@@ -32,6 +35,7 @@ interface SecretVersionDiffViewProps {
|
||||
showHeader?: boolean;
|
||||
customHeader?: JSX.Element;
|
||||
excludedFieldsHighlight?: string[];
|
||||
onDiscard?: VoidFunction;
|
||||
}
|
||||
|
||||
const isObject = (obj: JsonValue): obj is JsonObject => {
|
||||
@@ -225,15 +229,12 @@ const renderJsonWithDiffs = (
|
||||
|
||||
const getLineClass = (different: boolean) => {
|
||||
if (!different) return "flex";
|
||||
return isOldVersion ? "flex bg-red-950 text-red-300" : "flex bg-green-950 text-green-300";
|
||||
return isOldVersion
|
||||
? "flex bg-red-500/50 rounded-sm text-red-300"
|
||||
: "flex bg-green-500/50 rounded-sm text-green-300";
|
||||
};
|
||||
|
||||
const getHighlightClass = (different: boolean) => {
|
||||
if (!different) return "";
|
||||
return isOldVersion ? "bg-red-900 rounded px-1" : "bg-green-900 rounded px-1";
|
||||
};
|
||||
|
||||
const prefix = isDifferent ? (isOldVersion ? "-" : "+") : " ";
|
||||
const prefix = isDifferent ? (isOldVersion ? " -" : " +") : " ";
|
||||
const keyDisplay = keyName ? `"${keyName}": ` : "";
|
||||
const comma = !isLastItem ? "," : "";
|
||||
|
||||
@@ -255,8 +256,8 @@ const renderJsonWithDiffs = (
|
||||
<div className="w-4 flex-shrink-0">{prefix}</div>
|
||||
<div>
|
||||
{indent}
|
||||
{keyName && <span className={getHighlightClass(isDifferent)}>{keyDisplay}</span>}
|
||||
<span className={getHighlightClass(isDifferent)}>{valueDisplay}</span>
|
||||
{keyName && <span>{keyDisplay}</span>}
|
||||
<span>{valueDisplay}</span>
|
||||
{comma}
|
||||
</div>
|
||||
</div>
|
||||
@@ -269,8 +270,8 @@ const renderJsonWithDiffs = (
|
||||
<div className="w-4 flex-shrink-0">{prefix}</div>
|
||||
<div>
|
||||
{indent}
|
||||
{keyName && <span className={getHighlightClass(isDifferent)}>{keyDisplay}</span>}
|
||||
<span className={getHighlightClass(isDifferent)}>[]</span>
|
||||
{keyName && <span>{keyDisplay}</span>}
|
||||
<span>[]</span>
|
||||
{comma}
|
||||
</div>
|
||||
</div>
|
||||
@@ -283,8 +284,8 @@ const renderJsonWithDiffs = (
|
||||
<div className="w-4 flex-shrink-0">{prefix}</div>
|
||||
<div>
|
||||
{indent}
|
||||
{keyName && <span className={getHighlightClass(isDifferent)}>{keyDisplay}</span>}
|
||||
<span className={getHighlightClass(isDifferent)}>{"{}"}</span>
|
||||
{keyName && <span>{keyDisplay}</span>}
|
||||
<span>{"{}"}</span>
|
||||
{comma}
|
||||
</div>
|
||||
</div>
|
||||
@@ -320,16 +321,12 @@ const renderJsonWithDiffs = (
|
||||
<div key={reactKey}>
|
||||
<div className={getLineClass(isContainerAddedOrRemoved)}>
|
||||
<div className="w-4 flex-shrink-0">
|
||||
{isContainerAddedOrRemoved ? (isOldVersion ? "-" : "+") : " "}
|
||||
{isContainerAddedOrRemoved ? (isOldVersion ? " -" : " +") : " "}
|
||||
</div>
|
||||
<div>
|
||||
{indent}
|
||||
{keyName && (
|
||||
<span className={isContainerAddedOrRemoved ? getHighlightClass(true) : ""}>
|
||||
{keyDisplay}
|
||||
</span>
|
||||
)}
|
||||
<span className={isContainerAddedOrRemoved ? getHighlightClass(true) : ""}>[</span>
|
||||
{keyName && <span>{keyDisplay}</span>}
|
||||
<span>[</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -357,11 +354,11 @@ const renderJsonWithDiffs = (
|
||||
|
||||
<div className={getLineClass(isContainerAddedOrRemoved)}>
|
||||
<div className="w-4 flex-shrink-0">
|
||||
{isContainerAddedOrRemoved ? (isOldVersion ? "-" : "+") : " "}
|
||||
{isContainerAddedOrRemoved ? (isOldVersion ? " -" : " +") : " "}
|
||||
</div>
|
||||
<div>
|
||||
{indent}
|
||||
<span className={isContainerAddedOrRemoved ? getHighlightClass(true) : ""}>]</span>
|
||||
<span>]</span>
|
||||
{comma}
|
||||
</div>
|
||||
</div>
|
||||
@@ -376,16 +373,12 @@ const renderJsonWithDiffs = (
|
||||
<div key={reactKey}>
|
||||
<div className={getLineClass(isContainerAddedOrRemoved)}>
|
||||
<div className="w-4 flex-shrink-0">
|
||||
{isContainerAddedOrRemoved ? (isOldVersion ? "-" : "+") : " "}
|
||||
{isContainerAddedOrRemoved ? (isOldVersion ? " -" : " +") : " "}
|
||||
</div>
|
||||
<div>
|
||||
{indent}
|
||||
{keyName && (
|
||||
<span className={isContainerAddedOrRemoved ? getHighlightClass(true) : ""}>
|
||||
{keyDisplay}
|
||||
</span>
|
||||
)}
|
||||
<span className={isContainerAddedOrRemoved ? getHighlightClass(true) : ""}>{"{"}</span>
|
||||
{keyName && <span>{keyDisplay}</span>}
|
||||
<span>{"{"}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -414,12 +407,11 @@ const renderJsonWithDiffs = (
|
||||
|
||||
<div className={getLineClass(isContainerAddedOrRemoved)}>
|
||||
<div className="w-4 flex-shrink-0">
|
||||
{isContainerAddedOrRemoved ? (isOldVersion ? "-" : "+") : " "}
|
||||
{isContainerAddedOrRemoved ? (isOldVersion ? " -" : " +") : " "}
|
||||
</div>
|
||||
<div>
|
||||
{indent}
|
||||
<span className={isContainerAddedOrRemoved ? getHighlightClass(true) : ""}>{"}"}</span>
|
||||
{comma}
|
||||
<span>{"}"}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -475,7 +467,9 @@ const formatDeletedJson = (json: JsonValue): JSX.Element => {
|
||||
|
||||
const cleanVersionForComparison = (version: Version): JsonValue => {
|
||||
const { id, version: versionNumber, ...cleanVersion } = version;
|
||||
return cleanVersion;
|
||||
return Object.fromEntries(
|
||||
Object.entries(cleanVersion).filter((entry) => typeof entry[1] !== "undefined")
|
||||
);
|
||||
};
|
||||
|
||||
export const SecretVersionDiffView = ({
|
||||
@@ -484,7 +478,8 @@ export const SecretVersionDiffView = ({
|
||||
onToggleCollapse,
|
||||
showHeader = true,
|
||||
customHeader,
|
||||
excludedFieldsHighlight = ["metadata", "tags"]
|
||||
excludedFieldsHighlight = ["metadata", "tags"],
|
||||
onDiscard
|
||||
}: SecretVersionDiffViewProps) => {
|
||||
const oldContainerRef = useRef<HTMLDivElement>(null);
|
||||
const newContainerRef = useRef<HTMLDivElement>(null);
|
||||
@@ -527,7 +522,7 @@ export const SecretVersionDiffView = ({
|
||||
}
|
||||
|
||||
oldVersionContent = (
|
||||
<div className="font-mono text-sm">
|
||||
<div className="w-fit min-w-[100%] font-mono text-sm">
|
||||
{renderJsonWithDiffs(
|
||||
cleanOldVersion,
|
||||
diffPaths,
|
||||
@@ -543,7 +538,7 @@ export const SecretVersionDiffView = ({
|
||||
</div>
|
||||
);
|
||||
newVersionContent = (
|
||||
<div className="font-mono text-sm">
|
||||
<div className="w-fit min-w-[100%] font-mono text-sm">
|
||||
{renderJsonWithDiffs(
|
||||
cleanNewVersion,
|
||||
diffPaths,
|
||||
@@ -583,19 +578,19 @@ export const SecretVersionDiffView = ({
|
||||
if (item.isDeleted) {
|
||||
textStyle = "line-through text-red-300";
|
||||
changeBadge = (
|
||||
<span className="ml-2 rounded-md bg-mineshaft-600 px-2 py-0.5 text-xs font-medium">
|
||||
<span className="ml-2 whitespace-nowrap rounded-md bg-mineshaft-600 px-2 py-0.5 text-xs font-medium">
|
||||
{isSecret ? "Secret" : "Folder"} Deleted
|
||||
</span>
|
||||
);
|
||||
} else if (item.isAdded) {
|
||||
changeBadge = (
|
||||
<span className="ml-2 rounded-md bg-mineshaft-600 px-2 py-0.5 text-xs font-medium">
|
||||
<span className="ml-2 whitespace-nowrap rounded-md bg-mineshaft-600 px-2 py-0.5 text-xs font-medium">
|
||||
{isSecret ? "Secret" : "Folder"} Added
|
||||
</span>
|
||||
);
|
||||
} else if (item.isUpdated) {
|
||||
changeBadge = (
|
||||
<span className="ml-2 rounded-md bg-mineshaft-600 px-2 py-0.5 text-xs font-medium">
|
||||
<span className="ml-2 whitespace-nowrap rounded-md bg-mineshaft-600 px-2 py-0.5 text-xs font-medium">
|
||||
{isSecret ? "Secret" : "Folder"} Updated
|
||||
</span>
|
||||
);
|
||||
@@ -615,32 +610,48 @@ export const SecretVersionDiffView = ({
|
||||
tabIndex={0}
|
||||
aria-expanded={!collapsed}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<span className={textStyle}>{key}</span>
|
||||
<div className="flex min-w-0 flex-1 items-center">
|
||||
<p className={twMerge(textStyle, "truncate")}>{key}</p>
|
||||
{changeBadge}
|
||||
</div>
|
||||
<FontAwesomeIcon icon={collapsed ? faChevronDown : faChevronUp} className="text-gray-400" />
|
||||
{onDiscard && (
|
||||
<Tooltip side="left" content="Discard change">
|
||||
<IconButton
|
||||
ariaLabel="discard-change"
|
||||
variant="plain"
|
||||
colorSchema="danger"
|
||||
size="sm"
|
||||
className="ml-2"
|
||||
onClick={onDiscard}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
<FontAwesomeIcon
|
||||
icon={collapsed ? faChevronDown : faChevronUp}
|
||||
className="ml-2 text-gray-400"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="overflow-hidden rounded-lg border border-mineshaft-600 bg-mineshaft-800">
|
||||
<div className="overflow-hidden border border-b-0 border-mineshaft-600 bg-mineshaft-800 first:rounded-t last:rounded-b last:border-b">
|
||||
{showHeader && renderHeader()}
|
||||
|
||||
{!collapsed && (
|
||||
<div className="border-t border-mineshaft-700 bg-mineshaft-900 px-6 py-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="border-t border-mineshaft-700 bg-mineshaft-900 p-3 text-mineshaft-100">
|
||||
<div className="flex gap-3">
|
||||
<div
|
||||
ref={oldContainerRef}
|
||||
className="thin-scrollbar max-h-96 overflow-auto whitespace-pre rounded border border-mineshaft-600 bg-mineshaft-900 p-4"
|
||||
className="thin-scrollbar max-h-96 flex-1 overflow-auto whitespace-pre"
|
||||
>
|
||||
{oldVersionContent}
|
||||
</div>
|
||||
|
||||
<div className="max-h-96 w-[0.05rem] self-stretch bg-mineshaft-600" />
|
||||
<div
|
||||
ref={newContainerRef}
|
||||
className="thin-scrollbar max-h-96 overflow-auto whitespace-pre rounded border border-mineshaft-600 bg-mineshaft-900 p-4"
|
||||
className="thin-scrollbar max-h-96 flex-1 overflow-auto whitespace-pre"
|
||||
>
|
||||
{newVersionContent}
|
||||
</div>
|
||||
|
||||
@@ -12,7 +12,7 @@ const CommitDetailsPageQueryParamsSchema = z.object({
|
||||
});
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/commits/$environment/$folderId/$commitId/"
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/commits/$environment/$folderId/$commitId/"
|
||||
)({
|
||||
component: CommitDetailsPage,
|
||||
validateSearch: zodValidator(CommitDetailsPageQueryParamsSchema),
|
||||
@@ -33,7 +33,7 @@ export const Route = createFileRoute(
|
||||
links: context.project.environments.map((el) => ({
|
||||
label: el.name,
|
||||
link: linkOptions({
|
||||
to: "/projects/$projectId/secret-manager/secrets/$envSlug",
|
||||
to: "/projects/secret-management/$projectId/secrets/$envSlug",
|
||||
params: {
|
||||
projectId: params.projectId,
|
||||
envSlug: el.slug
|
||||
@@ -56,7 +56,7 @@ export const Route = createFileRoute(
|
||||
{
|
||||
label: "Commits",
|
||||
link: linkOptions({
|
||||
to: "/projects/$projectId/secret-manager/commits/$environment/$folderId",
|
||||
to: "/projects/secret-management/$projectId/commits/$environment/$folderId",
|
||||
params: {
|
||||
projectId: params.projectId,
|
||||
environment: params.environment,
|
||||
|
||||
@@ -31,7 +31,7 @@ export const CommitsPage = () => {
|
||||
|
||||
const handleSelectCommit = (commitId: string) => {
|
||||
navigate({
|
||||
to: "/projects/$projectId/secret-manager/commits/$environment/$folderId/$commitId",
|
||||
to: "/projects/secret-management/$projectId/commits/$environment/$folderId/$commitId",
|
||||
params: {
|
||||
projectId: currentWorkspace.id,
|
||||
folderId,
|
||||
@@ -52,7 +52,7 @@ export const CommitsPage = () => {
|
||||
title="Commits"
|
||||
description="Track, inspect, and restore your secrets and folders with confidence. View the complete history of changes made to your environment, examine specific modifications at each commit point, and preview the exact impact before rolling back to previous states."
|
||||
/>
|
||||
<NoticeBannerV2 title="" className="mb-2">
|
||||
<NoticeBannerV2 title="Secret Snapshots Update" className="mb-2">
|
||||
<p className="my-1 text-sm text-mineshaft-300">
|
||||
Secret Snapshots have been officially renamed to Commits. Going forward, all secret
|
||||
changes will be tracked as Commits. If you made changes before this update, you can
|
||||
|
||||
+68
-168
@@ -1,29 +1,17 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
faArrowDownWideShort,
|
||||
faArrowUpWideShort,
|
||||
faCodeCommit,
|
||||
faCopy,
|
||||
faSearch
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { format, formatDistanceToNow } from "date-fns";
|
||||
import { formatDistanceToNow } from "date-fns";
|
||||
|
||||
import { Button, Input, Spinner } from "@app/components/v2";
|
||||
import { Button, ContentLoader, EmptyState, IconButton, Input } from "@app/components/v2";
|
||||
import { CopyButton } from "@app/components/v2/CopyButton";
|
||||
import { useGetFolderCommitHistory } from "@app/hooks/api/folderCommits";
|
||||
|
||||
interface CommitActorMetadata {
|
||||
email?: string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
interface Commit {
|
||||
id: string;
|
||||
message: string;
|
||||
createdAt: string;
|
||||
actorType: string;
|
||||
actorMetadata?: CommitActorMetadata;
|
||||
}
|
||||
import { Commit, useGetFolderCommitHistory } from "@app/hooks/api/folderCommits";
|
||||
|
||||
const formatTimeAgo = (timestamp: string): string => {
|
||||
return formatDistanceToNow(new Date(timestamp), { addSuffix: true });
|
||||
@@ -40,58 +28,40 @@ const CommitItem = ({
|
||||
onSelectCommit: (commitId: string, tab: string) => void;
|
||||
}) => {
|
||||
return (
|
||||
<div className="border-b border-zinc-800 last:border-b-0">
|
||||
<div className="px-4 py-4 transition-colors duration-200 hover:bg-zinc-800">
|
||||
<div className="flex flex-col sm:flex-row sm:justify-between">
|
||||
<div className="w-5/6 flex-1">
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
variant="link"
|
||||
className="truncate text-left text-white hover:underline"
|
||||
isFullWidth
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onSelectCommit(commit.id, "tab-commit-details");
|
||||
}}
|
||||
>
|
||||
{commit.message}
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-white-400 mt-2 flex flex-wrap items-center gap-4 text-sm">
|
||||
<span className="flex items-center text-mineshaft-300">
|
||||
{commit.actorMetadata?.email || commit.actorMetadata?.name || commit.actorType}
|
||||
<p className="ml-1 mr-1">committed</p>
|
||||
<time dateTime={commit.createdAt}>{formatTimeAgo(commit.createdAt)}</time>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-2 flex w-1/6 items-center justify-end sm:mt-0">
|
||||
<div className="flex items-center space-x-1">
|
||||
<Button
|
||||
variant="link"
|
||||
className="text-white hover:underline"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onSelectCommit(commit.id, "tab-commit-details");
|
||||
}}
|
||||
>
|
||||
<code className="text-white-400 px-3 py-1 font-mono text-sm">
|
||||
{commit.id?.substring(0, 11)}
|
||||
</code>
|
||||
</Button>
|
||||
<CopyButton
|
||||
value={commit.id}
|
||||
name={commit.id}
|
||||
size="sm"
|
||||
variant="plain"
|
||||
color="text-mineshaft-400"
|
||||
icon={faCopy}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onSelectCommit(commit.id, "tab-commit-details");
|
||||
}}
|
||||
className="w-full border border-b-0 border-mineshaft-600 bg-mineshaft-800 first:rounded-t-md last:rounded-b-md last:border-b"
|
||||
>
|
||||
<div className="flex gap-2 px-4 py-3 transition-colors duration-200 hover:bg-zinc-800">
|
||||
<div className="flex min-w-0 flex-1 flex-col items-start">
|
||||
<p className="block w-full truncate text-left text-sm text-mineshaft-100">
|
||||
{commit.message}
|
||||
</p>
|
||||
<p className="text-left text-xs text-mineshaft-300">
|
||||
{commit.actorMetadata?.email || commit.actorMetadata?.name || commit.actorType}{" "}
|
||||
committed <time dateTime={commit.createdAt}>{formatTimeAgo(commit.createdAt)}</time>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<code className="mt-0.5 font-mono text-xs text-mineshaft-400">
|
||||
{commit.id?.substring(0, 11)}
|
||||
</code>
|
||||
<CopyButton
|
||||
value={commit.id}
|
||||
name={commit.id}
|
||||
size="xs"
|
||||
variant="plain"
|
||||
color="text-mineshaft-400"
|
||||
icon={faCopy}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -108,24 +78,16 @@ const DateGroup = ({
|
||||
onSelectCommit: (commitId: string, tab: string) => void;
|
||||
}) => {
|
||||
return (
|
||||
<div className="mb-8 last:mb-0 last:pb-2">
|
||||
<div className="mb-4 flex items-center">
|
||||
<div className="relative mr-3 flex h-6 w-6 items-center justify-center">
|
||||
<div className="z-10 h-3 w-3 rounded-full border-2 border-mineshaft-600 bg-bunker-800" />
|
||||
<div className="absolute left-0 right-0 top-1/2 h-0.5 -translate-y-1/2 bg-mineshaft-600" />
|
||||
</div>
|
||||
<h2 className="text-sm text-white">Commits on {date}</h2>
|
||||
<div className="mt-4 first:mt-0">
|
||||
<div className="mb-4 ml-[0.15rem] flex items-center">
|
||||
<FontAwesomeIcon icon={faCodeCommit} className="text-mineshaft-400" />
|
||||
<h2 className="ml-4 text-sm text-mineshaft-400">Commits on {date}</h2>
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
<div className="absolute bottom-0 left-3 top-0 w-0.5 bg-mineshaft-600" />
|
||||
<div className="absolute bottom-0 left-3 top-0 w-[0.1rem] bg-mineshaft-500" />
|
||||
<div className="ml-10">
|
||||
{commits.map((commit) => (
|
||||
<div key={commit.id} className="relative mb-3 pb-1">
|
||||
<div className="overflow-hidden rounded-md border border-solid border-mineshaft-600">
|
||||
<CommitItem commit={commit} onSelectCommit={onSelectCommit} />
|
||||
</div>
|
||||
</div>
|
||||
<CommitItem key={commit.id} commit={commit} onSelectCommit={onSelectCommit} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
@@ -147,10 +109,8 @@ export const CommitHistoryTab = ({
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [debouncedSearchTerm, setDebouncedSearchTerm] = useState("");
|
||||
const [sortDirection, setSortDirection] = useState<"asc" | "desc">("desc");
|
||||
const [offset, setOffset] = useState(0);
|
||||
const [allCommits, setAllCommits] = useState<Commit[]>([]);
|
||||
const debounceTimeoutRef = useRef<NodeJS.Timeout>();
|
||||
const limit = 5;
|
||||
const limit = 10;
|
||||
|
||||
// Debounce search term
|
||||
useEffect(() => {
|
||||
@@ -170,55 +130,20 @@ export const CommitHistoryTab = ({
|
||||
}, [searchTerm]);
|
||||
|
||||
const {
|
||||
data: response,
|
||||
data: groupedCommits,
|
||||
isLoading,
|
||||
isFetching
|
||||
fetchNextPage,
|
||||
isFetchingNextPage,
|
||||
hasNextPage
|
||||
} = useGetFolderCommitHistory({
|
||||
workspaceId: projectId,
|
||||
environment,
|
||||
directory: secretPath,
|
||||
offset,
|
||||
limit,
|
||||
search: debouncedSearchTerm,
|
||||
sort: sortDirection
|
||||
});
|
||||
|
||||
const commits = response?.commits || [];
|
||||
const hasMore = response?.hasMore || false;
|
||||
|
||||
// Reset accumulated commits when search or sort changes
|
||||
useEffect(() => {
|
||||
setAllCommits([]);
|
||||
setOffset(0);
|
||||
}, [debouncedSearchTerm, sortDirection]);
|
||||
|
||||
// Accumulate commits instead of replacing them
|
||||
useEffect(() => {
|
||||
if (commits.length > 0) {
|
||||
if (offset === 0) {
|
||||
// First load or after search/sort change - replace all commits
|
||||
setAllCommits(commits);
|
||||
} else {
|
||||
// Subsequent loads - append new commits
|
||||
setAllCommits((prev) => [...prev, ...commits]);
|
||||
}
|
||||
}
|
||||
}, [commits, offset]);
|
||||
|
||||
const groupedCommits = useMemo(() => {
|
||||
return allCommits.reduce(
|
||||
(acc, commit) => {
|
||||
const date = format(new Date(commit.createdAt), "MMM d, yyyy");
|
||||
if (!acc[date]) {
|
||||
acc[date] = [];
|
||||
}
|
||||
acc[date].push(commit);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, Commit[]>
|
||||
);
|
||||
}, [allCommits]);
|
||||
|
||||
const handleSort = useCallback(() => {
|
||||
setSortDirection((prev) => (prev === "desc" ? "asc" : "desc"));
|
||||
}, []);
|
||||
@@ -227,50 +152,39 @@ export const CommitHistoryTab = ({
|
||||
setSearchTerm(value);
|
||||
}, []);
|
||||
|
||||
const loadMoreCommits = useCallback(() => {
|
||||
if (hasMore && !isFetching) {
|
||||
setOffset((prev) => prev + limit);
|
||||
}
|
||||
}, [hasMore, isFetching, limit]);
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="mt-4 w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<p className="mb-4 text-xl font-semibold text-mineshaft-100">Commit History</p>
|
||||
<div className="mb-4 flex flex-col sm:flex-row sm:justify-end">
|
||||
<div className="flex w-full flex-wrap items-center gap-2">
|
||||
<div className="relative flex-grow">
|
||||
<Input
|
||||
leftIcon={<FontAwesomeIcon icon={faSearch} aria-hidden="true" />}
|
||||
placeholder="Search commits..."
|
||||
className="h-10 w-full rounded-md border-transparent bg-zinc-800 pl-9 pr-3 text-sm text-white placeholder-gray-400 focus:border-gray-600 focus:ring-primary-500/20"
|
||||
onChange={(e) => handleSearch(e.target.value)}
|
||||
value={searchTerm}
|
||||
aria-label="Search commits"
|
||||
/>
|
||||
<div className="absolute left-3 top-1/2 -translate-y-1/2 transform text-gray-400">
|
||||
<FontAwesomeIcon icon={faSearch} aria-hidden="true" />
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
<IconButton
|
||||
variant="outline_bg"
|
||||
size="md"
|
||||
className="flex h-10 items-center justify-center gap-2 rounded-md bg-zinc-800 px-4 py-2 text-sm text-white transition-colors duration-200 hover:bg-zinc-700 focus:outline-none focus:ring-2 focus:ring-primary-500"
|
||||
size="sm"
|
||||
className="flex h-[2.4rem] items-center justify-center gap-2 rounded-md"
|
||||
onClick={handleSort}
|
||||
aria-label={`Sort by date ${sortDirection === "desc" ? "ascending" : "descending"}`}
|
||||
ariaLabel={`Sort by date ${sortDirection === "desc" ? "ascending" : "descending"}`}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={sortDirection === "desc" ? faArrowDownWideShort : faArrowUpWideShort}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</Button>
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isLoading && offset === 0 ? (
|
||||
<div className="flex h-64 items-center justify-center">
|
||||
<Spinner size="lg" aria-label="Loading commits" />
|
||||
</div>
|
||||
{isLoading ? (
|
||||
<ContentLoader className="h-80" />
|
||||
) : (
|
||||
<div className="space-y-8">
|
||||
{Object.keys(groupedCommits).length > 0 ? (
|
||||
<div>
|
||||
{groupedCommits && Object.keys(groupedCommits).length > 0 ? (
|
||||
<>
|
||||
{Object.entries(groupedCommits).map(([date, dateCommits]) => (
|
||||
<DateGroup
|
||||
@@ -282,34 +196,20 @@ export const CommitHistoryTab = ({
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<div className="text-white-400 flex min-h-40 flex-col items-center justify-center rounded-lg bg-zinc-900 py-8 text-center">
|
||||
<FontAwesomeIcon
|
||||
icon={faSearch}
|
||||
className="text-white-500 mb-3 text-3xl"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<p>No matching commits found. Try a different search term.</p>
|
||||
</div>
|
||||
<EmptyState title="No commits found." icon={faCodeCommit} />
|
||||
)}
|
||||
|
||||
{hasMore && (
|
||||
{hasNextPage && (
|
||||
<div className="flex justify-center pb-2">
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
size="md"
|
||||
className="rounded-md bg-zinc-900 px-6 py-2 text-sm font-medium text-white transition-colors duration-200 hover:bg-zinc-800 focus:outline-none focus:ring-2 focus:ring-primary-500"
|
||||
onClick={loadMoreCommits}
|
||||
disabled={isFetching}
|
||||
size="sm"
|
||||
className="ml-10 mt-4 w-full"
|
||||
onClick={() => fetchNextPage()}
|
||||
disabled={isFetchingNextPage}
|
||||
isLoading={isFetchingNextPage}
|
||||
aria-label="Load more commits"
|
||||
>
|
||||
{isFetching ? (
|
||||
<>
|
||||
<Spinner size="sm" className="mr-2" />
|
||||
Loading...
|
||||
</>
|
||||
) : (
|
||||
"Load more commits"
|
||||
)}
|
||||
Load More Commits
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -13,7 +13,7 @@ const CommitsPageQueryParamsSchema = z.object({
|
||||
});
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/commits/$environment/$folderId/"
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/commits/$environment/$folderId/"
|
||||
)({
|
||||
component: CommitsPage,
|
||||
validateSearch: zodValidator(CommitsPageQueryParamsSchema),
|
||||
@@ -34,7 +34,7 @@ export const Route = createFileRoute(
|
||||
links: context.project.environments.map((el) => ({
|
||||
label: el.name,
|
||||
link: linkOptions({
|
||||
to: "/projects/$projectId/secret-manager/secrets/$envSlug",
|
||||
to: "/projects/secret-management/$projectId/secrets/$envSlug",
|
||||
params: {
|
||||
projectId: params.projectId,
|
||||
envSlug: el.slug
|
||||
@@ -57,7 +57,7 @@ export const Route = createFileRoute(
|
||||
{
|
||||
label: "Commits",
|
||||
link: linkOptions({
|
||||
to: "/projects/$projectId/secret-manager/commits/$environment/$folderId",
|
||||
to: "/projects/secret-management/$projectId/commits/$environment/$folderId",
|
||||
params: {
|
||||
projectId: params.projectId,
|
||||
environment: params.environment,
|
||||
|
||||
@@ -3,7 +3,7 @@ import { createFileRoute } from "@tanstack/react-router";
|
||||
import { IPAllowListPage } from "./IPAllowlistPage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/allowlist"
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/allowlist"
|
||||
)({
|
||||
component: () => IPAllowListPage
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@ import { IntegrationsListPageTabs } from "@app/types/integrations";
|
||||
import { IntegrationDetailsByIDPage } from "./IntegrationsDetailsByIDPage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/$integrationId"
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/$integrationId"
|
||||
)({
|
||||
component: IntegrationDetailsByIDPage,
|
||||
beforeLoad: ({ context, params }) => {
|
||||
@@ -15,7 +15,7 @@ export const Route = createFileRoute(
|
||||
{
|
||||
label: "Integrations",
|
||||
link: linkOptions({
|
||||
to: "/projects/$projectId/secret-manager/integrations",
|
||||
to: "/projects/secret-management/$projectId/integrations",
|
||||
params,
|
||||
search: {
|
||||
selectedTab: IntegrationsListPageTabs.NativeIntegrations
|
||||
|
||||
+30
-30
@@ -42,7 +42,7 @@ export const redirectForProviderAuth = (
|
||||
switch (integrationOption.slug) {
|
||||
case "gcp-secret-manager":
|
||||
navigate({
|
||||
to: "/projects/$projectId/secret-manager/integrations/gcp-secret-manager/authorize",
|
||||
to: "/projects/secret-management/$projectId/integrations/gcp-secret-manager/authorize",
|
||||
params: {
|
||||
projectId
|
||||
}
|
||||
@@ -54,7 +54,7 @@ export const redirectForProviderAuth = (
|
||||
return;
|
||||
}
|
||||
navigate({
|
||||
to: "/projects/$projectId/secret-manager/integrations/azure-key-vault/authorize",
|
||||
to: "/projects/secret-management/$projectId/integrations/azure-key-vault/authorize",
|
||||
params: {
|
||||
projectId
|
||||
},
|
||||
@@ -76,7 +76,7 @@ export const redirectForProviderAuth = (
|
||||
}
|
||||
case "aws-parameter-store":
|
||||
navigate({
|
||||
to: "/projects/$projectId/secret-manager/integrations/aws-parameter-store/authorize",
|
||||
to: "/projects/secret-management/$projectId/integrations/aws-parameter-store/authorize",
|
||||
params: {
|
||||
projectId
|
||||
}
|
||||
@@ -84,7 +84,7 @@ export const redirectForProviderAuth = (
|
||||
break;
|
||||
case "aws-secret-manager":
|
||||
navigate({
|
||||
to: "/projects/$projectId/secret-manager/integrations/aws-secret-manager/authorize",
|
||||
to: "/projects/secret-management/$projectId/integrations/aws-secret-manager/authorize",
|
||||
params: {
|
||||
projectId
|
||||
}
|
||||
@@ -120,7 +120,7 @@ export const redirectForProviderAuth = (
|
||||
}
|
||||
case "github":
|
||||
navigate({
|
||||
to: "/projects/$projectId/secret-manager/integrations/github/auth-mode-selection",
|
||||
to: "/projects/secret-management/$projectId/integrations/github/auth-mode-selection",
|
||||
params: {
|
||||
projectId
|
||||
}
|
||||
@@ -128,7 +128,7 @@ export const redirectForProviderAuth = (
|
||||
break;
|
||||
case "gitlab":
|
||||
navigate({
|
||||
to: "/projects/$projectId/secret-manager/integrations/gitlab/authorize",
|
||||
to: "/projects/secret-management/$projectId/integrations/gitlab/authorize",
|
||||
params: {
|
||||
projectId
|
||||
}
|
||||
@@ -136,7 +136,7 @@ export const redirectForProviderAuth = (
|
||||
break;
|
||||
case "render":
|
||||
navigate({
|
||||
to: "/projects/$projectId/secret-manager/integrations/render/authorize",
|
||||
to: "/projects/secret-management/$projectId/integrations/render/authorize",
|
||||
params: {
|
||||
projectId
|
||||
}
|
||||
@@ -144,7 +144,7 @@ export const redirectForProviderAuth = (
|
||||
break;
|
||||
case "flyio":
|
||||
navigate({
|
||||
to: "/projects/$projectId/secret-manager/integrations/flyio/authorize",
|
||||
to: "/projects/secret-management/$projectId/integrations/flyio/authorize",
|
||||
params: {
|
||||
projectId
|
||||
}
|
||||
@@ -152,7 +152,7 @@ export const redirectForProviderAuth = (
|
||||
break;
|
||||
case "circleci":
|
||||
navigate({
|
||||
to: "/projects/$projectId/secret-manager/integrations/circleci/authorize",
|
||||
to: "/projects/secret-management/$projectId/integrations/circleci/authorize",
|
||||
params: {
|
||||
projectId
|
||||
}
|
||||
@@ -160,7 +160,7 @@ export const redirectForProviderAuth = (
|
||||
break;
|
||||
case "databricks":
|
||||
navigate({
|
||||
to: "/projects/$projectId/secret-manager/integrations/databricks/authorize",
|
||||
to: "/projects/secret-management/$projectId/integrations/databricks/authorize",
|
||||
params: {
|
||||
projectId
|
||||
}
|
||||
@@ -168,7 +168,7 @@ export const redirectForProviderAuth = (
|
||||
break;
|
||||
case "laravel-forge":
|
||||
navigate({
|
||||
to: "/projects/$projectId/secret-manager/integrations/laravel-forge/authorize",
|
||||
to: "/projects/secret-management/$projectId/integrations/laravel-forge/authorize",
|
||||
params: {
|
||||
projectId
|
||||
}
|
||||
@@ -176,7 +176,7 @@ export const redirectForProviderAuth = (
|
||||
break;
|
||||
case "travisci":
|
||||
navigate({
|
||||
to: "/projects/$projectId/secret-manager/integrations/travisci/authorize",
|
||||
to: "/projects/secret-management/$projectId/integrations/travisci/authorize",
|
||||
params: {
|
||||
projectId
|
||||
}
|
||||
@@ -184,7 +184,7 @@ export const redirectForProviderAuth = (
|
||||
break;
|
||||
case "supabase":
|
||||
navigate({
|
||||
to: "/projects/$projectId/secret-manager/integrations/supabase/authorize",
|
||||
to: "/projects/secret-management/$projectId/integrations/supabase/authorize",
|
||||
params: {
|
||||
projectId
|
||||
}
|
||||
@@ -192,7 +192,7 @@ export const redirectForProviderAuth = (
|
||||
break;
|
||||
case "checkly":
|
||||
navigate({
|
||||
to: "/projects/$projectId/secret-manager/integrations/checkly/authorize",
|
||||
to: "/projects/secret-management/$projectId/integrations/checkly/authorize",
|
||||
params: {
|
||||
projectId
|
||||
}
|
||||
@@ -200,7 +200,7 @@ export const redirectForProviderAuth = (
|
||||
break;
|
||||
case "qovery":
|
||||
navigate({
|
||||
to: "/projects/$projectId/secret-manager/integrations/qovery/authorize",
|
||||
to: "/projects/secret-management/$projectId/integrations/qovery/authorize",
|
||||
params: {
|
||||
projectId
|
||||
}
|
||||
@@ -208,7 +208,7 @@ export const redirectForProviderAuth = (
|
||||
break;
|
||||
case "railway":
|
||||
navigate({
|
||||
to: "/projects/$projectId/secret-manager/integrations/railway/authorize",
|
||||
to: "/projects/secret-management/$projectId/integrations/railway/authorize",
|
||||
params: {
|
||||
projectId
|
||||
}
|
||||
@@ -216,7 +216,7 @@ export const redirectForProviderAuth = (
|
||||
break;
|
||||
case "terraform-cloud":
|
||||
navigate({
|
||||
to: "/projects/$projectId/secret-manager/integrations/terraform-cloud/authorize",
|
||||
to: "/projects/secret-management/$projectId/integrations/terraform-cloud/authorize",
|
||||
params: {
|
||||
projectId
|
||||
}
|
||||
@@ -224,7 +224,7 @@ export const redirectForProviderAuth = (
|
||||
break;
|
||||
case "hashicorp-vault":
|
||||
navigate({
|
||||
to: "/projects/$projectId/secret-manager/integrations/hashicorp-vault/authorize",
|
||||
to: "/projects/secret-management/$projectId/integrations/hashicorp-vault/authorize",
|
||||
params: {
|
||||
projectId
|
||||
}
|
||||
@@ -232,7 +232,7 @@ export const redirectForProviderAuth = (
|
||||
break;
|
||||
case "cloudflare-pages":
|
||||
navigate({
|
||||
to: "/projects/$projectId/secret-manager/integrations/cloudflare-pages/authorize",
|
||||
to: "/projects/secret-management/$projectId/integrations/cloudflare-pages/authorize",
|
||||
params: {
|
||||
projectId
|
||||
}
|
||||
@@ -240,7 +240,7 @@ export const redirectForProviderAuth = (
|
||||
break;
|
||||
case "cloudflare-workers":
|
||||
navigate({
|
||||
to: "/projects/$projectId/secret-manager/integrations/cloudflare-workers/authorize",
|
||||
to: "/projects/secret-management/$projectId/integrations/cloudflare-workers/authorize",
|
||||
params: {
|
||||
projectId
|
||||
}
|
||||
@@ -257,7 +257,7 @@ export const redirectForProviderAuth = (
|
||||
}
|
||||
case "codefresh":
|
||||
navigate({
|
||||
to: "/projects/$projectId/secret-manager/integrations/codefresh/authorize",
|
||||
to: "/projects/secret-management/$projectId/integrations/codefresh/authorize",
|
||||
params: {
|
||||
projectId
|
||||
}
|
||||
@@ -265,7 +265,7 @@ export const redirectForProviderAuth = (
|
||||
break;
|
||||
case "digital-ocean-app-platform":
|
||||
navigate({
|
||||
to: "/projects/$projectId/secret-manager/integrations/digital-ocean-app-platform/authorize",
|
||||
to: "/projects/secret-management/$projectId/integrations/digital-ocean-app-platform/authorize",
|
||||
params: {
|
||||
projectId
|
||||
}
|
||||
@@ -273,7 +273,7 @@ export const redirectForProviderAuth = (
|
||||
break;
|
||||
case "cloud-66":
|
||||
navigate({
|
||||
to: "/projects/$projectId/secret-manager/integrations/cloud-66/authorize",
|
||||
to: "/projects/secret-management/$projectId/integrations/cloud-66/authorize",
|
||||
params: {
|
||||
projectId
|
||||
}
|
||||
@@ -281,7 +281,7 @@ export const redirectForProviderAuth = (
|
||||
break;
|
||||
case "northflank":
|
||||
navigate({
|
||||
to: "/projects/$projectId/secret-manager/integrations/northflank/authorize",
|
||||
to: "/projects/secret-management/$projectId/integrations/northflank/authorize",
|
||||
params: {
|
||||
projectId
|
||||
}
|
||||
@@ -289,7 +289,7 @@ export const redirectForProviderAuth = (
|
||||
break;
|
||||
case "windmill":
|
||||
navigate({
|
||||
to: "/projects/$projectId/secret-manager/integrations/windmill/authorize",
|
||||
to: "/projects/secret-management/$projectId/integrations/windmill/authorize",
|
||||
params: {
|
||||
projectId
|
||||
}
|
||||
@@ -297,7 +297,7 @@ export const redirectForProviderAuth = (
|
||||
break;
|
||||
case "teamcity":
|
||||
navigate({
|
||||
to: "/projects/$projectId/secret-manager/integrations/teamcity/authorize",
|
||||
to: "/projects/secret-management/$projectId/integrations/teamcity/authorize",
|
||||
params: {
|
||||
projectId
|
||||
}
|
||||
@@ -305,7 +305,7 @@ export const redirectForProviderAuth = (
|
||||
break;
|
||||
case "hasura-cloud":
|
||||
navigate({
|
||||
to: "/projects/$projectId/secret-manager/integrations/hasura-cloud/authorize",
|
||||
to: "/projects/secret-management/$projectId/integrations/hasura-cloud/authorize",
|
||||
params: {
|
||||
projectId
|
||||
}
|
||||
@@ -313,7 +313,7 @@ export const redirectForProviderAuth = (
|
||||
break;
|
||||
case "rundeck":
|
||||
navigate({
|
||||
to: "/projects/$projectId/secret-manager/integrations/rundeck/authorize",
|
||||
to: "/projects/secret-management/$projectId/integrations/rundeck/authorize",
|
||||
params: {
|
||||
projectId
|
||||
}
|
||||
@@ -321,7 +321,7 @@ export const redirectForProviderAuth = (
|
||||
break;
|
||||
case "azure-devops":
|
||||
navigate({
|
||||
to: "/projects/$projectId/secret-manager/integrations/azure-devops/authorize",
|
||||
to: "/projects/secret-management/$projectId/integrations/azure-devops/authorize",
|
||||
params: {
|
||||
projectId
|
||||
}
|
||||
@@ -329,7 +329,7 @@ export const redirectForProviderAuth = (
|
||||
break;
|
||||
case "octopus-deploy":
|
||||
navigate({
|
||||
to: "/projects/$projectId/secret-manager/integrations/octopus-deploy/authorize",
|
||||
to: "/projects/secret-management/$projectId/integrations/octopus-deploy/authorize",
|
||||
params: {
|
||||
projectId
|
||||
}
|
||||
|
||||
+1
-1
@@ -59,7 +59,7 @@ export const IntegrationRow = ({
|
||||
<Tr
|
||||
onClick={() =>
|
||||
navigate({
|
||||
to: "/projects/$projectId/secret-manager/integrations/$integrationId",
|
||||
to: "/projects/secret-management/$projectId/integrations/$integrationId",
|
||||
params: {
|
||||
integrationId: integration.id,
|
||||
projectId: currentWorkspace.id
|
||||
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
import { TBitbucketSync } from "@app/hooks/api/secretSyncs/types/bitbucket-sync";
|
||||
|
||||
import { getSecretSyncDestinationColValues } from "../helpers";
|
||||
import { SecretSyncTableCell } from "../SecretSyncTableCell";
|
||||
|
||||
type Props = {
|
||||
secretSync: TBitbucketSync;
|
||||
};
|
||||
|
||||
export const BitbucketSyncDestinationCol = ({ secretSync }: Props) => {
|
||||
const { primaryText, secondaryText } = getSecretSyncDestinationColValues(secretSync);
|
||||
|
||||
return <SecretSyncTableCell primaryText={primaryText} secondaryText={secondaryText} />;
|
||||
};
|
||||
+3
@@ -6,6 +6,7 @@ import { AwsSecretsManagerSyncDestinationCol } from "./AwsSecretsManagerSyncDest
|
||||
import { AzureAppConfigurationDestinationSyncCol } from "./AzureAppConfigurationDestinationSyncCol";
|
||||
import { AzureDevOpsSyncDestinationCol } from "./AzureDevOpsSyncDestinationCol";
|
||||
import { AzureKeyVaultDestinationSyncCol } from "./AzureKeyVaultDestinationSyncCol";
|
||||
import { BitbucketSyncDestinationCol } from "./BitbucketSyncDestinationCol";
|
||||
import { CamundaSyncDestinationCol } from "./CamundaSyncDestinationCol";
|
||||
import { ChecklySyncDestinationCol } from "./ChecklySyncDestinationCol";
|
||||
import { CloudflarePagesSyncDestinationCol } from "./CloudflarePagesSyncDestinationCol";
|
||||
@@ -88,6 +89,8 @@ export const SecretSyncDestinationCol = ({ secretSync }: Props) => {
|
||||
return <ChecklySyncDestinationCol secretSync={secretSync} />;
|
||||
case SecretSync.Supabase:
|
||||
return <SupabaseSyncDestinationCol secretSync={secretSync} />;
|
||||
case SecretSync.Bitbucket:
|
||||
return <BitbucketSyncDestinationCol secretSync={secretSync} />;
|
||||
default:
|
||||
throw new Error(
|
||||
`Unhandled Secret Sync Destination Col: ${(secretSync as TSecretSync).destination}`
|
||||
|
||||
+4
@@ -174,6 +174,10 @@ export const getSecretSyncDestinationColValues = (secretSync: TSecretSync) => {
|
||||
primaryText = destinationConfig.projectName;
|
||||
secondaryText = "Supabase Project";
|
||||
break;
|
||||
case SecretSync.Bitbucket:
|
||||
primaryText = destinationConfig.workspaceSlug;
|
||||
secondaryText = destinationConfig.repositorySlug;
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unhandled Destination Col Values ${destination}`);
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ const IntegrationsListPageQuerySchema = z.object({
|
||||
});
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/"
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/"
|
||||
)({
|
||||
component: IntegrationsListPage,
|
||||
validateSearch: zodValidator(IntegrationsListPageQuerySchema),
|
||||
@@ -36,7 +36,7 @@ export const Route = createFileRoute(
|
||||
});
|
||||
} catch {
|
||||
throw redirect({
|
||||
to: "/projects/$projectId/secret-manager/integrations",
|
||||
to: "/projects/secret-management/$projectId/integrations",
|
||||
params: {
|
||||
projectId
|
||||
},
|
||||
@@ -46,7 +46,7 @@ export const Route = createFileRoute(
|
||||
|
||||
if (secretSyncs.length) {
|
||||
throw redirect({
|
||||
to: "/projects/$projectId/secret-manager/integrations",
|
||||
to: "/projects/secret-management/$projectId/integrations",
|
||||
params: {
|
||||
projectId
|
||||
},
|
||||
@@ -62,7 +62,7 @@ export const Route = createFileRoute(
|
||||
});
|
||||
} catch {
|
||||
throw redirect({
|
||||
to: "/projects/$projectId/secret-manager/integrations",
|
||||
to: "/projects/secret-management/$projectId/integrations",
|
||||
params: {
|
||||
projectId
|
||||
},
|
||||
@@ -72,7 +72,7 @@ export const Route = createFileRoute(
|
||||
|
||||
if (integrations.length) {
|
||||
throw redirect({
|
||||
to: "/projects/$projectId/secret-manager/integrations",
|
||||
to: "/projects/secret-management/$projectId/integrations",
|
||||
params: {
|
||||
projectId
|
||||
},
|
||||
@@ -81,7 +81,7 @@ export const Route = createFileRoute(
|
||||
}
|
||||
|
||||
throw redirect({
|
||||
to: "/projects/$projectId/secret-manager/integrations",
|
||||
to: "/projects/secret-management/$projectId/integrations",
|
||||
params: {
|
||||
projectId
|
||||
},
|
||||
|
||||
@@ -667,7 +667,7 @@ export const OverviewPage = () => {
|
||||
const envIndex = visibleEnvs.findIndex((el) => slug === el.slug);
|
||||
if (envIndex !== -1) {
|
||||
navigate({
|
||||
to: "/projects/$projectId/secret-manager/secrets/$envSlug",
|
||||
to: "/projects/secret-management/$projectId/secrets/$envSlug",
|
||||
params: {
|
||||
projectId: workspaceId,
|
||||
envSlug: slug
|
||||
@@ -1420,7 +1420,7 @@ export const OverviewPage = () => {
|
||||
iconSize="3x"
|
||||
>
|
||||
<Link
|
||||
to="/projects/$projectId/secret-manager/settings"
|
||||
to="/projects/secret-management/$projectId/settings"
|
||||
params={{
|
||||
projectId: workspaceId
|
||||
}}
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ type Props = {
|
||||
|
||||
export const FolderBreadCrumbs = ({ secretPath = "/", onResetSearch }: Props) => {
|
||||
const navigate = useNavigate({
|
||||
from: "/projects/$projectId/secret-manager/overview"
|
||||
from: "/projects/secret-management/$projectId/overview"
|
||||
});
|
||||
|
||||
const onFolderCrumbClick = (index: number) => {
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ export const QuickSearchDynamicSecretItem = ({
|
||||
onClose
|
||||
}: Props) => {
|
||||
const navigate = useNavigate({
|
||||
from: "/projects/$projectId/secret-manager/overview"
|
||||
from: "/projects/secret-management/$projectId/overview"
|
||||
});
|
||||
|
||||
const [groupDynamicSecret] = dynamicSecretGroup;
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ type Props = {
|
||||
|
||||
export const QuickSearchFolderItem = ({ folderGroup, onClose }: Props) => {
|
||||
const navigate = useNavigate({
|
||||
from: "/projects/$projectId/secret-manager/overview"
|
||||
from: "/projects/secret-management/$projectId/overview"
|
||||
});
|
||||
|
||||
const [groupFolder] = folderGroup;
|
||||
|
||||
+1
-1
@@ -46,7 +46,7 @@ export const QuickSearchSecretItem = ({
|
||||
isSingleEnv,
|
||||
search
|
||||
}: Props) => {
|
||||
const navigate = useNavigate({ from: "/projects/$projectId/secret-manager/overview" });
|
||||
const navigate = useNavigate({ from: "/projects/secret-management/$projectId/overview" });
|
||||
const envSlugMap = new Map(environments.map((env) => [env.slug, env]));
|
||||
const [isUrlCopied, , setIsUrlCopied] = useTimedReset<boolean>({
|
||||
initialState: false
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ type Props = {
|
||||
|
||||
export const QuickSearchSecretRotationItem = ({ secretRotationGroup, onClose }: Props) => {
|
||||
const navigate = useNavigate({
|
||||
from: "/projects/$projectId/secret-manager/overview"
|
||||
from: "/projects/secret-management/$projectId/overview"
|
||||
});
|
||||
|
||||
const [groupSecretRotation] = secretRotationGroup;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export const getExpandedRowStyle = (scrollOffset: number) => ({
|
||||
marginLeft: scrollOffset,
|
||||
width: "calc(100vw - 355px)", // 350px accounts for sidebar and margin
|
||||
width: "calc(100vw - 275px)", // accounts for sidebar and margin
|
||||
maxWidth: "1270px" // largest width of table on ultra-wide
|
||||
});
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ const SecretOverviewPageQuerySchema = z.object({
|
||||
});
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/overview"
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/overview"
|
||||
)({
|
||||
component: OverviewPage,
|
||||
validateSearch: zodValidator(SecretOverviewPageQuerySchema),
|
||||
@@ -24,7 +24,7 @@ export const Route = createFileRoute(
|
||||
{
|
||||
label: "Secrets",
|
||||
link: linkOptions({
|
||||
to: "/projects/$projectId/secret-manager/overview",
|
||||
to: "/projects/secret-management/$projectId/overview",
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
+2
-2
@@ -156,11 +156,11 @@ export const SecretApprovalRequestChangeItem = ({
|
||||
</div>
|
||||
<div className="mb-2">
|
||||
<div className="text-sm font-medium text-mineshaft-300">Tags</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<div className="flex flex-wrap gap-y-2">
|
||||
{(secretVersion?.tags?.length ?? 0) ? (
|
||||
secretVersion?.tags?.map(({ slug, id: tagId, color }) => (
|
||||
<Tag
|
||||
className="flex w-min items-center space-x-2"
|
||||
className="flex w-min items-center space-x-1.5 border border-mineshaft-500 bg-mineshaft-800"
|
||||
key={`${secretVersion.id}-${tagId}`}
|
||||
>
|
||||
<div
|
||||
|
||||
+1
-1
@@ -72,7 +72,7 @@ export const generateCommitText = (commits: { op: CommitType }[] = [], isReplica
|
||||
if (score[CommitType.DELETE])
|
||||
text.push(
|
||||
<span className="deleted-commit">
|
||||
{Boolean(text.length) && "and"}
|
||||
{Boolean(text.length) && " and "}
|
||||
{score[CommitType.DELETE]} Secret{score[CommitType.DELETE] !== 1 && "s"}
|
||||
<span className="text-red-600"> Deleted</span>
|
||||
</span>
|
||||
|
||||
@@ -9,7 +9,7 @@ const SecretApprovalPageQueryParams = z.object({
|
||||
});
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/approval"
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/approval"
|
||||
)({
|
||||
component: SecretApprovalsPage,
|
||||
validateSearch: zodValidator(SecretApprovalPageQueryParams),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable no-case-declarations */
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { Helmet } from "react-helmet";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -48,6 +49,9 @@ import { useGetProjectSecretsDetails } from "@app/hooks/api/dashboard";
|
||||
import { DashboardSecretsOrderBy } from "@app/hooks/api/dashboard/types";
|
||||
import { useGetFolderCommitsCount } from "@app/hooks/api/folderCommits";
|
||||
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||
import { PendingAction } from "@app/hooks/api/secretFolders/types";
|
||||
import { useCreateCommit } from "@app/hooks/api/secrets/mutations";
|
||||
import { SecretV3RawSanitized } from "@app/hooks/api/types";
|
||||
import { usePathAccessPolicies } from "@app/hooks/usePathAccessPolicies";
|
||||
import { hasSecretReadValueOrDescribePermission } from "@app/lib/fn/permission";
|
||||
import { RequestAccessModal } from "@app/pages/secret-manager/SecretApprovalsPage/components/AccessApprovalRequest/components/RequestAccessModal";
|
||||
@@ -56,6 +60,7 @@ import { SecretRotationListView } from "@app/pages/secret-manager/SecretDashboar
|
||||
import { SecretTableResourceCount } from "../OverviewPage/components/SecretTableResourceCount";
|
||||
import { SecretV2MigrationSection } from "../OverviewPage/components/SecretV2MigrationSection";
|
||||
import { ActionBar } from "./components/ActionBar";
|
||||
import { CommitForm } from "./components/CommitForm";
|
||||
import { CreateSecretForm } from "./components/CreateSecretForm";
|
||||
import { DynamicSecretListView } from "./components/DynamicSecretListView";
|
||||
import { FolderListView } from "./components/FolderListView";
|
||||
@@ -65,8 +70,11 @@ import { SecretImportListView } from "./components/SecretImportListView";
|
||||
import { SecretListView, SecretNoAccessListView } from "./components/SecretListView";
|
||||
import { SnapshotView } from "./components/SnapshotView";
|
||||
import {
|
||||
PendingChanges,
|
||||
PopUpNames,
|
||||
StoreProvider,
|
||||
useBatchMode,
|
||||
useBatchModeActions,
|
||||
usePopUpAction,
|
||||
usePopUpState,
|
||||
useSelectedSecretActions,
|
||||
@@ -94,8 +102,11 @@ const Page = () => {
|
||||
});
|
||||
|
||||
const { permission } = useProjectPermission();
|
||||
const { mutateAsync: createCommit } = useCreateCommit();
|
||||
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const { isBatchMode, pendingChanges } = useBatchMode();
|
||||
const { loadPendingChanges, setExistingKeys } = useBatchModeActions();
|
||||
|
||||
const {
|
||||
offset,
|
||||
@@ -128,6 +139,12 @@ const Page = () => {
|
||||
const projectSlug = currentWorkspace?.slug || "";
|
||||
const secretPath = (routerQueryParams.secretPath as string) || "/";
|
||||
|
||||
useEffect(() => {
|
||||
if (isBatchMode && workspaceId && environment && secretPath) {
|
||||
loadPendingChanges({ workspaceId, environment, secretPath });
|
||||
}
|
||||
}, [isBatchMode, workspaceId, environment, secretPath, loadPendingChanges]);
|
||||
|
||||
const canReadSecret = hasSecretReadValueOrDescribePermission(
|
||||
permission,
|
||||
ProjectPermissionSecretActions.DescribeSecret,
|
||||
@@ -217,7 +234,7 @@ const Page = () => {
|
||||
type: "error"
|
||||
});
|
||||
navigate({
|
||||
to: "/projects/$projectId/secret-manager/overview",
|
||||
to: "/projects/secret-management/$projectId/overview",
|
||||
params: {
|
||||
projectId: workspaceId
|
||||
}
|
||||
@@ -293,6 +310,30 @@ const Page = () => {
|
||||
});
|
||||
const isProtectedBranch = Boolean(boardPolicy);
|
||||
|
||||
const handleCreateCommit = async (changes: PendingChanges, message: string) => {
|
||||
try {
|
||||
await createCommit({
|
||||
workspaceId,
|
||||
environment,
|
||||
secretPath,
|
||||
pendingChanges: changes,
|
||||
message
|
||||
});
|
||||
createNotification({
|
||||
text: isProtectedBranch
|
||||
? "Requested changes have been sent for review"
|
||||
: "Changes committed successfully",
|
||||
type: "success"
|
||||
});
|
||||
} catch (error) {
|
||||
createNotification({
|
||||
text: "Failed to commit changes",
|
||||
type: "error"
|
||||
});
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const {
|
||||
data: snapshotList,
|
||||
isFetchingNextPage: isFetchingNextSnapshotList,
|
||||
@@ -345,7 +386,7 @@ const Page = () => {
|
||||
const handleOnClickRollbackMode = () => {
|
||||
if (isPITEnabled) {
|
||||
navigate({
|
||||
to: "/projects/$projectId/secret-manager/commits/$environment/$folderId",
|
||||
to: "/projects/secret-management/$projectId/commits/$environment/$folderId",
|
||||
params: {
|
||||
projectId: workspaceId,
|
||||
folderId,
|
||||
@@ -376,9 +417,21 @@ const Page = () => {
|
||||
imports?.length ||
|
||||
dynamicSecrets?.length ||
|
||||
secretRotations?.length ||
|
||||
noAccessSecretCount
|
||||
noAccessSecretCount ||
|
||||
pendingChanges.secrets.length ||
|
||||
pendingChanges.folders.length
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (data && isBatchMode) {
|
||||
const existingSecretKeys = [...(secrets?.map((s) => s.key) || [])];
|
||||
|
||||
const existingFolderNames = folders?.map((f) => f.name) || [];
|
||||
|
||||
setExistingKeys(existingSecretKeys, existingFolderNames);
|
||||
}
|
||||
}, [data, isBatchMode, setExistingKeys, secrets, importedSecrets, folders]);
|
||||
|
||||
const handleSortToggle = () =>
|
||||
setOrderDirection((state) =>
|
||||
state === OrderByDirection.ASC ? OrderByDirection.DESC : OrderByDirection.ASC
|
||||
@@ -506,6 +559,143 @@ const Page = () => {
|
||||
setDebouncedSearchFilter("");
|
||||
};
|
||||
|
||||
const getMergedSecretsWithPending = () => {
|
||||
if (!isBatchMode || pendingChanges.secrets.length === 0) {
|
||||
return secrets;
|
||||
}
|
||||
|
||||
const mergedSecrets = [...(secrets || [])];
|
||||
|
||||
pendingChanges.secrets.forEach((change) => {
|
||||
switch (change.type) {
|
||||
case PendingAction.Create:
|
||||
mergedSecrets.unshift({
|
||||
id: change.id,
|
||||
key: change.secretKey,
|
||||
value: change.secretValue,
|
||||
comment: change.secretComment || "",
|
||||
skipMultilineEncoding: change.skipMultilineEncoding || false,
|
||||
tags: change.tags || [],
|
||||
secretMetadata: change.secretMetadata || [],
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
version: 1,
|
||||
isPending: true,
|
||||
pendingAction: PendingAction.Create
|
||||
} as unknown as SecretV3RawSanitized);
|
||||
break;
|
||||
|
||||
case PendingAction.Update:
|
||||
const updateIndex = mergedSecrets.findIndex((s) => s.key === change.secretKey);
|
||||
if (updateIndex >= 0) {
|
||||
mergedSecrets[updateIndex] = {
|
||||
...mergedSecrets[updateIndex],
|
||||
key: change.newSecretName || change.secretKey,
|
||||
value:
|
||||
change.secretValue !== undefined
|
||||
? change.secretValue
|
||||
: mergedSecrets[updateIndex].value,
|
||||
comment:
|
||||
change.secretComment !== undefined
|
||||
? change.secretComment
|
||||
: mergedSecrets[updateIndex].comment,
|
||||
skipMultilineEncoding:
|
||||
change.skipMultilineEncoding !== undefined
|
||||
? change.skipMultilineEncoding
|
||||
: mergedSecrets[updateIndex].skipMultilineEncoding,
|
||||
secretMetadata: change.secretMetadata || mergedSecrets[updateIndex].secretMetadata,
|
||||
isPending: true,
|
||||
pendingAction: PendingAction.Update,
|
||||
tags: change.tags
|
||||
? change.tags?.map((tag) => ({
|
||||
id: tag.id,
|
||||
slug: tag.slug,
|
||||
projectId: workspaceId,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
__v: 0
|
||||
})) || []
|
||||
: mergedSecrets[updateIndex].tags
|
||||
};
|
||||
}
|
||||
break;
|
||||
|
||||
case PendingAction.Delete:
|
||||
const deleteIndex = mergedSecrets.findIndex((s) => s.key === change.secretKey);
|
||||
if (deleteIndex >= 0) {
|
||||
mergedSecrets[deleteIndex] = {
|
||||
...mergedSecrets[deleteIndex],
|
||||
isPending: true,
|
||||
pendingAction: PendingAction.Delete
|
||||
};
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
return mergedSecrets;
|
||||
};
|
||||
|
||||
const getMergedFoldersWithPending = () => {
|
||||
if (!isBatchMode || pendingChanges.folders.length === 0) {
|
||||
return folders;
|
||||
}
|
||||
|
||||
const mergedFolders = [...(folders || [])];
|
||||
|
||||
pendingChanges.folders.forEach((change) => {
|
||||
switch (change.type) {
|
||||
case PendingAction.Create:
|
||||
mergedFolders.unshift({
|
||||
id: change.id,
|
||||
name: change.folderName,
|
||||
description: change.description,
|
||||
parentId: null,
|
||||
isPending: true,
|
||||
pendingAction: PendingAction.Create
|
||||
} as any);
|
||||
break;
|
||||
|
||||
case PendingAction.Update:
|
||||
const updateIndex = mergedFolders.findIndex((f) => f.id === change.id);
|
||||
if (updateIndex >= 0) {
|
||||
mergedFolders[updateIndex] = {
|
||||
...mergedFolders[updateIndex],
|
||||
name: change.folderName,
|
||||
description:
|
||||
change.description !== undefined
|
||||
? change.description
|
||||
: mergedFolders[updateIndex].description,
|
||||
isPending: true,
|
||||
pendingAction: PendingAction.Update
|
||||
};
|
||||
}
|
||||
break;
|
||||
|
||||
case PendingAction.Delete:
|
||||
const deleteIndex = mergedFolders.findIndex((f) => f.id === change.id);
|
||||
if (deleteIndex >= 0) {
|
||||
mergedFolders[deleteIndex] = {
|
||||
...mergedFolders[deleteIndex],
|
||||
isPending: true,
|
||||
pendingAction: PendingAction.Delete
|
||||
};
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
return mergedFolders;
|
||||
};
|
||||
|
||||
const mergedSecrets = getMergedSecretsWithPending();
|
||||
const mergedFolders = getMergedFoldersWithPending();
|
||||
return (
|
||||
<div className="container mx-auto flex max-w-7xl flex-col text-mineshaft-50 dark:[color-scheme:dark]">
|
||||
<PageHeader
|
||||
@@ -563,6 +753,7 @@ const Page = () => {
|
||||
projectSlug={projectSlug}
|
||||
secretPath={secretPath}
|
||||
isVisible={isVisible}
|
||||
isBatchMode={isBatchMode}
|
||||
filter={filter}
|
||||
tags={tags}
|
||||
onVisibilityToggle={handleToggleVisibility}
|
||||
@@ -698,9 +889,9 @@ const Page = () => {
|
||||
importedSecrets={importedSecrets}
|
||||
/>
|
||||
)}
|
||||
{Boolean(folders?.length) && (
|
||||
{Boolean(mergedFolders?.length) && (
|
||||
<FolderListView
|
||||
folders={folders}
|
||||
folders={mergedFolders}
|
||||
environment={environment}
|
||||
workspaceId={workspaceId}
|
||||
secretPath={secretPath}
|
||||
@@ -718,9 +909,9 @@ const Page = () => {
|
||||
{canReadSecretRotations && Boolean(secretRotations?.length) && (
|
||||
<SecretRotationListView secretRotations={secretRotations} />
|
||||
)}
|
||||
{canReadSecret && Boolean(secrets?.length) && (
|
||||
{canReadSecret && Boolean(mergedSecrets?.length) && (
|
||||
<SecretListView
|
||||
secrets={secrets}
|
||||
secrets={mergedSecrets}
|
||||
tags={tags}
|
||||
isVisible={isVisible}
|
||||
environment={environment}
|
||||
@@ -731,6 +922,15 @@ const Page = () => {
|
||||
usedBySecretSyncs={usedBySecretSyncs}
|
||||
/>
|
||||
)}
|
||||
{(pendingChanges.secrets.length > 0 || pendingChanges.folders.length > 0) && (
|
||||
<CommitForm
|
||||
onCommit={handleCreateCommit}
|
||||
environment={environment}
|
||||
workspaceId={workspaceId}
|
||||
secretPath={secretPath}
|
||||
isCommitting={false}
|
||||
/>
|
||||
)}
|
||||
{noAccessSecretCount > 0 && <SecretNoAccessListView count={noAccessSecretCount} />}
|
||||
{!canReadSecret &&
|
||||
!canReadDynamicSecret &&
|
||||
@@ -738,25 +938,34 @@ const Page = () => {
|
||||
folders?.length === 0 && <PermissionDeniedBanner />}
|
||||
</div>
|
||||
</div>
|
||||
{!isDetailsLoading && totalCount > 0 && (
|
||||
<Pagination
|
||||
startAdornment={
|
||||
<SecretTableResourceCount
|
||||
dynamicSecretCount={totalDynamicSecretCount}
|
||||
importCount={totalImportCount}
|
||||
secretCount={totalSecretCount}
|
||||
folderCount={totalFolderCount}
|
||||
secretRotationCount={totalSecretRotationCount}
|
||||
/>
|
||||
}
|
||||
className="rounded-b-md border-t border-solid border-t-mineshaft-600"
|
||||
count={totalCount}
|
||||
page={page}
|
||||
perPage={perPage}
|
||||
onChangePage={(newPage) => setPage(newPage)}
|
||||
onChangePerPage={handlePerPageChange}
|
||||
/>
|
||||
)}
|
||||
{!isDetailsLoading &&
|
||||
(totalCount > 0 ||
|
||||
pendingChanges.secrets.length > 0 ||
|
||||
pendingChanges.folders.length > 0) && (
|
||||
<Pagination
|
||||
startAdornment={
|
||||
<SecretTableResourceCount
|
||||
dynamicSecretCount={totalDynamicSecretCount}
|
||||
importCount={totalImportCount}
|
||||
secretCount={
|
||||
totalSecretCount +
|
||||
pendingChanges.secrets.filter((s) => s.type === PendingAction.Create).length
|
||||
}
|
||||
folderCount={
|
||||
totalFolderCount +
|
||||
pendingChanges.folders.filter((f) => f.type === PendingAction.Create).length
|
||||
}
|
||||
secretRotationCount={totalSecretRotationCount}
|
||||
/>
|
||||
}
|
||||
className="rounded-b-md border-t border-solid border-t-mineshaft-600"
|
||||
count={totalCount + pendingChanges.secrets.length + pendingChanges.folders.length}
|
||||
page={page}
|
||||
perPage={perPage}
|
||||
onChangePage={(newPage) => setPage(newPage)}
|
||||
onChangePerPage={handlePerPageChange}
|
||||
/>
|
||||
)}
|
||||
<Modal
|
||||
isOpen={createSecretPopUp.isOpen}
|
||||
onOpenChange={(state) => togglePopUp(PopUpNames.CreateSecretForm, state)}
|
||||
@@ -772,6 +981,7 @@ const Page = () => {
|
||||
secretPath={secretPath}
|
||||
autoCapitalize={currentWorkspace?.autoCapitalization}
|
||||
isProtectedBranch={isProtectedBranch}
|
||||
isBatchMode={isBatchMode}
|
||||
/>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
|
||||
@@ -1,13 +1,218 @@
|
||||
/* eslint-disable no-nested-ternary */
|
||||
import { createContext, ReactNode, useContext, useEffect, useRef } from "react";
|
||||
import { useRouter } from "@tanstack/react-router";
|
||||
import { createStore, StateCreator, StoreApi, useStore } from "zustand";
|
||||
import { useShallow } from "zustand/react/shallow";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { PendingAction } from "@app/hooks/api/secretFolders/types";
|
||||
import { SecretV3RawSanitized } from "@app/hooks/api/secrets/types";
|
||||
|
||||
// akhilmhdh: Don't remove this file if ur thinking why use zustand just for selected selects state
|
||||
// This is first step and the whole secret crud will be moved to this global page scope state
|
||||
// this will allow more stuff like undo grouping stuffs etc
|
||||
|
||||
// Base interface for all pending changes
|
||||
export interface BasePendingChange {
|
||||
id: string;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
// Secret-related change types
|
||||
export interface PendingSecretCreate extends BasePendingChange {
|
||||
resourceType: "secret";
|
||||
type: PendingAction.Create;
|
||||
secretKey: string;
|
||||
secretValue: string;
|
||||
secretComment?: string;
|
||||
skipMultilineEncoding?: boolean;
|
||||
tags?: { id: string; slug: string }[];
|
||||
secretMetadata?: { key: string; value: string }[];
|
||||
originalKey?: string;
|
||||
}
|
||||
|
||||
export interface PendingSecretUpdate extends BasePendingChange {
|
||||
resourceType: "secret";
|
||||
type: PendingAction.Update;
|
||||
secretKey: string;
|
||||
newSecretName?: string;
|
||||
originalValue?: string;
|
||||
secretValue?: string;
|
||||
originalComment?: string;
|
||||
secretComment?: string;
|
||||
originalSkipMultilineEncoding?: boolean;
|
||||
skipMultilineEncoding?: boolean;
|
||||
originalTags?: { id: string; slug: string }[];
|
||||
tags?: { id: string; slug: string }[];
|
||||
originalSecretMetadata?: { key: string; value: string }[];
|
||||
secretMetadata?: { key: string; value: string }[];
|
||||
existingSecret: SecretV3RawSanitized;
|
||||
}
|
||||
|
||||
export interface PendingSecretDelete extends BasePendingChange {
|
||||
resourceType: "secret";
|
||||
type: PendingAction.Delete;
|
||||
secretKey: string;
|
||||
secretValue: string;
|
||||
}
|
||||
|
||||
// Folder-related change types
|
||||
export interface PendingFolderCreate extends BasePendingChange {
|
||||
resourceType: "folder";
|
||||
type: PendingAction.Create;
|
||||
id: string;
|
||||
folderName: string;
|
||||
description?: string;
|
||||
parentPath: string;
|
||||
}
|
||||
|
||||
export interface PendingFolderUpdate extends BasePendingChange {
|
||||
resourceType: "folder";
|
||||
type: PendingAction.Update;
|
||||
originalFolderName: string;
|
||||
folderName: string;
|
||||
id: string;
|
||||
originalDescription?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface PendingFolderDelete extends BasePendingChange {
|
||||
resourceType: "folder";
|
||||
type: PendingAction.Delete;
|
||||
id: string;
|
||||
folderName: string;
|
||||
folderPath: string;
|
||||
}
|
||||
|
||||
// Union types for each resource
|
||||
export type PendingSecretChange = PendingSecretCreate | PendingSecretUpdate | PendingSecretDelete;
|
||||
export type PendingFolderChange = PendingFolderCreate | PendingFolderUpdate | PendingFolderDelete;
|
||||
export type PendingChange = PendingSecretChange | PendingFolderChange;
|
||||
|
||||
// Grouped changes for better processing
|
||||
export interface PendingChanges {
|
||||
secrets: PendingSecretChange[];
|
||||
folders: PendingFolderChange[];
|
||||
}
|
||||
|
||||
// Context interface for batch operations
|
||||
export interface BatchContext {
|
||||
workspaceId: string;
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
}
|
||||
|
||||
const normalizeValue = (value: any): string | boolean | undefined => {
|
||||
if (value === null || value === undefined || value === "") {
|
||||
return undefined;
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
const areValuesEqual = (value1: any, value2: any): boolean => {
|
||||
const normalized1 = normalizeValue(value1);
|
||||
const normalized2 = normalizeValue(value2);
|
||||
|
||||
if (normalized1 === undefined && normalized2 === undefined) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return normalized1 === normalized2;
|
||||
};
|
||||
|
||||
const areArraysEqual = (arr1: any[] | undefined, arr2: any[] | undefined): boolean => {
|
||||
// Handle undefined/null arrays
|
||||
if (!arr1 && !arr2) return true;
|
||||
if (!arr1 || !arr2) return false;
|
||||
|
||||
// Compare lengths
|
||||
if (arr1.length !== arr2.length) return false;
|
||||
|
||||
// Deep comparison using JSON stringify (for simple objects)
|
||||
return JSON.stringify(arr1.sort()) === JSON.stringify(arr2.sort());
|
||||
};
|
||||
|
||||
const cleanupRevertedSecretFields = (update: PendingSecretUpdate): PendingSecretUpdate | null => {
|
||||
const cleaned = { ...update };
|
||||
let hasChanges = false;
|
||||
|
||||
if (
|
||||
cleaned.secretValue !== undefined &&
|
||||
!areValuesEqual(cleaned.secretValue, cleaned.originalValue)
|
||||
) {
|
||||
hasChanges = true;
|
||||
} else {
|
||||
cleaned.secretValue = undefined;
|
||||
}
|
||||
|
||||
if (
|
||||
cleaned.secretComment !== undefined &&
|
||||
(!areValuesEqual(cleaned.secretComment, cleaned.originalComment) ||
|
||||
!areValuesEqual(cleaned.secretComment, cleaned.existingSecret.comment))
|
||||
) {
|
||||
hasChanges = true;
|
||||
} else {
|
||||
cleaned.secretComment = undefined;
|
||||
}
|
||||
|
||||
if (
|
||||
cleaned.skipMultilineEncoding !== undefined &&
|
||||
cleaned.skipMultilineEncoding !== cleaned.originalSkipMultilineEncoding
|
||||
) {
|
||||
hasChanges = true;
|
||||
} else {
|
||||
cleaned.skipMultilineEncoding = undefined;
|
||||
}
|
||||
|
||||
if (cleaned.tags !== undefined && !areArraysEqual(cleaned.tags, cleaned.originalTags)) {
|
||||
hasChanges = true;
|
||||
} else {
|
||||
cleaned.tags = undefined;
|
||||
}
|
||||
|
||||
if (
|
||||
cleaned.secretMetadata !== undefined &&
|
||||
!areArraysEqual(cleaned.secretMetadata, cleaned.originalSecretMetadata)
|
||||
) {
|
||||
hasChanges = true;
|
||||
} else {
|
||||
cleaned.secretMetadata = undefined;
|
||||
}
|
||||
|
||||
if (cleaned.newSecretName !== undefined && cleaned.newSecretName !== cleaned.secretKey) {
|
||||
hasChanges = true;
|
||||
} else {
|
||||
cleaned.newSecretName = undefined;
|
||||
}
|
||||
|
||||
// If no changes remain, return null to indicate this update should be removed
|
||||
return hasChanges ? cleaned : null;
|
||||
};
|
||||
|
||||
const cleanupRevertedFolderFields = (update: PendingFolderUpdate): PendingFolderUpdate | null => {
|
||||
const cleaned = { ...update };
|
||||
let hasChanges = false;
|
||||
|
||||
if (
|
||||
cleaned.folderName !== undefined &&
|
||||
!areValuesEqual(cleaned.folderName, cleaned.originalFolderName)
|
||||
) {
|
||||
hasChanges = true;
|
||||
}
|
||||
|
||||
if (
|
||||
cleaned.description !== undefined &&
|
||||
!areValuesEqual(cleaned.description, cleaned.originalDescription)
|
||||
) {
|
||||
hasChanges = true;
|
||||
} else {
|
||||
cleaned.description = undefined;
|
||||
}
|
||||
|
||||
// If no changes remain, return null to indicate this update should be removed
|
||||
return hasChanges ? cleaned : null;
|
||||
};
|
||||
|
||||
type SelectedSecretState = {
|
||||
selectedSecret: Record<string, SecretV3RawSanitized>;
|
||||
action: {
|
||||
@@ -16,14 +221,16 @@ type SelectedSecretState = {
|
||||
set: (secrets: Record<string, SecretV3RawSanitized>) => void;
|
||||
};
|
||||
};
|
||||
const createSelectedSecretStore: StateCreator<SelectedSecretState> = (set) => ({
|
||||
|
||||
const createSelectedSecretStore: StateCreator<CombinedState, [], [], SelectedSecretState> = (
|
||||
set
|
||||
) => ({
|
||||
selectedSecret: {},
|
||||
action: {
|
||||
toggle: (secret) =>
|
||||
set((state) => {
|
||||
const isChecked = Boolean(state.selectedSecret?.[secret.id]);
|
||||
const newChecks = { ...state.selectedSecret };
|
||||
// remove selection if its present else add it
|
||||
if (isChecked) delete newChecks[secret.id];
|
||||
else newChecks[secret.id] = secret;
|
||||
return { selectedSecret: newChecks };
|
||||
@@ -33,6 +240,432 @@ const createSelectedSecretStore: StateCreator<SelectedSecretState> = (set) => ({
|
||||
}
|
||||
});
|
||||
|
||||
type BatchModeState = {
|
||||
isBatchMode: boolean;
|
||||
pendingChanges: PendingChanges;
|
||||
pendingChangesByContext: Map<string, PendingChanges>;
|
||||
existingSecretKeys: Set<string>;
|
||||
existingFolderNames: Set<string>;
|
||||
currentContext: BatchContext | null;
|
||||
batchActions: {
|
||||
addPendingChange: (change: PendingChange, context: BatchContext) => void;
|
||||
loadPendingChanges: (context: BatchContext) => void;
|
||||
clearAllPendingChanges: (context: BatchContext) => void;
|
||||
setExistingKeys: (secretKeys: string[], folderNames: string[]) => void;
|
||||
getTotalPendingChangesCount: () => number;
|
||||
removePendingChange: (changeId: string, resourceType: string, context: BatchContext) => void;
|
||||
};
|
||||
};
|
||||
|
||||
const generateContextKey = (workspaceId: string, environment: string, secretPath: string) => {
|
||||
return `${workspaceId}_${environment}_${secretPath}`;
|
||||
};
|
||||
|
||||
const createBatchModeStore: StateCreator<CombinedState, [], [], BatchModeState> = (set, get) => ({
|
||||
isBatchMode: true, // Always enabled by default
|
||||
pendingChanges: { secrets: [], folders: [] },
|
||||
pendingChangesByContext: new Map<string, PendingChanges>(),
|
||||
currentContext: null,
|
||||
existingSecretKeys: new Set<string>(),
|
||||
existingFolderNames: new Set<string>(),
|
||||
batchActions: {
|
||||
addPendingChange: (change: PendingChange, context: BatchContext) =>
|
||||
set((state) => {
|
||||
const contextKey = generateContextKey(
|
||||
context.workspaceId,
|
||||
context.environment,
|
||||
context.secretPath
|
||||
);
|
||||
|
||||
// Get existing changes for this context or create new empty state
|
||||
const existingChanges = state.pendingChangesByContext.get(contextKey) || {
|
||||
secrets: [],
|
||||
folders: []
|
||||
};
|
||||
const newChanges = { ...existingChanges };
|
||||
|
||||
if (change.resourceType === "folder") {
|
||||
const existingFolder =
|
||||
state.existingFolderNames.has(change.folderName) ||
|
||||
newChanges.folders.some((f) => f.folderName === change.folderName);
|
||||
|
||||
if (change.type === PendingAction.Create && existingFolder) {
|
||||
createNotification({
|
||||
text: "A folder with this name already exists",
|
||||
type: "error"
|
||||
});
|
||||
return { pendingChanges: newChanges };
|
||||
}
|
||||
if (
|
||||
change.type === PendingAction.Update &&
|
||||
change.folderName !== change.originalFolderName &&
|
||||
existingFolder
|
||||
) {
|
||||
createNotification({
|
||||
text: "A folder with this name already exists",
|
||||
type: "error"
|
||||
});
|
||||
return { pendingChanges: newChanges };
|
||||
}
|
||||
}
|
||||
|
||||
if (change.resourceType === "secret") {
|
||||
const existingSecret =
|
||||
state.existingSecretKeys.has(change.secretKey) ||
|
||||
newChanges.secrets.some(
|
||||
(s) =>
|
||||
(s.secretKey === change.secretKey && s.type !== PendingAction.Create) ||
|
||||
(change.type === PendingAction.Create &&
|
||||
change.originalKey !== change.secretKey &&
|
||||
s.secretKey === change.secretKey)
|
||||
);
|
||||
|
||||
if (change.type === PendingAction.Create && existingSecret) {
|
||||
createNotification({
|
||||
text: "A secret with this name already exists",
|
||||
type: "error"
|
||||
});
|
||||
return { pendingChanges: newChanges };
|
||||
}
|
||||
|
||||
const existingNewSecretName =
|
||||
change.type === PendingAction.Update &&
|
||||
change.newSecretName &&
|
||||
change.newSecretName !== change.secretKey &&
|
||||
(state.existingSecretKeys.has(change.newSecretName) ||
|
||||
newChanges.secrets.some(
|
||||
(s) =>
|
||||
(s.secretKey === change.newSecretName ||
|
||||
(s.type === PendingAction.Update &&
|
||||
s.newSecretName === change.newSecretName)) &&
|
||||
s.id !== change.id
|
||||
));
|
||||
|
||||
if (existingNewSecretName) {
|
||||
createNotification({
|
||||
text: "A secret with this name already exists",
|
||||
type: "error"
|
||||
});
|
||||
return { pendingChanges: newChanges };
|
||||
}
|
||||
}
|
||||
|
||||
if (change.resourceType === "secret") {
|
||||
const secretChanges = [...newChanges.secrets];
|
||||
|
||||
if (change.type === PendingAction.Create) {
|
||||
const existingCreateIndex = secretChanges.findIndex(
|
||||
(c) =>
|
||||
c.type === PendingAction.Create &&
|
||||
(c.secretKey === change.secretKey || c.secretKey === change.originalKey)
|
||||
);
|
||||
|
||||
if (existingCreateIndex >= 0) {
|
||||
secretChanges[existingCreateIndex] = {
|
||||
...secretChanges[existingCreateIndex],
|
||||
...change,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
} else {
|
||||
secretChanges.push(change);
|
||||
}
|
||||
} else if (change.type === PendingAction.Update) {
|
||||
const existingCreateIndex = secretChanges.findIndex(
|
||||
(c) => c.type === PendingAction.Create && c.id === change.id
|
||||
);
|
||||
|
||||
if (existingCreateIndex >= 0) {
|
||||
const existingCreate = secretChanges[existingCreateIndex] as PendingSecretCreate;
|
||||
secretChanges[existingCreateIndex] = {
|
||||
...existingCreate,
|
||||
secretKey: change.newSecretName || change.secretKey || existingCreate.secretKey,
|
||||
secretValue:
|
||||
change.secretValue !== undefined
|
||||
? change.secretValue
|
||||
: existingCreate.secretValue,
|
||||
secretComment:
|
||||
change.secretComment !== undefined
|
||||
? change.secretComment
|
||||
: existingCreate.secretComment,
|
||||
skipMultilineEncoding:
|
||||
change.skipMultilineEncoding !== undefined
|
||||
? change.skipMultilineEncoding
|
||||
: existingCreate.skipMultilineEncoding,
|
||||
tags: change.tags !== undefined ? change.tags : existingCreate.tags,
|
||||
secretMetadata:
|
||||
change.secretMetadata !== undefined
|
||||
? change.secretMetadata
|
||||
: existingCreate.secretMetadata,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
} else {
|
||||
const existingUpdateIndex = secretChanges.findIndex(
|
||||
(c) => c.type === PendingAction.Update && c.id === change.id
|
||||
);
|
||||
|
||||
if (existingUpdateIndex >= 0) {
|
||||
const existingUpdate = secretChanges[existingUpdateIndex] as PendingSecretUpdate;
|
||||
const mergedUpdate: PendingSecretUpdate = {
|
||||
...existingUpdate,
|
||||
secretKey: existingUpdate.secretKey,
|
||||
originalValue: existingUpdate.originalValue,
|
||||
originalComment: existingUpdate.originalComment,
|
||||
originalSkipMultilineEncoding: existingUpdate.originalSkipMultilineEncoding,
|
||||
originalTags: existingUpdate.originalTags,
|
||||
originalSecretMetadata: existingUpdate.originalSecretMetadata,
|
||||
|
||||
newSecretName:
|
||||
change.newSecretName !== undefined
|
||||
? change.newSecretName
|
||||
: existingUpdate.newSecretName,
|
||||
secretValue:
|
||||
change.secretValue !== undefined
|
||||
? change.secretValue
|
||||
: existingUpdate.secretValue,
|
||||
secretComment:
|
||||
change.secretComment !== undefined
|
||||
? change.secretComment
|
||||
: existingUpdate.secretComment,
|
||||
skipMultilineEncoding:
|
||||
change.skipMultilineEncoding !== undefined
|
||||
? change.skipMultilineEncoding
|
||||
: existingUpdate.skipMultilineEncoding,
|
||||
tags: change.tags !== undefined ? change.tags : existingUpdate.tags,
|
||||
secretMetadata:
|
||||
change.secretMetadata !== undefined
|
||||
? change.secretMetadata
|
||||
: existingUpdate.secretMetadata,
|
||||
existingSecret: existingUpdate.existingSecret,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
// Clean up reverted fields and check if any changes remain
|
||||
const cleanedUpdate = cleanupRevertedSecretFields(mergedUpdate);
|
||||
|
||||
if (cleanedUpdate) {
|
||||
// Still has changes, keep the update
|
||||
secretChanges[existingUpdateIndex] = cleanedUpdate;
|
||||
} else {
|
||||
// No changes remain, remove the pending update
|
||||
secretChanges.splice(existingUpdateIndex, 1);
|
||||
}
|
||||
} else {
|
||||
// New update - clean it up before adding
|
||||
const cleanedUpdate = cleanupRevertedSecretFields(change);
|
||||
if (cleanedUpdate) {
|
||||
secretChanges.push(cleanedUpdate);
|
||||
}
|
||||
// If cleanedUpdate is null, don't add it (no actual changes)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
secretChanges.push(change);
|
||||
}
|
||||
|
||||
newChanges.secrets = secretChanges;
|
||||
} else if (change.resourceType === "folder") {
|
||||
const folderChanges = [...newChanges.folders];
|
||||
|
||||
if (change.type === PendingAction.Create) {
|
||||
const existingCreateIndex = folderChanges.findIndex(
|
||||
(c) => c.type === PendingAction.Create && c.folderName === change.folderName
|
||||
);
|
||||
|
||||
if (existingCreateIndex >= 0) {
|
||||
folderChanges[existingCreateIndex] = {
|
||||
...folderChanges[existingCreateIndex],
|
||||
...change,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
} else {
|
||||
folderChanges.push(change);
|
||||
}
|
||||
} else if (change.type === PendingAction.Update) {
|
||||
const existingCreateIndex = folderChanges.findIndex(
|
||||
(c) => c.type === PendingAction.Create && c.folderName === change.originalFolderName
|
||||
);
|
||||
|
||||
if (existingCreateIndex >= 0) {
|
||||
const existingCreate = folderChanges[existingCreateIndex] as PendingFolderCreate;
|
||||
folderChanges[existingCreateIndex] = {
|
||||
...existingCreate,
|
||||
folderName: change.folderName || existingCreate.folderName,
|
||||
description:
|
||||
change.description !== undefined
|
||||
? change.description
|
||||
: existingCreate.description,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
} else {
|
||||
const existingUpdateIndex = folderChanges.findIndex(
|
||||
(c) => c.type === PendingAction.Update && c.id === change.id
|
||||
);
|
||||
|
||||
if (existingUpdateIndex >= 0) {
|
||||
const existingUpdate = folderChanges[existingUpdateIndex] as PendingFolderUpdate;
|
||||
|
||||
const mergedUpdate: PendingFolderUpdate = {
|
||||
...existingUpdate,
|
||||
originalFolderName: existingUpdate.originalFolderName,
|
||||
originalDescription: existingUpdate.originalDescription,
|
||||
|
||||
folderName:
|
||||
change.folderName !== undefined ? change.folderName : existingUpdate.folderName,
|
||||
description:
|
||||
change.description !== undefined
|
||||
? change.description
|
||||
: existingUpdate.description,
|
||||
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
// Clean up reverted fields and check if any changes remain
|
||||
const cleanedUpdate = cleanupRevertedFolderFields(mergedUpdate);
|
||||
|
||||
if (cleanedUpdate) {
|
||||
// Still has changes, keep the update
|
||||
folderChanges[existingUpdateIndex] = cleanedUpdate;
|
||||
} else {
|
||||
// No changes remain, remove the pending update
|
||||
folderChanges.splice(existingUpdateIndex, 1);
|
||||
}
|
||||
} else {
|
||||
// New update - clean it up before adding
|
||||
const cleanedUpdate = cleanupRevertedFolderFields(change);
|
||||
if (cleanedUpdate) {
|
||||
folderChanges.push(cleanedUpdate);
|
||||
}
|
||||
// If cleanedUpdate is null, don't add it (no actual changes)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
folderChanges.push(change);
|
||||
}
|
||||
|
||||
newChanges.folders = folderChanges;
|
||||
}
|
||||
|
||||
const updatedContextMap = new Map(state.pendingChangesByContext);
|
||||
updatedContextMap.set(contextKey, newChanges);
|
||||
|
||||
const currentChanges =
|
||||
contextKey ===
|
||||
generateContextKey(
|
||||
state.currentContext?.workspaceId || context.workspaceId,
|
||||
state.currentContext?.environment || context.environment,
|
||||
state.currentContext?.secretPath || context.secretPath
|
||||
)
|
||||
? newChanges
|
||||
: state.pendingChanges;
|
||||
|
||||
return {
|
||||
pendingChangesByContext: updatedContextMap,
|
||||
pendingChanges: currentChanges,
|
||||
currentContext: context
|
||||
};
|
||||
}),
|
||||
|
||||
removePendingChange: (changeId: string, resourceType: string, context: BatchContext) =>
|
||||
set((state) => {
|
||||
const contextKey = generateContextKey(
|
||||
context.workspaceId,
|
||||
context.environment,
|
||||
context.secretPath
|
||||
);
|
||||
|
||||
// Get existing changes for this context
|
||||
const existingChanges = state.pendingChangesByContext.get(contextKey) || {
|
||||
secrets: [],
|
||||
folders: []
|
||||
};
|
||||
const newChanges = { ...existingChanges };
|
||||
|
||||
if (resourceType === "secret") {
|
||||
newChanges.secrets = newChanges.secrets.filter((c) => c.id !== changeId);
|
||||
} else if (resourceType === "folder") {
|
||||
newChanges.folders = newChanges.folders.filter((c) => c.id !== changeId);
|
||||
}
|
||||
|
||||
const updatedContextMap = new Map(state.pendingChangesByContext);
|
||||
updatedContextMap.set(contextKey, newChanges);
|
||||
|
||||
const isCurrentContext =
|
||||
state.currentContext &&
|
||||
contextKey ===
|
||||
generateContextKey(
|
||||
state.currentContext.workspaceId,
|
||||
state.currentContext.environment,
|
||||
state.currentContext.secretPath
|
||||
);
|
||||
|
||||
return {
|
||||
pendingChangesByContext: updatedContextMap,
|
||||
pendingChanges: isCurrentContext ? newChanges : state.pendingChanges
|
||||
};
|
||||
}),
|
||||
|
||||
loadPendingChanges: (context) => {
|
||||
const contextKey = generateContextKey(
|
||||
context.workspaceId,
|
||||
context.environment,
|
||||
context.secretPath
|
||||
);
|
||||
|
||||
set((state) => {
|
||||
const contextChanges = state.pendingChangesByContext.get(contextKey) || {
|
||||
secrets: [],
|
||||
folders: []
|
||||
};
|
||||
|
||||
return {
|
||||
pendingChanges: contextChanges,
|
||||
currentContext: context
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
clearAllPendingChanges: (context) => {
|
||||
const contextKey = generateContextKey(
|
||||
context.workspaceId,
|
||||
context.environment,
|
||||
context.secretPath
|
||||
);
|
||||
|
||||
set((state) => {
|
||||
// Clear changes for this specific context
|
||||
const updatedContextMap = new Map(state.pendingChangesByContext);
|
||||
updatedContextMap.delete(contextKey);
|
||||
|
||||
// If this is the current context, also clear the active pending changes
|
||||
const isCurrentContext =
|
||||
state.currentContext &&
|
||||
contextKey ===
|
||||
generateContextKey(
|
||||
state.currentContext.workspaceId,
|
||||
state.currentContext.environment,
|
||||
state.currentContext.secretPath
|
||||
);
|
||||
|
||||
return {
|
||||
pendingChangesByContext: updatedContextMap,
|
||||
pendingChanges: isCurrentContext ? { secrets: [], folders: [] } : state.pendingChanges
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
setExistingKeys: (secretKeys, folderNames) =>
|
||||
set({
|
||||
existingSecretKeys: new Set(secretKeys),
|
||||
existingFolderNames: new Set(folderNames)
|
||||
}),
|
||||
|
||||
getTotalPendingChangesCount: () => {
|
||||
const state = get();
|
||||
return state.pendingChanges.secrets.length + state.pendingChanges.folders.length;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export enum PopUpNames {
|
||||
CreateSecretForm = "create-secret-form"
|
||||
}
|
||||
@@ -58,13 +691,14 @@ const createPopUpStore: StateCreator<PopUpState> = (set) => ({
|
||||
}
|
||||
});
|
||||
|
||||
type CombinedState = SelectedSecretState & PopUpState;
|
||||
type CombinedState = SelectedSecretState & PopUpState & BatchModeState;
|
||||
const StoreContext = createContext<StoreApi<CombinedState> | null>(null);
|
||||
export const StoreProvider = ({ children }: { children: ReactNode }) => {
|
||||
const storeRef = useRef<StoreApi<CombinedState>>(
|
||||
createStore<CombinedState>((...a) => ({
|
||||
...createSelectedSecretStore(...a),
|
||||
...createPopUpStore(...a)
|
||||
...createPopUpStore(...a),
|
||||
...createBatchModeStore(...a)
|
||||
}))
|
||||
);
|
||||
const router = useRouter();
|
||||
@@ -99,3 +733,17 @@ export const useSelectedSecretActions = () => useStoreContext(useShallow((state)
|
||||
export const usePopUpState = (id: PopUpNames) =>
|
||||
useStoreContext(useShallow((state) => state.popUp?.[id] || { isOpen: false }));
|
||||
export const usePopUpAction = () => useStoreContext(useShallow((state) => state.popUpActions));
|
||||
|
||||
export const useBatchMode = () =>
|
||||
useStoreContext(
|
||||
useShallow((state) => ({
|
||||
isBatchMode: state.isBatchMode,
|
||||
pendingChanges: state.pendingChanges,
|
||||
currentContext: state.currentContext,
|
||||
totalChangesCount: state.batchActions.getTotalPendingChangesCount(),
|
||||
secretChangesCount: state.pendingChanges.secrets.length,
|
||||
folderChangesCount: state.pendingChanges.folders.length
|
||||
}))
|
||||
);
|
||||
|
||||
export const useBatchModeActions = () => useStoreContext(useShallow((state) => state.batchActions));
|
||||
|
||||
+28
@@ -77,12 +77,15 @@ import {
|
||||
} from "@app/hooks/api/dashboard/queries";
|
||||
import { UsedBySecretSyncs } from "@app/hooks/api/dashboard/types";
|
||||
import { secretApprovalRequestKeys } from "@app/hooks/api/secretApprovalRequest/queries";
|
||||
import { PendingAction } from "@app/hooks/api/secretFolders/types";
|
||||
import { fetchProjectSecrets, secretKeys } from "@app/hooks/api/secrets/queries";
|
||||
import { ApiErrorTypes, SecretType, TApiErrors, WsTag } from "@app/hooks/api/types";
|
||||
import { SecretSearchInput } from "@app/pages/secret-manager/OverviewPage/components/SecretSearchInput";
|
||||
|
||||
import {
|
||||
PendingFolderCreate,
|
||||
PopUpNames,
|
||||
useBatchModeActions,
|
||||
usePopUpAction,
|
||||
useSelectedSecretActions,
|
||||
useSelectedSecrets
|
||||
@@ -112,6 +115,7 @@ type Props = {
|
||||
filter: Filter;
|
||||
tags?: WsTag[];
|
||||
isVisible?: boolean;
|
||||
isBatchMode?: boolean;
|
||||
snapshotCount: number;
|
||||
isSnapshotCountLoading?: boolean;
|
||||
protectedBranchPolicyName?: string;
|
||||
@@ -142,6 +146,7 @@ export const ActionBar = ({
|
||||
filter,
|
||||
tags = [],
|
||||
isVisible,
|
||||
isBatchMode,
|
||||
snapshotCount,
|
||||
isSnapshotCountLoading,
|
||||
onSearchChange,
|
||||
@@ -182,6 +187,7 @@ export const ActionBar = ({
|
||||
options: { onSuccess: undefined }
|
||||
});
|
||||
const queryClient = useQueryClient();
|
||||
const { addPendingChange } = useBatchModeActions();
|
||||
|
||||
const selectedSecrets = useSelectedSecrets();
|
||||
const { reset: resetSelectedSecret } = useSelectedSecretActions();
|
||||
@@ -192,6 +198,28 @@ export const ActionBar = ({
|
||||
|
||||
const handleFolderCreate = async (folderName: string, description: string | null) => {
|
||||
try {
|
||||
if (isBatchMode) {
|
||||
const folderId = `${folderName}`;
|
||||
const pendingFolderCreate: PendingFolderCreate = {
|
||||
id: folderId,
|
||||
resourceType: "folder",
|
||||
type: PendingAction.Create,
|
||||
folderName,
|
||||
description: description || undefined,
|
||||
parentPath: secretPath,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
addPendingChange(pendingFolderCreate, {
|
||||
workspaceId,
|
||||
environment,
|
||||
secretPath
|
||||
});
|
||||
|
||||
handlePopUpClose("addFolder");
|
||||
return;
|
||||
}
|
||||
|
||||
await createFolder({
|
||||
name: folderName,
|
||||
path: secretPath,
|
||||
|
||||
+7
-2
@@ -8,7 +8,12 @@ import { TextArea } from "@app/components/v2/TextArea/TextArea";
|
||||
|
||||
type Props = {
|
||||
onCreateFolder?: (folderName: string, description: string | null) => Promise<void>;
|
||||
onUpdateFolder?: (folderName: string, description: string | null) => Promise<void>;
|
||||
onUpdateFolder?: (
|
||||
folderName: string,
|
||||
description: string | null,
|
||||
oldFolderName?: string,
|
||||
oldFolderDescription?: string
|
||||
) => Promise<void>;
|
||||
isEdit?: boolean;
|
||||
defaultFolderName?: string;
|
||||
defaultDescription?: string;
|
||||
@@ -69,7 +74,7 @@ export const FolderForm = ({
|
||||
const descriptionShaped = description && description.trim() !== "" ? description : null;
|
||||
|
||||
if (isEdit) {
|
||||
await onUpdateFolder?.(name, descriptionShaped);
|
||||
await onUpdateFolder?.(name, descriptionShaped, defaultFolderName, defaultDescription);
|
||||
} else {
|
||||
await onCreateFolder?.(name, descriptionShaped);
|
||||
}
|
||||
|
||||
+446
@@ -0,0 +1,446 @@
|
||||
/* eslint-disable jsx-a11y/label-has-associated-control */
|
||||
import React, { useCallback, useState } from "react";
|
||||
import { faCodeCommit, faEye, faFolder, faKey } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { Badge, Button, Input, Modal, ModalContent } from "@app/components/v2";
|
||||
import { PendingAction } from "@app/hooks/api/secretFolders/types";
|
||||
import { SecretVersionDiffView } from "@app/pages/secret-manager/CommitDetailsPage/components/SecretVersionDiffView";
|
||||
|
||||
import {
|
||||
PendingChange,
|
||||
PendingChanges,
|
||||
useBatchMode,
|
||||
useBatchModeActions
|
||||
} from "../../SecretMainPage.store";
|
||||
|
||||
interface CommitFormProps {
|
||||
onCommit: (changes: PendingChanges, commitMessage: string) => Promise<void>;
|
||||
isCommitting?: boolean;
|
||||
environment: string;
|
||||
workspaceId: string;
|
||||
secretPath: string;
|
||||
}
|
||||
|
||||
interface ResourceChangeProps {
|
||||
change: PendingChange;
|
||||
environment: string;
|
||||
workspaceId: string;
|
||||
secretPath: string;
|
||||
}
|
||||
|
||||
type RenderResourceProps = {
|
||||
onDiscard: () => void;
|
||||
change: PendingChange;
|
||||
};
|
||||
|
||||
const RenderSecretChanges = ({ onDiscard, change }: RenderResourceProps) => {
|
||||
if (change.resourceType !== "secret") return null;
|
||||
|
||||
if (change.type === PendingAction.Create) {
|
||||
return (
|
||||
<SecretVersionDiffView
|
||||
onDiscard={onDiscard}
|
||||
item={{
|
||||
secretKey: change.secretKey,
|
||||
isAdded: true,
|
||||
type: "secret",
|
||||
id: change.id,
|
||||
versions: [
|
||||
{
|
||||
version: 1, // placeholder, not used
|
||||
secretKey: change.secretKey,
|
||||
secretValue: change.secretValue,
|
||||
tags: change.tags,
|
||||
secretMetadata: change.secretMetadata,
|
||||
skipMultilineEncoding: change.skipMultilineEncoding,
|
||||
comment: change.secretComment
|
||||
}
|
||||
]
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (change.type === PendingAction.Update) {
|
||||
const { existingSecret } = change;
|
||||
|
||||
const hasKeyChange = change.newSecretName && change.secretKey !== change.newSecretName;
|
||||
const hasValueChange = change.secretValue !== change.originalValue;
|
||||
const hasCommentChange = change.secretComment !== change.originalComment;
|
||||
const hasMultilineChange =
|
||||
change.skipMultilineEncoding !== change.originalSkipMultilineEncoding;
|
||||
const hasTagsChange = JSON.stringify(change.tags) !== JSON.stringify(change.originalTags);
|
||||
const hasMetadataChange =
|
||||
JSON.stringify(change.secretMetadata) !== JSON.stringify(change.originalSecretMetadata);
|
||||
|
||||
const hasChanges = [
|
||||
hasKeyChange,
|
||||
hasValueChange,
|
||||
hasCommentChange,
|
||||
hasMultilineChange,
|
||||
hasTagsChange,
|
||||
hasMetadataChange
|
||||
].some(Boolean);
|
||||
|
||||
if (!hasChanges) return null;
|
||||
|
||||
return (
|
||||
<SecretVersionDiffView
|
||||
onDiscard={onDiscard}
|
||||
item={{
|
||||
secretKey: change.secretKey,
|
||||
isUpdated: true,
|
||||
type: "secret",
|
||||
id: change.id,
|
||||
versions: [
|
||||
{
|
||||
version: 1, // placeholder, not used
|
||||
secretKey: change.newSecretName ? existingSecret.key : undefined,
|
||||
secretValue:
|
||||
change.secretValue !== undefined ? (existingSecret.value ?? "") : undefined,
|
||||
tags: change.tags ? (existingSecret.tags?.map((tag) => tag.slug) ?? []) : undefined,
|
||||
secretMetadata: change.secretMetadata ? existingSecret.secretMetadata : undefined,
|
||||
skipMultilineEncoding:
|
||||
typeof change.skipMultilineEncoding === "boolean"
|
||||
? existingSecret.skipMultilineEncoding
|
||||
: undefined,
|
||||
comment: change.secretComment !== undefined ? existingSecret.comment : undefined
|
||||
},
|
||||
{
|
||||
version: 2, // placeholder, not used
|
||||
secretKey: change.newSecretName,
|
||||
secretValue: change.secretValue,
|
||||
tags: change.tags?.map((tag) => tag.slug),
|
||||
secretMetadata: change.secretMetadata,
|
||||
skipMultilineEncoding: change.skipMultilineEncoding,
|
||||
comment: change.secretComment
|
||||
}
|
||||
]
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (change.type === PendingAction.Delete) {
|
||||
const { secretKey, secretValue } = change;
|
||||
return (
|
||||
<SecretVersionDiffView
|
||||
onDiscard={onDiscard}
|
||||
item={{
|
||||
secretKey: change.secretKey,
|
||||
isDeleted: true,
|
||||
type: "secret",
|
||||
id: change.id,
|
||||
versions: [
|
||||
{
|
||||
version: 1, // placeholder, not used
|
||||
secretKey,
|
||||
secretValue
|
||||
}
|
||||
]
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const RenderFolderChanges = ({ onDiscard, change }: RenderResourceProps) => {
|
||||
if (change.resourceType !== "folder") return null;
|
||||
|
||||
if (change.type === PendingAction.Create) {
|
||||
return (
|
||||
<SecretVersionDiffView
|
||||
onDiscard={onDiscard}
|
||||
item={{
|
||||
folderName: change.folderName,
|
||||
isAdded: true,
|
||||
type: "folder",
|
||||
id: change.id,
|
||||
versions: [
|
||||
{
|
||||
version: 1,
|
||||
name: change.folderName,
|
||||
description: change.description
|
||||
}
|
||||
]
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (change.type === PendingAction.Update) {
|
||||
const hasNameChange = change.folderName !== change.originalFolderName;
|
||||
const hasDescriptionChange = change.description !== change.originalDescription;
|
||||
|
||||
const hasChanges = [hasNameChange, hasDescriptionChange].some(Boolean);
|
||||
|
||||
if (!hasChanges) return null;
|
||||
|
||||
return (
|
||||
<SecretVersionDiffView
|
||||
onDiscard={onDiscard}
|
||||
item={{
|
||||
folderName: change.folderName,
|
||||
isUpdated: true,
|
||||
type: "folder",
|
||||
id: change.id,
|
||||
versions: [
|
||||
{
|
||||
version: 1, // placeholder, not used
|
||||
name: change.folderName ? change.originalFolderName : undefined,
|
||||
description: change.description ? change.originalDescription : undefined
|
||||
},
|
||||
{
|
||||
version: 2, // placeholder, not used
|
||||
name: change.folderName,
|
||||
description: change.description
|
||||
}
|
||||
]
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (change.type === PendingAction.Delete) {
|
||||
return (
|
||||
<SecretVersionDiffView
|
||||
onDiscard={onDiscard}
|
||||
item={{
|
||||
folderName: change.folderName,
|
||||
isDeleted: true,
|
||||
type: "folder",
|
||||
id: change.id,
|
||||
versions: [
|
||||
{
|
||||
version: 1,
|
||||
name: change.folderName
|
||||
}
|
||||
]
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const ResourceChange: React.FC<ResourceChangeProps> = ({
|
||||
change,
|
||||
environment,
|
||||
workspaceId,
|
||||
secretPath
|
||||
}) => {
|
||||
const { removePendingChange } = useBatchModeActions();
|
||||
|
||||
const handleDeletePending = useCallback(
|
||||
(changeType: string, id: string) => {
|
||||
removePendingChange(id, changeType, {
|
||||
workspaceId,
|
||||
environment,
|
||||
secretPath
|
||||
});
|
||||
},
|
||||
[change.resourceType, change.id]
|
||||
);
|
||||
|
||||
return change.resourceType === "secret" ? (
|
||||
<RenderSecretChanges
|
||||
key={change.id}
|
||||
change={change}
|
||||
onDiscard={() => handleDeletePending(change.resourceType, change.id)}
|
||||
/>
|
||||
) : (
|
||||
<RenderFolderChanges
|
||||
key={change.id}
|
||||
change={change}
|
||||
onDiscard={() => handleDeletePending(change.resourceType, change.id)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const CommitForm: React.FC<CommitFormProps> = ({
|
||||
onCommit,
|
||||
isCommitting = false,
|
||||
environment,
|
||||
workspaceId,
|
||||
secretPath
|
||||
}) => {
|
||||
const { isBatchMode, pendingChanges, totalChangesCount } = useBatchMode();
|
||||
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [commitMessage, setCommitMessage] = useState("");
|
||||
const { clearAllPendingChanges } = useBatchModeActions();
|
||||
|
||||
if (!isBatchMode || totalChangesCount === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleCommit = async () => {
|
||||
if (!commitMessage.trim()) {
|
||||
return;
|
||||
}
|
||||
await onCommit(pendingChanges, commitMessage);
|
||||
clearAllPendingChanges({
|
||||
workspaceId,
|
||||
environment,
|
||||
secretPath
|
||||
});
|
||||
setIsModalOpen(false);
|
||||
setCommitMessage("");
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Floating Panel */}
|
||||
{!isModalOpen && (
|
||||
<div className="fixed bottom-4 left-1/2 z-40 w-full max-w-3xl -translate-x-1/2 self-center rounded-lg border border-yellow/30 bg-mineshaft-800 shadow-2xl lg:left-auto lg:translate-x-0">
|
||||
<div className="flex items-center justify-between p-4">
|
||||
{/* Left Content */}
|
||||
<div className="flex-1">
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-2 w-2 rounded-full bg-yellow-500" />
|
||||
<span className="font-medium text-mineshaft-100">Pending Changes</span>
|
||||
<Badge variant="primary" className="text-xs">
|
||||
{totalChangesCount} Change{totalChangesCount !== 1 ? "s" : ""}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<p className="text-sm leading-5 text-mineshaft-400">
|
||||
Review pending changes and commit them to apply the updates.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Right Buttons */}
|
||||
<div className="ml-6 mt-0.5 flex items-center gap-3">
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => clearAllPendingChanges({ workspaceId, environment, secretPath })}
|
||||
isDisabled={totalChangesCount === 0}
|
||||
variant="outline_bg"
|
||||
className="px-4 hover:border-red/40 hover:bg-red/[0.1]"
|
||||
>
|
||||
Discard
|
||||
</Button>
|
||||
<Button
|
||||
variant="solid"
|
||||
leftIcon={<FontAwesomeIcon icon={faEye} />}
|
||||
onClick={() => setIsModalOpen(true)}
|
||||
isDisabled={totalChangesCount === 0}
|
||||
className="px-6"
|
||||
>
|
||||
Review Changes
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Commit Modal */}
|
||||
<Modal isOpen={isModalOpen} onOpenChange={setIsModalOpen}>
|
||||
<ModalContent
|
||||
title={
|
||||
<div className="flex items-center gap-2">
|
||||
<FontAwesomeIcon icon={faCodeCommit} className="text-mineshaft-400" />
|
||||
Commit Changes
|
||||
<Badge variant="primary" className="mt-[0.05rem]">
|
||||
{totalChangesCount} Change{totalChangesCount !== 1 ? "s" : ""}
|
||||
</Badge>
|
||||
</div>
|
||||
}
|
||||
subTitle={"Write a commit message and review the changes you're about to commit."}
|
||||
className="max-h-[90vh] max-w-[95%] md:max-w-7xl"
|
||||
>
|
||||
<div className="space-y-6">
|
||||
{/* Changes List */}
|
||||
<div className="space-y-6">
|
||||
<div className="max-h-[50vh] space-y-4 overflow-y-auto">
|
||||
{/* Folder Changes */}
|
||||
{pendingChanges.folders.length > 0 && (
|
||||
<div>
|
||||
<h4 className="mb-4 flex items-center gap-2 border-b border-mineshaft-700 pb-2 text-sm font-semibold text-mineshaft-200">
|
||||
<FontAwesomeIcon icon={faFolder} className="text-mineshaft-300" />
|
||||
Folders ({pendingChanges.folders.length})
|
||||
</h4>
|
||||
<div>
|
||||
{pendingChanges.folders.map((change) => (
|
||||
<ResourceChange
|
||||
key={change.id}
|
||||
change={change}
|
||||
environment={environment}
|
||||
workspaceId={workspaceId}
|
||||
secretPath={secretPath}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Secret Changes */}
|
||||
{pendingChanges.secrets.length > 0 && (
|
||||
<div>
|
||||
<h4 className="mb-4 flex items-center gap-2 border-b border-mineshaft-700 pb-2 text-sm font-semibold text-mineshaft-200">
|
||||
<FontAwesomeIcon icon={faKey} className="mr-1 text-mineshaft-300" />
|
||||
Secrets ({pendingChanges.secrets.length})
|
||||
</h4>
|
||||
<div>
|
||||
{pendingChanges.secrets.map((change) => (
|
||||
<ResourceChange
|
||||
key={change.id}
|
||||
change={change}
|
||||
environment={environment}
|
||||
workspaceId={workspaceId}
|
||||
secretPath={secretPath}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Commit Message */}
|
||||
<div>
|
||||
<label className="mb-2 block text-sm font-medium text-mineshaft-200">
|
||||
Commit Message <span className="text-red-400">*</span>
|
||||
</label>
|
||||
<Input
|
||||
value={commitMessage}
|
||||
onChange={(e) => setCommitMessage(e.target.value)}
|
||||
placeholder="Describe your changes..."
|
||||
className="w-full"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex justify-end gap-3">
|
||||
<Button
|
||||
variant="plain"
|
||||
colorSchema="secondary"
|
||||
onClick={() => setIsModalOpen(false)}
|
||||
isDisabled={isCommitting}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleCommit}
|
||||
isLoading={isCommitting}
|
||||
isDisabled={isCommitting || !commitMessage.trim()}
|
||||
leftIcon={<FontAwesomeIcon icon={faCodeCommit} />}
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
>
|
||||
{isCommitting ? "Committing..." : "Commit Changes"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { CommitForm } from "./CommitForm";
|
||||
+31
-2
@@ -12,9 +12,15 @@ import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
|
||||
import { getKeyValue } from "@app/helpers/parseEnvVar";
|
||||
import { useCreateSecretV3, useCreateWsTag, useGetWsTags } from "@app/hooks/api";
|
||||
import { PendingAction } from "@app/hooks/api/secretFolders/types";
|
||||
import { SecretType } from "@app/hooks/api/types";
|
||||
|
||||
import { PopUpNames, usePopUpAction } from "../../SecretMainPage.store";
|
||||
import {
|
||||
PendingSecretCreate,
|
||||
PopUpNames,
|
||||
useBatchModeActions,
|
||||
usePopUpAction
|
||||
} from "../../SecretMainPage.store";
|
||||
|
||||
const typeSchema = z.object({
|
||||
key: z.string().trim().min(1, { message: "Secret key is required" }),
|
||||
@@ -31,6 +37,7 @@ type Props = {
|
||||
// modal props
|
||||
autoCapitalize?: boolean;
|
||||
isProtectedBranch?: boolean;
|
||||
isBatchMode?: boolean;
|
||||
};
|
||||
|
||||
export const CreateSecretForm = ({
|
||||
@@ -38,7 +45,8 @@ export const CreateSecretForm = ({
|
||||
workspaceId,
|
||||
secretPath = "/",
|
||||
autoCapitalize = true,
|
||||
isProtectedBranch = false
|
||||
isProtectedBranch = false,
|
||||
isBatchMode = false
|
||||
}: Props) => {
|
||||
const {
|
||||
register,
|
||||
@@ -53,6 +61,7 @@ export const CreateSecretForm = ({
|
||||
|
||||
const { mutateAsync: createSecretV3 } = useCreateSecretV3();
|
||||
const createWsTag = useCreateWsTag();
|
||||
const { addPendingChange } = useBatchModeActions();
|
||||
|
||||
const { permission } = useProjectPermission();
|
||||
const canReadTags = permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.Tags);
|
||||
@@ -85,6 +94,26 @@ export const CreateSecretForm = ({
|
||||
|
||||
const handleFormSubmit = async ({ key, value, tags }: TFormSchema) => {
|
||||
try {
|
||||
if (isBatchMode) {
|
||||
const pendingSecretCreate: PendingSecretCreate = {
|
||||
id: key,
|
||||
type: PendingAction.Create,
|
||||
secretKey: key,
|
||||
secretValue: value || "",
|
||||
secretComment: "",
|
||||
tags: tags?.map((el) => ({ id: el.value, slug: el.label })),
|
||||
timestamp: Date.now(),
|
||||
resourceType: "secret"
|
||||
};
|
||||
addPendingChange(pendingSecretCreate, {
|
||||
workspaceId,
|
||||
environment,
|
||||
secretPath
|
||||
});
|
||||
closePopUp(PopUpNames.CreateSecretForm);
|
||||
reset();
|
||||
return;
|
||||
}
|
||||
await createSecretV3({
|
||||
environment,
|
||||
workspaceId,
|
||||
|
||||
+5
-9
@@ -1,10 +1,5 @@
|
||||
import { subject } from "@casl/ability";
|
||||
import {
|
||||
faClose,
|
||||
faFingerprint,
|
||||
faPencilSquare,
|
||||
faWarning
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { faEdit, faFingerprint, faTrash, faWarning } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
@@ -187,7 +182,7 @@ export const DynamicSecretListView = ({
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center space-x-4 border-l border-mineshaft-600 px-3 py-3">
|
||||
<div className="flex w-16 items-center justify-between border-l border-mineshaft-600 px-3 py-3">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionDynamicSecretActions.EditRootCredential}
|
||||
a={subject(ProjectPermissionSub.DynamicSecrets, {
|
||||
@@ -210,7 +205,7 @@ export const DynamicSecretListView = ({
|
||||
}}
|
||||
isDisabled={!isAllowed || isRevoking}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPencilSquare} size="lg" />
|
||||
<FontAwesomeIcon icon={faEdit} />
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
@@ -228,6 +223,7 @@ export const DynamicSecretListView = ({
|
||||
<IconButton
|
||||
ariaLabel="delete-dynamic-secret"
|
||||
variant="plain"
|
||||
colorSchema="danger"
|
||||
size="md"
|
||||
className="p-0 opacity-0 group-hover:opacity-100"
|
||||
onClick={(evt) => {
|
||||
@@ -236,7 +232,7 @@ export const DynamicSecretListView = ({
|
||||
}}
|
||||
isDisabled={!isAllowed || isRevoking}
|
||||
>
|
||||
<FontAwesomeIcon icon={faClose} size="lg" />
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
|
||||
+183
-51
@@ -1,7 +1,15 @@
|
||||
import { subject } from "@casl/ability";
|
||||
import { faClose, faFolder, faInfoCircle, faPencilSquare } from "@fortawesome/free-solid-svg-icons";
|
||||
import {
|
||||
faClose,
|
||||
faEdit,
|
||||
faFolder,
|
||||
faInfoCircle,
|
||||
faPencilSquare,
|
||||
faTrash
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useNavigate, useSearch } from "@tanstack/react-router";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
@@ -11,8 +19,15 @@ import { ROUTE_PATHS } from "@app/const/routes";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { useDeleteFolder, useUpdateFolder } from "@app/hooks/api";
|
||||
import { TSecretFolder } from "@app/hooks/api/secretFolders/types";
|
||||
import { PendingAction, TSecretFolder } from "@app/hooks/api/secretFolders/types";
|
||||
|
||||
import {
|
||||
PendingFolderCreate,
|
||||
PendingFolderDelete,
|
||||
PendingFolderUpdate,
|
||||
useBatchMode,
|
||||
useBatchModeActions
|
||||
} from "../../SecretMainPage.store";
|
||||
import { FolderForm } from "../ActionBar/FolderForm";
|
||||
|
||||
type Props = {
|
||||
@@ -42,10 +57,62 @@ export const FolderListView = ({
|
||||
|
||||
const { mutateAsync: updateFolder } = useUpdateFolder();
|
||||
const { mutateAsync: deleteFolder } = useDeleteFolder();
|
||||
const { isBatchMode } = useBatchMode();
|
||||
const { addPendingChange, removePendingChange } = useBatchModeActions();
|
||||
|
||||
const handleFolderUpdate = async (newFolderName: string, newFolderDescription: string | null) => {
|
||||
const handleFolderUpdate = async (
|
||||
newFolderName: string,
|
||||
newFolderDescription: string | null,
|
||||
oldFolderName?: string,
|
||||
oldFolderDescription?: string
|
||||
) => {
|
||||
try {
|
||||
const { id: folderId } = popUp.updateFolder.data as TSecretFolder;
|
||||
const updateFolderData = popUp.updateFolder.data;
|
||||
if (!updateFolderData) throw new Error("Update folder data is required");
|
||||
const { id: folderId, pendingAction, isPending } = updateFolderData as TSecretFolder;
|
||||
|
||||
if (isBatchMode) {
|
||||
const isEditingPendingCreation = isPending && pendingAction === PendingAction.Create;
|
||||
|
||||
if (isEditingPendingCreation) {
|
||||
const updatedCreate: PendingFolderCreate = {
|
||||
id: folderId,
|
||||
type: PendingAction.Create,
|
||||
folderName: newFolderName,
|
||||
description: newFolderDescription || undefined,
|
||||
parentPath: secretPath,
|
||||
timestamp: Date.now(),
|
||||
resourceType: "folder"
|
||||
};
|
||||
|
||||
addPendingChange(updatedCreate, {
|
||||
workspaceId,
|
||||
environment,
|
||||
secretPath
|
||||
});
|
||||
} else {
|
||||
const updateChange: PendingFolderUpdate = {
|
||||
id: folderId,
|
||||
type: PendingAction.Update,
|
||||
originalFolderName: oldFolderName || "",
|
||||
folderName: newFolderName,
|
||||
originalDescription: oldFolderDescription,
|
||||
description: newFolderDescription || undefined,
|
||||
timestamp: Date.now(),
|
||||
resourceType: "folder"
|
||||
};
|
||||
|
||||
addPendingChange(updateChange, {
|
||||
workspaceId,
|
||||
environment,
|
||||
secretPath
|
||||
});
|
||||
}
|
||||
|
||||
handlePopUpClose("updateFolder");
|
||||
return;
|
||||
}
|
||||
|
||||
await updateFolder({
|
||||
folderId,
|
||||
name: newFolderName,
|
||||
@@ -68,15 +135,45 @@ export const FolderListView = ({
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeletePending = (id: string) => {
|
||||
removePendingChange(id, "folder", {
|
||||
workspaceId,
|
||||
environment,
|
||||
secretPath
|
||||
});
|
||||
};
|
||||
|
||||
const handleFolderDelete = async () => {
|
||||
try {
|
||||
const { id: folderId } = popUp.deleteFolder.data as TSecretFolder;
|
||||
const folderData = popUp.deleteFolder?.data as TSecretFolder;
|
||||
|
||||
if (isBatchMode) {
|
||||
const pendingFolderDelete: PendingFolderDelete = {
|
||||
id: folderData.id,
|
||||
folderName: folderData.name,
|
||||
folderPath: secretPath,
|
||||
resourceType: "folder",
|
||||
type: PendingAction.Delete,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
addPendingChange(pendingFolderDelete, {
|
||||
workspaceId,
|
||||
environment,
|
||||
secretPath
|
||||
});
|
||||
|
||||
handlePopUpClose("deleteFolder");
|
||||
return;
|
||||
}
|
||||
|
||||
await deleteFolder({
|
||||
folderId,
|
||||
folderId: folderData.id,
|
||||
path: secretPath,
|
||||
environment,
|
||||
projectId: workspaceId
|
||||
});
|
||||
|
||||
handlePopUpClose("deleteFolder");
|
||||
createNotification({
|
||||
type: "success",
|
||||
@@ -91,7 +188,10 @@ export const FolderListView = ({
|
||||
}
|
||||
};
|
||||
|
||||
const handleFolderClick = (name: string) => {
|
||||
const handleFolderClick = (name: string, isPending?: boolean) => {
|
||||
if (isPending) {
|
||||
return;
|
||||
}
|
||||
const path = `${secretPathQueryparam === "/" ? "" : secretPathQueryparam}/${name}`;
|
||||
navigate({
|
||||
search: (el) => ({ ...el, secretPath: path })
|
||||
@@ -100,10 +200,16 @@ export const FolderListView = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
{folders.map(({ name, id, description }) => (
|
||||
{folders.map(({ name, id, description, pendingAction, isPending }) => (
|
||||
<div
|
||||
key={id}
|
||||
className="group flex cursor-pointer border-b border-mineshaft-600 hover:bg-mineshaft-700"
|
||||
className={twMerge(
|
||||
"group flex cursor-pointer border-b border-mineshaft-600 hover:bg-mineshaft-700",
|
||||
isPending && "bg-mineshaft-700/60",
|
||||
pendingAction === PendingAction.Delete && "border-l-2 border-l-red-600/75",
|
||||
pendingAction === PendingAction.Update && "border-l-2 border-l-yellow-600/75",
|
||||
pendingAction === PendingAction.Create && "border-l-2 border-l-green-600/75"
|
||||
)}
|
||||
>
|
||||
<div className="flex w-11 items-center px-5 py-3 text-yellow-700">
|
||||
<FontAwesomeIcon icon={faFolder} />
|
||||
@@ -113,9 +219,9 @@ export const FolderListView = ({
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(evt) => {
|
||||
if (evt.key === "Enter") handleFolderClick(name);
|
||||
if (evt.key === "Enter") handleFolderClick(name, isPending);
|
||||
}}
|
||||
onClick={() => handleFolderClick(name)}
|
||||
onClick={() => handleFolderClick(name, isPending)}
|
||||
>
|
||||
{name}
|
||||
{description && (
|
||||
@@ -128,46 +234,72 @@ export const FolderListView = ({
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center space-x-4 border-l border-mineshaft-600 px-3 py-3">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={subject(ProjectPermissionSub.SecretFolders, { environment, secretPath })}
|
||||
renderTooltip
|
||||
allowedLabel="Edit"
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
ariaLabel="edit-folder"
|
||||
variant="plain"
|
||||
size="sm"
|
||||
className="p-0 opacity-0 group-hover:opacity-100"
|
||||
onClick={() => handlePopUpOpen("updateFolder", { id, name, description })}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPencilSquare} size="lg" />
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={subject(ProjectPermissionSub.SecretFolders, { environment, secretPath })}
|
||||
renderTooltip
|
||||
allowedLabel="Delete"
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
ariaLabel="delete-folder"
|
||||
variant="plain"
|
||||
size="md"
|
||||
className="p-0 opacity-0 group-hover:opacity-100"
|
||||
onClick={() => handlePopUpOpen("deleteFolder", { id, name })}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
<FontAwesomeIcon icon={faClose} size="lg" />
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
{isPending ? (
|
||||
<div className="flex items-center space-x-4 border-l border-mineshaft-600 px-3 py-3">
|
||||
<IconButton
|
||||
ariaLabel="edit-folder"
|
||||
variant="plain"
|
||||
size="sm"
|
||||
className="p-0 opacity-0 group-hover:opacity-100"
|
||||
isDisabled
|
||||
onClick={() => {}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPencilSquare} size="lg" />
|
||||
</IconButton>
|
||||
|
||||
<IconButton
|
||||
ariaLabel="delete-folder"
|
||||
variant="plain"
|
||||
size="md"
|
||||
className="p-0 opacity-0 group-hover:opacity-100"
|
||||
onClick={() => handleDeletePending(id)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faClose} size="lg" />
|
||||
</IconButton>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex w-16 items-center justify-between border-l border-mineshaft-600 px-3 py-3">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={subject(ProjectPermissionSub.SecretFolders, { environment, secretPath })}
|
||||
renderTooltip
|
||||
allowedLabel="Edit"
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
ariaLabel="edit-folder"
|
||||
variant="plain"
|
||||
size="sm"
|
||||
className="p-0 opacity-0 group-hover:opacity-100"
|
||||
onClick={() => handlePopUpOpen("updateFolder", { id, name, description })}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
<FontAwesomeIcon icon={faEdit} />
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={subject(ProjectPermissionSub.SecretFolders, { environment, secretPath })}
|
||||
renderTooltip
|
||||
allowedLabel="Delete"
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
ariaLabel="delete-folder"
|
||||
variant="plain"
|
||||
colorSchema="danger"
|
||||
size="md"
|
||||
className="p-0 opacity-0 group-hover:opacity-100"
|
||||
onClick={() => handlePopUpOpen("deleteFolder", { id, name })}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<Modal
|
||||
|
||||
+3
-3
@@ -3,13 +3,13 @@ import { subject } from "@casl/ability";
|
||||
import { useSortable } from "@dnd-kit/sortable";
|
||||
import {
|
||||
faCalendarCheck,
|
||||
faClose,
|
||||
faFileImport,
|
||||
faFolder,
|
||||
faInfoCircle,
|
||||
faKey,
|
||||
faRotate,
|
||||
faSearch,
|
||||
faTrash,
|
||||
faUpDown,
|
||||
faWarning,
|
||||
faXmark
|
||||
@@ -237,7 +237,7 @@ export const SecretImportItem = ({
|
||||
</ProjectPermissionCan>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center space-x-4 border-l border-mineshaft-600 px-4 py-2">
|
||||
<div className="flex w-16 items-center justify-between border-l border-mineshaft-600 py-3.5 pl-4 pr-3">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={subject(ProjectPermissionSub.SecretImports, {
|
||||
@@ -281,7 +281,7 @@ export const SecretImportItem = ({
|
||||
}}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
<FontAwesomeIcon icon={faClose} size="lg" />
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
|
||||
+8
-3
@@ -52,6 +52,7 @@ import {
|
||||
useWorkspace
|
||||
} from "@app/context";
|
||||
import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types";
|
||||
import { getProjectBaseURL } from "@app/helpers/project";
|
||||
import { usePopUp, useToggle } from "@app/hooks";
|
||||
import { useGetSecretVersion } from "@app/hooks/api";
|
||||
import { ActorType } from "@app/hooks/api/auditLogs/enums";
|
||||
@@ -980,7 +981,9 @@ export const SecretDetailSidebar = ({
|
||||
className="z-[100] capitalize"
|
||||
>
|
||||
<Link
|
||||
to="/projects/$projectId/members/$membershipId"
|
||||
to={
|
||||
`${getProjectBaseURL(currentWorkspace.type)}/members/$membershipId` as const
|
||||
}
|
||||
params={{
|
||||
projectId: currentWorkspace.id,
|
||||
membershipId: user.membershipId
|
||||
@@ -1011,7 +1014,9 @@ export const SecretDetailSidebar = ({
|
||||
className="z-[100]"
|
||||
>
|
||||
<Link
|
||||
to="/projects/$projectId/identities/$identityId"
|
||||
to={
|
||||
`${getProjectBaseURL(currentWorkspace.type)}/identities/$identityId` as const
|
||||
}
|
||||
params={{
|
||||
projectId: currentWorkspace.id,
|
||||
identityId: identity.id
|
||||
@@ -1078,7 +1083,7 @@ export const SecretDetailSidebar = ({
|
||||
isDisabled={isSubmitting || !isDirty || !isAllowed}
|
||||
isLoading={isSubmitting}
|
||||
>
|
||||
Save Changes
|
||||
Apply Changes
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
|
||||
+491
-276
@@ -38,7 +38,7 @@ import { WsTag } from "@app/hooks/api/types";
|
||||
import { subject } from "@casl/ability";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { memo, useEffect } from "react";
|
||||
import { memo, useCallback, useEffect, useRef } from "react";
|
||||
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import {
|
||||
@@ -50,6 +50,7 @@ import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionCo
|
||||
import { hasSecretReadValueOrDescribePermission } from "@app/lib/fn/permission";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faEyeSlash, faKey, faRotate } from "@fortawesome/free-solid-svg-icons";
|
||||
import { PendingAction } from "@app/hooks/api/secretFolders/types";
|
||||
import {
|
||||
FontAwesomeSpriteName,
|
||||
formSchema,
|
||||
@@ -57,6 +58,7 @@ import {
|
||||
TFormSchema
|
||||
} from "./SecretListView.utils";
|
||||
import { CollapsibleSecretImports } from "./CollapsibleSecretImports";
|
||||
import { useBatchModeActions } from "../../SecretMainPage.store";
|
||||
|
||||
export const HIDDEN_SECRET_VALUE = "******";
|
||||
export const HIDDEN_SECRET_VALUE_API_MASK = "<hidden-by-infisical>";
|
||||
@@ -86,6 +88,8 @@ type Props = {
|
||||
isImported: boolean;
|
||||
}[];
|
||||
}[];
|
||||
isPending?: boolean;
|
||||
pendingAction?: PendingAction;
|
||||
};
|
||||
|
||||
export const SecretItem = memo(
|
||||
@@ -102,7 +106,9 @@ export const SecretItem = memo(
|
||||
environment,
|
||||
secretPath,
|
||||
handleSecretShare,
|
||||
importedBy
|
||||
importedBy,
|
||||
isPending,
|
||||
pendingAction
|
||||
}: Props) => {
|
||||
const { handlePopUpOpen, handlePopUpToggle, handlePopUpClose, popUp } = usePopUp([
|
||||
"editSecret"
|
||||
@@ -110,6 +116,18 @@ export const SecretItem = memo(
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { permission } = useProjectPermission();
|
||||
const { isRotatedSecret } = secret;
|
||||
const { removePendingChange } = useBatchModeActions();
|
||||
|
||||
const autoSaveTimeoutRef = useRef<NodeJS.Timeout>();
|
||||
const isAutoSavingRef = useRef(false);
|
||||
|
||||
const handleDeletePending = (pendingSecret: SecretV3RawSanitized) => {
|
||||
removePendingChange(pendingSecret.id, "secret", {
|
||||
workspaceId: currentWorkspace.id,
|
||||
environment,
|
||||
secretPath
|
||||
});
|
||||
};
|
||||
|
||||
const canEditSecretValue = permission.can(
|
||||
ProjectPermissionSecretActions.Edit,
|
||||
@@ -151,7 +169,6 @@ export const SecretItem = memo(
|
||||
});
|
||||
|
||||
const secretName = watch("key");
|
||||
|
||||
const overrideAction = watch("overrideAction");
|
||||
const hasComment = Boolean(watch("comment"));
|
||||
|
||||
@@ -167,6 +184,56 @@ export const SecretItem = memo(
|
||||
name: "tags"
|
||||
});
|
||||
|
||||
const isOverriden =
|
||||
overrideAction === SecretActionType.Created || overrideAction === SecretActionType.Modified;
|
||||
const hasTagsApplied = Boolean(fields.length);
|
||||
|
||||
const autoSaveChanges = useCallback(
|
||||
async (data: TFormSchema) => {
|
||||
if (isAutoSavingRef.current) return;
|
||||
if (
|
||||
data.overrideAction === SecretActionType.Created ||
|
||||
data.overrideAction === SecretActionType.Modified
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
isAutoSavingRef.current = true;
|
||||
try {
|
||||
await onSaveSecret(secret, { ...secret, ...data }, () => {
|
||||
reset();
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Auto-save failed:", error);
|
||||
} finally {
|
||||
isAutoSavingRef.current = false;
|
||||
}
|
||||
},
|
||||
[secret, onSaveSecret, importedBy, reset]
|
||||
);
|
||||
|
||||
const formValues = watch();
|
||||
|
||||
useEffect(() => {
|
||||
if (autoSaveTimeoutRef.current) {
|
||||
clearTimeout(autoSaveTimeoutRef.current);
|
||||
}
|
||||
|
||||
if (isDirty && !isSubmitting && !isAutoSavingRef.current) {
|
||||
const debounceTime = 600;
|
||||
|
||||
autoSaveTimeoutRef.current = setTimeout(() => {
|
||||
autoSaveChanges(formValues);
|
||||
}, debounceTime);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (autoSaveTimeoutRef.current) {
|
||||
clearTimeout(autoSaveTimeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, [formValues, isDirty, isSubmitting, autoSaveChanges, isPending]);
|
||||
|
||||
const isReadOnly =
|
||||
hasSecretReadValueOrDescribePermission(
|
||||
permission,
|
||||
@@ -188,6 +255,9 @@ export const SecretItem = memo(
|
||||
})
|
||||
);
|
||||
|
||||
const isReadOnlySecret =
|
||||
isReadOnly || isRotatedSecret || (isPending && pendingAction === PendingAction.Delete);
|
||||
|
||||
const { secretValueHidden } = secret;
|
||||
|
||||
const [isSecValueCopied, setIsSecValueCopied] = useToggle(false);
|
||||
@@ -199,10 +269,6 @@ export const SecretItem = memo(
|
||||
return () => clearTimeout(timer);
|
||||
}, [isSecValueCopied]);
|
||||
|
||||
const isOverriden =
|
||||
overrideAction === SecretActionType.Created || overrideAction === SecretActionType.Modified;
|
||||
const hasTagsApplied = Boolean(fields.length);
|
||||
|
||||
const handleOverrideClick = () => {
|
||||
if (isOverriden) {
|
||||
// override need not be flagged delete if it was never saved in server
|
||||
@@ -267,20 +333,27 @@ export const SecretItem = memo(
|
||||
setIsSecValueCopied.on();
|
||||
};
|
||||
|
||||
const isInAutoSaveMode = isDirty && !isSubmitting && !isOverriden;
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
||||
<div
|
||||
className={twMerge(
|
||||
"border-b border-mineshaft-600 bg-mineshaft-800 shadow-none hover:bg-mineshaft-700",
|
||||
isDirty && "border-primary-400/50",
|
||||
isRotatedSecret && "bg-mineshaft-700/60"
|
||||
isRotatedSecret && "bg-mineshaft-700/60",
|
||||
isPending && "bg-mineshaft-700/60",
|
||||
pendingAction === PendingAction.Delete && "border-l-2 border-l-red-600/75",
|
||||
pendingAction === PendingAction.Update && "border-l-2 border-l-yellow-600/75",
|
||||
pendingAction === PendingAction.Create && "border-l-2 border-l-green-600/75"
|
||||
)}
|
||||
>
|
||||
<div className="group flex">
|
||||
<div
|
||||
className={twMerge(
|
||||
"flex h-11 w-11 items-center justify-center px-4 py-3 text-mineshaft-300",
|
||||
isDirty && "text-primary"
|
||||
isDirty && "text-primary",
|
||||
isPending && "ml-[-2px]"
|
||||
)}
|
||||
>
|
||||
{secret.isRotatedSecret ? (
|
||||
@@ -364,7 +437,7 @@ export const SecretItem = memo(
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<InfisicalSecretInput
|
||||
isReadOnly={isReadOnly || isRotatedSecret}
|
||||
isReadOnly={isReadOnlySecret}
|
||||
key="secret-value"
|
||||
isVisible={isVisible && !secretValueHidden}
|
||||
canEditButNotView={secretValueHidden && !isOverriden}
|
||||
@@ -377,275 +450,160 @@ export const SecretItem = memo(
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
key="actions"
|
||||
className="flex h-full flex-shrink-0 self-start transition-all group-hover:gap-x-2"
|
||||
>
|
||||
<Tooltip content="Copy secret">
|
||||
<IconButton
|
||||
isDisabled={secret.secretValueHidden}
|
||||
ariaLabel="copy-value"
|
||||
variant="plain"
|
||||
size="sm"
|
||||
className="w-0 overflow-hidden p-0 group-hover:w-5"
|
||||
onClick={copyTokenToClipboard}
|
||||
>
|
||||
<FontAwesomeSymbol
|
||||
className="h-3.5 w-3"
|
||||
symbolName={
|
||||
isSecValueCopied
|
||||
? FontAwesomeSpriteName.Check
|
||||
: FontAwesomeSpriteName.ClipboardCopy
|
||||
}
|
||||
/>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={subject(ProjectPermissionSub.Secrets, {
|
||||
environment,
|
||||
secretPath,
|
||||
secretName,
|
||||
secretTags: selectedTagSlugs
|
||||
})}
|
||||
{pendingAction !== PendingAction.Create && pendingAction !== PendingAction.Delete && (
|
||||
<div
|
||||
key="actions"
|
||||
className="flex h-full flex-shrink-0 self-start transition-all group-hover:gap-x-2"
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Modal>
|
||||
<ModalTrigger asChild>
|
||||
<IconButton
|
||||
className="w-0 overflow-hidden p-0 group-hover:w-5"
|
||||
variant="plain"
|
||||
size="md"
|
||||
ariaLabel="reference-tree"
|
||||
isDisabled={!isAllowed || !hasSecretReference(secret?.value)}
|
||||
>
|
||||
<Tooltip
|
||||
content={
|
||||
hasSecretReference(secret?.value)
|
||||
? "Secret Reference Tree"
|
||||
: "Secret does not contain references"
|
||||
}
|
||||
>
|
||||
<FontAwesomeSymbol
|
||||
className="h-3.5 w-3.5"
|
||||
symbolName={FontAwesomeSpriteName.SecretReferenceTree}
|
||||
/>
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
</ModalTrigger>
|
||||
<ModalContent
|
||||
title="Secret Reference Details"
|
||||
subTitle="Visual breakdown of secrets referenced by this secret."
|
||||
onOpenAutoFocus={(e) => e.preventDefault()} // prevents secret input from displaying value on open
|
||||
>
|
||||
<SecretReferenceTree
|
||||
secretPath={secretPath}
|
||||
environment={environment}
|
||||
secretKey={secret?.key}
|
||||
/>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<DropdownMenu>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={subject(ProjectPermissionSub.Secrets, {
|
||||
environment,
|
||||
secretPath,
|
||||
secretName,
|
||||
secretTags: selectedTagSlugs
|
||||
})}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuTrigger asChild disabled={!isAllowed}>
|
||||
<IconButton
|
||||
ariaLabel="tags"
|
||||
variant="plain"
|
||||
size="sm"
|
||||
className={twMerge(
|
||||
"w-0 overflow-hidden p-0 group-hover:w-5 data-[state=open]:w-5",
|
||||
hasTagsApplied && "w-5 text-primary"
|
||||
)}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
<Tooltip content="Tags">
|
||||
<FontAwesomeSymbol
|
||||
className="h-3.5 w-3.5"
|
||||
symbolName={FontAwesomeSpriteName.Tags}
|
||||
/>
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
</DropdownMenuTrigger>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>Add tags to this secret</DropdownMenuLabel>
|
||||
{tags.map((tag) => {
|
||||
const { id: tagId, slug, color } = tag;
|
||||
|
||||
const isTagSelected = selectedTagsGroupById?.[tagId];
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleTagSelect(tag)}
|
||||
key={`${secret.id}-${tagId}`}
|
||||
icon={
|
||||
isTagSelected && (
|
||||
<FontAwesomeSymbol
|
||||
symbolName={FontAwesomeSpriteName.CheckedCircle}
|
||||
className="h-3 w-3"
|
||||
/>
|
||||
)
|
||||
}
|
||||
iconPos="right"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<div
|
||||
className="mr-2 h-2 w-2 rounded-full"
|
||||
style={{ background: color || "#bec2c8" }}
|
||||
/>
|
||||
{slug}
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
<DropdownMenuItem className="px-1.5" asChild>
|
||||
<Button
|
||||
size="xs"
|
||||
className="w-full"
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
leftIcon={
|
||||
<FontAwesomeSymbol
|
||||
symbolName={FontAwesomeSpriteName.Tags}
|
||||
className="h-3 w-3"
|
||||
/>
|
||||
}
|
||||
onClick={onCreateTag}
|
||||
>
|
||||
Create a tag
|
||||
</Button>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={subject(ProjectPermissionSub.Secrets, {
|
||||
environment,
|
||||
secretPath,
|
||||
secretName,
|
||||
secretTags: selectedTagSlugs
|
||||
})}
|
||||
renderTooltip
|
||||
allowedLabel="Override"
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Tooltip content="Copy secret">
|
||||
<IconButton
|
||||
ariaLabel="override-value"
|
||||
isDisabled={!isAllowed}
|
||||
isDisabled={secret.secretValueHidden}
|
||||
ariaLabel="copy-value"
|
||||
variant="plain"
|
||||
size="sm"
|
||||
onClick={handleOverrideClick}
|
||||
className={twMerge(
|
||||
"w-0 overflow-hidden p-0 group-hover:w-5",
|
||||
isOverriden && "w-5 text-primary"
|
||||
)}
|
||||
className="w-0 overflow-hidden p-0 group-hover:w-5"
|
||||
onClick={copyTokenToClipboard}
|
||||
>
|
||||
<FontAwesomeSymbol
|
||||
symbolName={FontAwesomeSpriteName.Override}
|
||||
className="h-3.5 w-3.5"
|
||||
/>
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<Popover>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={subject(ProjectPermissionSub.Secrets, {
|
||||
environment,
|
||||
secretPath,
|
||||
secretName,
|
||||
secretTags: selectedTagSlugs
|
||||
})}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<PopoverTrigger asChild disabled={!isAllowed}>
|
||||
<IconButton
|
||||
className={twMerge(
|
||||
"w-0 overflow-hidden p-0 group-hover:w-5",
|
||||
hasComment && "w-5 text-primary"
|
||||
)}
|
||||
variant="plain"
|
||||
size="md"
|
||||
ariaLabel="add-comment"
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
<Tooltip content="Comment">
|
||||
<FontAwesomeSymbol
|
||||
className="h-3.5 w-3.5"
|
||||
symbolName={FontAwesomeSpriteName.Comment}
|
||||
/>
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
</PopoverTrigger>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<IconButton
|
||||
isDisabled={secret.secretValueHidden || !currentWorkspace.secretSharing}
|
||||
className="w-0 overflow-hidden p-0 group-hover:w-5"
|
||||
variant="plain"
|
||||
size="md"
|
||||
ariaLabel="share-secret"
|
||||
onClick={handleSecretShare}
|
||||
>
|
||||
<Tooltip content="Share Secret">
|
||||
<FontAwesomeSymbol
|
||||
className="h-3.5 w-3.5"
|
||||
symbolName={FontAwesomeSpriteName.ShareSecret}
|
||||
/>
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
<PopoverContent
|
||||
className="w-auto border border-mineshaft-600 bg-mineshaft-800 p-2 drop-shadow-2xl"
|
||||
sticky="always"
|
||||
>
|
||||
<FormControl label="Comment" className="mb-0">
|
||||
<TextArea
|
||||
className="border border-mineshaft-600 text-sm"
|
||||
rows={8}
|
||||
cols={30}
|
||||
{...register("comment")}
|
||||
/>
|
||||
</FormControl>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
<AnimatePresence mode="wait">
|
||||
{!isDirty ? (
|
||||
<motion.div
|
||||
key="options"
|
||||
className="flex h-10 flex-shrink-0 items-center space-x-4 px-[0.64rem]"
|
||||
initial={{ x: 0, opacity: 0 }}
|
||||
animate={{ x: 0, opacity: 1 }}
|
||||
exit={{ x: 10, opacity: 0 }}
|
||||
>
|
||||
<Tooltip content="More">
|
||||
<IconButton
|
||||
ariaLabel="more"
|
||||
variant="plain"
|
||||
size="md"
|
||||
className="h-5 w-4 p-0 opacity-0 group-hover:opacity-100"
|
||||
onClick={() => onDetailViewSecret(secret)}
|
||||
>
|
||||
<FontAwesomeSymbol
|
||||
symbolName={FontAwesomeSpriteName.More}
|
||||
className="h-5 w-4"
|
||||
className="h-3.5 w-3"
|
||||
symbolName={
|
||||
isSecValueCopied
|
||||
? FontAwesomeSpriteName.Check
|
||||
: FontAwesomeSpriteName.ClipboardCopy
|
||||
}
|
||||
/>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={subject(ProjectPermissionSub.Secrets, {
|
||||
environment,
|
||||
secretPath,
|
||||
secretName,
|
||||
secretTags: selectedTagSlugs
|
||||
})}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Modal>
|
||||
<ModalTrigger asChild>
|
||||
<IconButton
|
||||
className="w-0 overflow-hidden p-0 group-hover:w-5"
|
||||
variant="plain"
|
||||
size="md"
|
||||
ariaLabel="reference-tree"
|
||||
isDisabled={!isAllowed || !hasSecretReference(secret?.value)}
|
||||
>
|
||||
<Tooltip
|
||||
content={
|
||||
hasSecretReference(secret?.value)
|
||||
? "Secret Reference Tree"
|
||||
: "Secret does not contain references"
|
||||
}
|
||||
>
|
||||
<FontAwesomeSymbol
|
||||
className="h-3.5 w-3.5"
|
||||
symbolName={FontAwesomeSpriteName.SecretReferenceTree}
|
||||
/>
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
</ModalTrigger>
|
||||
<ModalContent
|
||||
title="Secret Reference Details"
|
||||
subTitle="Visual breakdown of secrets referenced by this secret."
|
||||
onOpenAutoFocus={(e) => e.preventDefault()}
|
||||
>
|
||||
<SecretReferenceTree
|
||||
secretPath={secretPath}
|
||||
environment={environment}
|
||||
secretKey={secret?.key}
|
||||
/>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<DropdownMenu>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={subject(ProjectPermissionSub.Secrets, {
|
||||
environment,
|
||||
secretPath,
|
||||
secretName,
|
||||
secretTags: selectedTagSlugs
|
||||
})}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuTrigger asChild disabled={!isAllowed}>
|
||||
<IconButton
|
||||
ariaLabel="tags"
|
||||
variant="plain"
|
||||
size="sm"
|
||||
className={twMerge(
|
||||
"w-0 overflow-hidden p-0 group-hover:w-5 data-[state=open]:w-5",
|
||||
hasTagsApplied && "w-5 text-primary"
|
||||
)}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
<Tooltip content="Tags">
|
||||
<FontAwesomeSymbol
|
||||
className="h-3.5 w-3.5"
|
||||
symbolName={FontAwesomeSpriteName.Tags}
|
||||
/>
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
</DropdownMenuTrigger>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>Add tags to this secret</DropdownMenuLabel>
|
||||
{tags.map((tag) => {
|
||||
const { id: tagId, slug, color } = tag;
|
||||
|
||||
const isTagSelected = selectedTagsGroupById?.[tagId];
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleTagSelect(tag)}
|
||||
key={`${secret.id}-${tagId}`}
|
||||
icon={
|
||||
isTagSelected && (
|
||||
<FontAwesomeSymbol
|
||||
symbolName={FontAwesomeSpriteName.CheckedCircle}
|
||||
className="h-3 w-3"
|
||||
/>
|
||||
)
|
||||
}
|
||||
iconPos="right"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<div
|
||||
className="mr-2 h-2 w-2 rounded-full"
|
||||
style={{ background: color || "#bec2c8" }}
|
||||
/>
|
||||
{slug}
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
<DropdownMenuItem className="px-1.5" asChild>
|
||||
<Button
|
||||
size="xs"
|
||||
className="w-full"
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
leftIcon={
|
||||
<FontAwesomeSymbol
|
||||
symbolName={FontAwesomeSpriteName.Tags}
|
||||
className="h-3 w-3"
|
||||
/>
|
||||
}
|
||||
onClick={onCreateTag}
|
||||
>
|
||||
Create a tag
|
||||
</Button>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={subject(ProjectPermissionSub.Secrets, {
|
||||
environment,
|
||||
secretPath,
|
||||
@@ -653,30 +611,287 @@ export const SecretItem = memo(
|
||||
secretTags: selectedTagSlugs
|
||||
})}
|
||||
renderTooltip
|
||||
allowedLabel={isRotatedSecret ? "Cannot Delete Rotated Secret" : "Delete"} // just using label for isRotatedSecret, disabled below
|
||||
allowedLabel="Override"
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
ariaLabel="override-value"
|
||||
isDisabled={!isAllowed}
|
||||
variant="plain"
|
||||
size="sm"
|
||||
onClick={handleOverrideClick}
|
||||
className={twMerge(
|
||||
"w-0 overflow-hidden p-0 group-hover:w-5",
|
||||
isOverriden && "w-5 text-primary"
|
||||
)}
|
||||
>
|
||||
<FontAwesomeSymbol
|
||||
symbolName={FontAwesomeSpriteName.Override}
|
||||
className="h-3.5 w-3.5"
|
||||
/>
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<Popover>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={subject(ProjectPermissionSub.Secrets, {
|
||||
environment,
|
||||
secretPath,
|
||||
secretName,
|
||||
secretTags: selectedTagSlugs
|
||||
})}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<PopoverTrigger asChild disabled={!isAllowed}>
|
||||
<IconButton
|
||||
className={twMerge(
|
||||
"w-0 overflow-hidden p-0 group-hover:w-5",
|
||||
hasComment && "w-5 text-primary"
|
||||
)}
|
||||
variant="plain"
|
||||
size="md"
|
||||
ariaLabel="add-comment"
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
<Tooltip content="Comment">
|
||||
<FontAwesomeSymbol
|
||||
className="h-3.5 w-3.5"
|
||||
symbolName={FontAwesomeSpriteName.Comment}
|
||||
/>
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
</PopoverTrigger>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<IconButton
|
||||
isDisabled={secret.secretValueHidden || !currentWorkspace.secretSharing}
|
||||
className="w-0 overflow-hidden p-0 group-hover:w-5"
|
||||
variant="plain"
|
||||
size="md"
|
||||
ariaLabel="share-secret"
|
||||
onClick={handleSecretShare}
|
||||
>
|
||||
<Tooltip content="Share Secret">
|
||||
<FontAwesomeSymbol
|
||||
className="h-3.5 w-3.5"
|
||||
symbolName={FontAwesomeSpriteName.ShareSecret}
|
||||
/>
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
<PopoverContent
|
||||
className="w-auto border border-mineshaft-600 bg-mineshaft-800 p-2 drop-shadow-2xl"
|
||||
sticky="always"
|
||||
>
|
||||
<FormControl label="Comment" className="mb-0">
|
||||
<TextArea
|
||||
className="border border-mineshaft-600 text-sm"
|
||||
rows={8}
|
||||
cols={30}
|
||||
{...register("comment")}
|
||||
/>
|
||||
</FormControl>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
)}
|
||||
{pendingAction === PendingAction.Create && (
|
||||
<div
|
||||
key="actions"
|
||||
className="flex h-full flex-shrink-0 self-start transition-all group-hover:gap-x-2"
|
||||
>
|
||||
<DropdownMenu>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={subject(ProjectPermissionSub.Secrets, {
|
||||
environment,
|
||||
secretPath,
|
||||
secretName,
|
||||
secretTags: selectedTagSlugs
|
||||
})}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuTrigger asChild disabled={!isAllowed}>
|
||||
<IconButton
|
||||
ariaLabel="tags"
|
||||
variant="plain"
|
||||
size="sm"
|
||||
className={twMerge(
|
||||
"w-0 overflow-hidden p-0 group-hover:w-5 data-[state=open]:w-5",
|
||||
hasTagsApplied && "w-5 text-primary"
|
||||
)}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
<Tooltip content="Tags">
|
||||
<FontAwesomeSymbol
|
||||
className="h-3.5 w-3.5"
|
||||
symbolName={FontAwesomeSpriteName.Tags}
|
||||
/>
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
</DropdownMenuTrigger>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>Add tags to this secret</DropdownMenuLabel>
|
||||
{tags.map((tag) => {
|
||||
const { id: tagId, slug, color } = tag;
|
||||
|
||||
const isTagSelected = selectedTagsGroupById?.[tagId];
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleTagSelect(tag)}
|
||||
key={`${secret.id}-${tagId}`}
|
||||
icon={
|
||||
isTagSelected && (
|
||||
<FontAwesomeSymbol
|
||||
symbolName={FontAwesomeSpriteName.CheckedCircle}
|
||||
className="h-3 w-3"
|
||||
/>
|
||||
)
|
||||
}
|
||||
iconPos="right"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<div
|
||||
className="mr-2 h-2 w-2 rounded-full"
|
||||
style={{ background: color || "#bec2c8" }}
|
||||
/>
|
||||
{slug}
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
<DropdownMenuItem className="px-1.5" asChild>
|
||||
<Button
|
||||
size="xs"
|
||||
className="w-full"
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
leftIcon={
|
||||
<FontAwesomeSymbol
|
||||
symbolName={FontAwesomeSpriteName.Tags}
|
||||
className="h-3 w-3"
|
||||
/>
|
||||
}
|
||||
onClick={onCreateTag}
|
||||
>
|
||||
Create a tag
|
||||
</Button>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<AnimatePresence mode="wait">
|
||||
{isInAutoSaveMode ? (
|
||||
<motion.div
|
||||
key="auto-save-mode"
|
||||
className="flex w-[63px] flex-shrink-0 items-center justify-between px-3"
|
||||
initial={{ x: -10, opacity: 0 }}
|
||||
animate={{ x: 0, opacity: 1 }}
|
||||
exit={{ x: -10, opacity: 0 }}
|
||||
>
|
||||
<div className="h-10 w-12" />
|
||||
</motion.div>
|
||||
) : !isDirty ? (
|
||||
isPending ? (
|
||||
<motion.div
|
||||
key="options"
|
||||
className="flex w-[63px] flex-shrink-0 items-center justify-between px-3"
|
||||
initial={{ x: 0, opacity: 0 }}
|
||||
animate={{ x: 0, opacity: 1 }}
|
||||
exit={{ x: 10, opacity: 0 }}
|
||||
>
|
||||
<Tooltip content="More">
|
||||
<IconButton
|
||||
ariaLabel="more"
|
||||
variant="plain"
|
||||
size="md"
|
||||
className="h-5 w-4 p-0 opacity-0 group-hover:opacity-100"
|
||||
onClick={() => onDetailViewSecret(secret)}
|
||||
isDisabled={pendingAction !== "update"}
|
||||
>
|
||||
<FontAwesomeSymbol
|
||||
symbolName={FontAwesomeSpriteName.More}
|
||||
className="h-5 w-4"
|
||||
/>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip content="Discard">
|
||||
<IconButton
|
||||
ariaLabel="delete-value"
|
||||
variant="plain"
|
||||
colorSchema="danger"
|
||||
size="md"
|
||||
className="p-0 opacity-0 group-hover:opacity-100"
|
||||
onClick={() => onDeleteSecret(secret)}
|
||||
isDisabled={!isAllowed || isRotatedSecret}
|
||||
onClick={() => handleDeletePending(secret)}
|
||||
>
|
||||
<FontAwesomeSymbol
|
||||
symbolName={FontAwesomeSpriteName.Close}
|
||||
className="h-5 w-4"
|
||||
/>
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</motion.div>
|
||||
</Tooltip>
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div
|
||||
key="options"
|
||||
className="flex w-[63px] flex-shrink-0 items-center justify-between px-3"
|
||||
initial={{ x: 0, opacity: 0 }}
|
||||
animate={{ x: 0, opacity: 1 }}
|
||||
exit={{ x: 10, opacity: 0 }}
|
||||
>
|
||||
<Tooltip content="More">
|
||||
<IconButton
|
||||
ariaLabel="more"
|
||||
variant="plain"
|
||||
size="md"
|
||||
className="h-5 w-4 p-0 opacity-0 group-hover:opacity-100"
|
||||
onClick={() => onDetailViewSecret(secret)}
|
||||
>
|
||||
<FontAwesomeSymbol
|
||||
symbolName={FontAwesomeSpriteName.More}
|
||||
className="h-5 w-4"
|
||||
/>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={subject(ProjectPermissionSub.Secrets, {
|
||||
environment,
|
||||
secretPath,
|
||||
secretName,
|
||||
secretTags: selectedTagSlugs
|
||||
})}
|
||||
renderTooltip
|
||||
allowedLabel={isRotatedSecret ? "Cannot Delete Rotated Secret" : "Delete"}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
ariaLabel="delete-value"
|
||||
variant="plain"
|
||||
colorSchema="danger"
|
||||
size="md"
|
||||
className="p-0 opacity-0 group-hover:opacity-100"
|
||||
onClick={() => onDeleteSecret(secret)}
|
||||
isDisabled={!isAllowed || isRotatedSecret}
|
||||
>
|
||||
<FontAwesomeSymbol
|
||||
symbolName={FontAwesomeSpriteName.Trash}
|
||||
className="h-4 w-3"
|
||||
/>
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</motion.div>
|
||||
)
|
||||
) : (
|
||||
<motion.div
|
||||
key="options-save"
|
||||
className="flex h-10 flex-shrink-0 items-center space-x-4 px-3"
|
||||
className="flex w-[63px] flex-shrink-0 items-center justify-between px-3"
|
||||
initial={{ x: -10, opacity: 0 }}
|
||||
animate={{ x: 0, opacity: 1 }}
|
||||
exit={{ x: -10, opacity: 0 }}
|
||||
|
||||
+188
-15
@@ -11,13 +11,24 @@ import { dashboardKeys } from "@app/hooks/api/dashboard/queries";
|
||||
import { UsedBySecretSyncs } from "@app/hooks/api/dashboard/types";
|
||||
import { commitKeys } from "@app/hooks/api/folderCommits/queries";
|
||||
import { secretApprovalRequestKeys } from "@app/hooks/api/secretApprovalRequest/queries";
|
||||
import { PendingAction } from "@app/hooks/api/secretFolders/types";
|
||||
import { secretKeys } from "@app/hooks/api/secrets/queries";
|
||||
import { SecretType, SecretV3RawSanitized } from "@app/hooks/api/secrets/types";
|
||||
import { secretSnapshotKeys } from "@app/hooks/api/secretSnapshots/queries";
|
||||
import { WsTag } from "@app/hooks/api/types";
|
||||
import { useNavigationBlocker } from "@app/hooks/useNavigationBlocker";
|
||||
import { AddShareSecretModal } from "@app/pages/organization/SecretSharingPage/components/ShareSecret/AddShareSecretModal";
|
||||
|
||||
import { useSelectedSecretActions, useSelectedSecrets } from "../../SecretMainPage.store";
|
||||
import {
|
||||
PendingSecretChange,
|
||||
PendingSecretCreate,
|
||||
PendingSecretDelete,
|
||||
PendingSecretUpdate,
|
||||
useBatchMode,
|
||||
useBatchModeActions,
|
||||
useSelectedSecretActions,
|
||||
useSelectedSecrets
|
||||
} from "../../SecretMainPage.store";
|
||||
import { CollapsibleSecretImports } from "./CollapsibleSecretImports";
|
||||
import { SecretDetailSidebar } from "./SecretDetailSidebar";
|
||||
import { HIDDEN_SECRET_VALUE, HIDDEN_SECRET_VALUE_API_MASK, SecretItem } from "./SecretItem";
|
||||
@@ -63,22 +74,29 @@ export const SecretListView = ({
|
||||
|
||||
// strip of side effect queries
|
||||
const { mutateAsync: createSecretV3 } = useCreateSecretV3({
|
||||
options: {
|
||||
onSuccess: undefined
|
||||
}
|
||||
options: { onSuccess: undefined }
|
||||
});
|
||||
const { mutateAsync: updateSecretV3 } = useUpdateSecretV3({
|
||||
options: {
|
||||
onSuccess: undefined
|
||||
}
|
||||
options: { onSuccess: undefined }
|
||||
});
|
||||
const { mutateAsync: deleteSecretV3 } = useDeleteSecretV3({
|
||||
options: {
|
||||
onSuccess: undefined
|
||||
}
|
||||
options: { onSuccess: undefined }
|
||||
});
|
||||
|
||||
const selectedSecrets = useSelectedSecrets();
|
||||
const { toggle: toggleSelectedSecret } = useSelectedSecretActions();
|
||||
const { isBatchMode, pendingChanges } = useBatchMode();
|
||||
useNavigationBlocker({
|
||||
shouldBlock: pendingChanges.secrets.length > 0 || pendingChanges.folders.length > 0,
|
||||
message:
|
||||
"You have unsaved changes. If you leave now, your work will be lost. Do you want to continue?",
|
||||
context: {
|
||||
workspaceId,
|
||||
environment,
|
||||
secretPath
|
||||
}
|
||||
});
|
||||
const { addPendingChange } = useBatchModeActions();
|
||||
|
||||
const handleSecretOperation = async (
|
||||
operation: "create" | "update" | "delete",
|
||||
@@ -170,11 +188,56 @@ export const SecretListView = ({
|
||||
);
|
||||
};
|
||||
|
||||
function getTrueOriginalSecret(
|
||||
currentSecret: SecretV3RawSanitized,
|
||||
pendingSecrets: PendingSecretChange[]
|
||||
): Omit<SecretV3RawSanitized, "tags"> & {
|
||||
tags?: { id: string; slug: string }[];
|
||||
} {
|
||||
// Find if there's already a pending change for this secret
|
||||
const existingChange = pendingSecrets.find((change) => change.id === currentSecret.id);
|
||||
|
||||
if (!existingChange || existingChange.type === "create") {
|
||||
// If no existing change or it's a creation, current secret is the original
|
||||
return {
|
||||
...currentSecret,
|
||||
tags: currentSecret.tags?.map((tag) => ({
|
||||
id: tag.id,
|
||||
slug: tag.slug
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
if (existingChange.type === "update") {
|
||||
// If there's an existing update, reconstruct the original from the stored original values
|
||||
return {
|
||||
...currentSecret,
|
||||
key: existingChange.secretKey, // Original key
|
||||
value: existingChange.originalValue || currentSecret.value,
|
||||
comment: existingChange.originalComment || currentSecret.comment,
|
||||
skipMultilineEncoding:
|
||||
existingChange.originalSkipMultilineEncoding ?? currentSecret.skipMultilineEncoding,
|
||||
tags:
|
||||
existingChange.originalTags?.map((tag) => ({
|
||||
id: tag.id,
|
||||
slug: tag.slug
|
||||
})) || currentSecret.tags,
|
||||
secretMetadata: existingChange.originalSecretMetadata || currentSecret.secretMetadata
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...currentSecret,
|
||||
tags: currentSecret.tags?.map((tag) => ({ id: tag.id, slug: tag.slug }))
|
||||
};
|
||||
}
|
||||
|
||||
// Improved handleSaveSecret function
|
||||
const handleSaveSecret = useCallback(
|
||||
async (
|
||||
orgSecret: SecretV3RawSanitized,
|
||||
modSecret: Omit<SecretV3RawSanitized, "tags"> & {
|
||||
tags?: { id: string }[];
|
||||
tags?: { id: string; name?: string; slug?: string }[];
|
||||
secretMetadata?: { key: string; value: string }[];
|
||||
},
|
||||
cb?: () => void
|
||||
@@ -192,7 +255,9 @@ export const SecretListView = ({
|
||||
reminderNote,
|
||||
reminderRecipients,
|
||||
secretMetadata,
|
||||
isReminderEvent
|
||||
isReminderEvent,
|
||||
isPending,
|
||||
pendingAction
|
||||
} = modSecret;
|
||||
const hasKeyChanged = oldKey !== key && key;
|
||||
|
||||
@@ -246,6 +311,80 @@ export const SecretListView = ({
|
||||
|
||||
// shared secret change
|
||||
if (!isSharedSecUnchanged && !personalAction) {
|
||||
if (isBatchMode) {
|
||||
const isEditingPendingCreation = isPending && pendingAction === PendingAction.Create;
|
||||
|
||||
if (isEditingPendingCreation) {
|
||||
const updatedCreate: PendingSecretCreate = {
|
||||
id: orgSecret.id,
|
||||
type: PendingAction.Create,
|
||||
secretKey: key,
|
||||
secretValue: value || "",
|
||||
secretComment: comment || "",
|
||||
skipMultilineEncoding: modSecret.skipMultilineEncoding || false,
|
||||
tags: tags?.map((tag) => ({ id: tag.id, slug: tag.name || tag.slug || "" })) || [],
|
||||
secretMetadata: secretMetadata || [],
|
||||
timestamp: Date.now(),
|
||||
resourceType: "secret",
|
||||
originalKey: oldKey
|
||||
};
|
||||
|
||||
addPendingChange(updatedCreate, {
|
||||
workspaceId,
|
||||
environment,
|
||||
secretPath
|
||||
});
|
||||
} else {
|
||||
const trueOriginalSecret = getTrueOriginalSecret(orgSecret, pendingChanges.secrets);
|
||||
|
||||
const updateChange: PendingSecretUpdate = {
|
||||
id: orgSecret.id,
|
||||
type: PendingAction.Update,
|
||||
secretKey: trueOriginalSecret.key,
|
||||
...(key !== trueOriginalSecret.key && { newSecretName: key }),
|
||||
...(value !== trueOriginalSecret.value && {
|
||||
originalValue: trueOriginalSecret.value,
|
||||
secretValue: value
|
||||
}),
|
||||
...(comment !== trueOriginalSecret.comment && {
|
||||
originalComment: trueOriginalSecret.comment,
|
||||
secretComment: comment
|
||||
}),
|
||||
...(modSecret.skipMultilineEncoding !==
|
||||
trueOriginalSecret.skipMultilineEncoding && {
|
||||
originalSkipMultilineEncoding: trueOriginalSecret.skipMultilineEncoding,
|
||||
skipMultilineEncoding: modSecret.skipMultilineEncoding
|
||||
}),
|
||||
...(!isSameTags && {
|
||||
originalTags:
|
||||
trueOriginalSecret.tags?.map((tag) => ({ id: tag.id, slug: tag.slug })) || [],
|
||||
tags: tags?.map((tag) => ({ id: tag.id, slug: tag.name || tag.slug || "" })) || []
|
||||
}),
|
||||
...(JSON.stringify(secretMetadata) !==
|
||||
JSON.stringify(trueOriginalSecret.secretMetadata) && {
|
||||
originalSecretMetadata: trueOriginalSecret.secretMetadata || [],
|
||||
secretMetadata: secretMetadata || []
|
||||
}),
|
||||
|
||||
timestamp: Date.now(),
|
||||
resourceType: "secret",
|
||||
existingSecret: orgSecret
|
||||
};
|
||||
|
||||
addPendingChange(updateChange, {
|
||||
workspaceId,
|
||||
environment,
|
||||
secretPath
|
||||
});
|
||||
}
|
||||
|
||||
if (!isReminderEvent) {
|
||||
handlePopUpClose("secretDetail");
|
||||
}
|
||||
if (cb) cb();
|
||||
return;
|
||||
}
|
||||
|
||||
await handleSecretOperation("update", SecretType.Shared, oldKey, {
|
||||
value,
|
||||
tags: tagIds,
|
||||
@@ -314,12 +453,41 @@ export const SecretListView = ({
|
||||
});
|
||||
}
|
||||
},
|
||||
[environment, secretPath, isProtectedBranch]
|
||||
[
|
||||
environment,
|
||||
secretPath,
|
||||
isProtectedBranch,
|
||||
isBatchMode,
|
||||
workspaceId,
|
||||
addPendingChange,
|
||||
pendingChanges.secrets
|
||||
]
|
||||
);
|
||||
|
||||
const handleSecretDelete = useCallback(async () => {
|
||||
const { key, id: secretId } = popUp.deleteSecret?.data as SecretV3RawSanitized;
|
||||
const { key, id: secretId, value } = popUp.deleteSecret?.data as SecretV3RawSanitized;
|
||||
try {
|
||||
if (isBatchMode) {
|
||||
const deleteChange: PendingSecretDelete = {
|
||||
id: `${secretId}`,
|
||||
type: PendingAction.Delete,
|
||||
secretKey: key,
|
||||
secretValue: value || "",
|
||||
timestamp: Date.now(),
|
||||
resourceType: "secret"
|
||||
};
|
||||
|
||||
addPendingChange(deleteChange, {
|
||||
workspaceId,
|
||||
environment,
|
||||
secretPath
|
||||
});
|
||||
|
||||
handlePopUpClose("deleteSecret");
|
||||
handlePopUpClose("secretDetail");
|
||||
return;
|
||||
}
|
||||
|
||||
await handleSecretOperation("delete", SecretType.Shared, key, { secretId });
|
||||
// wrap this in another function and then reuse
|
||||
queryClient.invalidateQueries({
|
||||
@@ -362,7 +530,10 @@ export const SecretListView = ({
|
||||
(popUp.deleteSecret?.data as SecretV3RawSanitized)?.key,
|
||||
environment,
|
||||
secretPath,
|
||||
isProtectedBranch
|
||||
isProtectedBranch,
|
||||
isBatchMode,
|
||||
workspaceId,
|
||||
addPendingChange
|
||||
]);
|
||||
|
||||
// for optimization on minimise re-rendering of secret items
|
||||
@@ -401,6 +572,8 @@ export const SecretListView = ({
|
||||
value: secret.valueOverride ?? secret.value
|
||||
})
|
||||
}
|
||||
isPending={secret.isPending}
|
||||
pendingAction={secret.pendingAction}
|
||||
/>
|
||||
))}
|
||||
<DeleteActionModal
|
||||
|
||||
+6
-3
@@ -13,7 +13,8 @@ import {
|
||||
faLock,
|
||||
faProjectDiagram,
|
||||
faShare,
|
||||
faTags
|
||||
faTags,
|
||||
faTrash
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { z } from "zod";
|
||||
|
||||
@@ -82,7 +83,8 @@ export enum FontAwesomeSpriteName {
|
||||
ReplicatedSecretKey = "secret-replicated",
|
||||
ShareSecret = "share-secret",
|
||||
KeyLock = "key-lock",
|
||||
SecretReferenceTree = "secret-reference-tree"
|
||||
SecretReferenceTree = "secret-reference-tree",
|
||||
Trash = "trash"
|
||||
}
|
||||
|
||||
// this is an optimization technique
|
||||
@@ -101,5 +103,6 @@ export const FontAwesomeSpriteSymbols = [
|
||||
{ icon: faClone, symbol: FontAwesomeSpriteName.ReplicatedSecretKey },
|
||||
{ icon: faShare, symbol: FontAwesomeSpriteName.ShareSecret },
|
||||
{ icon: faLock, symbol: FontAwesomeSpriteName.KeyLock },
|
||||
{ icon: faProjectDiagram, symbol: FontAwesomeSpriteName.SecretReferenceTree }
|
||||
{ icon: faProjectDiagram, symbol: FontAwesomeSpriteName.SecretReferenceTree },
|
||||
{ icon: faTrash, symbol: FontAwesomeSpriteName.Trash }
|
||||
];
|
||||
|
||||
+5
-7
@@ -2,11 +2,11 @@ import { useState } from "react";
|
||||
import { subject } from "@casl/ability";
|
||||
import {
|
||||
faAsterisk,
|
||||
faClose,
|
||||
faEdit,
|
||||
faInfoCircle,
|
||||
faKey,
|
||||
faRotate
|
||||
faRotate,
|
||||
faTrash
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
@@ -48,7 +48,7 @@ export const SecretRotationItem = ({
|
||||
<div className="text- flex w-11 items-center py-2 pl-5 text-mineshaft-400">
|
||||
<FontAwesomeIcon icon={faRotate} />
|
||||
</div>
|
||||
<div className="flex flex-grow items-center border-r border-mineshaft-600 py-2 pl-4 pr-2">
|
||||
<div className="flex flex-grow items-center py-2 pl-4 pr-2">
|
||||
<div className="flex w-full flex-wrap items-center">
|
||||
<span>{name}</span>
|
||||
<Tag className="mx-2.5 flex items-center gap-1 px-1.5 py-0 text-xs normal-case">
|
||||
@@ -141,7 +141,7 @@ export const SecretRotationItem = ({
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key="options"
|
||||
className="flex h-10 flex-shrink-0 items-center space-x-1 px-[0.665rem]"
|
||||
className="flex w-16 items-center justify-between border-l border-mineshaft-600 px-2 py-3"
|
||||
initial={{ x: 0, opacity: 0 }}
|
||||
animate={{ x: 0, opacity: 1 }}
|
||||
exit={{ x: 10, opacity: 0 }}
|
||||
@@ -159,7 +159,6 @@ export const SecretRotationItem = ({
|
||||
<IconButton
|
||||
ariaLabel="Edit rotation"
|
||||
variant="plain"
|
||||
size="md"
|
||||
isDisabled={!isAllowed}
|
||||
className="opacity-0 group-hover:opacity-100"
|
||||
onClick={(e) => {
|
||||
@@ -185,7 +184,6 @@ export const SecretRotationItem = ({
|
||||
ariaLabel="Delete rotation"
|
||||
variant="plain"
|
||||
colorSchema="danger"
|
||||
size="md"
|
||||
className="opacity-0 group-hover:opacity-100"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
@@ -193,7 +191,7 @@ export const SecretRotationItem = ({
|
||||
}}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
<FontAwesomeIcon icon={faClose} size="lg" />
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
|
||||
@@ -13,7 +13,7 @@ const SecretDashboardPageQueryParamsSchema = z.object({
|
||||
tags: z.string().catch("")
|
||||
});
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/secrets/$envSlug"
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/secrets/$envSlug"
|
||||
)({
|
||||
component: SecretDashboardPage,
|
||||
validateSearch: zodValidator(SecretDashboardPageQueryParamsSchema),
|
||||
@@ -28,7 +28,7 @@ export const Route = createFileRoute(
|
||||
{
|
||||
label: "Secrets",
|
||||
link: linkOptions({
|
||||
to: "/projects/$projectId/secret-manager/overview",
|
||||
to: "/projects/secret-management/$projectId/overview",
|
||||
params
|
||||
})
|
||||
},
|
||||
@@ -39,7 +39,7 @@ export const Route = createFileRoute(
|
||||
links: context.project.environments.map((el) => ({
|
||||
label: el.name,
|
||||
link: linkOptions({
|
||||
to: "/projects/$projectId/secret-manager/secrets/$envSlug",
|
||||
to: "/projects/secret-management/$projectId/secrets/$envSlug",
|
||||
params: {
|
||||
projectId: params.projectId,
|
||||
envSlug: el.slug
|
||||
|
||||
@@ -172,7 +172,7 @@ const Page = () => {
|
||||
PostgreSQL and Microsoft SQL Server Rotations can now be created from the{" "}
|
||||
<Link
|
||||
className="text-mineshaft-100 underline decoration-primary underline-offset-2 hover:text-mineshaft-200"
|
||||
to="/projects/$projectId/secret-manager/overview"
|
||||
to="/projects/secret-management/$projectId/overview"
|
||||
params={{ projectId: currentWorkspace.id }}
|
||||
>
|
||||
Secret Manager Dashboard
|
||||
@@ -407,7 +407,7 @@ const Page = () => {
|
||||
Infisical is revamping its Secret Rotation experience. Navigate to the{" "}
|
||||
<Link
|
||||
className="text-mineshaft-100 underline decoration-primary underline-offset-2 hover:text-mineshaft-200"
|
||||
to="/projects/$projectId/secret-manager/overview"
|
||||
to="/projects/secret-management/$projectId/overview"
|
||||
params={{ projectId: currentWorkspace.id }}
|
||||
>
|
||||
Secret Manager Dashboard
|
||||
@@ -424,7 +424,7 @@ const Page = () => {
|
||||
<Button
|
||||
onClick={() =>
|
||||
navigate({
|
||||
to: "/projects/$projectId/secret-manager/overview",
|
||||
to: "/projects/secret-management/$projectId/overview",
|
||||
params: { projectId: currentWorkspace.id }
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { createFileRoute } from "@tanstack/react-router";
|
||||
import { SecretRotationPage } from "./SecretRotationPage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/secret-rotation"
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/secret-rotation"
|
||||
)({
|
||||
component: SecretRotationPage,
|
||||
beforeLoad: ({ context }) => {
|
||||
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
import { GenericFieldLabel } from "@app/components/secret-syncs";
|
||||
import { TBitbucketSync } from "@app/hooks/api/secretSyncs/types/bitbucket-sync";
|
||||
|
||||
type Props = {
|
||||
secretSync: TBitbucketSync;
|
||||
};
|
||||
|
||||
export const BitbucketSyncDestinationSection = ({ secretSync }: Props) => {
|
||||
const {
|
||||
destinationConfig: { workspaceSlug, repositorySlug, environmentId }
|
||||
} = secretSync;
|
||||
|
||||
return (
|
||||
<>
|
||||
<GenericFieldLabel label="Workspace">{workspaceSlug}</GenericFieldLabel>
|
||||
<GenericFieldLabel label="Repository">{repositorySlug}</GenericFieldLabel>
|
||||
{environmentId && (
|
||||
<GenericFieldLabel label="Deployment Environment">{environmentId}</GenericFieldLabel>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
+4
@@ -17,6 +17,7 @@ import { AwsSecretsManagerSyncDestinationSection } from "./AwsSecretsManagerSync
|
||||
import { AzureAppConfigurationSyncDestinationSection } from "./AzureAppConfigurationSyncDestinationSection";
|
||||
import { AzureDevOpsSyncDestinationSection } from "./AzureDevOpsSyncDestinationSection";
|
||||
import { AzureKeyVaultSyncDestinationSection } from "./AzureKeyVaultSyncDestinationSection";
|
||||
import { BitbucketSyncDestinationSection } from "./BitbucketSyncDestinationSection";
|
||||
import { CamundaSyncDestinationSection } from "./CamundaSyncDestinationSection";
|
||||
import { ChecklySyncDestinationSection } from "./ChecklySyncDestinationSection";
|
||||
import { CloudflarePagesSyncDestinationSection } from "./CloudflarePagesSyncDestinationSection";
|
||||
@@ -134,6 +135,9 @@ export const SecretSyncDestinationSection = ({ secretSync, onEditDestination }:
|
||||
case SecretSync.Supabase:
|
||||
DestinationComponents = <SupabaseSyncDestinationSection secretSync={secretSync} />;
|
||||
break;
|
||||
case SecretSync.Bitbucket:
|
||||
DestinationComponents = <BitbucketSyncDestinationSection secretSync={secretSync} />;
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unhandled Destination Section components: ${destination}`);
|
||||
}
|
||||
|
||||
+1
@@ -65,6 +65,7 @@ export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) =
|
||||
case SecretSync.Railway:
|
||||
case SecretSync.Supabase:
|
||||
case SecretSync.Checkly:
|
||||
case SecretSync.Bitbucket:
|
||||
AdditionalSyncOptionsComponent = null;
|
||||
break;
|
||||
default:
|
||||
|
||||
@@ -5,7 +5,7 @@ import { IntegrationsListPageTabs } from "@app/types/integrations";
|
||||
import { SecretSyncDetailsByIDPage } from "./SecretSyncDetailsByIDPage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/secret-syncs/$destination/$syncId"
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/secret-syncs/$destination/$syncId"
|
||||
)({
|
||||
component: SecretSyncDetailsByIDPage,
|
||||
beforeLoad: ({ context, params }) => {
|
||||
@@ -15,7 +15,7 @@ export const Route = createFileRoute(
|
||||
{
|
||||
label: "Integrations",
|
||||
link: linkOptions({
|
||||
to: "/projects/$projectId/secret-manager/integrations",
|
||||
to: "/projects/secret-management/$projectId/integrations",
|
||||
params,
|
||||
search: {
|
||||
selectedTab: IntegrationsListPageTabs.SecretSyncs
|
||||
|
||||
@@ -4,9 +4,10 @@ import { useTranslation } from "react-i18next";
|
||||
import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { ProjectVersion } from "@app/hooks/api/workspace/types";
|
||||
import { ProjectGeneralTab } from "@app/pages/project/SettingsPage/components/ProjectGeneralTab";
|
||||
|
||||
import { EncryptionTab } from "./components/EncryptionTab";
|
||||
import { ProjectGeneralTab } from "./components/ProjectGeneralTab";
|
||||
import { SecretSettingsTab } from "./components/ProjectGeneralTab";
|
||||
import { WebhooksTab } from "./components/WebhooksTab";
|
||||
import { WorkflowIntegrationTab } from "./components/WorkflowIntegrationSection";
|
||||
|
||||
@@ -15,6 +16,7 @@ export const SettingsPage = () => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const tabs = [
|
||||
{ name: "General", key: "tab-project-general", Component: ProjectGeneralTab },
|
||||
{ name: "Secrets Management", key: "tab-secret-general", Component: SecretSettingsTab },
|
||||
{
|
||||
name: "Encryption",
|
||||
key: "tab-project-encryption",
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ import { SecretSharingSection } from "../SecretSharingSection";
|
||||
import { SecretSnapshotsLegacySection } from "../SecretSnapshotsLegacySection";
|
||||
import { SecretTagsSection } from "../SecretTagsSection";
|
||||
|
||||
export const ProjectGeneralTab = () => {
|
||||
export const SecretSettingsTab = () => {
|
||||
return (
|
||||
<div>
|
||||
<EnvironmentSection />
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
export { ProjectGeneralTab } from "./ProjectGeneralTab";
|
||||
export { SecretSettingsTab } from "./ProjectGeneralTab";
|
||||
|
||||
@@ -3,7 +3,7 @@ import { createFileRoute } from "@tanstack/react-router";
|
||||
import { SettingsPage } from "./SettingsPage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/settings"
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/settings"
|
||||
)({
|
||||
component: SettingsPage,
|
||||
beforeLoad: ({ context }) => {
|
||||
|
||||
+1
-1
@@ -68,7 +68,7 @@ export const AWSParameterStoreAuthorizeIntegrationPage = () => {
|
||||
});
|
||||
|
||||
navigate({
|
||||
to: "/projects/$projectId/secret-manager/integrations/aws-parameter-store/create",
|
||||
to: "/projects/secret-management/$projectId/integrations/aws-parameter-store/create",
|
||||
params: {
|
||||
projectId: currentWorkspace.id
|
||||
},
|
||||
|
||||
+2
-2
@@ -5,7 +5,7 @@ import { IntegrationsListPageTabs } from "@app/types/integrations";
|
||||
import { AWSParameterStoreAuthorizeIntegrationPage } from "./AwsParameterStoreAuthorizePage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/aws-parameter-store/authorize"
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/aws-parameter-store/authorize"
|
||||
)({
|
||||
component: AWSParameterStoreAuthorizeIntegrationPage,
|
||||
beforeLoad: ({ context, params }) => {
|
||||
@@ -15,7 +15,7 @@ export const Route = createFileRoute(
|
||||
{
|
||||
label: "Integrations",
|
||||
link: linkOptions({
|
||||
to: "/projects/$projectId/secret-manager/integrations",
|
||||
to: "/projects/secret-management/$projectId/integrations",
|
||||
params,
|
||||
search: {
|
||||
selectedTab: IntegrationsListPageTabs.NativeIntegrations
|
||||
|
||||
+1
-1
@@ -156,7 +156,7 @@ export const AWSParameterStoreConfigurePage = () => {
|
||||
setPathErrorText("");
|
||||
|
||||
navigate({
|
||||
to: "/projects/$projectId/secret-manager/integrations",
|
||||
to: "/projects/secret-management/$projectId/integrations",
|
||||
params: {
|
||||
projectId: currentWorkspace.id
|
||||
},
|
||||
|
||||
+2
-2
@@ -11,7 +11,7 @@ const AwsParameterStoreConfigurePageQueryParamsSchema = z.object({
|
||||
});
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/aws-parameter-store/create"
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/aws-parameter-store/create"
|
||||
)({
|
||||
component: AWSParameterStoreConfigurePage,
|
||||
validateSearch: zodValidator(AwsParameterStoreConfigurePageQueryParamsSchema),
|
||||
@@ -22,7 +22,7 @@ export const Route = createFileRoute(
|
||||
{
|
||||
label: "Integrations",
|
||||
link: linkOptions({
|
||||
to: "/projects/$projectId/secret-manager/integrations",
|
||||
to: "/projects/secret-management/$projectId/integrations",
|
||||
params,
|
||||
search: {
|
||||
selectedTab: IntegrationsListPageTabs.NativeIntegrations
|
||||
|
||||
+1
-1
@@ -66,7 +66,7 @@ export const AWSSecretManagerAuthorizePage = () => {
|
||||
})
|
||||
});
|
||||
navigate({
|
||||
to: "/projects/$projectId/secret-manager/integrations/aws-secret-manager/create",
|
||||
to: "/projects/secret-management/$projectId/integrations/aws-secret-manager/create",
|
||||
params: {
|
||||
projectId: currentWorkspace.id
|
||||
},
|
||||
|
||||
+2
-2
@@ -5,7 +5,7 @@ import { IntegrationsListPageTabs } from "@app/types/integrations";
|
||||
import { AWSSecretManagerAuthorizePage } from "./AwsSecretManagerAuthorizePage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/aws-secret-manager/authorize"
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/aws-secret-manager/authorize"
|
||||
)({
|
||||
component: AWSSecretManagerAuthorizePage,
|
||||
beforeLoad: ({ context, params }) => {
|
||||
@@ -15,7 +15,7 @@ export const Route = createFileRoute(
|
||||
{
|
||||
label: "Integrations",
|
||||
link: linkOptions({
|
||||
to: "/projects/$projectId/secret-manager/integrations",
|
||||
to: "/projects/secret-management/$projectId/integrations",
|
||||
params,
|
||||
search: {
|
||||
selectedTab: IntegrationsListPageTabs.NativeIntegrations
|
||||
|
||||
+1
-1
@@ -204,7 +204,7 @@ export const AwsSecretManagerConfigurePage = () => {
|
||||
});
|
||||
|
||||
navigate({
|
||||
to: "/projects/$projectId/secret-manager/integrations",
|
||||
to: "/projects/secret-management/$projectId/integrations",
|
||||
params: {
|
||||
projectId: currentWorkspace.id
|
||||
},
|
||||
|
||||
+2
-2
@@ -11,7 +11,7 @@ const AwsSecretManagerConfigurePageQueryParamsSchema = z.object({
|
||||
});
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/aws-secret-manager/create"
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/aws-secret-manager/create"
|
||||
)({
|
||||
component: AwsSecretManagerConfigurePage,
|
||||
validateSearch: zodValidator(AwsSecretManagerConfigurePageQueryParamsSchema),
|
||||
@@ -22,7 +22,7 @@ export const Route = createFileRoute(
|
||||
{
|
||||
label: "Integrations",
|
||||
link: linkOptions({
|
||||
to: "/projects/$projectId/secret-manager/integrations",
|
||||
to: "/projects/secret-management/$projectId/integrations",
|
||||
params,
|
||||
search: {
|
||||
selectedTab: IntegrationsListPageTabs.NativeIntegrations
|
||||
|
||||
+1
-1
@@ -131,7 +131,7 @@ export const AzureAppConfigurationConfigurePage = () => {
|
||||
});
|
||||
|
||||
navigate({
|
||||
to: "/projects/$projectId/secret-manager/integrations",
|
||||
to: "/projects/secret-management/$projectId/integrations",
|
||||
params: {
|
||||
projectId: currentWorkspace.id
|
||||
},
|
||||
|
||||
+2
-2
@@ -11,7 +11,7 @@ const AzureAppConfigurationPageQueryParamsSchema = z.object({
|
||||
});
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/azure-app-configuration/create"
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/azure-app-configuration/create"
|
||||
)({
|
||||
component: AzureAppConfigurationConfigurePage,
|
||||
validateSearch: zodValidator(AzureAppConfigurationPageQueryParamsSchema),
|
||||
@@ -22,7 +22,7 @@ export const Route = createFileRoute(
|
||||
{
|
||||
label: "Integrations",
|
||||
link: linkOptions({
|
||||
to: "/projects/$projectId/secret-manager/integrations",
|
||||
to: "/projects/secret-management/$projectId/integrations",
|
||||
params,
|
||||
search: {
|
||||
selectedTab: IntegrationsListPageTabs.NativeIntegrations
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ export const AzureAppConfigurationOauthCallbackPage = () => {
|
||||
});
|
||||
|
||||
navigate({
|
||||
to: "/projects/$projectId/secret-manager/integrations/azure-app-configuration/create",
|
||||
to: "/projects/secret-management/$projectId/integrations/azure-app-configuration/create",
|
||||
params: {
|
||||
projectId: currentWorkspace.id
|
||||
},
|
||||
|
||||
+2
-2
@@ -12,7 +12,7 @@ export const AzureAppConfigurationOauthCallbackPageQueryParamsSchema = z.object(
|
||||
});
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/azure-app-configuration/oauth2/callback"
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/azure-app-configuration/oauth2/callback"
|
||||
)({
|
||||
component: AzureAppConfigurationOauthCallbackPage,
|
||||
validateSearch: zodValidator(AzureAppConfigurationOauthCallbackPageQueryParamsSchema),
|
||||
@@ -23,7 +23,7 @@ export const Route = createFileRoute(
|
||||
{
|
||||
label: "Integrations",
|
||||
link: linkOptions({
|
||||
to: "/projects/$projectId/secret-manager/integrations",
|
||||
to: "/projects/secret-management/$projectId/integrations",
|
||||
params,
|
||||
search: {
|
||||
selectedTab: IntegrationsListPageTabs.NativeIntegrations
|
||||
|
||||
+1
-1
@@ -39,7 +39,7 @@ export const AzureDevopsAuthorizePage = () => {
|
||||
setIsLoading(false);
|
||||
|
||||
navigate({
|
||||
to: "/projects/$projectId/secret-manager/integrations/azure-devops/create",
|
||||
to: "/projects/secret-management/$projectId/integrations/azure-devops/create",
|
||||
params: {
|
||||
projectId: currentWorkspace.id
|
||||
},
|
||||
|
||||
@@ -5,7 +5,7 @@ import { IntegrationsListPageTabs } from "@app/types/integrations";
|
||||
import { AzureDevopsAuthorizePage } from "./AzureDevopsAuthorizePage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/azure-devops/authorize"
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/azure-devops/authorize"
|
||||
)({
|
||||
component: AzureDevopsAuthorizePage,
|
||||
beforeLoad: ({ context, params }) => {
|
||||
@@ -15,7 +15,7 @@ export const Route = createFileRoute(
|
||||
{
|
||||
label: "Integrations",
|
||||
link: linkOptions({
|
||||
to: "/projects/$projectId/secret-manager/integrations",
|
||||
to: "/projects/secret-management/$projectId/integrations",
|
||||
params,
|
||||
search: {
|
||||
selectedTab: IntegrationsListPageTabs.NativeIntegrations
|
||||
|
||||
+1
-1
@@ -80,7 +80,7 @@ export const AzureDevopsConfigurePage = () => {
|
||||
setIsLoading(false);
|
||||
|
||||
navigate({
|
||||
to: "/projects/$projectId/secret-manager/integrations",
|
||||
to: "/projects/secret-management/$projectId/integrations",
|
||||
params: {
|
||||
projectId: currentWorkspace.id
|
||||
},
|
||||
|
||||
@@ -11,7 +11,7 @@ const AzureDevopsConfigurePageQueryParamsSchema = z.object({
|
||||
});
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/azure-devops/create"
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/azure-devops/create"
|
||||
)({
|
||||
component: AzureDevopsConfigurePage,
|
||||
validateSearch: zodValidator(AzureDevopsConfigurePageQueryParamsSchema),
|
||||
@@ -22,7 +22,7 @@ export const Route = createFileRoute(
|
||||
{
|
||||
label: "Integrations",
|
||||
link: linkOptions({
|
||||
to: "/projects/$projectId/secret-manager/integrations",
|
||||
to: "/projects/secret-management/$projectId/integrations",
|
||||
params,
|
||||
search: {
|
||||
selectedTab: IntegrationsListPageTabs.NativeIntegrations
|
||||
|
||||
@@ -11,7 +11,7 @@ const PageQueryParamsSchema = z.object({
|
||||
});
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/azure-key-vault/authorize"
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/azure-key-vault/authorize"
|
||||
)({
|
||||
component: AzureKeyVaultAuthorizePage,
|
||||
validateSearch: PageQueryParamsSchema,
|
||||
@@ -22,7 +22,7 @@ export const Route = createFileRoute(
|
||||
{
|
||||
label: "Integrations",
|
||||
link: linkOptions({
|
||||
to: "/projects/$projectId/secret-manager/integrations",
|
||||
to: "/projects/secret-management/$projectId/integrations",
|
||||
params,
|
||||
search: {
|
||||
selectedTab: IntegrationsListPageTabs.NativeIntegrations
|
||||
|
||||
+1
-1
@@ -88,7 +88,7 @@ export const AzureKeyVaultConfigurePage = () => {
|
||||
setIsLoading(false);
|
||||
|
||||
navigate({
|
||||
to: "/projects/$projectId/secret-manager/integrations",
|
||||
to: "/projects/secret-management/$projectId/integrations",
|
||||
params: {
|
||||
projectId: currentWorkspace.id
|
||||
},
|
||||
|
||||
@@ -11,7 +11,7 @@ const AzureKeyVaultConfigurePageQueryParamsSchema = z.object({
|
||||
});
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/azure-key-vault/create"
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/azure-key-vault/create"
|
||||
)({
|
||||
component: AzureKeyVaultConfigurePage,
|
||||
validateSearch: zodValidator(AzureKeyVaultConfigurePageQueryParamsSchema),
|
||||
@@ -22,7 +22,7 @@ export const Route = createFileRoute(
|
||||
{
|
||||
label: "Integrations",
|
||||
link: linkOptions({
|
||||
to: "/projects/$projectId/secret-manager/integrations",
|
||||
to: "/projects/secret-management/$projectId/integrations",
|
||||
params,
|
||||
search: {
|
||||
selectedTab: IntegrationsListPageTabs.NativeIntegrations
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ export const AzureKeyVaultOauthCallbackPage = () => {
|
||||
});
|
||||
|
||||
navigate({
|
||||
to: "/projects/$projectId/secret-manager/integrations/azure-key-vault/create",
|
||||
to: "/projects/secret-management/$projectId/integrations/azure-key-vault/create",
|
||||
params: {
|
||||
projectId: currentWorkspace.id
|
||||
},
|
||||
|
||||
+2
-2
@@ -12,7 +12,7 @@ export const AzureKeyVaultOauthCallbackQueryParamsSchema = z.object({
|
||||
});
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/azure-key-vault/oauth2/callback"
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/azure-key-vault/oauth2/callback"
|
||||
)({
|
||||
component: AzureKeyVaultOauthCallbackPage,
|
||||
validateSearch: zodValidator(AzureKeyVaultOauthCallbackQueryParamsSchema),
|
||||
@@ -23,7 +23,7 @@ export const Route = createFileRoute(
|
||||
{
|
||||
label: "Integrations",
|
||||
link: linkOptions({
|
||||
to: "/projects/$projectId/secret-manager/integrations",
|
||||
to: "/projects/secret-management/$projectId/integrations",
|
||||
params,
|
||||
search: {
|
||||
selectedTab: IntegrationsListPageTabs.NativeIntegrations
|
||||
|
||||
+1
-1
@@ -148,7 +148,7 @@ export const BitbucketConfigurePage = () => {
|
||||
text: "Successfully created integration"
|
||||
});
|
||||
navigate({
|
||||
to: "/projects/$projectId/secret-manager/integrations",
|
||||
to: "/projects/secret-management/$projectId/integrations",
|
||||
params: {
|
||||
projectId: currentWorkspace.id
|
||||
},
|
||||
|
||||
@@ -11,7 +11,7 @@ const BitbucketConfigurePageQueryParamsSchema = z.object({
|
||||
});
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/bitbucket/create"
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/bitbucket/create"
|
||||
)({
|
||||
component: BitbucketConfigurePage,
|
||||
validateSearch: zodValidator(BitbucketConfigurePageQueryParamsSchema),
|
||||
@@ -22,7 +22,7 @@ export const Route = createFileRoute(
|
||||
{
|
||||
label: "Integrations",
|
||||
link: linkOptions({
|
||||
to: "/projects/$projectId/secret-manager/integrations",
|
||||
to: "/projects/secret-management/$projectId/integrations",
|
||||
params,
|
||||
search: {
|
||||
selectedTab: IntegrationsListPageTabs.NativeIntegrations
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@ export const BitbucketOauthCallbackPage = () => {
|
||||
});
|
||||
|
||||
navigate({
|
||||
to: "/projects/$projectId/secret-manager/integrations/bitbucket/create",
|
||||
to: "/projects/secret-management/$projectId/integrations/bitbucket/create",
|
||||
params: {
|
||||
projectId: currentWorkspace.id
|
||||
},
|
||||
|
||||
@@ -12,7 +12,7 @@ export const BitbucketOauthCallbackQueryParamsSchema = z.object({
|
||||
});
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/bitbucket/oauth2/callback"
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/bitbucket/oauth2/callback"
|
||||
)({
|
||||
component: BitbucketOauthCallbackPage,
|
||||
validateSearch: zodValidator(BitbucketOauthCallbackQueryParamsSchema),
|
||||
@@ -23,7 +23,7 @@ export const Route = createFileRoute(
|
||||
{
|
||||
label: "Integrations",
|
||||
link: linkOptions({
|
||||
to: "/projects/$projectId/secret-manager/integrations",
|
||||
to: "/projects/secret-management/$projectId/integrations",
|
||||
params,
|
||||
search: {
|
||||
selectedTab: IntegrationsListPageTabs.NativeIntegrations
|
||||
|
||||
+1
-1
@@ -36,7 +36,7 @@ export const ChecklyAuthorizePage = () => {
|
||||
setIsLoading(false);
|
||||
|
||||
navigate({
|
||||
to: "/projects/$projectId/secret-manager/integrations/checkly/create",
|
||||
to: "/projects/secret-management/$projectId/integrations/checkly/create",
|
||||
params: {
|
||||
projectId: currentWorkspace.id
|
||||
},
|
||||
|
||||
@@ -5,7 +5,7 @@ import { IntegrationsListPageTabs } from "@app/types/integrations";
|
||||
import { ChecklyAuthorizePage } from "./ChecklyAuthorizePage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/checkly/authorize"
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/checkly/authorize"
|
||||
)({
|
||||
component: ChecklyAuthorizePage,
|
||||
beforeLoad: ({ context, params }) => {
|
||||
@@ -15,7 +15,7 @@ export const Route = createFileRoute(
|
||||
{
|
||||
label: "Integrations",
|
||||
link: linkOptions({
|
||||
to: "/projects/$projectId/secret-manager/integrations",
|
||||
to: "/projects/secret-management/$projectId/integrations",
|
||||
params,
|
||||
search: {
|
||||
selectedTab: IntegrationsListPageTabs.NativeIntegrations
|
||||
|
||||
+1
-1
@@ -113,7 +113,7 @@ export const ChecklyConfigurePage = () => {
|
||||
setIsLoading(false);
|
||||
|
||||
navigate({
|
||||
to: "/projects/$projectId/secret-manager/integrations",
|
||||
to: "/projects/secret-management/$projectId/integrations",
|
||||
params: {
|
||||
projectId: currentWorkspace.id
|
||||
},
|
||||
|
||||
@@ -11,7 +11,7 @@ const ChecklyConfigurePageQueryParamsSchema = z.object({
|
||||
});
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/checkly/create"
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/checkly/create"
|
||||
)({
|
||||
component: ChecklyConfigurePage,
|
||||
validateSearch: zodValidator(ChecklyConfigurePageQueryParamsSchema),
|
||||
@@ -22,7 +22,7 @@ export const Route = createFileRoute(
|
||||
{
|
||||
label: "Integrations",
|
||||
link: linkOptions({
|
||||
to: "/projects/$projectId/secret-manager/integrations",
|
||||
to: "/projects/secret-management/$projectId/integrations",
|
||||
params,
|
||||
search: {
|
||||
selectedTab: IntegrationsListPageTabs.NativeIntegrations
|
||||
|
||||
+1
-1
@@ -36,7 +36,7 @@ export const CircleCIAuthorizePage = () => {
|
||||
setIsLoading(false);
|
||||
|
||||
navigate({
|
||||
to: "/projects/$projectId/secret-manager/integrations/circleci/create",
|
||||
to: "/projects/secret-management/$projectId/integrations/circleci/create",
|
||||
params: {
|
||||
projectId: currentWorkspace.id
|
||||
},
|
||||
|
||||
@@ -5,7 +5,7 @@ import { IntegrationsListPageTabs } from "@app/types/integrations";
|
||||
import { CircleCIAuthorizePage } from "./CircleCIAuthorizePage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/circleci/authorize"
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/circleci/authorize"
|
||||
)({
|
||||
component: CircleCIAuthorizePage,
|
||||
beforeLoad: ({ context, params }) => {
|
||||
@@ -15,7 +15,7 @@ export const Route = createFileRoute(
|
||||
{
|
||||
label: "Integrations",
|
||||
link: linkOptions({
|
||||
to: "/projects/$projectId/secret-manager/integrations",
|
||||
to: "/projects/secret-management/$projectId/integrations",
|
||||
params,
|
||||
search: {
|
||||
selectedTab: IntegrationsListPageTabs.NativeIntegrations
|
||||
|
||||
+1
-1
@@ -103,7 +103,7 @@ export const CircleCIConfigurePage = () => {
|
||||
text: "Successfully created integration"
|
||||
});
|
||||
navigate({
|
||||
to: "/projects/$projectId/secret-manager/integrations",
|
||||
to: "/projects/secret-management/$projectId/integrations",
|
||||
params: {
|
||||
projectId: currentWorkspace.id
|
||||
},
|
||||
|
||||
@@ -11,7 +11,7 @@ const CircleCIConfigurePageQueryParamsSchema = z.object({
|
||||
});
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/circleci/create"
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/circleci/create"
|
||||
)({
|
||||
component: CircleCIConfigurePage,
|
||||
validateSearch: zodValidator(CircleCIConfigurePageQueryParamsSchema),
|
||||
@@ -22,7 +22,7 @@ export const Route = createFileRoute(
|
||||
{
|
||||
label: "Integrations",
|
||||
link: linkOptions({
|
||||
to: "/projects/$projectId/secret-manager/integrations",
|
||||
to: "/projects/secret-management/$projectId/integrations",
|
||||
params,
|
||||
search: {
|
||||
selectedTab: IntegrationsListPageTabs.NativeIntegrations
|
||||
|
||||
+1
-1
@@ -34,7 +34,7 @@ export const Cloud66AuthorizePage = () => {
|
||||
setIsLoading(false);
|
||||
|
||||
navigate({
|
||||
to: "/projects/$projectId/secret-manager/integrations/cloud-66/create",
|
||||
to: "/projects/secret-management/$projectId/integrations/cloud-66/create",
|
||||
params: {
|
||||
projectId: currentWorkspace.id
|
||||
},
|
||||
|
||||
@@ -5,7 +5,7 @@ import { IntegrationsListPageTabs } from "@app/types/integrations";
|
||||
import { Cloud66AuthorizePage } from "./Cloud66AuthorizePage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/cloud-66/authorize"
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/cloud-66/authorize"
|
||||
)({
|
||||
component: Cloud66AuthorizePage,
|
||||
beforeLoad: ({ context, params }) => {
|
||||
@@ -15,7 +15,7 @@ export const Route = createFileRoute(
|
||||
{
|
||||
label: "Integrations",
|
||||
link: linkOptions({
|
||||
to: "/projects/$projectId/secret-manager/integrations",
|
||||
to: "/projects/secret-management/$projectId/integrations",
|
||||
params,
|
||||
search: {
|
||||
selectedTab: IntegrationsListPageTabs.NativeIntegrations
|
||||
|
||||
+1
-1
@@ -75,7 +75,7 @@ export const Cloud66ConfigurePage = () => {
|
||||
setIsLoading(false);
|
||||
|
||||
navigate({
|
||||
to: "/projects/$projectId/secret-manager/integrations",
|
||||
to: "/projects/secret-management/$projectId/integrations",
|
||||
params: {
|
||||
projectId: currentWorkspace.id
|
||||
},
|
||||
|
||||
@@ -11,7 +11,7 @@ const Cloud66ConfigurePageQueryParamsSchema = z.object({
|
||||
});
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/cloud-66/create"
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/cloud-66/create"
|
||||
)({
|
||||
component: Cloud66ConfigurePage,
|
||||
validateSearch: zodValidator(Cloud66ConfigurePageQueryParamsSchema),
|
||||
@@ -22,7 +22,7 @@ export const Route = createFileRoute(
|
||||
{
|
||||
label: "Integrations",
|
||||
link: linkOptions({
|
||||
to: "/projects/$projectId/secret-manager/integrations",
|
||||
to: "/projects/secret-management/$projectId/integrations",
|
||||
params,
|
||||
search: {
|
||||
selectedTab: IntegrationsListPageTabs.NativeIntegrations
|
||||
|
||||
+1
-1
@@ -41,7 +41,7 @@ export const CloudflarePagesAuthorizePage = () => {
|
||||
setIsLoading(false);
|
||||
|
||||
navigate({
|
||||
to: "/projects/$projectId/secret-manager/integrations/cloudflare-pages/create",
|
||||
to: "/projects/secret-management/$projectId/integrations/cloudflare-pages/create",
|
||||
params: {
|
||||
projectId: currentWorkspace.id
|
||||
},
|
||||
|
||||
+2
-2
@@ -5,7 +5,7 @@ import { IntegrationsListPageTabs } from "@app/types/integrations";
|
||||
import { CloudflarePagesAuthorizePage } from "./CloudflarePagesAuthorizePage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/cloudflare-pages/authorize"
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/cloudflare-pages/authorize"
|
||||
)({
|
||||
component: CloudflarePagesAuthorizePage,
|
||||
beforeLoad: ({ context, params }) => {
|
||||
@@ -15,7 +15,7 @@ export const Route = createFileRoute(
|
||||
{
|
||||
label: "Integrations",
|
||||
link: linkOptions({
|
||||
to: "/projects/$projectId/secret-manager/integrations",
|
||||
to: "/projects/secret-management/$projectId/integrations",
|
||||
params,
|
||||
search: {
|
||||
selectedTab: IntegrationsListPageTabs.NativeIntegrations
|
||||
|
||||
+1
-1
@@ -94,7 +94,7 @@ export const CloudflarePagesConfigurePage = () => {
|
||||
setIsLoading(false);
|
||||
|
||||
navigate({
|
||||
to: "/projects/$projectId/secret-manager/integrations",
|
||||
to: "/projects/secret-management/$projectId/integrations",
|
||||
params: {
|
||||
projectId: currentWorkspace.id
|
||||
},
|
||||
|
||||
+2
-2
@@ -11,7 +11,7 @@ const CloudflarePagesConfigurePageQueryParamsSchema = z.object({
|
||||
});
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/cloudflare-pages/create"
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/cloudflare-pages/create"
|
||||
)({
|
||||
component: CloudflarePagesConfigurePage,
|
||||
validateSearch: zodValidator(CloudflarePagesConfigurePageQueryParamsSchema),
|
||||
@@ -22,7 +22,7 @@ export const Route = createFileRoute(
|
||||
{
|
||||
label: "Integrations",
|
||||
link: linkOptions({
|
||||
to: "/projects/$projectId/secret-manager/integrations",
|
||||
to: "/projects/secret-management/$projectId/integrations",
|
||||
params,
|
||||
search: {
|
||||
selectedTab: IntegrationsListPageTabs.NativeIntegrations
|
||||
|
||||
+1
-1
@@ -41,7 +41,7 @@ export const CloudflareWorkersAuthorizePage = () => {
|
||||
setIsLoading(false);
|
||||
|
||||
navigate({
|
||||
to: "/projects/$projectId/secret-manager/integrations/cloudflare-workers/create",
|
||||
to: "/projects/secret-management/$projectId/integrations/cloudflare-workers/create",
|
||||
params: {
|
||||
projectId: currentWorkspace.id
|
||||
},
|
||||
|
||||
+2
-2
@@ -5,7 +5,7 @@ import { IntegrationsListPageTabs } from "@app/types/integrations";
|
||||
import { CloudflareWorkersAuthorizePage } from "./CloudflareWorkersAuthorizePage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/cloudflare-workers/authorize"
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/cloudflare-workers/authorize"
|
||||
)({
|
||||
component: CloudflareWorkersAuthorizePage,
|
||||
beforeLoad: ({ context, params }) => {
|
||||
@@ -15,7 +15,7 @@ export const Route = createFileRoute(
|
||||
{
|
||||
label: "Integrations",
|
||||
link: linkOptions({
|
||||
to: "/projects/$projectId/secret-manager/integrations",
|
||||
to: "/projects/secret-management/$projectId/integrations",
|
||||
params,
|
||||
search: {
|
||||
selectedTab: IntegrationsListPageTabs.NativeIntegrations
|
||||
|
||||
+1
-1
@@ -67,7 +67,7 @@ export const CloudflareWorkersConfigurePage = () => {
|
||||
setIsLoading(false);
|
||||
|
||||
navigate({
|
||||
to: "/projects/$projectId/secret-manager/integrations",
|
||||
to: "/projects/secret-management/$projectId/integrations",
|
||||
params: {
|
||||
projectId: currentWorkspace.id
|
||||
},
|
||||
|
||||
+2
-2
@@ -11,7 +11,7 @@ const CloudflareWorkersConfigurePageQueryParamsSchema = z.object({
|
||||
});
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/cloudflare-workers/create"
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/cloudflare-workers/create"
|
||||
)({
|
||||
component: CloudflareWorkersConfigurePage,
|
||||
validateSearch: zodValidator(CloudflareWorkersConfigurePageQueryParamsSchema),
|
||||
@@ -22,7 +22,7 @@ export const Route = createFileRoute(
|
||||
{
|
||||
label: "Integrations",
|
||||
link: linkOptions({
|
||||
to: "/projects/$projectId/secret-manager/integrations",
|
||||
to: "/projects/secret-management/$projectId/integrations",
|
||||
params,
|
||||
search: {
|
||||
selectedTab: IntegrationsListPageTabs.NativeIntegrations
|
||||
|
||||
+1
-1
@@ -33,7 +33,7 @@ export const CodefreshAuthorizePage = () => {
|
||||
setIsLoading(false);
|
||||
|
||||
navigate({
|
||||
to: "/projects/$projectId/secret-manager/integrations/codefresh/create",
|
||||
to: "/projects/secret-management/$projectId/integrations/codefresh/create",
|
||||
params: {
|
||||
projectId: currentWorkspace.id
|
||||
},
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user