Skip to content
This repository has been archived by the owner on Nov 30, 2022. It is now read-only.

Commit

Permalink
Datastore Connection Filtering (#691)
Browse files Browse the repository at this point in the history
* Refactor routes into enums and create connections page

* Test switching back to double quotes

* Convert back to double quotes

* Add placeholder connection filters

* Set up api scaffolding

* Get basic grid going

* Initial grid card styling

* Fix simple eslint issues

* Add development config back in

* Finish draft of card

* Add working test button and landing page

* Add pagination and small fixes

* Fix testing issues

* Add auth tests for datastore connection page

* run formatter

* Update changelog

* update the create_test_data command to add connectionconfigs

* Disable create buttons & fix text overflow

* Update filter dropdown values

* Fix test timestamp bug

* Remove development variable

* Add working filter dropdowns

* Add outside click hook & polish things

* Fix imports

* Update changelog

* Update button hover color

* remove commented out code

* fix typo

* Remove Saas Option

* Fix welcome screen bug

* Remove edit button

* Fix lint and formatting issues

* removes commented-out code

Co-authored-by: Sean Preston <[email protected]>
Co-authored-by: eastandwestwind <[email protected]>
  • Loading branch information
3 people authored Jun 23, 2022
1 parent 106f29d commit 7c3bf98
Show file tree
Hide file tree
Showing 11 changed files with 584 additions and 205 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ The types of changes are:
* Added the ability to disable/enable a datastore from the frontend [#693] https://github.com/ethyca/fidesops/pull/693
* Adds Postgres and Redis health checks to health endpoint [#690](https://github.com/ethyca/fidesops/pull/690)
* Added health checks and better error messages on app startup for both db and cache [#686](https://github.com/ethyca/fidesops/pull/686)
* Datastore Connection Filters [#691](https://github.com/ethyca/fidesops/pull/691)

### Changed

Expand Down
4 changes: 4 additions & 0 deletions clients/admin-ui/src/features/common/Icon/ArrowDownLine.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,9 @@ import { createIcon } from "@fidesui/react";
export default createIcon({
displayName: "ArrowDownLineIcon",
viewBox: "0 0 24 24",
defaultProps: {
width: "20px",
height: "20px",
},
d: "M12 13.1719L16.95 8.22192L18.364 9.63592L12 15.9999L5.63599 9.63592L7.04999 8.22192L12 13.1719Z",
});
2 changes: 2 additions & 0 deletions clients/admin-ui/src/features/common/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// eslint-disable-next-line import/prefer-default-export
export { useOutsideClick } from "./useOutsideClick";
22 changes: 22 additions & 0 deletions clients/admin-ui/src/features/common/hooks/useOutsideClick.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { LegacyRef, useEffect, useRef } from "react";

// eslint-disable-next-line import/prefer-default-export
export const useOutsideClick = (handleClick: () => void) => {
const ref = useRef<HTMLDivElement | undefined>(undefined) as
| LegacyRef<HTMLDivElement>
| undefined;
useEffect(() => {
const handleOutsideClick = (event: MouseEvent) => {
// @ts-ignore
if (!ref.current?.contains(event.target)) {
handleClick();
}
};
document.addEventListener("mousedown", handleOutsideClick);
return () => {
document.removeEventListener("mousedown", handleOutsideClick);
};
}, [ref, handleClick]);

return { ref };
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { Box, Flex, IconButton, Spacer, Text } from "@fidesui/react";
import React, { useCallback, useState } from "react";

import { useOutsideClick } from "../common/hooks";
import { ArrowDownLineIcon } from "../common/Icon";
import { capitalize } from "../common/utils";

const useConnectionStatusMenu = () => {
const [isOpen, setIsOpen] = useState<boolean>(false);
const handleClick = useCallback(() => {
if (isOpen) {
setIsOpen(false);
}
}, [isOpen, setIsOpen]);

const { ref } = useOutsideClick(handleClick);

const toggleMenu = () => {
setIsOpen(!isOpen);
};

return {
isOpen,
toggleMenu,
ref,
};
};

type ConnectionDropdownProps = {
filterOptions: string[];
// eslint-disable-next-line react/require-default-props
value?: string;
setValue: (x: string) => void;
title: string;
};

const ConnectionDropdown: React.FC<ConnectionDropdownProps> = ({
filterOptions,
value,
setValue,
title,
}) => {
const { isOpen, toggleMenu, ref } = useConnectionStatusMenu();
const options = filterOptions.map((d) => (
<Flex
key={d}
height="36px"
_hover={{ bg: "gray.100" }}
alignItems="center"
padding="8px"
cursor="pointer"
onClick={() => {
setValue(d);
toggleMenu();
}}
>
<Text
marginLeft="8px"
fontSize="xs"
fontWeight="500"
color={d === value ? "complimentary.500" : "gray.700"}
lineHeight="16px"
>
{capitalize(d)}
</Text>
</Flex>
));

return (
<Box width="100%" position="relative" ref={ref}>
<Flex
borderRadius="6px"
border="1px"
borderColor={isOpen ? "primary.600" : "gray.200"}
height="32px"
paddingRight="14px"
paddingLeft="14px"
alignItems="center"
>
<Text
fontSize="14px"
fontWeight="400"
lineHeight="20px"
color={value ? "complimentary.500" : "gray.700"}
>
{value ? capitalize(value) : title}
</Text>
<Spacer />
<IconButton
variant="ghost"
size="xs"
aria-label="Datastore Type Dropdown"
onClick={() => toggleMenu()}
icon={<ArrowDownLineIcon />}
/>
</Flex>
{isOpen ? (
<Flex
marginTop="4px"
backgroundColor="white"
flexDirection="column"
border="1px"
width="100%"
borderColor="gray.200"
boxShadow="0px 1px 3px rgba(0, 0, 0, 0.1), 0px 1px 2px rgba(0, 0, 0, 0.06);"
borderRadius="4px"
position="absolute"
zIndex={1}
>
{options}
</Flex>
) : null}
</Box>
);
};

export default ConnectionDropdown;
Original file line number Diff line number Diff line change
@@ -1,89 +1,54 @@
import {
Input,
InputGroup,
InputLeftElement,
Select,
Stack,
useToast,
} from "@fidesui/react";
import { Input, InputGroup, InputLeftElement, Stack } from "@fidesui/react";
import React from "react";
import { useDispatch, useSelector } from "react-redux";

import { selectToken } from "../auth";
import { SearchLineIcon } from "../common/Icon";
import { capitalize } from "../common/utils";
import SystemTypeMenu from "./ConnectionDropdown";
import ConnectionStatusMenu from "./ConnectionStatusMenu";
import {
clearAllFilters,
requestCSVDownload,
selectPrivacyRequestFilters,
setRequestFrom,
setRequestId,
setRequestStatus,
setRequestTo,
} from "../privacy-requests";
import { PrivacyRequestStatus } from "../privacy-requests/types";
import { ConnectionType, SystemType, TestingStatus } from "./types";
selectDatastoreConnectionFilters,
setDisabledStatus,
setSearch,
setSystemType,
setTestingStatus,
} from "./datastore-connection.slice";
import { DisabledStatus, SystemType, TestingStatus } from "./types";

const useConstantFilters = () => {
const filters = useSelector(selectPrivacyRequestFilters);
const token = useSelector(selectToken);
const filters = useSelector(selectDatastoreConnectionFilters);
const dispatch = useDispatch();
const toast = useToast();
const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>) =>
dispatch(setRequestId(event.target.value));
const handleStatusChange = (event: React.ChangeEvent<HTMLSelectElement>) =>
dispatch(setRequestStatus(event.target.value as PrivacyRequestStatus));
const handleFromChange = (event: React.ChangeEvent<HTMLInputElement>) =>
dispatch(setRequestFrom(event?.target.value));
const handleToChange = (event: React.ChangeEvent<HTMLInputElement>) =>
dispatch(setRequestTo(event?.target.value));
const handleClearAllFilters = () => dispatch(clearAllFilters());
const handleDownloadClick = async () => {
let message;
try {
await requestCSVDownload({ ...filters, token });
} catch (error) {
if (error instanceof Error) {
message = error.message;
} else {
message = "Unknown error occurred";
}
}
if (message) {
toast({
description: `${message}`,
duration: 5000,
status: "error",
});
}
};
dispatch(setSearch(event.target.value));
const handleSystemTypeChange = (value: string) =>
dispatch(setSystemType(value));
const handleTestingStatusChange = (value: string) =>
dispatch(setTestingStatus(value));
const handleDisabledStatusChange = (value: string) =>
dispatch(setDisabledStatus(value));

return {
handleSearchChange,
handleStatusChange,
handleFromChange,
handleToChange,
handleClearAllFilters,
handleDownloadClick,
handleSystemTypeChange,
handleTestingStatusChange,
handleDisabledStatusChange,
...filters,
};
};

const DataStoreTypeOption: React.FC<{ status: ConnectionType }> = ({
status,
}) => <option value={status}>{capitalize(status)}</option>;

const SystemTypeOption: React.FC<{ status: SystemType }> = ({ status }) => (
<option value={status}>{capitalize(status)}</option>
);

const TestingStatusOption: React.FC<{ status: TestingStatus }> = ({
status,
}) => <option value={status}>{capitalize(status)}</option>;

const ConnectionFilters: React.FC = () => {
const { status, handleSearchChange, handleStatusChange, id } =
useConstantFilters();
const {
handleSearchChange,
handleSystemTypeChange,
handleTestingStatusChange,
handleDisabledStatusChange,
search,
// eslint-disable-next-line @typescript-eslint/naming-convention
system_type,
// eslint-disable-next-line @typescript-eslint/naming-convention
test_status,
// eslint-disable-next-line @typescript-eslint/naming-convention
disabled_status,
} = useConstantFilters();
return (
<Stack direction="row" spacing={4} mb={6}>
<InputGroup size="sm">
Expand All @@ -96,65 +61,31 @@ const ConnectionFilters: React.FC = () => {
placeholder="Search"
size="sm"
borderRadius="md"
value={id}
value={search}
name="search"
onChange={handleSearchChange}
/>
</InputGroup>
<Select
placeholder="Datastore Type"
size="sm"
minWidth="144px"
value={status || ""}
onChange={handleStatusChange}
borderRadius="md"
>
<DataStoreTypeOption status={ConnectionType.POSTGRES} />
<DataStoreTypeOption status={ConnectionType.MONGODB} />
<DataStoreTypeOption status={ConnectionType.MYSQL} />
<DataStoreTypeOption status={ConnectionType.HTTPS} />
<DataStoreTypeOption status={ConnectionType.REDSHIFT} />
<DataStoreTypeOption status={ConnectionType.SNOWFLAKE} />
<DataStoreTypeOption status={ConnectionType.MSSQL} />
<DataStoreTypeOption status={ConnectionType.MARIADB} />
<DataStoreTypeOption status={ConnectionType.BIGQUERY} />
<DataStoreTypeOption status={ConnectionType.MANUAL} />
</Select>
<Select
placeholder="System Type"
size="sm"
minWidth="144px"
value={status || ""}
onChange={handleStatusChange}
borderRadius="md"
>
<SystemTypeOption status={SystemType.SAAS} />
<SystemTypeOption status={SystemType.DATABASE} />
<SystemTypeOption status={SystemType.MANUAL} />
</Select>
<Select
placeholder="Testing Status"
size="sm"
minWidth="144px"
value={status || ""}
onChange={handleStatusChange}
borderRadius="md"
>
<TestingStatusOption status={TestingStatus.PASSED} />
<TestingStatusOption status={TestingStatus.FAILED} />
<TestingStatusOption status={TestingStatus.UNTESTED} />
</Select>
<Select
placeholder="Status"
size="sm"
minWidth="144px"
value={status || ""}
onChange={handleStatusChange}
borderRadius="md"
>
<option value="false">Enabled</option>
<option value="true">Disabled</option>
</Select>
<ConnectionStatusMenu />
<SystemTypeMenu
title="System Type"
filterOptions={Object.values(SystemType)}
value={system_type}
setValue={handleSystemTypeChange}
/>
<SystemTypeMenu
title="Testing Status"
filterOptions={Object.values(TestingStatus)}
value={test_status}
setValue={handleTestingStatusChange}
/>

<SystemTypeMenu
title="Status"
filterOptions={Object.values(DisabledStatus)}
value={disabled_status}
setValue={handleDisabledStatusChange}
/>
</Stack>
);
};
Expand Down
Loading

0 comments on commit 7c3bf98

Please sign in to comment.