diff --git a/changelog/8133-system-data-flow-antd-migration.yaml b/changelog/8133-system-data-flow-antd-migration.yaml new file mode 100644 index 00000000000..f26b11b558b --- /dev/null +++ b/changelog/8133-system-data-flow-antd-migration.yaml @@ -0,0 +1,4 @@ +type: Changed +description: Migrated system data flow components from Chakra/Formik to Ant Design +pr: 8133 +labels: [] diff --git a/clients/admin-ui/src/features/common/system-data-flow/DataFlowAccordion.tsx b/clients/admin-ui/src/features/common/system-data-flow/DataFlowAccordion.tsx index 49736a11a52..7250a819da3 100644 --- a/clients/admin-ui/src/features/common/system-data-flow/DataFlowAccordion.tsx +++ b/clients/admin-ui/src/features/common/system-data-flow/DataFlowAccordion.tsx @@ -1,4 +1,4 @@ -import { ChakraAccordion as Accordion } from "fidesui"; +import { Flex } from "fidesui"; import React from "react"; import { System } from "~/types/api"; @@ -14,12 +14,12 @@ export const DataFlowAccordion = ({ system, isSystemTab, }: DataFlowFormProps) => ( - + - + ); diff --git a/clients/admin-ui/src/features/common/system-data-flow/DataFlowAccordionForm.tsx b/clients/admin-ui/src/features/common/system-data-flow/DataFlowAccordionForm.tsx index b74cdc4b4f4..e6540ad3de1 100644 --- a/clients/admin-ui/src/features/common/system-data-flow/DataFlowAccordionForm.tsx +++ b/clients/admin-ui/src/features/common/system-data-flow/DataFlowAccordionForm.tsx @@ -1,40 +1,31 @@ -// External libraries import { Button, - ChakraAccordionButton as AccordionButton, - ChakraAccordionIcon as AccordionIcon, - ChakraAccordionItem as AccordionItem, - ChakraAccordionPanel as AccordionPanel, - ChakraFlex as Flex, - ChakraSpacer as Spacer, - ChakraStack as Stack, - ChakraTag as Tag, - ChakraText as Text, + Collapse, + CollapseProps, + Flex, Icons, - useChakraDisclosure as useDisclosure, + Space, + Tag, + Text, useMessage, } from "fidesui"; -import { Form, Formik, FormikHelpers } from "formik"; -import React, { useEffect, useMemo, useState } from "react"; +import React, { useCallback, useEffect, useMemo, useState } from "react"; -// Internal features +import { useAppDispatch } from "~/app/hooks"; import { isErrorResult } from "~/features/common/helpers"; -import { FormGuard } from "~/features/common/hooks/useIsAnyFormDirty"; +import { + registerForm, + unregisterForm, + updateDirtyFormState, +} from "~/features/common/hooks/dirty-forms.slice"; import { DataFlowSystemsDeleteTable } from "~/features/common/system-data-flow/DataFlowSystemsDeleteTable"; import DataFlowSystemsModal from "~/features/common/system-data-flow/DataFlowSystemsModal"; -// API types and hooks import { useGetAllSystemsQuery, useUpdateSystemMutation, } from "~/features/system"; import { DataFlow, System } from "~/types/api"; -const defaultInitialValues = { - dataFlowSystems: [] as DataFlow[], -}; - -export type FormValues = typeof defaultInitialValues; - type DataFlowAccordionItemProps = { isIngress?: boolean; system: System; @@ -47,9 +38,11 @@ export const DataFlowAccordionForm = ({ isSystemTab, }: DataFlowAccordionItemProps) => { const message = useMessage(); + const dispatch = useAppDispatch(); const flowType = isIngress ? "Source" : "Destination"; const pluralFlowType = `${flowType}s`; - const dataFlowSystemsModal = useDisclosure(); + const [modalOpen, setModalOpen] = useState(false); + const [isSubmitting, setIsSubmitting] = useState(false); const [updateSystemMutationTrigger] = useUpdateSystemMutation(); const { data: systems = [] } = useGetAllSystemsQuery(); @@ -64,21 +57,34 @@ export const DataFlowAccordionForm = ({ return dataFlows.filter((df) => systemFidesKeys.includes(df.fides_key)); }, [isIngress, system, systems]); - const [assignedDataFlow, setAssignedDataFlows] = + const [assignedDataFlows, setAssignedDataFlows] = useState(initialDataFlows); useEffect(() => { setAssignedDataFlows(initialDataFlows); }, [initialDataFlows]); - const handleSubmit = async ( - { dataFlowSystems }: FormValues, - { resetForm }: FormikHelpers, - ) => { + const isDirty = assignedDataFlows !== initialDataFlows; + + // FormGuard: register/unregister form and track dirty state via Redux + const formId = `${system.fides_key}:${flowType}`; + useEffect(() => { + dispatch(registerForm({ id: formId, name: `${flowType} Data Flow` })); + return () => { + dispatch(unregisterForm({ id: formId })); + }; + }, [dispatch, formId, flowType]); + + useEffect(() => { + dispatch(updateDirtyFormState({ id: formId, isDirty })); + }, [isDirty, dispatch, formId]); + + const handleSubmit = useCallback(async () => { + setIsSubmitting(true); const updatedSystem = { ...system, - ingress: isIngress ? dataFlowSystems : system.ingress, - egress: !isIngress ? dataFlowSystems : system.egress, + ingress: isIngress ? assignedDataFlows : system.ingress, + egress: !isIngress ? assignedDataFlows : system.egress, }; const result = await updateSystemMutationTrigger(updatedSystem); @@ -87,118 +93,122 @@ export const DataFlowAccordionForm = ({ } else { message.success(`${pluralFlowType} updated`); } + setIsSubmitting(false); + }, [ + system, + isIngress, + assignedDataFlows, + updateSystemMutationTrigger, + message, + pluralFlowType, + ]); + + const handleCancel = useCallback(() => { + setAssignedDataFlows(initialDataFlows); + }, [initialDataFlows]); - resetForm({ values: { dataFlowSystems } }); - }; + const handleDelete = useCallback((systemToDelete: System) => { + setAssignedDataFlows((prev) => + prev.filter((df) => df.fides_key !== systemToDelete.fides_key), + ); + }, []); + + const collapseItems: CollapseProps["items"] = useMemo( + () => [ + { + key: flowType, + label: ( + + + {pluralFlowType} + + {assignedDataFlows.length} + + ), + children: ( + + + + + + + + + {/* By conditionally rendering the modal, we force it to reset its state + whenever it opens */} + {modalOpen ? ( + setModalOpen(false)} + dataFlowSystems={assignedDataFlows} + onDataFlowSystemChange={setAssignedDataFlows} + flowType={flowType} + /> + ) : null} + + ), + }, + ], + [ + flowType, + isSystemTab, + pluralFlowType, + assignedDataFlows, + systems, + handleDelete, + isDirty, + handleCancel, + handleSubmit, + isSubmitting, + modalOpen, + system, + ], + ); return ( - - - - - {pluralFlowType} - - {/* Commented out until we get copy for the tooltips */} - {/* */} - - - {assignedDataFlow.length} - - - - - - - - - {({ isSubmitting, dirty, resetForm }) => ( -
- - - - -
- - -
- {/* By conditionally rendering the modal, we force it to reset its state - whenever it opens */} - {dataFlowSystemsModal.isOpen ? ( - - ) : null} - - )} -
-
-
-
+ ); }; diff --git a/clients/admin-ui/src/features/common/system-data-flow/DataFlowSystemsDeleteTable.tsx b/clients/admin-ui/src/features/common/system-data-flow/DataFlowSystemsDeleteTable.tsx index 669d092be42..704e7d65ad7 100644 --- a/clients/admin-ui/src/features/common/system-data-flow/DataFlowSystemsDeleteTable.tsx +++ b/clients/admin-ui/src/features/common/system-data-flow/DataFlowSystemsDeleteTable.tsx @@ -1,15 +1,4 @@ -import { - Button, - ChakraTable as Table, - ChakraTbody as Tbody, - ChakraTd as Td, - ChakraText as Text, - ChakraTh as Th, - ChakraThead as Thead, - ChakraTr as Tr, - Icons, -} from "fidesui"; -import { useFormikContext } from "formik"; +import { Button, Icons, Table, Text } from "fidesui"; import React from "react"; import { DataFlow, System } from "~/types/api"; @@ -17,59 +6,48 @@ import { DataFlow, System } from "~/types/api"; type Props = { systems: System[]; dataFlows: DataFlow[]; - onDataFlowSystemChange: (systems: DataFlow[]) => void; + onDelete: (system: System) => void; }; export const DataFlowSystemsDeleteTable = ({ systems, dataFlows, - onDataFlowSystemChange, + onDelete, }: Props) => { - const { setFieldValue } = useFormikContext(); - const dataFlowKeys = dataFlows.map((f) => f.fides_key); - - const onDelete = (dataFlow: System) => { - const updatedDataFlows = dataFlows.filter( - (dataFlowSystem) => dataFlowSystem.fides_key !== dataFlow.fides_key, - ); - setFieldValue("dataFlowSystems", updatedDataFlows); - onDataFlowSystemChange(updatedDataFlows); - }; + const dataSource = systems.filter((system) => + dataFlowKeys.includes(system.fides_key), + ); return ( - - - - - - - - {systems - .filter((system) => dataFlowKeys.includes(system.fides_key)) - .map((system) => ( - - - - - ))} - -
System -
- - {system.name} - - -
+ ( + {name} + ), + }, + { + title: "", + key: "actions", + align: "right" as const, + render: (_: unknown, system: System) => ( +
- - - - - - - - {allSystems.map((system) => { - const isAssigned = !!dataFlowSystems.find( - (assigned) => assigned.fides_key === system.fides_key, - ); - return ( - - - - - ); - })} - -
SystemSet as {flowType}
- - {system.name} - - - handleToggle(system)} - data-testid="assign-switch" - /> -
- + columns={[ + { + title: "System", + dataIndex: "name", + render: (name: string) => ( + {name} + ), + }, + { + title: `Set as ${flowType}`, + key: "toggle", + align: "right" as const, + render: (_: unknown, system: System) => { + const isAssigned = !!dataFlowSystems.find( + (assigned) => assigned.fides_key === system.fides_key, + ); + return ( + handleToggle(system)} + data-testid="assign-switch" + /> + ); + }, + }, + ]} + /> + ); };