Skip to content

Commit

Permalink
Update Domains page with ability to add/remove "organization" domains…
Browse files Browse the repository at this point in the history
…, view "administrator" domains set via security settings, and improve various UX bugs and copy (#4584)

Co-authored-by: Kelsey Thomas <101993653+Kelsey-Ethyca@users.noreply.github.com>
Co-authored-by: Neville Samuell <neville@ethyca.com>
  • Loading branch information
3 people authored Feb 1, 2024
1 parent 67a515a commit 79ea1b4
Show file tree
Hide file tree
Showing 16 changed files with 695 additions and 89 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ The types of changes are:
- Updating return value for crud.get_custom_fields_filtered [#4575](https://github.com/ethyca/fides/pull/4575)
- Updated user deletion confirmation flow to only require one confirmatory input [#4402](https://github.com/ethyca/fides/pull/4402)
- Moved `pymssl` to an optional dependency no longer installed by default with our python package [#4581](https://github.com/ethyca/fides/pull/4581)

- Fixed CORS domain update functionality [#4570](https://github.com/ethyca/fides/pull/4570)
- Update Domains page with ability to add/remove "organization" domains, view "administrator" domains set via security settings, and improve various UX bugs and copy [#4584](https://github.com/ethyca/fides/pull/4584)

### Fixed

Expand Down
243 changes: 243 additions & 0 deletions clients/admin-ui/cypress/e2e/domains.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
import { stubPlus } from "cypress/support/stubs";

// Mock response for GET /api/v1/config?api_set=true
const API_SET_CONFIG = {
security: {
cors_origins: ["https://example.com", "https://app.example.com"],
},
};

// Mock response for GET /api/v1/config?api_set=false
const CONFIG_SET_CONFIG = {
security: {
cors_origins: ["http://localhost"],
cors_origin_regex: "https://.*\\.example\\.com",
},
};

describe("Domains page", () => {
beforeEach(() => {
cy.login();
stubPlus(true);
});

it("can navigate to the Domains page", () => {
cy.visit("/");
cy.getByTestId("Domains-nav-link").click();
cy.getByTestId("management-domains");
});

describe("can view domains", () => {
describe("when existing domains are configured (both api-set and config-set)", () => {
beforeEach(() => {
cy.intercept("GET", "/api/v1/config?api_set=true", API_SET_CONFIG).as(
"getApiSetConfig"
);
cy.intercept(
"GET",
"/api/v1/config?api_set=false",
CONFIG_SET_CONFIG
).as("getConfigSetConfig");
cy.visit("/management/domains");
});

it("can display a loading state while fetching domain configuration", () => {
cy.getByTestId("api-set-domains-form").within(() => {
cy.get(".chakra-spinner");
});
cy.getByTestId("config-set-domains-form").within(() => {
cy.get(".chakra-spinner");
});

// After fetching, spinners should disappear
cy.wait("@getApiSetConfig");
cy.wait("@getConfigSetConfig");
cy.getByTestId("api-set-domains-form").within(() => {
cy.get(".chakra-spinner").should("not.exist");
});
cy.getByTestId("config-set-domains-form").within(() => {
cy.get(".chakra-spinner").should("not.exist");
});
});

it("can view existing domain configuration, with both api-set and config-set values", () => {
cy.wait("@getApiSetConfig");
cy.wait("@getConfigSetConfig");

cy.getByTestId("api-set-domains-form").within(() => {
cy.getByTestId("input-cors_origins[0]").should(
"have.value",
"https://example.com"
);
});

cy.getByTestId("config-set-domains-form").within(() => {
cy.getByTestId("input-config_cors_origins[0]").should(
"have.value",
"http://localhost"
);
cy.getByTestId("input-config_cors_origin_regex").should(
"have.value",
"https://.*\\.example\\.com"
);
});
});
});

describe("when no existing domains are configured", () => {
beforeEach(() => {
cy.intercept("GET", "/api/v1/config?api_set=true", {}).as(
"getApiSetConfig"
);
cy.intercept("GET", "/api/v1/config?api_set=false", {}).as(
"getConfigSetConfig"
);
cy.visit("/management/domains");
});

it("can view empty state", () => {
cy.wait("@getApiSetConfig");
cy.wait("@getConfigSetConfig");

cy.getByTestId("api-set-domains-form").within(() => {
cy.getByTestId("input-cors_origins[0]").should("not.exist");
});

cy.getByTestId("config-set-domains-form").within(() => {
cy.getByTestId("input-config_cors_origins[0]").should("not.exist");
cy.getByTestId("input-config_cors_origin_regex").should("not.exist");
cy.contains("No advanced domain settings configured.");
});
});
});
});

describe("can edit domains", () => {
beforeEach(() => {
cy.intercept("GET", "/api/v1/config?api_set=true", API_SET_CONFIG).as(
"getApiSetConfig"
);
cy.intercept("GET", "/api/v1/config?api_set=false", CONFIG_SET_CONFIG).as(
"getConfigSetConfig"
);
cy.visit("/management/domains");
cy.wait("@getApiSetConfig");
cy.wait("@getConfigSetConfig");
});

it("can edit an existing domain", () => {
const API_SET_CONFIG_UPDATED = {
security: {
cors_origins: ["https://www.example.com", "https://app.example.com"],
},
};
cy.intercept("PUT", "/api/v1/config", { statusCode: 200 }).as(
"putApiSetConfig"
);
cy.intercept(
"GET",
"/api/v1/config?api_set=true",
API_SET_CONFIG_UPDATED
).as("getApiSetConfig");

// Edit the first domain and save our changes
cy.getByTestId("input-cors_origins[0]")
.clear()
.type("https://www.example.com");
cy.getByTestId("save-btn").click();
cy.wait("@putApiSetConfig").then((interception) => {
const { body } = interception.request;
expect(body)
.to.have.nested.property("security.cors_origins")
.to.eql(["https://www.example.com", "https://app.example.com"]);
});
cy.wait("@getApiSetConfig");
cy.getByTestId("input-cors_origins[0]").should(
"have.value",
"https://www.example.com"
);
cy.getByTestId("toast-success-msg");
});

it("can remove an existing domain", () => {
const API_SET_CONFIG_UPDATED = {
security: {
cors_origins: ["https://app.example.com"],
},
};
cy.intercept("PUT", "/api/v1/config", { statusCode: 200 }).as(
"putApiSetConfig"
);
cy.intercept(
"GET",
"/api/v1/config?api_set=true",
API_SET_CONFIG_UPDATED
).as("getApiSetConfig");

// Delete the first domain and save our changes
cy.get("[aria-label='delete-domain']:first").click();
cy.getByTestId("save-btn").click();
cy.wait("@putApiSetConfig").then((interception) => {
const { body } = interception.request;
expect(body)
.to.have.nested.property("security.cors_origins")
.to.eql(["https://app.example.com"]);
});
cy.wait("@getApiSetConfig");
cy.getByTestId("input-cors_origins[0]").should(
"have.value",
"https://app.example.com"
);
cy.getByTestId("toast-success-msg");
});

it("can validate domains", () => {
cy.getByTestId("api-set-domains-form").within(() => {
cy.getByTestId("input-cors_origins[0]").clear().type("foo").blur();
cy.root().should("contain", "Domain must be a valid URL");
cy.getByTestId("save-btn").should("be.disabled");

cy.getByTestId("input-cors_origins[0]")
.clear()
.type("https://*.foo.com")
.blur();
cy.root().should("contain", "Domain cannot contain a wildcard");
cy.getByTestId("save-btn").should("be.disabled");

cy.getByTestId("input-cors_origins[0]")
.clear()
.type("https://foo.com/blog")
.blur();
cy.root().should("contain", "Domain cannot contain a path");
cy.getByTestId("save-btn").should("be.disabled");

cy.getByTestId("input-cors_origins[0]")
.clear()
.type("file://example.txt")
.blur();
cy.root().should("contain", "Domain must be a valid URL");
cy.getByTestId("save-btn").should("be.disabled");

cy.getByTestId("input-cors_origins[0]")
.clear()
.type("http:foo.com")
.blur();
cy.root().should("contain", "Domain must be a valid URL");
cy.getByTestId("save-btn").should("be.disabled");

cy.getByTestId("input-cors_origins[0]")
.clear()
.type("https://foo.com/")
.blur();
cy.root().should("contain", "Domain cannot contain a path");
cy.getByTestId("save-btn").should("be.disabled");

cy.getByTestId("input-cors_origins[0]")
.clear()
.type("https://foo.com")
.blur();
cy.getByTestId("save-btn").should("be.enabled");
});
});
});
});
2 changes: 1 addition & 1 deletion clients/admin-ui/src/features/common/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ const Layout = ({
isValidNotificationRoute;

return (
<Flex data-testid={title} direction="column" height="calc(100vh - 48px)">
<Flex data-testid={title} direction="column" height="100vh">
<Head>
<title>Fides Admin UI - {title}</title>
<meta name="description" content="Privacy Engineering Platform" />
Expand Down
11 changes: 10 additions & 1 deletion clients/admin-ui/src/features/common/form/FormSection.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { Box, BoxProps, Heading, Stack } from "@fidesui/react";
import { Box, BoxProps, Heading, Stack, Text } from "@fidesui/react";

import QuestionTooltip from "~/features/common/QuestionTooltip";

const FormSection = ({
title,
tooltip,
children,
...props
}: {
title: string;
tooltip?: string;
} & BoxProps) => (
<Box borderRadius="md" border="1px solid" borderColor="gray.200" {...props}>
<Heading
Expand All @@ -20,6 +24,11 @@ const FormSection = ({
textAlign="left"
>
{title}
{tooltip ? (
<Text as="span" mx={1}>
<QuestionTooltip label={tooltip} />
</Text>
) : undefined}
</Heading>
<Stack p={6} spacing={6}>
{children}
Expand Down
22 changes: 11 additions & 11 deletions clients/admin-ui/src/features/common/nav/v2/nav-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ describe("configureNavGroups", () => {
});

describe("fides cloud", () => {
it("shows domain records page when fides cloud is enabled", () => {
it("shows domain verification page when fides cloud is enabled", () => {
const navGroups = configureNavGroups({
config: NAV_CONFIG,
userScopes: ALL_SCOPES,
Expand All @@ -170,11 +170,11 @@ describe("configureNavGroups", () => {
expect(
navGroups[4].children
.map((c) => c.title)
.find((title) => title === "Domain records")
).toEqual("Domain records");
.find((title) => title === "Domain verification")
).toEqual("Domain verification");
});

it("does not show domain records page when fides cloud is disabled", () => {
it("does not show domain verification page when fides cloud is disabled", () => {
const navGroups = configureNavGroups({
config: NAV_CONFIG,
userScopes: ALL_SCOPES,
Expand All @@ -186,13 +186,13 @@ describe("configureNavGroups", () => {
expect(
navGroups[4].children
.map((c) => c.title)
.find((title) => title === "Domain records")
.find((title) => title === "Domain verification")
).toEqual(undefined);
});
});

describe("fides plus", () => {
it("shows cors management when plus and scopes are enabled", () => {
it("shows domain management when plus and scopes are enabled", () => {
const navGroups = configureNavGroups({
config: NAV_CONFIG,
userScopes: [
Expand All @@ -210,11 +210,11 @@ describe("configureNavGroups", () => {
.find((c) => c.title === "Domains")
).toEqual({
title: "Domains",
path: routes.CORS_CONFIGURATION_ROUTE,
path: routes.DOMAIN_MANAGEMENT_ROUTE,
});
});

it("hide cors management when plus is disabled", () => {
it("hide domain management when plus is disabled", () => {
const navGroups = configureNavGroups({
config: NAV_CONFIG,
userScopes: [
Expand All @@ -229,11 +229,11 @@ describe("configureNavGroups", () => {
expect(
navGroups[1].children
.map((c) => ({ title: c.title, path: c.path }))
.find((c) => c.title === "Manage domains")
.find((c) => c.title === "Domains")
).toEqual(undefined);
});

it("hide cors management when scopes are wrong", () => {
it("hide domain management when scopes are wrong", () => {
const navGroups = configureNavGroups({
config: NAV_CONFIG,
userScopes: [
Expand All @@ -248,7 +248,7 @@ describe("configureNavGroups", () => {
expect(
navGroups[1]?.children
.map((c) => ({ title: c.title, path: c.path }))
.find((c) => c.title === "Manage domains")
.find((c) => c.title === "Domains")
).toEqual(undefined);
});
});
Expand Down
4 changes: 2 additions & 2 deletions clients/admin-ui/src/features/common/nav/v2/nav-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,15 +193,15 @@ export const NAV_CONFIG: NavConfigGroup[] = [
scopes: [ScopeRegistryEnum.MESSAGING_CREATE_OR_UPDATE],
},
{
title: "Domain records",
title: "Domain verification",
path: routes.DOMAIN_RECORDS_ROUTE,
requiresPlus: true,
requiresFidesCloud: true,
scopes: [ScopeRegistryEnum.FIDES_CLOUD_CONFIG_READ],
},
{
title: "Domains",
path: routes.CORS_CONFIGURATION_ROUTE,
path: routes.DOMAIN_MANAGEMENT_ROUTE,
requiresPlus: true,
requiresFidesCloud: false,
scopes: [
Expand Down
2 changes: 1 addition & 1 deletion clients/admin-ui/src/features/common/nav/v2/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,5 @@ export const ABOUT_ROUTE = "/management/about";
export const CUSTOM_FIELDS_ROUTE = "/management/custom-fields";
export const EMAIL_TEMPLATES_ROUTE = "/management/email-templates";
export const DOMAIN_RECORDS_ROUTE = "/management/domain-records";
export const CORS_CONFIGURATION_ROUTE = "/management/cors-configuration";
export const DOMAIN_MANAGEMENT_ROUTE = "/management/domains";
export const GLOBAL_CONSENT_CONFIG_ROUTE = "/management/consent";
Loading

0 comments on commit 79ea1b4

Please sign in to comment.