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 = {
|
||||
id: string;
|
||||
name: string;
|
||||
|
||||
@@ -10,6 +10,7 @@ export type TSecretImport = {
|
||||
position: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
isReserved?: boolean;
|
||||
isReplication?: boolean;
|
||||
isReplicationSuccess?: boolean;
|
||||
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
|
||||
} from "@app/hooks/api";
|
||||
import { ApprovalStatus, CommitType, TWorkspaceUser } from "@app/hooks/api/types";
|
||||
import { formatReservedPaths } from "@app/lib/fn/string";
|
||||
|
||||
import { SecretApprovalRequestAction } from "./SecretApprovalRequestAction";
|
||||
import { SecretApprovalRequestChangeItem } from "./SecretApprovalRequestChangeItem";
|
||||
@@ -200,7 +201,9 @@ export const SecretApprovalRequestChanges = ({
|
||||
<div className="border-r border-mineshaft-500 pr-1">
|
||||
<FontAwesomeIcon icon={faFolder} className="text-primary" size="sm" />
|
||||
</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>
|
||||
|
||||
@@ -6,7 +6,6 @@ import { z } from "zod";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
FormControl,
|
||||
Modal,
|
||||
ModalContent,
|
||||
@@ -151,11 +150,25 @@ export const CreateSecretImportForm = ({
|
||||
name="isReplication"
|
||||
control={control}
|
||||
defaultValue={false}
|
||||
render={({ field }) => (
|
||||
<Checkbox isChecked={field.value} onCheckedChange={field.onChange} id="isReplication">
|
||||
Enable replication to synchronize changes. <br /> Warning: This will overwrite any
|
||||
existing secrets with the same name.
|
||||
</Checkbox>
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
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">
|
||||
|
||||
@@ -30,20 +30,17 @@ import {
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import { useToggle } from "@app/hooks";
|
||||
import { useResyncSecretReplication } from "@app/hooks/api";
|
||||
import { TSecretImport } from "@app/hooks/api/types";
|
||||
|
||||
type Props = {
|
||||
onDelete: () => void;
|
||||
environment: string;
|
||||
secretPath?: string;
|
||||
isReplication?: boolean;
|
||||
isReplicationSuccess?: boolean;
|
||||
replicationStatus?: string;
|
||||
lastReplicated?: string;
|
||||
importEnvName: string;
|
||||
importEnvPath: string;
|
||||
secretImport?: TSecretImport;
|
||||
isReplicationExpand?: boolean;
|
||||
importedSecrets: { key: string; value: string; overriden: { env: string; secretPath: string } }[];
|
||||
searchTerm: string;
|
||||
id: string;
|
||||
onExpandReplicateSecrets: (id: string) => void;
|
||||
};
|
||||
|
||||
// to show the environment and folder icon
|
||||
@@ -70,25 +67,29 @@ export const EnvFolderIcon = ({
|
||||
|
||||
export const SecretImportItem = ({
|
||||
onDelete,
|
||||
id,
|
||||
importEnvName,
|
||||
importEnvPath,
|
||||
isReplication,
|
||||
isReplicationExpand,
|
||||
importedSecrets = [],
|
||||
searchTerm = "",
|
||||
secretPath,
|
||||
environment,
|
||||
isReplicationSuccess,
|
||||
replicationStatus,
|
||||
lastReplicated
|
||||
secretImport,
|
||||
onExpandReplicateSecrets: onExpandReplicate
|
||||
}: Props) => {
|
||||
const {
|
||||
isReserved,
|
||||
id,
|
||||
isReplication,
|
||||
isReplicationSuccess,
|
||||
replicationStatus,
|
||||
lastReplicated,
|
||||
importEnv
|
||||
} = secretImport as TSecretImport;
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const [isExpanded, setIsExpanded] = useToggle();
|
||||
const { attributes, listeners, transform, transition, setNodeRef, isDragging } = useSortable({
|
||||
id
|
||||
});
|
||||
const resyncSecretReplication = useResyncSecretReplication();
|
||||
|
||||
useEffect(() => {
|
||||
const filteredSecrets = importedSecrets.filter((secret) =>
|
||||
secret.key.toUpperCase().includes(searchTerm.toUpperCase())
|
||||
@@ -134,24 +135,39 @@ export const SecretImportItem = ({
|
||||
}
|
||||
};
|
||||
|
||||
const handleRowClick = () => {
|
||||
if (isReplication) {
|
||||
onExpandReplicate(id);
|
||||
} else {
|
||||
setIsExpanded.toggle();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<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"
|
||||
ref={setNodeRef}
|
||||
tabIndex={0}
|
||||
style={style}
|
||||
onClick={() => setIsExpanded.toggle()}
|
||||
onKeyDown={() => setIsExpanded.toggle()}
|
||||
onClick={handleRowClick}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
handleRowClick();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="flex w-12 items-center px-4 py-2 text-green-700">
|
||||
<FontAwesomeIcon icon={faFileImport} />
|
||||
</div>
|
||||
<div className="flex flex-grow items-center px-4 py-2">
|
||||
<EnvFolderIcon
|
||||
env={importEnvName || ""}
|
||||
secretPath={importEnvPath}
|
||||
env={importEnv.slug || ""}
|
||||
secretPath={secretImport?.importPath || ""}
|
||||
isReplication={isReplication}
|
||||
/>
|
||||
</div>
|
||||
@@ -193,29 +209,31 @@ export const SecretImportItem = ({
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
||||
renderTooltip
|
||||
allowedLabel="Resync replicated secrets"
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
size="md"
|
||||
colorSchema="primary"
|
||||
variant="plain"
|
||||
ariaLabel="expand"
|
||||
className={twMerge(
|
||||
"p-0 opacity-0 group-hover:opacity-100",
|
||||
resyncSecretReplication.isLoading && "animate-spin opacity-100"
|
||||
)}
|
||||
isDisabled={!isAllowed}
|
||||
onClick={handleResyncSecretReplication}
|
||||
>
|
||||
<FontAwesomeIcon icon={faRotate} />
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
{isReplication && (
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
||||
renderTooltip
|
||||
allowedLabel="Resync replicated secrets"
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
size="md"
|
||||
colorSchema="primary"
|
||||
variant="plain"
|
||||
ariaLabel="expand"
|
||||
className={twMerge(
|
||||
"p-0 opacity-0 group-hover:opacity-100",
|
||||
resyncSecretReplication.isLoading && "animate-spin opacity-100"
|
||||
)}
|
||||
isDisabled={!isAllowed}
|
||||
onClick={handleResyncSecretReplication}
|
||||
>
|
||||
<FontAwesomeIcon icon={faRotate} />
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center space-x-4 border-l border-mineshaft-600 px-4 py-2">
|
||||
<ProjectPermissionCan
|
||||
@@ -264,7 +282,7 @@ export const SecretImportItem = ({
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
</div>
|
||||
{!isReplication && isExpanded && !isDragging && (
|
||||
{!isReplication && (isReplicationExpand || isExpanded) && !isDragging && (
|
||||
<td
|
||||
colSpan={3}
|
||||
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 { usePopUp } from "@app/hooks";
|
||||
import { useDeleteSecretImport, useUpdateSecretImport } from "@app/hooks/api";
|
||||
import { ReservedFolders } from "@app/hooks/api/secretFolders/types";
|
||||
import { TSecretImport } from "@app/hooks/api/secretImports/types";
|
||||
import { DecryptedSecret, WorkspaceEnv } from "@app/hooks/api/types";
|
||||
import { formatReservedPaths } from "@app/lib/fn/string";
|
||||
|
||||
import { SecretImportItem } from "./SecretImportItem";
|
||||
|
||||
@@ -50,7 +52,7 @@ export const computeImportedSecretRows = (
|
||||
importSecrets[i].secrets.forEach((el) => {
|
||||
overridenSec[el.key] = {
|
||||
env: importSecrets[i].environmentInfo.name,
|
||||
secretPath: importSecrets[i].secretPath
|
||||
secretPath: formatReservedPaths(importSecrets[i].secretPath)
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -91,6 +93,8 @@ export const SecretImportListView = ({
|
||||
"deleteSecretImport"
|
||||
] as const);
|
||||
|
||||
const [replicationSecrets, setReplicationSecrets] = useState<Record<string, boolean>>({});
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(MouseSensor, {}),
|
||||
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 (
|
||||
<>
|
||||
<DndContext
|
||||
@@ -159,29 +179,17 @@ export const SecretImportListView = ({
|
||||
>
|
||||
<SortableContext items={items} strategy={verticalListSortingStrategy}>
|
||||
{items?.map((item) => {
|
||||
const {
|
||||
importPath,
|
||||
importEnv,
|
||||
id,
|
||||
isReplication,
|
||||
replicationStatus,
|
||||
lastReplicated,
|
||||
isReplicationSuccess
|
||||
} = item;
|
||||
// TODO(akhilmhdh): change this and pass this whole object instead of one by one
|
||||
return (
|
||||
<SecretImportItem
|
||||
searchTerm={searchTerm}
|
||||
key={`imported-env-${id}`}
|
||||
id={id}
|
||||
isReplication={isReplication}
|
||||
importEnvPath={importPath}
|
||||
importEnvName={importEnv.name}
|
||||
lastReplicated={lastReplicated}
|
||||
replicationStatus={replicationStatus}
|
||||
isReplicationSuccess={isReplicationSuccess}
|
||||
key={`imported-env-${item.id}`}
|
||||
isReplicationExpand={replicationSecrets?.[item.id]}
|
||||
onExpandReplicateSecrets={handleOpenReplicationSecrets}
|
||||
secretImport={item}
|
||||
importedSecrets={computeImportedSecretRows(
|
||||
importEnv.slug,
|
||||
importPath,
|
||||
item.importEnv.slug,
|
||||
item.importPath,
|
||||
importedSecrets,
|
||||
secrets
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user