mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-21 05:00:56 +00:00
Merge pull request #4667 from Infisical/feat/in-platform-vault-migration-tooling
feat: in-platform migration tooling for Vault policies + scaffolding
This commit is contained in:
+61
-35
@@ -7,6 +7,7 @@ import { OrgMembershipRole } from "@app/helpers/roles";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
|
||||
import { SelectImportFromPlatformModal } from "./components/SelectImportFromPlatformModal";
|
||||
import { VaultConnectionSection } from "./components/VaultConnectionSection";
|
||||
|
||||
export const ExternalMigrationsTab = () => {
|
||||
const { hasOrgRole } = useOrgPermission();
|
||||
@@ -14,45 +15,70 @@ export const ExternalMigrationsTab = () => {
|
||||
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["selectImportPlatform"] as const);
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="flex justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="text-xl font-medium text-mineshaft-100">Import from external source</p>
|
||||
<div className="flex flex-col gap-6">
|
||||
{/* In-Platform Migration Tooling Section */}
|
||||
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4">
|
||||
<h2 className="text-xl font-medium text-mineshaft-100">In-Platform Migration Tooling</h2>
|
||||
<p className="mt-1 mb-6 text-sm text-gray-400">
|
||||
Configure platform connections to enable migration features throughout Infisical, such
|
||||
as importing policies and resources directly within the UI.
|
||||
</p>
|
||||
</div>
|
||||
<VaultConnectionSection />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://infisical.com/docs/documentation/platform/external-migrations/overview"
|
||||
>
|
||||
<div className="ml-2 inline-block rounded-md bg-yellow/20 px-1.5 pt-[0.04rem] pb-[0.03rem] text-sm text-yellow opacity-80 hover:opacity-100">
|
||||
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
|
||||
Docs
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowUpRightFromSquare}
|
||||
className="text-xxs mb-[0.07rem] ml-1.5"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
{/* Bulk Data Import Section */}
|
||||
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4">
|
||||
<h2 className="text-xl font-medium text-mineshaft-100">Bulk Data Import</h2>
|
||||
<p className="mt-1 mb-6 text-sm text-gray-400">
|
||||
Perform one-time bulk imports of data from external platforms.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={() => {
|
||||
handlePopUpOpen("selectImportPlatform");
|
||||
}}
|
||||
isDisabled={!hasOrgRole(OrgMembershipRole.Admin)}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
>
|
||||
Import
|
||||
</Button>
|
||||
</div>
|
||||
<p className="mb-4 text-gray-400">Import data from another platform to Infisical.</p>
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="text-base font-medium text-mineshaft-100">
|
||||
Import from external source
|
||||
</p>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://infisical.com/docs/documentation/platform/external-migrations/overview"
|
||||
>
|
||||
<div className="inline-block rounded-md bg-yellow/20 px-1.5 pt-[0.04rem] pb-[0.03rem] text-sm text-yellow opacity-80 hover:opacity-100">
|
||||
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
|
||||
Docs
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowUpRightFromSquare}
|
||||
className="text-xxs mb-[0.07rem] ml-1.5"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<p className="mt-1 text-sm text-gray-400">
|
||||
Import data from another platform to Infisical.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<SelectImportFromPlatformModal
|
||||
isOpen={popUp.selectImportPlatform.isOpen}
|
||||
onToggle={(state) => handlePopUpToggle("selectImportPlatform", state)}
|
||||
/>
|
||||
<Button
|
||||
onClick={() => {
|
||||
handlePopUpOpen("selectImportPlatform");
|
||||
}}
|
||||
isDisabled={!hasOrgRole(OrgMembershipRole.Admin)}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
>
|
||||
Import
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<SelectImportFromPlatformModal
|
||||
isOpen={popUp.selectImportPlatform.isOpen}
|
||||
onToggle={(state) => handlePopUpToggle("selectImportPlatform", state)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
+197
@@ -0,0 +1,197 @@
|
||||
import { useState } from "react";
|
||||
import { faEdit, faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Link } from "@tanstack/react-router";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Button,
|
||||
DeleteActionModal,
|
||||
EmptyState,
|
||||
Table,
|
||||
TableContainer,
|
||||
TableSkeleton,
|
||||
TBody,
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { useListAppConnections } from "@app/hooks/api/appConnections/queries";
|
||||
import {
|
||||
useDeleteVaultExternalMigrationConfig,
|
||||
useGetVaultExternalMigrationConfigs
|
||||
} from "@app/hooks/api/migration";
|
||||
import { TVaultExternalMigrationConfig } from "@app/hooks/api/migration/types";
|
||||
|
||||
import { VaultNamespaceConfigModal } from "./VaultNamespaceConfigModal";
|
||||
|
||||
export const VaultConnectionSection = () => {
|
||||
const [selectedConfig, setSelectedConfig] = useState<TVaultExternalMigrationConfig | null>(null);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const [configToDelete, setConfigToDelete] = useState<TVaultExternalMigrationConfig | null>(null);
|
||||
|
||||
const { data: configs = [], isPending: isLoadingConfigs } = useGetVaultExternalMigrationConfigs();
|
||||
const { data: appConnections = [] } = useListAppConnections();
|
||||
const { mutateAsync: deleteConfig } = useDeleteVaultExternalMigrationConfig();
|
||||
|
||||
const handleEdit = (config: TVaultExternalMigrationConfig) => {
|
||||
setSelectedConfig(config);
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
const handleAdd = () => {
|
||||
setSelectedConfig(null);
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
const handleDeleteClick = (config: TVaultExternalMigrationConfig) => {
|
||||
setConfigToDelete(config);
|
||||
setIsDeleteModalOpen(true);
|
||||
};
|
||||
|
||||
const handleDeleteConfirm = async () => {
|
||||
if (!configToDelete) return;
|
||||
|
||||
try {
|
||||
await deleteConfig({ id: configToDelete.id });
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Namespace configuration deleted successfully"
|
||||
});
|
||||
setIsDeleteModalOpen(false);
|
||||
setConfigToDelete(null);
|
||||
} catch (error) {
|
||||
console.error("Failed to delete namespace config:", error);
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Failed to delete namespace configuration"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const getConnectionName = (connectionId: string | null) => {
|
||||
if (!connectionId) return "None";
|
||||
const connection = appConnections.find((conn) => conn.id === connectionId);
|
||||
return connection?.name || "Unknown";
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<img
|
||||
src="/images/integrations/Vault.png"
|
||||
alt="HashiCorp Vault logo"
|
||||
className="h-10 w-10 rounded-md bg-bunker-500 p-2"
|
||||
/>
|
||||
<div>
|
||||
<h3 className="text-lg font-medium text-mineshaft-100">HashiCorp Vault</h3>
|
||||
<p className="text-sm text-gray-400">
|
||||
Enable in-platform migration tooling for policy imports, auth methods, and secret
|
||||
engine migrations
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
colorSchema="primary"
|
||||
type="submit"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={handleAdd}
|
||||
>
|
||||
Add Namespace
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>Namespace</Th>
|
||||
<Th>Connection</Th>
|
||||
<Th className="w-5" />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{isLoadingConfigs && (
|
||||
<TableSkeleton columns={3} innerKey="vault-configs-loading" rows={3} />
|
||||
)}
|
||||
{!isLoadingConfigs && configs.length === 0 && (
|
||||
<Tr>
|
||||
<Td colSpan={3}>
|
||||
<EmptyState title="No namespace configurations" icon={faPlus} className="py-8">
|
||||
<p className="mb-4 text-sm text-mineshaft-400">
|
||||
Add a namespace configuration to enable in-platform migration features.
|
||||
</p>
|
||||
</EmptyState>
|
||||
</Td>
|
||||
</Tr>
|
||||
)}
|
||||
{!isLoadingConfigs &&
|
||||
configs.map((config) => (
|
||||
<Tr key={config.id} className="group h-10">
|
||||
<Td>{config.namespace}</Td>
|
||||
<Td>{getConnectionName(config.connectionId)}</Td>
|
||||
<Td>
|
||||
<div className="flex items-center justify-end gap-2 opacity-0 transition-opacity group-hover:opacity-100">
|
||||
<Button
|
||||
variant="plain"
|
||||
colorSchema="secondary"
|
||||
size="xs"
|
||||
onClick={() => handleEdit(config)}
|
||||
leftIcon={<FontAwesomeIcon icon={faEdit} />}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
<Button
|
||||
variant="plain"
|
||||
colorSchema="danger"
|
||||
size="xs"
|
||||
onClick={() => handleDeleteClick(config)}
|
||||
leftIcon={<FontAwesomeIcon icon={faTrash} />}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</TBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
<p className="mt-4 text-xs text-mineshaft-400">
|
||||
Configure namespace-specific connections to enable in-platform migration features. Manage
|
||||
connections in the{" "}
|
||||
<Link
|
||||
to="/organization/app-connections"
|
||||
className="text-primary underline hover:text-primary-300"
|
||||
>
|
||||
App Connections
|
||||
</Link>{" "}
|
||||
section.
|
||||
</p>
|
||||
|
||||
<VaultNamespaceConfigModal
|
||||
isOpen={isModalOpen}
|
||||
onOpenChange={(open) => {
|
||||
setIsModalOpen(open);
|
||||
if (!open) setSelectedConfig(null);
|
||||
}}
|
||||
editConfig={selectedConfig || undefined}
|
||||
/>
|
||||
|
||||
<DeleteActionModal
|
||||
isOpen={isDeleteModalOpen}
|
||||
title={`Delete namespace configuration for "${configToDelete?.namespace}"?`}
|
||||
onChange={(open) => {
|
||||
setIsDeleteModalOpen(open);
|
||||
if (!open) setConfigToDelete(null);
|
||||
}}
|
||||
deleteKey="confirm"
|
||||
onDeleteApproved={handleDeleteConfirm}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+189
@@ -0,0 +1,189 @@
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Button,
|
||||
FilterableSelect,
|
||||
FormControl,
|
||||
Input,
|
||||
Modal,
|
||||
ModalContent
|
||||
} from "@app/components/v2";
|
||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||
import { useListAppConnections } from "@app/hooks/api/appConnections/queries";
|
||||
import {
|
||||
useCreateVaultExternalMigrationConfig,
|
||||
useUpdateVaultExternalMigrationConfig
|
||||
} from "@app/hooks/api/migration";
|
||||
import { TVaultExternalMigrationConfig } from "@app/hooks/api/migration/types";
|
||||
|
||||
const schema = z.object({
|
||||
namespace: z
|
||||
.string()
|
||||
.min(1, "Namespace is required. If you intend to use the root namespace, use root or /."),
|
||||
connectionId: z.string().min(1, "Connection is required")
|
||||
});
|
||||
|
||||
type FormData = z.infer<typeof schema>;
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
onOpenChange: (isOpen: boolean) => void;
|
||||
editConfig?: TVaultExternalMigrationConfig;
|
||||
};
|
||||
|
||||
export const VaultNamespaceConfigModal = ({ isOpen, onOpenChange, editConfig }: Props) => {
|
||||
const isEdit = Boolean(editConfig);
|
||||
|
||||
const { data: appConnections = [], isPending: isLoadingConnections } = useListAppConnections();
|
||||
|
||||
const vaultConnections = useMemo(
|
||||
() => appConnections.filter((conn) => conn.app === AppConnection.HCVault),
|
||||
[appConnections]
|
||||
);
|
||||
|
||||
const { mutateAsync: createConfig, isPending: isCreating } =
|
||||
useCreateVaultExternalMigrationConfig();
|
||||
const { mutateAsync: updateConfig, isPending: isUpdating } =
|
||||
useUpdateVaultExternalMigrationConfig();
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { errors, isSubmitting }
|
||||
} = useForm<FormData>({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: {
|
||||
namespace: "",
|
||||
connectionId: ""
|
||||
}
|
||||
});
|
||||
|
||||
// Reset form when editConfig changes or modal opens
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
reset({
|
||||
namespace: editConfig?.namespace || "",
|
||||
connectionId: editConfig?.connectionId || ""
|
||||
});
|
||||
}
|
||||
}, [isOpen, editConfig, reset]);
|
||||
|
||||
const onFormSubmit = async (data: FormData) => {
|
||||
try {
|
||||
if (isEdit && editConfig) {
|
||||
await updateConfig({
|
||||
id: editConfig.id,
|
||||
namespace: data.namespace,
|
||||
connectionId: data.connectionId
|
||||
});
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Namespace configuration updated successfully"
|
||||
});
|
||||
} else {
|
||||
await createConfig({
|
||||
namespace: data.namespace,
|
||||
connectionId: data.connectionId
|
||||
});
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Namespace configuration created successfully"
|
||||
});
|
||||
}
|
||||
reset();
|
||||
onOpenChange(false);
|
||||
} catch (error) {
|
||||
console.error("Failed to save namespace config:", error);
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: `Failed to ${isEdit ? "update" : "create"} namespace configuration`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
reset();
|
||||
onOpenChange(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onOpenChange={handleClose}>
|
||||
<ModalContent
|
||||
title={isEdit ? "Edit Namespace Configuration" : "Add Namespace Configuration"}
|
||||
subTitle={`Configure a HashiCorp Vault namespace ${isEdit ? "configuration" : "for migration tooling"}`}
|
||||
bodyClassName="overflow-visible"
|
||||
>
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="namespace"
|
||||
render={({ field }) => (
|
||||
<FormControl
|
||||
label="Namespace"
|
||||
isError={Boolean(errors.namespace)}
|
||||
errorText={errors.namespace?.message}
|
||||
className="mb-4"
|
||||
>
|
||||
<Input {...field} placeholder="e.g., admin, dev, prod" autoComplete="off" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
name="connectionId"
|
||||
render={({ field }) => {
|
||||
const selectedConnection = vaultConnections.find((conn) => conn.id === field.value);
|
||||
|
||||
return (
|
||||
<FormControl
|
||||
label="Vault Connection"
|
||||
isError={Boolean(errors.connectionId)}
|
||||
errorText={errors.connectionId?.message}
|
||||
tooltipText="Select a HashiCorp Vault app connection for this namespace"
|
||||
>
|
||||
<FilterableSelect
|
||||
value={selectedConnection || null}
|
||||
onChange={(newValue) => {
|
||||
const singleValue = Array.isArray(newValue) ? newValue[0] : newValue;
|
||||
if (singleValue && "id" in singleValue) {
|
||||
field.onChange(singleValue.id);
|
||||
} else {
|
||||
field.onChange("");
|
||||
}
|
||||
}}
|
||||
isLoading={isLoadingConnections}
|
||||
options={vaultConnections}
|
||||
placeholder="Select connection..."
|
||||
getOptionLabel={(option) => option.name}
|
||||
getOptionValue={(option) => option.id}
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="mt-8 flex items-center gap-2">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isSubmitting || isCreating || isUpdating}
|
||||
isDisabled={isSubmitting || isCreating || isUpdating}
|
||||
>
|
||||
{isEdit ? "Update" : "Create"}
|
||||
</Button>
|
||||
<Button colorSchema="secondary" variant="plain" onClick={handleClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user