Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions changelog/8133-system-data-flow-antd-migration.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
type: Changed
description: Migrated system data flow components from Chakra/Formik to Ant Design
pr: 8133
labels: []
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ChakraAccordion as Accordion } from "fidesui";
import { Flex } from "fidesui";
import React from "react";

import { System } from "~/types/api";
Expand All @@ -14,12 +14,12 @@ export const DataFlowAccordion = ({
system,
isSystemTab,
}: DataFlowFormProps) => (
<Accordion allowToggle data-testid="data-flow-accordion">
<Flex vertical data-testid="data-flow-accordion">
<DataFlowAccordionForm
system={system}
isIngress
isSystemTab={isSystemTab}
/>
<DataFlowAccordionForm system={system} isSystemTab={isSystemTab} />
</Accordion>
</Flex>
);
Original file line number Diff line number Diff line change
@@ -1,40 +1,30 @@
// 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,
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;
Expand All @@ -47,9 +37,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();
Expand All @@ -64,21 +56,34 @@ export const DataFlowAccordionForm = ({
return dataFlows.filter((df) => systemFidesKeys.includes(df.fides_key));
}, [isIngress, system, systems]);

const [assignedDataFlow, setAssignedDataFlows] =
const [assignedDataFlows, setAssignedDataFlows] =
useState<DataFlow[]>(initialDataFlows);

useEffect(() => {
setAssignedDataFlows(initialDataFlows);
}, [initialDataFlows]);

const handleSubmit = async (
{ dataFlowSystems }: FormValues,
{ resetForm }: FormikHelpers<FormValues>,
) => {
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);

Expand All @@ -87,118 +92,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: (
<Flex
align="center"
justify="start"
className={`h-[68px] flex-1 text-left ${isSystemTab ? "pl-4" : ""}`}
data-testid={`data-flow-button-${flowType}`}
>
<Text strong className="mr-2 text-sm leading-5">
{pluralFlowType}
</Text>
<Tag color="info" className="ml-2">
{assignedDataFlows.length}
</Tag>
</Flex>
),
children: (
<div
className="space-y-4 rounded-md bg-gray-50 p-6"
data-testid={`data-flow-panel-${flowType}`}
>
<Button
onClick={() => setModalOpen(true)}
type="primary"
size="small"
icon={<Icons.Settings />}
iconPlacement="end"
className="mb-4"
data-testid="assign-systems-btn"
>
{`Configure ${pluralFlowType.toLocaleLowerCase()}`}
</Button>
<DataFlowSystemsDeleteTable
systems={systems}
dataFlows={assignedDataFlows}
onDelete={handleDelete}
/>

<div className="mt-6 flex gap-2">
<Button
disabled={!isDirty}
onClick={handleCancel}
data-testid="cancel-btn"
>
Cancel
</Button>
<Button
type="primary"
onClick={handleSubmit}
loading={isSubmitting}
disabled={!isDirty}
data-testid="save-btn"
>
Save
</Button>
</div>
{/* By conditionally rendering the modal, we force it to reset its state
whenever it opens */}
{modalOpen ? (
<DataFlowSystemsModal
currentSystem={system}
systems={systems}
isOpen={modalOpen}
onClose={() => setModalOpen(false)}
dataFlowSystems={assignedDataFlows}
onDataFlowSystemChange={setAssignedDataFlows}
flowType={flowType}
/>
) : null}
</div>
),
},
],
[
flowType,
isSystemTab,
pluralFlowType,
assignedDataFlows,
systems,
handleDelete,
isDirty,
handleCancel,
handleSubmit,
isSubmitting,
modalOpen,
system,
],
);

return (
<AccordionItem>
<AccordionButton
height="68px"
paddingLeft={isSystemTab ? 6 : 2}
data-testid={`data-flow-button-${flowType}`}
>
<Flex
alignItems="center"
justifyContent="start"
flex={1}
textAlign="left"
>
<Text fontSize="sm" lineHeight={5} fontWeight="semibold" mr={2}>
{pluralFlowType}
</Text>
{/* Commented out until we get copy for the tooltips */}
{/* <QuestionTooltip label="helpful tip" /> */}

<Tag
ml={2}
backgroundColor="primary.400"
borderRadius="6px"
color="white"
>
{assignedDataFlow.length}
</Tag>
<Spacer />
<AccordionIcon />
</Flex>
</AccordionButton>
<AccordionPanel
backgroundColor="gray.50"
padding={6}
data-testid={`data-flow-panel-${flowType}`}
>
<Stack
borderRadius="md"
backgroundColor="gray.50"
aria-selected="true"
spacing={4}
data-testid="selected"
>
<Formik initialValues={defaultInitialValues} onSubmit={handleSubmit}>
{({ isSubmitting, dirty, resetForm }) => (
<Form>
<FormGuard
id={`${system.fides_key}:${flowType}`}
name={`${flowType} Data Flow`}
/>
<Button
onClick={dataFlowSystemsModal.onOpen}
type="primary"
size="small"
icon={<Icons.Settings />}
iconPlacement="end"
className="mb-4"
data-testid="assign-systems-btn"
>
{`Configure ${pluralFlowType.toLocaleLowerCase()}`}
</Button>
<DataFlowSystemsDeleteTable
systems={systems}
dataFlows={assignedDataFlow}
onDataFlowSystemChange={setAssignedDataFlows}
/>

<div className="mt-6 flex gap-2">
<Button
disabled={!dirty && assignedDataFlow === initialDataFlows}
onClick={() => {
setAssignedDataFlows(initialDataFlows);
resetForm({
values: { dataFlowSystems: initialDataFlows },
});
}}
data-testid="cancel-btn"
>
Cancel
</Button>
<Button
type="primary"
htmlType="submit"
loading={isSubmitting}
disabled={!dirty && assignedDataFlow === initialDataFlows}
data-testid="save-btn"
>
Save
</Button>
</div>
{/* By conditionally rendering the modal, we force it to reset its state
whenever it opens */}
{dataFlowSystemsModal.isOpen ? (
<DataFlowSystemsModal
currentSystem={system}
systems={systems}
isOpen={dataFlowSystemsModal.isOpen}
onClose={dataFlowSystemsModal.onClose}
dataFlowSystems={assignedDataFlow}
onDataFlowSystemChange={setAssignedDataFlows}
flowType={flowType}
/>
) : null}
</Form>
)}
</Formik>
</Stack>
</AccordionPanel>
</AccordionItem>
<Collapse
items={collapseItems}
data-testid={`data-flow-collapse-${flowType}`}
/>
);
};
Loading
Loading