Skip to content

Commit

Permalink
Add UI for configuring integrations for data detection/discovery (#4922)
Browse files Browse the repository at this point in the history
Co-authored-by: Lucano Vera <[email protected]>
Co-authored-by: Andrés Torres Marroquín <[email protected]>
  • Loading branch information
3 people authored May 30, 2024
1 parent cd772b2 commit 8309e83
Show file tree
Hide file tree
Showing 47 changed files with 1,457 additions and 108 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ The types of changes are:
### Added
- Deprecate LastServedNotice (lastservednoticev2) table [#4910](https://github.com/ethyca/fides/pull/4910)
- Added erasure support to the Recurly integration [#4891](https://github.com/ethyca/fides/pull/4891)
- Added UI for configuring integrations for detection/discovery [#4922](https://github.com/ethyca/fides/pull/4922)

### Changed
- Set default ports for local development of client projects (:3001 for privacy center and :3000 for admin-ui) [#4912](https://github.com/ethyca/fides/pull/4912)
Expand Down
149 changes: 149 additions & 0 deletions clients/admin-ui/cypress/e2e/integration-management.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import { stubPlus, stubSystemCrud } from "cypress/support/stubs";

import { INTEGRATION_MANAGEMENT_ROUTE } from "~/features/common/nav/v2/routes";

describe("Integration management for data detection & discovery", () => {
beforeEach(() => {
cy.login();
});

describe("accessing the page", () => {
it("can access the integration management page", () => {
stubPlus(true);
cy.visit(INTEGRATION_MANAGEMENT_ROUTE);
cy.getByTestId("integration-tabs").should("exist");
});

it("can't access without Plus", () => {
stubPlus(false);
cy.visit(INTEGRATION_MANAGEMENT_ROUTE);
cy.getByTestId("home-content").should("exist");
});
});

describe("main page", () => {
beforeEach(() => {
stubPlus(true);
});

it("should show an empty state when there are no integrations available", () => {
cy.intercept("GET", "/api/v1/connection*", {
fixture: "connectors/empty_list.json",
}).as("getConnections");
cy.visit(INTEGRATION_MANAGEMENT_ROUTE);
cy.wait("@getConnections");
cy.getByTestId("empty-state").should("exist");
});

describe("list view", () => {
beforeEach(() => {
cy.intercept("GET", "/api/v1/connection*", {
fixture: "connectors/bigquery_connection_list.json",
}).as("getConnections");
cy.visit(INTEGRATION_MANAGEMENT_ROUTE);
cy.wait("@getConnections");
});

it("should show a list of integrations", () => {
cy.getByTestId("integration-info-bq_integration").should("exist");
cy.getByTestId("empty-state").should("not.exist");
});

it("should navigate to management page when 'manage' button is clicked", () => {
cy.getByTestId("integration-info-bq_integration").within(() => {
cy.getByTestId("configure-btn").click();
cy.url().should("contain", "/bq_integration");
});
});
});

describe("adding an integration", () => {
beforeEach(() => {
cy.intercept("GET", "/api/v1/connection*", {
fixture: "connectors/bigquery_connection_list.json",
}).as("getConnections");
cy.visit(INTEGRATION_MANAGEMENT_ROUTE);
cy.wait("@getConnections");
});

it("should open modal", () => {
cy.getByTestId("add-integration-btn").click();
cy.getByTestId("add-modal-content")
.should("be.visible")
.within(() => {
cy.getByTestId("integration-info-bq_placeholder").should("exist");
});
});

it("should be able to add a new BigQuery integration", () => {
cy.intercept("PATCH", "/api/v1/connection").as("patchConnection");
cy.getByTestId("add-integration-btn").click();
cy.getByTestId("add-modal-content").within(() => {
cy.getByTestId("configure-btn").click();
});
cy.getByTestId("input-name").type("test name");
cy.getByTestId("input-description").type("test description");
cy.getByTestId("save-btn").click();
cy.wait("@patchConnection");
});

it("should be able to add a new integration with secrets", () => {
cy.intercept("PATCH", "/api/v1/connection").as("patchConnection");
cy.intercept("PUT", "/api/v1/connection/*/secret*").as(
"putConnectionSecrets"
);
cy.getByTestId("add-integration-btn").click();
cy.getByTestId("add-modal-content").within(() => {
cy.getByTestId("configure-btn").click();
});
cy.getByTestId("input-name").type("test name");
cy.getByTestId("input-keyfile_creds").type(`{"credentials": "test"}`, {
parseSpecialCharSequences: false,
});
cy.getByTestId("save-btn").click();
cy.wait("@patchConnection");
cy.wait("@putConnectionSecrets");
});

it("should be able to add a new integration associated with a system", () => {
stubSystemCrud();
cy.intercept("PATCH", "/api/v1/system/*/connection").as(
"patchSystemConnection"
);
cy.intercept("GET", "/api/v1/system", {
fixture: "systems/systems.json",
}).as("getSystems");
cy.getByTestId("add-integration-btn").click();
cy.getByTestId("add-modal-content").within(() => {
cy.getByTestId("configure-btn").click();
});
cy.getByTestId("input-name").type("test name");
cy.selectOption("input-system_fides_key", "Fidesctl System");
cy.getByTestId("save-btn").click();
cy.wait("@patchSystemConnection");
});
});
});

describe("detail view", () => {
beforeEach(() => {
stubPlus(true);
cy.intercept("GET", "/api/v1/connection/*", {
fixture: "connectors/bigquery_connection.json",
}).as("getConnection");
cy.visit("/integrations/bq_integration");
});

it("can edit integration with the modal", () => {
cy.intercept("PATCH", "/api/v1/connection").as("patchConnection");
cy.getByTestId("manage-btn").click();
cy.getByTestId("input-system_fides_key").should("not.exist");
cy.getByTestId("input-name")
.should("have.value", "BQ Integration")
.clear()
.type("A different name");
cy.getByTestId("save-btn").click();
cy.wait("@patchConnection");
});
});
});
4 changes: 4 additions & 0 deletions clients/admin-ui/cypress/e2e/system-integrations-plus.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ describe("System integrations", () => {
cy.intercept("GET", "/api/v1/connection_type/postgres/secret", {
fixture: "connectors/postgres_secret.json",
}).as("getPostgresConnectorSecret");
cy.intercept("GET", "/api/v1/plus/dictionary/system?size=2000", {
fixture: "dictionary-entries.json",
}).as("getDict");
stubPlus(true);
stubSystemCrud();
cy.visit(SYSTEM_ROUTE);
Expand All @@ -24,6 +27,7 @@ describe("System integrations", () => {
cy.getByTestId("more-btn").click();
cy.getByTestId("edit-btn").click();
});
cy.wait("@getDict");
cy.getByTestId("tab-Integrations").click();
cy.getByTestId("tab-panel-Integrations").should("exist");
});
Expand Down
4 changes: 3 additions & 1 deletion clients/admin-ui/cypress/e2e/systems-plus.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ describe("System management with Plus features", () => {
cy.wait("@getDictSystem");
cy.getByTestId("input-dpo").should("have.value", "[email protected]");
cy.getByTestId("tab-Data uses").click();
cy.getByTestId("tab-System information").click();
cy.getByTestId("tab-Information").click();
cy.getByTestId("tab-Data uses").click();
cy.getByTestId("confirmation-modal").should("not.exist");
});
Expand Down Expand Up @@ -232,10 +232,12 @@ describe("System management with Plus features", () => {
cy.intercept("POST", `/api/v1/plus/custom-metadata/custom-field/bulk`, {
body: {},
}).as("bulkUpdateCustomField");
stubVendorList();
});

it("can populate initial custom metadata", () => {
cy.visit(`${SYSTEM_ROUTE}/configure/demo_analytics_system`);
cy.wait(["@getSystem", "@getDictionaryEntries"]);

// Should not be able to save while form is untouched
cy.getByTestId("save-btn").should("be.disabled");
Expand Down
8 changes: 8 additions & 0 deletions clients/admin-ui/cypress/e2e/systems.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
import {
ADD_SYSTEMS_MANUAL_ROUTE,
ADD_SYSTEMS_ROUTE,
INTEGRATION_MANAGEMENT_ROUTE,
SYSTEM_ROUTE,
} from "~/features/common/nav/v2/routes";

Expand Down Expand Up @@ -291,6 +292,13 @@ describe("System management page", () => {
cy.url().should("contain", "/systems/configure/fidesctl_system");
});

it("Can access integration management page from system edit page", () => {
cy.visit("/systems/configure/fidesctl_system");
cy.wait("@getFidesctlSystem");
cy.getByTestId("integration-page-btn").click();
cy.url().should("contain", INTEGRATION_MANAGEMENT_ROUTE);
});

it("Can persist fields not directly in the form", () => {
cy.visit("/systems/configure/fidesctl_system");
cy.wait("@getFidesctlSystem");
Expand Down
12 changes: 11 additions & 1 deletion clients/admin-ui/cypress/e2e/taxonomy-plus.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,17 @@ describe("Taxonomy management with Plus features", () => {

cy.removeMultiValue(testIdMulti, "Eevee");
cy.removeMultiValue(testIdMulti, "Snorlax");
cy.selectOption(testIdMulti, "Eevee");

// clicking directly on the select element as we usually do hits the
// "remove" on the Charmander tag, so force it to find the dropdown
// indicator instead
cy.getByTestId(testIdMulti)
.find(".custom-select__dropdown-indicator")
.click();
cy.getByTestId(testIdMulti)
.find(".custom-select__menu-list")
.contains("Eevee")
.click();

["Charmander", "Eevee"].forEach((value) => {
cy.getSelectValueContainer(testIdMulti).contains(value);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"name": "BQ Integration",
"key": "bq_integration",
"description": "Description",
"connection_type": "bigquery",
"access": "read",
"created_at": "2024-05-24T13:35:57.509826+00:00",
"updated_at": "2024-05-24T13:35:57.509826+00:00",
"disabled": false,
"last_test_timestamp": null,
"last_test_succeeded": null,
"saas_config": null,
"secrets": null,
"authorized": false,
"enabled_actions": null
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"items": [
{
"name": "BQ Integration",
"key": "bq_integration",
"description": "Description",
"connection_type": "bigquery",
"access": "read",
"created_at": "2024-05-24T13:35:57.509826+00:00",
"updated_at": "2024-05-24T13:35:57.509826+00:00",
"disabled": false,
"last_test_timestamp": null,
"last_test_succeeded": null,
"saas_config": null,
"secrets": null,
"authorized": false,
"enabled_actions": null
}
],
"total": 1,
"page": 1,
"size": 50,
"pages": 1
}
6 changes: 6 additions & 0 deletions clients/admin-ui/cypress/fixtures/connectors/empty_list.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"items": [],
"total": 0,
"pages": 1,
"size": 5
}
1 change: 1 addition & 0 deletions clients/admin-ui/cypress/support/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import "./commands";
import "@fontsource/inter/400.css";
import "@fontsource/inter/500.css";
import "@fontsource/inter/600.css";
import "@fontsource/inter/700.css";

import { EnhancedStore } from "@reduxjs/toolkit";
Expand Down
58 changes: 58 additions & 0 deletions clients/admin-ui/src/features/common/Breadcrumbs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { Breadcrumb, BreadcrumbItem, BreadcrumbLink } from "fidesui";
import Link from "next/link";

export interface BreadcrumbsProps {
breadcrumbs: {
title: string;
link?: string;
onClick?: () => void;
isOpaque?: boolean;
}[];
}

/**
* Breadcrumbs component that shows the path to the current page with links to the previous pages.
* By default, the last breadcrumb is black, and the rest are gray.
* @param breadcrumbs - array of breadcrumbs
* @param breadcrumbs.title - title of the breadcrumb
* @param breadcrumbs.link - (optional) link to the page
* @param breadcrumbs.onClick - (optional) function to call when the breadcrumb is clicked
* @param breadcrumbs.isOpaque - (optional) if true, the breadcrumb will be black, otherwise gray
*/
const Breadcrumbs: React.FC<BreadcrumbsProps> = ({ breadcrumbs }) => (
<Breadcrumb
separator="->"
fontSize="2xl"
fontWeight="semibold"
data-testid="breadcrumbs"
>
{breadcrumbs.map((breadcumbItem, index) => {
const isLast = index + 1 === breadcrumbs.length;
const hasLink = !!breadcumbItem.link || !!breadcumbItem.onClick;
return (
<BreadcrumbItem
color={isLast || breadcumbItem.isOpaque ? "black" : "gray.500"}
key={breadcumbItem.title}
>
{hasLink ? (
<BreadcrumbLink
as={Link}
href={breadcumbItem.link}
isCurrentPage={isLast}
>
{breadcumbItem.title}
</BreadcrumbLink>
) : (
<BreadcrumbLink
_hover={{ textDecoration: "none", cursor: "default" }}
isCurrentPage={isLast}
>
{breadcumbItem.title}
</BreadcrumbLink>
)}
</BreadcrumbItem>
);
})}
</Breadcrumb>
);
export default Breadcrumbs;
10 changes: 4 additions & 6 deletions clients/admin-ui/src/features/common/DataTabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,20 +53,18 @@ const DataTabs = ({
}: Props & Omit<TabsProps, "children">) => (
<Tabs colorScheme="complimentary" {...other}>
<TabList width={border === "partial" ? "max-content" : undefined}>
{data.map((tab, index) => (
{data.map((tab) => (
<FidesTab
// eslint-disable-next-line react/no-array-index-key
key={index}
key={tab.label}
label={tab.label}
isDisabled={tab.isDisabled}
fontSize={other.fontSize}
/>
))}
</TabList>
<TabPanels>
{data.map((tab, index) => (
// eslint-disable-next-line react/no-array-index-key
<TabPanel px={0} key={index} data-testid={`tab-panel-${tab.label}`}>
{data.map((tab) => (
<TabPanel px={0} key={tab.label} data-testid={`tab-panel-${tab.label}`}>
{tab.content}
</TabPanel>
))}
Expand Down
Loading

0 comments on commit 8309e83

Please sign in to comment.