Skip to content

Commit

Permalink
Adding support for configuring enabled actions (#4374)
Browse files Browse the repository at this point in the history
Co-authored-by: Adam Sachs <adam@ethyca.com>
  • Loading branch information
galvana and adamsachs committed Nov 10, 2023
1 parent c9a465d commit ce3dd39
Show file tree
Hide file tree
Showing 37 changed files with 991 additions and 152 deletions.
4 changes: 1 addition & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,8 @@ The types of changes are:
- Erasure support for Qualtrics [#4371](https://github.com/ethyca/fides/pull/4371)
- Erasure support for Ada Chatbot [#4382](https://github.com/ethyca/fides/pull/4382)
- Erasure support for Typeform [#4366](https://github.com/ethyca/fides/pull/4366)

## Added

- Added notice that a system is GVL when adding/editing from system form [#4327](https://github.com/ethyca/fides/pull/4327)
- Added the ability to select the request types to enable per integration (for plus users) [#4374](https://github.com/ethyca/fides/pull/4374)

### Changed
- Add filtering and pagination to bulk vendor add table [#4351](https://github.com/ethyca/fides/pull/4351)
Expand Down
81 changes: 81 additions & 0 deletions clients/admin-ui/cypress/e2e/system-integrations-plus.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { stubPlus, stubSystemCrud } from "cypress/support/stubs";

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

describe("System integrations", () => {
beforeEach(() => {
cy.login();
cy.intercept("GET", "/api/v1/system", {
fixture: "systems/systems.json",
}).as("getSystems");
cy.intercept("GET", "/api/v1/connection_type*", {
fixture: "connectors/connection_types.json",
}).as("getConnectionTypes");
cy.intercept("GET", "/api/v1/connection_type/postgres/secret", {
fixture: "connectors/postgres_secret.json",
}).as("getPostgresConnectorSecret");
stubPlus(true);
stubSystemCrud();
cy.visit(SYSTEM_ROUTE);
});

it("should render the integration configuration panel when navigating to integrations tab", () => {
cy.getByTestId("system-fidesctl_system").within(() => {
cy.getByTestId("more-btn").click();
cy.getByTestId("edit-btn").click();
});
cy.getByTestId("tab-Integrations").click();
cy.getByTestId("tab-panel-Integrations").should("exist");
});

describe("Integration search", () => {
beforeEach(() => {
cy.getByTestId("system-fidesctl_system").within(() => {
cy.getByTestId("more-btn").click();
cy.getByTestId("edit-btn").click();
});
cy.getByTestId("tab-Integrations").click();
cy.getByTestId("select-dropdown-btn").click();
});

it("should display Shopify when searching with upper case letters", () => {
cy.getByTestId("input-search-integrations").type("Sho");
cy.getByTestId("select-dropdown-list")
.find('[role="menuitem"] p')
.should("contain.text", "Shopify");
});

it("should display Shopify when searching with lower case letters", () => {
cy.getByTestId("input-search-integrations").type("sho");
cy.getByTestId("select-dropdown-list")
.find('[role="menuitem"] p')
.should("contain.text", "Shopify");
});
});

describe("Integration form contents", () => {
beforeEach(() => {
cy.getByTestId("system-fidesctl_system").within(() => {
cy.getByTestId("more-btn").click();
cy.getByTestId("edit-btn").click();
});
cy.getByTestId("tab-Integrations").click();
cy.getByTestId("select-dropdown-btn").click();

cy.getByTestId("input-search-integrations").type("PostgreSQL");
cy.getByTestId("select-dropdown-list")
.contains('[role="menuitem"]', "PostgreSQL")
.click();
});

// Verify Postgres shows access and erasure by default
it("should display Request types (enabled-actions) field", () => {
cy.getByTestId("enabled-actions").should("exist");
cy.getByTestId("enabled-actions").within(() => {
cy.contains("access");
cy.contains("erasure");
cy.contains("consent").should("not.exist");
});
});
});
});
23 changes: 23 additions & 0 deletions clients/admin-ui/cypress/e2e/system-integrations.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ describe("System integrations", () => {
cy.intercept("GET", "/api/v1/connection_type*", {
fixture: "connectors/connection_types.json",
}).as("getConnectionTypes");
cy.intercept("GET", "/api/v1/connection_type/postgres/secret", {
fixture: "connectors/postgres_secret.json",
}).as("getPostgresConnectorSecret");
stubPlus(false);
stubSystemCrud();
cy.visit(SYSTEM_ROUTE);
Expand Down Expand Up @@ -49,4 +52,24 @@ describe("System integrations", () => {
.should("contain.text", "Shopify");
});
});

describe("Integration form contents", () => {
beforeEach(() => {
cy.getByTestId("system-fidesctl_system").within(() => {
cy.getByTestId("more-btn").click();
cy.getByTestId("edit-btn").click();
});
cy.getByTestId("tab-Integrations").click();
cy.getByTestId("select-dropdown-btn").click();

cy.getByTestId("input-search-integrations").type("PostgreSQL");
cy.getByTestId("select-dropdown-list")
.contains('[role="menuitem"]', "PostgreSQL")
.click();
});

it("should not Request types (enabled-actions) field", () => {
cy.getByTestId("enabled-actions").should("not.exist");
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@
"identifier": "postgres",
"type": "database",
"human_readable": "PostgreSQL",
"encoded_icon": null
"encoded_icon": null,
"supported_actions": ["access", "erasure"]
},
{
"identifier": "redshift",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,16 @@ import { useMemo, useState } from "react";

import { useAppDispatch, useAppSelector } from "~/app/hooks";
import DocsLink from "~/features/common/DocsLink";
import { useFeatures } from "~/features/common/features";
import RightArrow from "~/features/common/Icon/RightArrow";
import { DEFAULT_TOAST_PARAMS } from "~/features/common/toast";
import { useGetConnectionTypeSecretSchemaQuery } from "~/features/connection-type";
import TestConnectionMessage from "~/features/datastore-connections/system_portal_config/TestConnectionMessage";
import TestData from "~/features/datastore-connections/TestData";
import {
useCreatePlusSaasConnectionConfigMutation,
usePatchPlusSystemConnectionConfigsMutation,
} from "~/features/plus/plus.slice";
import {
ConnectionConfigSecretsRequest,
selectActiveSystem,
Expand All @@ -33,6 +38,7 @@ import {
} from "~/features/system/system.slice";
import {
AccessLevel,
ActionType,
BulkPutConnectionConfiguration,
ConnectionConfigurationResponse,
ConnectionSystemTypeMap,
Expand Down Expand Up @@ -80,6 +86,9 @@ const createSaasConnector = async (
instance_key: generateIntegrationKey(systemFidesKey, connectionOption),
saas_connector_type: connectionOption.identifier,
secrets: {},
...(values.enabled_actions
? { enabled_actions: values.enabled_actions }
: {}),
};

const params: CreateSaasConnectionConfig = {
Expand Down Expand Up @@ -111,6 +120,7 @@ export const patchConnectionConfig = async (
? connectionConfig.key
: generateIntegrationKey(systemFidesKey, connectionOption);

// the enabled_actions are conditionally added if plus is enabled
const params1: Omit<ConnectionConfigurationResponse, "created_at" | "name"> =
{
access: AccessLevel.WRITE,
Expand All @@ -120,6 +130,9 @@ export const patchConnectionConfig = async (
description: values.description,
disabled: false,
key,
...(values.enabled_actions
? { enabled_actions: values.enabled_actions as ActionType[] }
: {}),
};
const payload = await patchFunc({
systemFidesKey,
Expand Down Expand Up @@ -211,15 +224,20 @@ export const useConnectorForm = ({
});

const [createSassConnectionConfig] = useCreateSassConnectionConfigMutation();
const [createPlusSaasConnectionConfig] =
useCreatePlusSaasConnectionConfigMutation();
const [getAuthorizationUrl] = useLazyGetAuthorizationUrlQuery();
const [updateSystemConnectionSecrets] =
usePatchSystemConnectionSecretsMutation();
const [patchDatastoreConnection] = usePatchSystemConnectionConfigsMutation();
const [patchPlusDatastoreConnection] =
usePatchPlusSystemConnectionConfigsMutation();
const [deleteDatastoreConnection, deleteDatastoreConnectionResult] =
useDeleteSystemConnectionConfigMutation();
const { data: allDatasetConfigs } = useGetConnectionConfigDatasetConfigsQuery(
connectionConfig?.key || ""
);
const { plus: isPlusEnabled } = useFeatures();

const originalSecrets = useMemo(
() => (connectionConfig ? { ...connectionConfig.secrets } : {}),
Expand Down Expand Up @@ -255,7 +273,9 @@ export const useConnectorForm = ({
secretsSchema!,
connectionOption,
systemFidesKey,
createSassConnectionConfig
isPlusEnabled
? createPlusSaasConnectionConfig
: createSassConnectionConfig
);
// eslint-disable-next-line no-param-reassign
connectionConfig = response.connection;
Expand All @@ -265,7 +285,9 @@ export const useConnectorForm = ({
connectionOption,
systemFidesKey,
connectionConfig!,
patchDatastoreConnection
isPlusEnabled
? patchPlusDatastoreConnection
: patchDatastoreConnection
);
if (
!connectionConfig &&
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
Box,
Button,
ButtonGroup,
CircleHelpIcon,
Expand All @@ -16,7 +17,7 @@ import {
Tooltip,
VStack,
} from "@fidesui/react";
import { Option } from "common/form/inputs";
import { Option, SelectInput } from "common/form/inputs";
import {
ConnectionTypeSecretSchemaProperty,
ConnectionTypeSecretSchemaReponse,
Expand All @@ -28,6 +29,7 @@ import _ from "lodash";
import React from "react";
import { DatastoreConnectionStatus } from "src/features/datastore-connections/types";

import { useFeatures } from "~/features/common/features";
import DisableConnectionModal from "~/features/datastore-connections/DisableConnectionModal";
import DatasetConfigField from "~/features/datastore-connections/system_portal_config/forms/fields/DatasetConfigField/DatasetConfigField";
import {
Expand Down Expand Up @@ -95,6 +97,7 @@ const ConnectorParametersForm: React.FC<ConnectorParametersFormProps> = ({
}) => {
const [trigger, { isLoading, isFetching }] =
useLazyGetDatastoreConnectionStatusQuery();
const { plus: isPlusEnabled } = useFeatures();

const validateField = (label: string, value: string, type?: string) => {
let error;
Expand Down Expand Up @@ -228,6 +231,9 @@ const ConnectorParametersForm: React.FC<ConnectorParametersFormProps> = ({
connectionConfig.connection_type === ConnectionType.SAAS
? (connectionConfig.saas_config?.fides_key as string)
: connectionConfig.key;
initialValues.enabled_actions = (
connectionConfig.enabled_actions || []
).map((action) => action.toString());

// @ts-ignore
initialValues.secrets = { ...connectionConfig.secrets };
Expand All @@ -248,6 +254,13 @@ const ConnectorParametersForm: React.FC<ConnectorParametersFormProps> = ({

return initialValues;
}

if (_.isEmpty(initialValues.enabled_actions)) {
initialValues.enabled_actions = connectionOption.supported_actions.map(
(action) => action.toString()
);
}

return fillInDefaults(initialValues, secretsSchema);
};

Expand Down Expand Up @@ -351,7 +364,7 @@ const ConnectorParametersForm: React.FC<ConnectorParametersFormProps> = ({
<Field id="instance_key" name="instance_key">
{({ field }: { field: FieldInputProps<string> }) => (
<FormControl display="flex">
{getFormLabel("instance_key", "Integration Identifier")}
{getFormLabel("instance_key", "Integration identifier")}
<VStack align="flex-start" w="inherit">
<Input
{...field}
Expand Down Expand Up @@ -386,7 +399,6 @@ const ConnectorParametersForm: React.FC<ConnectorParametersFormProps> = ({
</Field>
)}
{/* Dynamic connector secret fields */}

{connectionOption.type !== SystemType.MANUAL && secretsSchema
? Object.entries(secretsSchema.properties).map(
([key, item]) => {
Expand All @@ -398,6 +410,73 @@ const ConnectorParametersForm: React.FC<ConnectorParametersFormProps> = ({
}
)
: null}
{isPlusEnabled &&
connectionOption.supported_actions.length > 1 && (
<Field
id="enabled_actions"
name="enabled_actions"
validate={(value: string[]) => {
let error;
if (!value || value.length === 0) {
error = "At least one request type must be selected";
}
return error;
}}
>
{({
field,
form,
}: {
field: FieldInputProps<string>;
form: any;
}) => (
<FormControl
data-testid="enabled-actions"
display="flex"
isInvalid={
form.touched.enabled_actions &&
form.errors.enabled_actions
}
isRequired
>
{/* Known as enabled_actions throughout the front-end and back-end but it's displayed to the user as "Request types" */}
{getFormLabel("enabled_actions", "Request types")}
<VStack align="flex-start" w="inherit">
<Box width="100%">
<SelectInput
options={connectionOption.supported_actions.map(
(action) => ({
label: action,
value: action,
})
)}
fieldName={field.name}
size="sm"
isMulti
/>
</Box>
<FormErrorMessage>
{props.errors.enabled_actions}
</FormErrorMessage>
</VStack>
<Tooltip
aria-label="The request types that are supported for this integration."
hasArrow
label="The request types that are supported for this integration."
placement="right-start"
openDelay={500}
>
<Flex alignItems="center" h="32px">
<CircleHelpIcon
marginLeft="8px"
_hover={{ cursor: "pointer" }}
/>
</Flex>
</Tooltip>
</FormControl>
)}
</Field>
)}
{SystemType.DATABASE === connectionOption.type &&
!isCreatingConnectionConfig ? (
<DatasetConfigField
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export type ConnectionConfigFormValues = {
description: string;
name: string;
instance_key?: string;
enabled_actions?: string[];
[key: string]: any;
};

Expand Down
Loading

0 comments on commit ce3dd39

Please sign in to comment.