Skip to content

Commit

Permalink
Form to add a system via yaml (#1062)
Browse files Browse the repository at this point in the history
* Add new system page

* Refactor YamlForm so it can be reused

* Fixup some types in existing system slice

* Add SystemYamlForm

* Add cypress test for system

* Update changelog

* Fix empty state and remove ellipsis for now

* Remove tests on more actions button

* Move changelog items to Unreleased
  • Loading branch information
allisonking authored and PSalant726 committed Sep 20, 2022
1 parent 29a9803 commit f31e96a
Show file tree
Hide file tree
Showing 17 changed files with 415 additions and 143 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ The types of changes are:

## [Unreleased](https://github.com/ethyca/fides/compare/1.8.4...main)

* Changed behavior of `load_default_taxonomy` to append instead of upsert [#1040](https://github.com/ethyca/fides/pull/1040)
* New page to add a system via yaml [#1062](https://github.com/ethyca/fides/pull/1062)

## [1.8.4](https://github.com/ethyca/fides/compare/1.8.3...1.8.4) - 2022-09-09

Expand All @@ -26,7 +28,6 @@ The types of changes are:

### Changed

* Changed behavior of `load_default_taxonomy` to append instead of upsert [#1040](https://github.com/ethyca/fides/pull/1040)
* Deleting a taxonomy field with children will now cascade delete all of its children as well. [#1042](https://github.com/ethyca/fides/pull/1042)

### Fixed
Expand Down
18 changes: 9 additions & 9 deletions clients/ctl/admin-ui/cypress/e2e/datasets.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,14 +283,14 @@ describe("Dataset", () => {
const datasetAsString = JSON.stringify(dataset);
// Cypress doesn't have a native "paste" command, so instead do change the value directly
// (.type() is too slow, even with 0 delay)
cy.getByTestId("input-datasetYaml")
cy.getByTestId("input-yaml")
.click()
.invoke("val", datasetAsString)
.trigger("change");
// type just one space character to make sure the text field triggers Formik's handlers
cy.getByTestId("input-datasetYaml").type(" ");
cy.getByTestId("input-yaml").type(" ");

cy.getByTestId("create-dataset-btn").click();
cy.getByTestId("submit-yaml-btn").click();
cy.wait("@postDataset").then((interception) => {
const { body } = interception.request;
expect(body).to.eql(dataset);
Expand Down Expand Up @@ -323,16 +323,16 @@ describe("Dataset", () => {
cy.visit("/dataset/new");
cy.getByTestId("upload-yaml-btn").click();
// type something that isn't yaml
cy.getByTestId("input-datasetYaml").type("invalid: invalid: invalid");
cy.getByTestId("create-dataset-btn").click();
cy.getByTestId("error-datasetYaml").should("contain", "Could not parse");
cy.getByTestId("input-yaml").type("invalid: invalid: invalid");
cy.getByTestId("submit-yaml-btn").click();
cy.getByTestId("error-yaml").should("contain", "Could not parse");

// type something that is valid yaml and let backend render an error
cy.getByTestId("input-datasetYaml")
cy.getByTestId("input-yaml")
.clear()
.type("valid yaml that is not a dataset");
cy.getByTestId("create-dataset-btn").click();
cy.getByTestId("error-datasetYaml").should("contain", "field required");
cy.getByTestId("submit-yaml-btn").click();
cy.getByTestId("error-yaml").should("contain", "field required");
});

it("Can create a dataset by connecting to a database", () => {
Expand Down
72 changes: 67 additions & 5 deletions clients/ctl/admin-ui/cypress/e2e/systems.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,13 @@ describe("System management page", () => {
});

it("Can render system cards", () => {
cy.getByTestId("system-fidesctl_system").within(() => {
cy.getByTestId("more-btn").click();
cy.getByTestId("edit-btn");
cy.getByTestId("delete-btn");
});
cy.getByTestId("system-fidesctl_system");
// Uncomment when we enable the more actions button
// cy.getByTestId("system-fidesctl_system").within(() => {
// cy.getByTestId("more-btn").click();
// cy.getByTestId("edit-btn");
// cy.getByTestId("delete-btn");
// });
cy.getByTestId("system-demo_analytics_system");
cy.getByTestId("system-demo_marketing_system");
});
Expand All @@ -40,4 +42,64 @@ describe("System management page", () => {
cy.getByTestId("system-demo_marketing_system");
});
});

describe("Can create a new system", () => {
it("Can create a system via yaml", () => {
cy.intercept("POST", "/api/v1/system", { fixture: "system.json" }).as(
"postSystem"
);
cy.visit("/system/new");
cy.getByTestId("upload-yaml-btn").click();
cy.fixture("system.json").then((system) => {
const systemAsString = JSON.stringify(system);
// Cypress doesn't have a native "paste" command, so instead do change the value directly
// (.type() is too slow, even with 0 delay)
cy.getByTestId("input-yaml")
.click()
.invoke("val", systemAsString)
.trigger("change");
// type just one space character to make sure the text field triggers Formik's handlers
cy.getByTestId("input-yaml").type(" ");

cy.getByTestId("submit-yaml-btn").click();
cy.wait("@postSystem").then((interception) => {
const { body } = interception.request;
expect(body).to.eql(system);
});

// should navigate to the created system
cy.getByTestId("toast-success-msg");
cy.url().should("contain", `system`);
});
});

it("Can render errors in yaml", () => {
cy.intercept("POST", "/api/v1/system", {
statusCode: 422,
body: {
detail: [
{
loc: ["body", "fides_key"],
msg: "field required",
type: "value_error.missing",
},
{
loc: ["body", "system_type"],
msg: "field required",
type: "value_error.missing",
},
],
},
}).as("postSystem");
cy.visit("/system/new");
cy.getByTestId("upload-yaml-btn").click();

// invalid system with missing fields
cy.getByTestId("input-yaml")
.clear()
.type("valid yaml that is not a system");
cy.getByTestId("submit-yaml-btn").click();
cy.getByTestId("error-yaml").should("contain", "field required");
});
});
});
31 changes: 31 additions & 0 deletions clients/ctl/admin-ui/cypress/fixtures/system.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"fides_key": "demo_analytics_system",
"organization_fides_key": "default_organization",
"tags": null,
"name": "Demo Analytics System",
"description": "A system used for analyzing customer behaviour.",
"registry_id": null,
"meta": null,
"fidesctl_meta": null,
"system_type": "Service",
"data_responsibility_title": "Controller",
"privacy_declarations": [
{
"name": "Analyze customer behaviour for improvements.",
"data_categories": ["user.contact", "user.device.cookie_id"],
"data_use": "improve.system",
"data_qualifier": "aggregated.anonymized.unlinked_pseudonymized.pseudonymized.identified",
"data_subjects": ["customer"],
"dataset_references": ["demo_users_dataset"]
}
],
"system_dependencies": null,
"joint_controller": null,
"third_country_transfers": ["USA", "CAN"],
"administrating_department": "Engineering",
"data_protection_impact_assessment": {
"is_required": true,
"progress": "Complete",
"link": "https://example.org/analytics_system_data_protection_impact_assessment"
}
}
101 changes: 101 additions & 0 deletions clients/ctl/admin-ui/src/features/YamlForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { Box, Button, Text } from "@fidesui/react";
import { Form, Formik, FormikHelpers } from "formik";
import yaml from "js-yaml";

import { CustomTextArea } from "~/features/common/form/inputs";
import { getErrorMessage, isYamlException } from "~/features/common/helpers";
import { RTKResult } from "~/features/common/types";

const initialValues = { yaml: "" };
type FormValues = typeof initialValues;

interface Props<T> {
description: string;
submitButtonText: string;
onCreate: (yaml: unknown) => RTKResult<T>;
onSuccess: (data: T) => void;
}

const YamlForm = <T extends unknown>({
description,
submitButtonText,
onCreate,
onSuccess,
}: Props<T>) => {
const handleCreate = async (
newValues: FormValues,
formikHelpers: FormikHelpers<FormValues>
) => {
const { setErrors } = formikHelpers;
const parsedYaml = yaml.load(newValues.yaml, { json: true });
const result = await onCreate(parsedYaml);

if ("error" in result) {
const errorMessage = getErrorMessage(result.error);
setErrors({ yaml: errorMessage });
} else if ("data" in result) {
onSuccess(result.data);
}
};

const validate = (newValues: FormValues) => {
try {
const parsedYaml = yaml.load(newValues.yaml, { json: true });
if (!parsedYaml) {
return { yaml: "Could not parse the supplied YAML" };
}
} catch (error) {
if (isYamlException(error)) {
return {
yaml: `Could not parse the supplied YAML: \n\n${error.message}`,
};
}
return { yaml: "Could not parse the supplied YAML" };
}
return {};
};

return (
<Formik
initialValues={initialValues}
validate={validate}
onSubmit={handleCreate}
validateOnChange={false}
validateOnBlur={false}
>
{({ isSubmitting }) => (
<Form>
<Text size="sm" color="gray.700" mb={4}>
{description}
</Text>
{/* note: the error is more helpful in a monospace font, so apply Menlo to the whole Box */}
<Box mb={4} whiteSpace="pre-line" fontFamily="Menlo">
<CustomTextArea
name="yaml"
textAreaProps={{
fontWeight: 400,
lineHeight: "150%",
color: "gray.800",
fontSize: "13px",
height: "50vh",
width: "100%",
mb: "2",
}}
/>
</Box>
<Button
size="sm"
colorScheme="primary"
type="submit"
disabled={isSubmitting}
data-testid="submit-yaml-btn"
>
{submitButtonText}
</Button>
</Form>
)}
</Formik>
);
};

export default YamlForm;
9 changes: 9 additions & 0 deletions clients/ctl/admin-ui/src/features/common/types.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
import { RTKErrorResult } from "~/types/errors";

export interface TreeNode {
label: string;
value: string;
children: TreeNode[];
}

export type RTKResult<T> = Promise<
| {
data: T;
}
| { error: RTKErrorResult["error"] }
>;
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { useAppDispatch } from "~/app/hooks";
import { getErrorMessage, isErrorResult } from "~/features/common/helpers";
import { QuestionIcon } from "~/features/common/Icon";
import { DEFAULT_ORGANIZATION_FIDES_KEY } from "~/features/organization";
import { System } from "~/types/api";

import {
CustomCreatableMultiSelect,
Expand All @@ -18,7 +17,15 @@ import {
import { useCreateSystemMutation } from "../system/system.slice";
import { changeReviewStep, setSystemFidesKey } from "./config-wizard.slice";

type FormValues = Partial<System>;
const initialValues = {
description: "",
fides_key: "",
name: "",
organization_fides_key: DEFAULT_ORGANIZATION_FIDES_KEY,
tags: [],
system_type: "",
};
type FormValues = typeof initialValues;

const DescribeSystemsForm = ({
handleCancelSetup,
Expand All @@ -32,15 +39,6 @@ const DescribeSystemsForm = ({

const toast = useToast();

const initialValues = {
description: "",
fides_key: "",
name: "",
organization_fides_key: DEFAULT_ORGANIZATION_FIDES_KEY,
tags: [],
system_type: "",
};

const handleSubmit = async (values: FormValues) => {
const systemBody = {
description: values.description,
Expand Down
Loading

0 comments on commit f31e96a

Please sign in to comment.