mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
feat: implemented replication to a folder strategy ui
This commit is contained in:
@@ -1,3 +1,7 @@
|
|||||||
|
export enum ReservedFolders {
|
||||||
|
SecretReplication = "__reserve_replication_"
|
||||||
|
}
|
||||||
|
|
||||||
export type TSecretFolder = {
|
export type TSecretFolder = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export type TSecretImport = {
|
|||||||
position: string;
|
position: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
|
isReserved?: boolean;
|
||||||
isReplication?: boolean;
|
isReplication?: boolean;
|
||||||
isReplicationSuccess?: boolean;
|
isReplicationSuccess?: boolean;
|
||||||
replicationStatus?: string;
|
replicationStatus?: string;
|
||||||
|
|||||||
9
frontend/src/lib/fn/string.ts
Normal file
9
frontend/src/lib/fn/string.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { ReservedFolders } from "@app/hooks/api/secretFolders/types";
|
||||||
|
|
||||||
|
export const formatReservedPaths = (secretPath: string) => {
|
||||||
|
const i = secretPath.indexOf(ReservedFolders.SecretReplication);
|
||||||
|
if (i !== -1) {
|
||||||
|
return secretPath.slice(0, i);
|
||||||
|
}
|
||||||
|
return secretPath;
|
||||||
|
};
|
||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
useUpdateSecretApprovalReviewStatus
|
useUpdateSecretApprovalReviewStatus
|
||||||
} from "@app/hooks/api";
|
} from "@app/hooks/api";
|
||||||
import { ApprovalStatus, CommitType, TWorkspaceUser } from "@app/hooks/api/types";
|
import { ApprovalStatus, CommitType, TWorkspaceUser } from "@app/hooks/api/types";
|
||||||
|
import { formatReservedPaths } from "@app/lib/fn/string";
|
||||||
|
|
||||||
import { SecretApprovalRequestAction } from "./SecretApprovalRequestAction";
|
import { SecretApprovalRequestAction } from "./SecretApprovalRequestAction";
|
||||||
import { SecretApprovalRequestChangeItem } from "./SecretApprovalRequestChangeItem";
|
import { SecretApprovalRequestChangeItem } from "./SecretApprovalRequestChangeItem";
|
||||||
@@ -200,7 +201,9 @@ export const SecretApprovalRequestChanges = ({
|
|||||||
<div className="border-r border-mineshaft-500 pr-1">
|
<div className="border-r border-mineshaft-500 pr-1">
|
||||||
<FontAwesomeIcon icon={faFolder} className="text-primary" size="sm" />
|
<FontAwesomeIcon icon={faFolder} className="text-primary" size="sm" />
|
||||||
</div>
|
</div>
|
||||||
<div className="pl-2 pb-0.5 text-sm">{secretApprovalRequestDetails.secretPath}</div>
|
<div className="pl-2 pb-0.5 text-sm">
|
||||||
|
{formatReservedPaths(secretApprovalRequestDetails.secretPath)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { z } from "zod";
|
|||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Checkbox,
|
|
||||||
FormControl,
|
FormControl,
|
||||||
Modal,
|
Modal,
|
||||||
ModalContent,
|
ModalContent,
|
||||||
@@ -151,11 +150,25 @@ export const CreateSecretImportForm = ({
|
|||||||
name="isReplication"
|
name="isReplication"
|
||||||
control={control}
|
control={control}
|
||||||
defaultValue={false}
|
defaultValue={false}
|
||||||
render={({ field }) => (
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
<Checkbox isChecked={field.value} onCheckedChange={field.onChange} id="isReplication">
|
<FormControl
|
||||||
Enable replication to synchronize changes. <br /> Warning: This will overwrite any
|
isError={Boolean(error?.message)}
|
||||||
existing secrets with the same name.
|
errorText={error?.message}
|
||||||
</Checkbox>
|
helperText={
|
||||||
|
value
|
||||||
|
? "Manual control over when updates propagate in approval mode, giving you the flexibility to push changes as needed."
|
||||||
|
: "Instantaneous updates from the linked source on approval mode, ensuring real-time synchronization."
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
value={value ? "true" : "false"}
|
||||||
|
onValueChange={(val) => onChange(val === "true")}
|
||||||
|
className="w-full border border-mineshaft-500"
|
||||||
|
>
|
||||||
|
<SelectItem value="false">Linked Mode</SelectItem>
|
||||||
|
<SelectItem value="true">Replication Mode</SelectItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<div className="mt-7 flex items-center">
|
<div className="mt-7 flex items-center">
|
||||||
|
|||||||
@@ -30,20 +30,17 @@ import {
|
|||||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||||
import { useToggle } from "@app/hooks";
|
import { useToggle } from "@app/hooks";
|
||||||
import { useResyncSecretReplication } from "@app/hooks/api";
|
import { useResyncSecretReplication } from "@app/hooks/api";
|
||||||
|
import { TSecretImport } from "@app/hooks/api/types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
environment: string;
|
environment: string;
|
||||||
secretPath?: string;
|
secretPath?: string;
|
||||||
isReplication?: boolean;
|
secretImport?: TSecretImport;
|
||||||
isReplicationSuccess?: boolean;
|
isReplicationExpand?: boolean;
|
||||||
replicationStatus?: string;
|
|
||||||
lastReplicated?: string;
|
|
||||||
importEnvName: string;
|
|
||||||
importEnvPath: string;
|
|
||||||
importedSecrets: { key: string; value: string; overriden: { env: string; secretPath: string } }[];
|
importedSecrets: { key: string; value: string; overriden: { env: string; secretPath: string } }[];
|
||||||
searchTerm: string;
|
searchTerm: string;
|
||||||
id: string;
|
onExpandReplicateSecrets: (id: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
// to show the environment and folder icon
|
// to show the environment and folder icon
|
||||||
@@ -70,25 +67,29 @@ export const EnvFolderIcon = ({
|
|||||||
|
|
||||||
export const SecretImportItem = ({
|
export const SecretImportItem = ({
|
||||||
onDelete,
|
onDelete,
|
||||||
id,
|
isReplicationExpand,
|
||||||
importEnvName,
|
|
||||||
importEnvPath,
|
|
||||||
isReplication,
|
|
||||||
importedSecrets = [],
|
importedSecrets = [],
|
||||||
searchTerm = "",
|
searchTerm = "",
|
||||||
secretPath,
|
secretPath,
|
||||||
environment,
|
environment,
|
||||||
isReplicationSuccess,
|
secretImport,
|
||||||
replicationStatus,
|
onExpandReplicateSecrets: onExpandReplicate
|
||||||
lastReplicated
|
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
|
const {
|
||||||
|
isReserved,
|
||||||
|
id,
|
||||||
|
isReplication,
|
||||||
|
isReplicationSuccess,
|
||||||
|
replicationStatus,
|
||||||
|
lastReplicated,
|
||||||
|
importEnv
|
||||||
|
} = secretImport as TSecretImport;
|
||||||
const { currentWorkspace } = useWorkspace();
|
const { currentWorkspace } = useWorkspace();
|
||||||
const [isExpanded, setIsExpanded] = useToggle();
|
const [isExpanded, setIsExpanded] = useToggle();
|
||||||
const { attributes, listeners, transform, transition, setNodeRef, isDragging } = useSortable({
|
const { attributes, listeners, transform, transition, setNodeRef, isDragging } = useSortable({
|
||||||
id
|
id
|
||||||
});
|
});
|
||||||
const resyncSecretReplication = useResyncSecretReplication();
|
const resyncSecretReplication = useResyncSecretReplication();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const filteredSecrets = importedSecrets.filter((secret) =>
|
const filteredSecrets = importedSecrets.filter((secret) =>
|
||||||
secret.key.toUpperCase().includes(searchTerm.toUpperCase())
|
secret.key.toUpperCase().includes(searchTerm.toUpperCase())
|
||||||
@@ -134,24 +135,39 @@ export const SecretImportItem = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleRowClick = () => {
|
||||||
|
if (isReplication) {
|
||||||
|
onExpandReplicate(id);
|
||||||
|
} else {
|
||||||
|
setIsExpanded.toggle();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
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",
|
||||||
|
isReserved && "hidden"
|
||||||
|
)}
|
||||||
role="button"
|
role="button"
|
||||||
ref={setNodeRef}
|
ref={setNodeRef}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
style={style}
|
style={style}
|
||||||
onClick={() => setIsExpanded.toggle()}
|
onClick={handleRowClick}
|
||||||
onKeyDown={() => setIsExpanded.toggle()}
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
handleRowClick();
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex w-12 items-center px-4 py-2 text-green-700">
|
<div className="flex w-12 items-center px-4 py-2 text-green-700">
|
||||||
<FontAwesomeIcon icon={faFileImport} />
|
<FontAwesomeIcon icon={faFileImport} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-grow items-center px-4 py-2">
|
<div className="flex flex-grow items-center px-4 py-2">
|
||||||
<EnvFolderIcon
|
<EnvFolderIcon
|
||||||
env={importEnvName || ""}
|
env={importEnv.slug || ""}
|
||||||
secretPath={importEnvPath}
|
secretPath={secretImport?.importPath || ""}
|
||||||
isReplication={isReplication}
|
isReplication={isReplication}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -193,29 +209,31 @@ export const SecretImportItem = ({
|
|||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
<ProjectPermissionCan
|
{isReplication && (
|
||||||
I={ProjectPermissionActions.Edit}
|
<ProjectPermissionCan
|
||||||
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
I={ProjectPermissionActions.Edit}
|
||||||
renderTooltip
|
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
||||||
allowedLabel="Resync replicated secrets"
|
renderTooltip
|
||||||
>
|
allowedLabel="Resync replicated secrets"
|
||||||
{(isAllowed) => (
|
>
|
||||||
<IconButton
|
{(isAllowed) => (
|
||||||
size="md"
|
<IconButton
|
||||||
colorSchema="primary"
|
size="md"
|
||||||
variant="plain"
|
colorSchema="primary"
|
||||||
ariaLabel="expand"
|
variant="plain"
|
||||||
className={twMerge(
|
ariaLabel="expand"
|
||||||
"p-0 opacity-0 group-hover:opacity-100",
|
className={twMerge(
|
||||||
resyncSecretReplication.isLoading && "animate-spin opacity-100"
|
"p-0 opacity-0 group-hover:opacity-100",
|
||||||
)}
|
resyncSecretReplication.isLoading && "animate-spin opacity-100"
|
||||||
isDisabled={!isAllowed}
|
)}
|
||||||
onClick={handleResyncSecretReplication}
|
isDisabled={!isAllowed}
|
||||||
>
|
onClick={handleResyncSecretReplication}
|
||||||
<FontAwesomeIcon icon={faRotate} />
|
>
|
||||||
</IconButton>
|
<FontAwesomeIcon icon={faRotate} />
|
||||||
)}
|
</IconButton>
|
||||||
</ProjectPermissionCan>
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-4 border-l border-mineshaft-600 px-4 py-2">
|
<div className="flex items-center space-x-4 border-l border-mineshaft-600 px-4 py-2">
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
@@ -264,7 +282,7 @@ export const SecretImportItem = ({
|
|||||||
</ProjectPermissionCan>
|
</ProjectPermissionCan>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{!isReplication && isExpanded && !isDragging && (
|
{!isReplication && (isReplicationExpand || isExpanded) && !isDragging && (
|
||||||
<td
|
<td
|
||||||
colSpan={3}
|
colSpan={3}
|
||||||
className={`bg-bunker-800 ${isExpanded && "border-b-2 border-mineshaft-500"}`}
|
className={`bg-bunker-800 ${isExpanded && "border-b-2 border-mineshaft-500"}`}
|
||||||
|
|||||||
@@ -16,8 +16,10 @@ import { createNotification } from "@app/components/notifications";
|
|||||||
import { DeleteActionModal } from "@app/components/v2";
|
import { DeleteActionModal } from "@app/components/v2";
|
||||||
import { usePopUp } from "@app/hooks";
|
import { usePopUp } from "@app/hooks";
|
||||||
import { useDeleteSecretImport, useUpdateSecretImport } from "@app/hooks/api";
|
import { useDeleteSecretImport, useUpdateSecretImport } from "@app/hooks/api";
|
||||||
|
import { ReservedFolders } from "@app/hooks/api/secretFolders/types";
|
||||||
import { TSecretImport } from "@app/hooks/api/secretImports/types";
|
import { TSecretImport } from "@app/hooks/api/secretImports/types";
|
||||||
import { DecryptedSecret, WorkspaceEnv } from "@app/hooks/api/types";
|
import { DecryptedSecret, WorkspaceEnv } from "@app/hooks/api/types";
|
||||||
|
import { formatReservedPaths } from "@app/lib/fn/string";
|
||||||
|
|
||||||
import { SecretImportItem } from "./SecretImportItem";
|
import { SecretImportItem } from "./SecretImportItem";
|
||||||
|
|
||||||
@@ -50,7 +52,7 @@ export const computeImportedSecretRows = (
|
|||||||
importSecrets[i].secrets.forEach((el) => {
|
importSecrets[i].secrets.forEach((el) => {
|
||||||
overridenSec[el.key] = {
|
overridenSec[el.key] = {
|
||||||
env: importSecrets[i].environmentInfo.name,
|
env: importSecrets[i].environmentInfo.name,
|
||||||
secretPath: importSecrets[i].secretPath
|
secretPath: formatReservedPaths(importSecrets[i].secretPath)
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -91,6 +93,8 @@ export const SecretImportListView = ({
|
|||||||
"deleteSecretImport"
|
"deleteSecretImport"
|
||||||
] as const);
|
] as const);
|
||||||
|
|
||||||
|
const [replicationSecrets, setReplicationSecrets] = useState<Record<string, boolean>>({});
|
||||||
|
|
||||||
const sensors = useSensors(
|
const sensors = useSensors(
|
||||||
useSensor(MouseSensor, {}),
|
useSensor(MouseSensor, {}),
|
||||||
useSensor(TouchSensor, {}),
|
useSensor(TouchSensor, {}),
|
||||||
@@ -149,6 +153,22 @@ export const SecretImportListView = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleOpenReplicationSecrets = (replicationImportId: string) => {
|
||||||
|
console.log(secretImports);
|
||||||
|
const reservedImport = secretImports.find(
|
||||||
|
({ isReserved, importPath, importEnv }) =>
|
||||||
|
importEnv.slug === environment &&
|
||||||
|
isReserved &&
|
||||||
|
importPath === `/${ReservedFolders.SecretReplication}${replicationImportId}`
|
||||||
|
);
|
||||||
|
if (reservedImport) {
|
||||||
|
setReplicationSecrets((state) => ({
|
||||||
|
...state,
|
||||||
|
[reservedImport.id]: !state?.[reservedImport.id]
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DndContext
|
<DndContext
|
||||||
@@ -159,29 +179,17 @@ export const SecretImportListView = ({
|
|||||||
>
|
>
|
||||||
<SortableContext items={items} strategy={verticalListSortingStrategy}>
|
<SortableContext items={items} strategy={verticalListSortingStrategy}>
|
||||||
{items?.map((item) => {
|
{items?.map((item) => {
|
||||||
const {
|
// TODO(akhilmhdh): change this and pass this whole object instead of one by one
|
||||||
importPath,
|
|
||||||
importEnv,
|
|
||||||
id,
|
|
||||||
isReplication,
|
|
||||||
replicationStatus,
|
|
||||||
lastReplicated,
|
|
||||||
isReplicationSuccess
|
|
||||||
} = item;
|
|
||||||
return (
|
return (
|
||||||
<SecretImportItem
|
<SecretImportItem
|
||||||
searchTerm={searchTerm}
|
searchTerm={searchTerm}
|
||||||
key={`imported-env-${id}`}
|
key={`imported-env-${item.id}`}
|
||||||
id={id}
|
isReplicationExpand={replicationSecrets?.[item.id]}
|
||||||
isReplication={isReplication}
|
onExpandReplicateSecrets={handleOpenReplicationSecrets}
|
||||||
importEnvPath={importPath}
|
secretImport={item}
|
||||||
importEnvName={importEnv.name}
|
|
||||||
lastReplicated={lastReplicated}
|
|
||||||
replicationStatus={replicationStatus}
|
|
||||||
isReplicationSuccess={isReplicationSuccess}
|
|
||||||
importedSecrets={computeImportedSecretRows(
|
importedSecrets={computeImportedSecretRows(
|
||||||
importEnv.slug,
|
item.importEnv.slug,
|
||||||
importPath,
|
item.importPath,
|
||||||
importedSecrets,
|
importedSecrets,
|
||||||
secrets
|
secrets
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user