feat: implemented replication to a folder strategy ui

This commit is contained in:
=
2024-05-30 20:40:51 +05:30
parent 5db6ac711c
commit a13d4a4970
7 changed files with 127 additions and 71 deletions

View File

@@ -1,3 +1,7 @@
export enum ReservedFolders {
SecretReplication = "__reserve_replication_"
}
export type TSecretFolder = {
id: string;
name: string;

View File

@@ -10,6 +10,7 @@ export type TSecretImport = {
position: string;
createdAt: string;
updatedAt: string;
isReserved?: boolean;
isReplication?: boolean;
isReplicationSuccess?: boolean;
replicationStatus?: string;

View 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;
};

View File

@@ -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>

View File

@@ -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">

View File

@@ -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"}`}

View File

@@ -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
)}