From f31e96a497954ee161340b90185c51b61ef93c48 Mon Sep 17 00:00:00 2001 From: Allison King Date: Mon, 12 Sep 2022 14:38:26 -0400 Subject: [PATCH] Form to add a system via yaml (#1062) * 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 --- CHANGELOG.md | 3 +- .../ctl/admin-ui/cypress/e2e/datasets.cy.ts | 18 ++-- .../ctl/admin-ui/cypress/e2e/systems.cy.ts | 72 ++++++++++++- .../ctl/admin-ui/cypress/fixtures/system.json | 31 ++++++ .../ctl/admin-ui/src/features/YamlForm.tsx | 101 +++++++++++++++++ .../admin-ui/src/features/common/types.tsx | 9 ++ .../config-wizard/DescribeSystemsForm.tsx | 20 ++-- .../src/features/dataset/DatasetYamlForm.tsx | 102 ++++-------------- .../src/features/system/SystemCard.tsx | 39 +++---- .../src/features/system/SystemYamlForm.tsx | 57 ++++++++++ .../src/features/system/SystemsManagement.tsx | 2 +- .../src/features/system/system.slice.ts | 4 +- .../features/taxonomy/TaxonomyFormBase.tsx | 3 +- .../admin-ui/src/features/taxonomy/hooks.tsx | 3 +- .../admin-ui/src/features/taxonomy/types.ts | 9 -- .../ctl/admin-ui/src/pages/system/index.tsx | 19 +++- .../admin-ui/src/pages/system/new/index.tsx | 66 ++++++++++++ 17 files changed, 415 insertions(+), 143 deletions(-) create mode 100644 clients/ctl/admin-ui/cypress/fixtures/system.json create mode 100644 clients/ctl/admin-ui/src/features/YamlForm.tsx create mode 100644 clients/ctl/admin-ui/src/features/system/SystemYamlForm.tsx create mode 100644 clients/ctl/admin-ui/src/pages/system/new/index.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 84ecff418f2..c669e1960fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 @@ -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 diff --git a/clients/ctl/admin-ui/cypress/e2e/datasets.cy.ts b/clients/ctl/admin-ui/cypress/e2e/datasets.cy.ts index b00c8472125..f2ccddb0233 100644 --- a/clients/ctl/admin-ui/cypress/e2e/datasets.cy.ts +++ b/clients/ctl/admin-ui/cypress/e2e/datasets.cy.ts @@ -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); @@ -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", () => { diff --git a/clients/ctl/admin-ui/cypress/e2e/systems.cy.ts b/clients/ctl/admin-ui/cypress/e2e/systems.cy.ts index 932cec171a3..3ba00016a0c 100644 --- a/clients/ctl/admin-ui/cypress/e2e/systems.cy.ts +++ b/clients/ctl/admin-ui/cypress/e2e/systems.cy.ts @@ -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"); }); @@ -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"); + }); + }); }); diff --git a/clients/ctl/admin-ui/cypress/fixtures/system.json b/clients/ctl/admin-ui/cypress/fixtures/system.json new file mode 100644 index 00000000000..8521ba77784 --- /dev/null +++ b/clients/ctl/admin-ui/cypress/fixtures/system.json @@ -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" + } +} diff --git a/clients/ctl/admin-ui/src/features/YamlForm.tsx b/clients/ctl/admin-ui/src/features/YamlForm.tsx new file mode 100644 index 00000000000..92be6aaa346 --- /dev/null +++ b/clients/ctl/admin-ui/src/features/YamlForm.tsx @@ -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 { + description: string; + submitButtonText: string; + onCreate: (yaml: unknown) => RTKResult; + onSuccess: (data: T) => void; +} + +const YamlForm = ({ + description, + submitButtonText, + onCreate, + onSuccess, +}: Props) => { + const handleCreate = async ( + newValues: FormValues, + formikHelpers: FormikHelpers + ) => { + 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 ( + + {({ isSubmitting }) => ( +
+ + {description} + + {/* note: the error is more helpful in a monospace font, so apply Menlo to the whole Box */} + + + + +
+ )} +
+ ); +}; + +export default YamlForm; diff --git a/clients/ctl/admin-ui/src/features/common/types.tsx b/clients/ctl/admin-ui/src/features/common/types.tsx index afeac7ad186..9577b0db53a 100644 --- a/clients/ctl/admin-ui/src/features/common/types.tsx +++ b/clients/ctl/admin-ui/src/features/common/types.tsx @@ -1,5 +1,14 @@ +import { RTKErrorResult } from "~/types/errors"; + export interface TreeNode { label: string; value: string; children: TreeNode[]; } + +export type RTKResult = Promise< + | { + data: T; + } + | { error: RTKErrorResult["error"] } +>; diff --git a/clients/ctl/admin-ui/src/features/config-wizard/DescribeSystemsForm.tsx b/clients/ctl/admin-ui/src/features/config-wizard/DescribeSystemsForm.tsx index f506e0ce166..34975636812 100644 --- a/clients/ctl/admin-ui/src/features/config-wizard/DescribeSystemsForm.tsx +++ b/clients/ctl/admin-ui/src/features/config-wizard/DescribeSystemsForm.tsx @@ -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, @@ -18,7 +17,15 @@ import { import { useCreateSystemMutation } from "../system/system.slice"; import { changeReviewStep, setSystemFidesKey } from "./config-wizard.slice"; -type FormValues = Partial; +const initialValues = { + description: "", + fides_key: "", + name: "", + organization_fides_key: DEFAULT_ORGANIZATION_FIDES_KEY, + tags: [], + system_type: "", +}; +type FormValues = typeof initialValues; const DescribeSystemsForm = ({ handleCancelSetup, @@ -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, diff --git a/clients/ctl/admin-ui/src/features/dataset/DatasetYamlForm.tsx b/clients/ctl/admin-ui/src/features/dataset/DatasetYamlForm.tsx index acd4762666f..5d3f6479866 100644 --- a/clients/ctl/admin-ui/src/features/dataset/DatasetYamlForm.tsx +++ b/clients/ctl/admin-ui/src/features/dataset/DatasetYamlForm.tsx @@ -1,18 +1,12 @@ -import { Box, Button, Text, useToast } from "@fidesui/react"; -import { Form, Formik, FormikHelpers } from "formik"; -import yaml from "js-yaml"; +import { useToast } from "@fidesui/react"; import { useRouter } from "next/router"; import { Dataset } from "~/types/api"; -import { CustomTextArea } from "../common/form/inputs"; -import { getErrorMessage, isYamlException } from "../common/helpers"; import { successToastParams } from "../common/toast"; +import YamlForm from "../YamlForm"; import { setActiveDataset, useCreateDatasetMutation } from "./dataset.slice"; -const initialValues = { datasetYaml: "" }; -type FormValues = typeof initialValues; - // handle the common case where everything is nested under a `dataset` key interface NestedDataset { dataset: Dataset[]; @@ -26,94 +20,38 @@ export function isDatasetArray(value: unknown): value is NestedDataset { ); } +const DESCRIPTION = + "Get started creating your first dataset by pasting your dataset yaml below! You may have received this yaml from a colleague or your Ethyca developer support engineer."; + const DatasetYamlForm = () => { const [createDataset] = useCreateDatasetMutation(); const toast = useToast(); const router = useRouter(); - const handleCreate = async ( - newValues: FormValues, - formikHelpers: FormikHelpers - ) => { - const { setErrors } = formikHelpers; - const parsedYaml = yaml.load(newValues.datasetYaml, { json: true }); + const handleCreate = async (yaml: unknown) => { let dataset; - if (isDatasetArray(parsedYaml)) { - [dataset] = parsedYaml.dataset; + if (isDatasetArray(yaml)) { + [dataset] = yaml.dataset; } else { - dataset = parsedYaml; + dataset = yaml; } - const result = await createDataset(dataset); - if ("error" in result) { - const errorMessage = getErrorMessage(result.error); - setErrors({ datasetYaml: errorMessage }); - } else if ("data" in result) { - toast(successToastParams("Successfully loaded new dataset YAML")); - setActiveDataset(result.data); - router.push(`/dataset/${result.data.fides_key}`); - } + return createDataset(dataset); }; - const validate = (newValues: FormValues) => { - try { - const parsedYaml = yaml.load(newValues.datasetYaml, { json: true }); - if (!parsedYaml) { - return { datasetYaml: "Could not parse the supplied YAML" }; - } - } catch (error) { - if (isYamlException(error)) { - return { - datasetYaml: `Could not parse the supplied YAML: \n\n${error.message}`, - }; - } - return { datasetYaml: "Could not parse the supplied YAML" }; - } - return {}; + const handleSuccess = (newDataset: Dataset) => { + toast(successToastParams("Successfully loaded new dataset YAML")); + setActiveDataset(newDataset); + router.push(`/dataset/${newDataset.fides_key}`); }; return ( - - {({ isSubmitting }) => ( -
- - Get started creating your first dataset by pasting your dataset yaml - below! You may have received this yaml from a colleague or your - Ethyca developer support engineer. - - {/* note: the error is more helpful in a monospace font, so apply Menlo to the whole Box */} - - - - -
- )} -
+ + description={DESCRIPTION} + submitButtonText="Create dataset" + onCreate={handleCreate} + onSuccess={handleSuccess} + /> ); }; diff --git a/clients/ctl/admin-ui/src/features/system/SystemCard.tsx b/clients/ctl/admin-ui/src/features/system/SystemCard.tsx index 20d6f9e8b72..2ebb135457c 100644 --- a/clients/ctl/admin-ui/src/features/system/SystemCard.tsx +++ b/clients/ctl/admin-ui/src/features/system/SystemCard.tsx @@ -17,6 +17,7 @@ interface SystemCardProps { } const SystemCard = ({ system }: SystemCardProps) => { // TODO fides#1035, fides#1036 + const showMoreActions = false; // disable while feature is not implemented yet const handleEdit = () => {}; const handleDelete = () => {}; @@ -30,24 +31,26 @@ const SystemCard = ({ system }: SystemCardProps) => { {system.description} - - } - aria-label="more actions" - variant="unstyled" - size="sm" - data-testid="more-btn" - /> - - - Edit - - - Delete - - - + {showMoreActions ? ( + + } + aria-label="more actions" + variant="unstyled" + size="sm" + data-testid="more-btn" + /> + + + Edit + + + Delete + + + + ) : null} ); }; diff --git a/clients/ctl/admin-ui/src/features/system/SystemYamlForm.tsx b/clients/ctl/admin-ui/src/features/system/SystemYamlForm.tsx new file mode 100644 index 00000000000..097bbb76640 --- /dev/null +++ b/clients/ctl/admin-ui/src/features/system/SystemYamlForm.tsx @@ -0,0 +1,57 @@ +import { useToast } from "@fidesui/react"; +import { useRouter } from "next/router"; + +import { System } from "~/types/api"; + +import { successToastParams } from "../common/toast"; +import YamlForm from "../YamlForm"; +import { useCreateSystemMutation } from "./system.slice"; + +// handle the common case where everything is nested under a `system` key +interface NestedSystem { + system: System[]; +} +export function isSystemArray(value: unknown): value is NestedSystem { + return ( + typeof value === "object" && + value != null && + "system" in value && + Array.isArray((value as any).system) + ); +} + +const DESCRIPTION = + "Get started creating your system by pasting your system YAML below! You may have received this YAML from a colleague or your Ethyca developer support engineer."; + +const SystemYamlForm = () => { + const [createSystem] = useCreateSystemMutation(); + const toast = useToast(); + const router = useRouter(); + + const handleCreate = async (yaml: unknown) => { + let system; + if (isSystemArray(yaml)) { + [system] = yaml.system; + } else { + system = yaml; + } + + return createSystem(system); + }; + + const handleSuccess = () => { + toast(successToastParams("Successfully loaded new system YAML")); + router.push(`/system`); + }; + + return ( + + description={DESCRIPTION} + submitButtonText="Create system" + onCreate={handleCreate} + onSuccess={handleSuccess} + /> + ); +}; + +export default SystemYamlForm; diff --git a/clients/ctl/admin-ui/src/features/system/SystemsManagement.tsx b/clients/ctl/admin-ui/src/features/system/SystemsManagement.tsx index 709db6e86dc..a379fe60ac2 100644 --- a/clients/ctl/admin-ui/src/features/system/SystemsManagement.tsx +++ b/clients/ctl/admin-ui/src/features/system/SystemsManagement.tsx @@ -25,7 +25,7 @@ const SystemsManagement = ({ systems }: Props) => { }, [systems, searchFilter]); if (!systems || !systems.length) { - return
Empty state
; + return
No systems registered.
; } return ( diff --git a/clients/ctl/admin-ui/src/features/system/system.slice.ts b/clients/ctl/admin-ui/src/features/system/system.slice.ts index a8c8630fe10..4d6535dfbae 100644 --- a/clients/ctl/admin-ui/src/features/system/system.slice.ts +++ b/clients/ctl/admin-ui/src/features/system/system.slice.ts @@ -27,7 +27,9 @@ export const systemApi = createApi({ query: (fides_key) => ({ url: `system/${fides_key}/` }), providesTags: ["System"], }), - createSystem: build.mutation<{}, Partial>({ + // we accept 'unknown' as well since the user can paste anything in, and we rely + // on the backend to do the validation for us + createSystem: build.mutation({ query: (body) => ({ url: `system/`, method: "POST", diff --git a/clients/ctl/admin-ui/src/features/taxonomy/TaxonomyFormBase.tsx b/clients/ctl/admin-ui/src/features/taxonomy/TaxonomyFormBase.tsx index b20417b02f3..c81edbff940 100644 --- a/clients/ctl/admin-ui/src/features/taxonomy/TaxonomyFormBase.tsx +++ b/clients/ctl/admin-ui/src/features/taxonomy/TaxonomyFormBase.tsx @@ -17,11 +17,12 @@ import * as Yup from "yup"; import { CustomTextArea, CustomTextInput } from "~/features/common/form/inputs"; import { isErrorResult, parseError } from "~/features/common/helpers"; import { successToastParams } from "~/features/common/toast"; +import { RTKResult } from "~/features/common/types"; import { RTKErrorResult } from "~/types/errors"; import { parentKeyFromFidesKey } from "./helpers"; import TaxonomyEntityTag from "./TaxonomyEntityTag"; -import { Labels, RTKResult, TaxonomyEntity } from "./types"; +import { Labels, TaxonomyEntity } from "./types"; export type FormValues = Partial & Pick; diff --git a/clients/ctl/admin-ui/src/features/taxonomy/hooks.tsx b/clients/ctl/admin-ui/src/features/taxonomy/hooks.tsx index cafeee3105f..29cf5eb83cf 100644 --- a/clients/ctl/admin-ui/src/features/taxonomy/hooks.tsx +++ b/clients/ctl/admin-ui/src/features/taxonomy/hooks.tsx @@ -1,5 +1,6 @@ import { ReactNode } from "react"; +import { RTKResult } from "~/features/common/types"; import { DataCategory, DataQualifier, @@ -44,7 +45,7 @@ import { useUpdateDataCategoryMutation, } from "./taxonomy.slice"; import type { FormValues } from "./TaxonomyFormBase"; -import { Labels, RTKResult, TaxonomyEntity } from "./types"; +import { Labels, TaxonomyEntity } from "./types"; export interface TaxonomyHookData { data?: TaxonomyEntity[]; diff --git a/clients/ctl/admin-ui/src/features/taxonomy/types.ts b/clients/ctl/admin-ui/src/features/taxonomy/types.ts index 44f477d9c2c..ce9c7996435 100644 --- a/clients/ctl/admin-ui/src/features/taxonomy/types.ts +++ b/clients/ctl/admin-ui/src/features/taxonomy/types.ts @@ -1,5 +1,3 @@ -import { RTKErrorResult } from "~/types/errors"; - import { TreeNode } from "../common/types"; export interface TaxonomyEntityNode extends TreeNode { @@ -22,10 +20,3 @@ export interface Labels { description: string; parent_key?: string; } - -export type RTKResult = Promise< - | { - data: T; - } - | { error: RTKErrorResult["error"] } ->; diff --git a/clients/ctl/admin-ui/src/pages/system/index.tsx b/clients/ctl/admin-ui/src/pages/system/index.tsx index d7c6fa84812..a86279c8338 100644 --- a/clients/ctl/admin-ui/src/pages/system/index.tsx +++ b/clients/ctl/admin-ui/src/pages/system/index.tsx @@ -1,5 +1,6 @@ -import { Heading, Spinner } from "@fidesui/react"; +import { Box, Button, Heading, Spinner } from "@fidesui/react"; import type { NextPage } from "next"; +import NextLink from "next/link"; import React from "react"; import Layout from "~/features/common/Layout"; @@ -20,9 +21,19 @@ const Systems: NextPage = () => { return ( - - System Management - + + + System Management + + + {isLoading ? : } ); diff --git a/clients/ctl/admin-ui/src/pages/system/new/index.tsx b/clients/ctl/admin-ui/src/pages/system/new/index.tsx new file mode 100644 index 00000000000..25d0e84165b --- /dev/null +++ b/clients/ctl/admin-ui/src/pages/system/new/index.tsx @@ -0,0 +1,66 @@ +import { + Box, + Breadcrumb, + BreadcrumbItem, + Button, + Heading, + Stack, + Text, +} from "@fidesui/react"; +import type { NextPage } from "next"; +import NextLink from "next/link"; +import { useState } from "react"; + +import Layout from "~/features/common/Layout"; +import SystemYamlForm from "~/features/system/SystemYamlForm"; + +const NewSystem: NextPage = () => { + const [generateMethod, setGenerateMethod] = useState< + "yaml" | "manual" | null + >(null); + return ( + + + Create a new system + + + + + System Connections + + + Create a new system + + + + + + + Choose whether to upload a new system YAML or manually generate a + system. + + + + + + + + {generateMethod === "yaml" ? : null} + + + + ); +}; + +export default NewSystem;