From 2bc94111023d65796a8f356f9d5833fb8bfddd24 Mon Sep 17 00:00:00 2001 From: chriscalhoun1974 <68459950+chriscalhoun1974@users.noreply.github.com> Date: Wed, 12 Oct 2022 13:54:04 -0400 Subject: [PATCH 01/58] 1203 - Individually select and reprocess DSRs that have errored #1203 --- .../privacy-requests/ReprocessButton.tsx | 62 ++++++++++ .../features/privacy-requests/RequestRow.tsx | 114 ++++++++++-------- .../subject-request/RequestDetails.tsx | 49 +------- 3 files changed, 130 insertions(+), 95 deletions(-) create mode 100644 clients/admin-ui/src/features/privacy-requests/ReprocessButton.tsx diff --git a/clients/admin-ui/src/features/privacy-requests/ReprocessButton.tsx b/clients/admin-ui/src/features/privacy-requests/ReprocessButton.tsx new file mode 100644 index 00000000000..f4b977ef12a --- /dev/null +++ b/clients/admin-ui/src/features/privacy-requests/ReprocessButton.tsx @@ -0,0 +1,62 @@ +import { Button, ButtonProps } from "@fidesui/react"; +import React, { useState } from "react"; + +import { useAlert, useAPIHelper } from "../common/hooks"; +import { useRetryMutation } from "./privacy-requests.slice"; +import { PrivacyRequest } from "./types"; + +type ReprocessButtonProps = { + buttonProps?: ButtonProps; + ref?: React.LegacyRef; + subjectRequest: PrivacyRequest; +}; + +const ReprocessButton: React.FC = ({ + buttonProps, + ref, + subjectRequest, +}) => { + const { successAlert } = useAlert(); + const { handleError } = useAPIHelper(); + const [retry] = useRetryMutation(); + const [isReprocessing, setIsReprocessing] = useState(false); + + const handleReprocessClick = () => { + setIsReprocessing(true); + retry(subjectRequest) + .unwrap() + .then(() => { + successAlert(`Data subject request is now being reprocessed.`); + }) + .catch((error) => { + handleError(error); + }) + .finally(() => { + setIsReprocessing(false); + }); + }; + + return ( + + ); +}; + +export default ReprocessButton; diff --git a/clients/admin-ui/src/features/privacy-requests/RequestRow.tsx b/clients/admin-ui/src/features/privacy-requests/RequestRow.tsx index aa213d86a8c..642ba7ef89a 100644 --- a/clients/admin-ui/src/features/privacy-requests/RequestRow.tsx +++ b/clients/admin-ui/src/features/privacy-requests/RequestRow.tsx @@ -13,7 +13,7 @@ import { Text, Tr, useClipboard, - useToast, + useToast } from "@fidesui/react"; import DaysLeftTag from "common/DaysLeftTag"; import { formatDate } from "common/utils"; @@ -26,8 +26,9 @@ import RequestStatusBadge from "../common/RequestStatusBadge"; import DenyPrivacyRequestModal from "./DenyPrivacyRequestModal"; import { useApproveRequestMutation, - useDenyRequestMutation, + useDenyRequestMutation } from "./privacy-requests.slice"; +import ReprocessButton from "./ReprocessButton"; import { PrivacyRequest } from "./types"; const useRequestRow = (request: PrivacyRequest) => { @@ -215,53 +216,68 @@ const RequestRow: React.FC<{ request: PrivacyRequest }> = ({ request }) => { shadow="base" borderRadius="md" > - {request.status === "pending" ? ( - <> - - - { - setDenialReason(e.target.value); - }} - /> - - ) : null} - + {(() => { + switch (request.status) { + case "error": + return ( + + ); + case "pending": + return ( + <> + + + { + setDenialReason(e.target.value); + }} + /> + + ); + default: + return null; + } + })()} + {/* Hamburger menu */} { const { id, status, policy } = subjectRequest; - const [retry] = useRetryMutation(); - const toast = useToast(); - const [isRetrying, setRetrying] = useState(false); - - const handleRetry = async () => { - setRetrying(true); - retry(subjectRequest) - .unwrap() - .catch((error) => { - let errorMsg = "An unexpected error occurred. Please try again."; - if (isErrorWithDetail(error)) { - errorMsg = error.data.detail; - } else if (isErrorWithDetailArray(error)) { - errorMsg = error.data.detail[0].msg; - } - toast({ - status: "error", - description: errorMsg, - }); - }) - .finally(() => { - setRetrying(false); - }); - }; return ( <> @@ -84,16 +50,7 @@ const RequestDetails = ({ subjectRequest }: RequestDetailsProps) => { {status === "error" && ( - + )} From 1a828242a37453cbe7bd7dcf41d766a06c6fdd8e Mon Sep 17 00:00:00 2001 From: Neville Samuell Date: Sat, 15 Oct 2022 13:16:35 -0400 Subject: [PATCH 02/58] Fix Typescript errors for consent cookie --- clients/privacy-center/components/ConsentItemCard.tsx | 2 +- clients/privacy-center/features/consent/helpers.ts | 2 +- clients/privacy-center/features/consent/types.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/clients/privacy-center/components/ConsentItemCard.tsx b/clients/privacy-center/components/ConsentItemCard.tsx index 9666f0a12f5..edd5f67485e 100644 --- a/clients/privacy-center/components/ConsentItemCard.tsx +++ b/clients/privacy-center/components/ConsentItemCard.tsx @@ -10,7 +10,7 @@ import { HStack, } from "@fidesui/react"; import { ExternalLinkIcon } from "@chakra-ui/icons"; -import { ConsentItem } from "../types"; +import { ConsentItem } from "~/features/consent/types"; type SetConsentValueProp = { setConsentValue: (x: boolean) => void; diff --git a/clients/privacy-center/features/consent/helpers.ts b/clients/privacy-center/features/consent/helpers.ts index 8b65b0d32e2..43fcca999fe 100644 --- a/clients/privacy-center/features/consent/helpers.ts +++ b/clients/privacy-center/features/consent/helpers.ts @@ -69,7 +69,7 @@ export const makeCookieKeyConsent = ( const consent = item.consentValue === undefined ? item.defaultValue : item.consentValue; - item.cookieKeys.forEach((cookieKey) => { + item.cookieKeys?.forEach((cookieKey) => { const previousConsent = cookieKeyConsent[cookieKey]; cookieKeyConsent[cookieKey] = previousConsent === undefined ? consent : previousConsent && consent; diff --git a/clients/privacy-center/features/consent/types.ts b/clients/privacy-center/features/consent/types.ts index 1f7494f6867..e702852c499 100644 --- a/clients/privacy-center/features/consent/types.ts +++ b/clients/privacy-center/features/consent/types.ts @@ -6,7 +6,7 @@ export type ConsentItem = { url: string; defaultValue: boolean; consentValue?: boolean; - cookieKeys: string[]; + cookieKeys?: string[]; }; export type ApiUserConsent = { From 9099cef09d0216c271d3e1edd3f13c5179b1fa58 Mon Sep 17 00:00:00 2001 From: Paul Sanders Date: Mon, 17 Oct 2022 08:26:58 -0400 Subject: [PATCH 03/58] Add bulk restart from failure endpoint endpoint --- .../v1/endpoints/privacy_request_endpoints.py | 110 ++++++++++-- src/fides/api/ops/api/v1/urn_registry.py | 1 + .../test_privacy_request_endpoints.py | 163 ++++++++++++++++++ 3 files changed, 255 insertions(+), 19 deletions(-) diff --git a/src/fides/api/ops/api/v1/endpoints/privacy_request_endpoints.py b/src/fides/api/ops/api/v1/endpoints/privacy_request_endpoints.py index 58f0df68da0..b178a3229bb 100644 --- a/src/fides/api/ops/api/v1/endpoints/privacy_request_endpoints.py +++ b/src/fides/api/ops/api/v1/endpoints/privacy_request_endpoints.py @@ -48,6 +48,7 @@ from fides.api.ops.api.v1.urn_registry import ( PRIVACY_REQUEST_ACCESS_MANUAL_WEBHOOK_INPUT, PRIVACY_REQUEST_APPROVE, + PRIVACY_REQUEST_BULK_RETRY, PRIVACY_REQUEST_DENY, PRIVACY_REQUEST_MANUAL_ERASURE, PRIVACY_REQUEST_MANUAL_INPUT, @@ -996,6 +997,68 @@ async def resume_with_erasure_confirmation( ) +@router.post( + PRIVACY_REQUEST_BULK_RETRY, + status_code=HTTP_200_OK, + response_model=BulkPostPrivacyRequests, + dependencies=[ + Security(verify_oauth_client, scopes=[PRIVACY_REQUEST_CALLBACK_RESUME]) + ], +) +async def bulk_restart_privacy_request_from_failure( + privacy_request_ids: List[str], + *, + db: Session = Depends(deps.get_db), +) -> BulkPostPrivacyRequests: + """Bulk restart a of privacy request from failure.""" + + succeeded: List[PrivacyRequestResponse] = [] + failed: List[Dict[str, Any]] = [] + + # privacy_request = PrivacyRequest.get(db, object_id=request_id) + + for privacy_request_id in privacy_request_ids: + privacy_request = PrivacyRequest.get(db, object_id=privacy_request_id) + + if not privacy_request: + failed.append( + { + "message": f"No privacy request found with id '{privacy_request_id}'", + "data": {"privacy_request_id": privacy_request_id}, + } + ) + continue + + if privacy_request.status != PrivacyRequestStatus.error: + failed.append( + { + "message": f"Cannot restart privacy request from failure: privacy request '{privacy_request.id}' status = {privacy_request.status.value}.", + "data": {"privacy_request_id": privacy_request_id}, + } + ) + continue + + failed_details: Optional[ + CheckpointActionRequired + ] = privacy_request.get_failed_checkpoint_details() + if not failed_details: + failed.append( + { + "message": f"Cannot restart privacy request from failure '{privacy_request.id}'; no failed step or collection.", + "data": {"privacy_request_id": privacy_request_id}, + } + ) + continue + + succeeded.append( + _process_privacy_request_restart( + privacy_request, failed_details.step, failed_details.collection, db + ) + ) + + return BulkPostPrivacyRequests(succeeded=succeeded, failed=failed) + + @router.post( PRIVACY_REQUEST_RETRY, status_code=HTTP_200_OK, @@ -1029,27 +1092,10 @@ async def restart_privacy_request_from_failure( detail=f"Cannot restart privacy request from failure '{privacy_request.id}'; no failed step or collection.", ) - failed_step: CurrentStep = failed_details.step - failed_collection: Optional[CollectionAddress] = failed_details.collection - - logger.info( - "Restarting failed privacy request '%s' from '%s step, 'collection '%s'", - privacy_request_id, - failed_step, - failed_collection, + return _process_privacy_request_restart( + privacy_request, failed_details.step, failed_details.collection, db ) - privacy_request.status = PrivacyRequestStatus.in_processing - privacy_request.save(db=db) - queue_privacy_request( - privacy_request_id=privacy_request.id, - from_step=failed_step.value, - ) - - privacy_request.cache_failed_checkpoint_details() # Reset failed step and collection to None - - return privacy_request - def review_privacy_request( db: Session, @@ -1434,3 +1480,29 @@ async def resume_privacy_request_from_requires_input( ) return privacy_request + + +def _process_privacy_request_restart( + privacy_request: PrivacyRequest, + failed_step: CurrentStep, + failed_collection: Optional[CollectionAddress], + db: Session, +) -> PrivacyRequestResponse: + + logger.info( + "Restarting failed privacy request '%s' from '%s step, 'collection '%s'", + privacy_request.id, + failed_step, + failed_collection, + ) + + privacy_request.status = PrivacyRequestStatus.in_processing + privacy_request.save(db=db) + queue_privacy_request( + privacy_request_id=privacy_request.id, + from_step=failed_step.value, + ) + + privacy_request.cache_failed_checkpoint_details() # Reset failed step and collection to None + + return privacy_request diff --git a/src/fides/api/ops/api/v1/urn_registry.py b/src/fides/api/ops/api/v1/urn_registry.py index cee67929362..df1242dbd22 100644 --- a/src/fides/api/ops/api/v1/urn_registry.py +++ b/src/fides/api/ops/api/v1/urn_registry.py @@ -49,6 +49,7 @@ # Privacy request URLs PRIVACY_REQUESTS = "/privacy-request" PRIVACY_REQUEST_APPROVE = "/privacy-request/administrate/approve" +PRIVACY_REQUEST_BULK_RETRY = "/privacy-request/bulk/retry" PRIVACY_REQUEST_DENY = "/privacy-request/administrate/deny" REQUEST_STATUS_LOGS = "/privacy-request/{privacy_request_id}/log" PRIVACY_REQUEST_VERIFY_IDENTITY = "/privacy-request/{privacy_request_id}/verify" diff --git a/tests/ops/api/v1/endpoints/test_privacy_request_endpoints.py b/tests/ops/api/v1/endpoints/test_privacy_request_endpoints.py index 6c1e7d32f81..e7d31e07669 100644 --- a/tests/ops/api/v1/endpoints/test_privacy_request_endpoints.py +++ b/tests/ops/api/v1/endpoints/test_privacy_request_endpoints.py @@ -38,6 +38,7 @@ DATASETS, PRIVACY_REQUEST_ACCESS_MANUAL_WEBHOOK_INPUT, PRIVACY_REQUEST_APPROVE, + PRIVACY_REQUEST_BULK_RETRY, PRIVACY_REQUEST_DENY, PRIVACY_REQUEST_MANUAL_ERASURE, PRIVACY_REQUEST_MANUAL_INPUT, @@ -2574,6 +2575,168 @@ def test_resume_with_manual_count( privacy_request.delete(db) +class TestBulkRestartFromFailure: + @pytest.fixture(scope="function") + def url(self): + return f"{V1_URL_PREFIX}{PRIVACY_REQUEST_BULK_RETRY}" + + def test_restart_from_failure_not_authenticated(self, api_client, url): + data = ["1234", "5678"] + response = api_client.post(url, json=data, headers={}) + assert response.status_code == 401 + + def test_restart_from_failure_wrong_scope( + self, api_client, url, generate_auth_header + ): + auth_header = generate_auth_header(scopes=[PRIVACY_REQUEST_READ]) + data = ["1234", "5678"] + + response = api_client.post(url, json=data, headers=auth_header) + assert response.status_code == 403 + + @pytest.mark.usefixtures("privacy_requests") + def test_restart_from_failure_not_errored( + self, api_client, url, generate_auth_header + ): + auth_header = generate_auth_header(scopes=[PRIVACY_REQUEST_CALLBACK_RESUME]) + data = ["1234", "5678"] + + response = api_client.post(url, json=data, headers=auth_header) + assert response.status_code == 200 + + assert response.json()["succeeded"] == [] + + failed_ids = [ + x["data"]["privacy_request_id"] for x in response.json()["failed"] + ] + assert sorted(failed_ids) == sorted(data) + + def test_restart_from_failure_no_stopped_step( + self, api_client, url, generate_auth_header, db, privacy_requests + ): + auth_header = generate_auth_header(scopes=[PRIVACY_REQUEST_CALLBACK_RESUME]) + data = [privacy_requests[0].id] + + privacy_requests[0].status = PrivacyRequestStatus.error + privacy_requests[0].save(db) + + response = api_client.post(url, json=data, headers=auth_header) + + assert response.status_code == 200 + assert response.json()["succeeded"] == [] + + failed_ids = [ + x["data"]["privacy_request_id"] for x in response.json()["failed"] + ] + + assert privacy_requests[0].id in failed_ids + + @mock.patch( + "fides.api.ops.service.privacy_request.request_runner_service.run_privacy_request.delay" + ) + def test_restart_from_failure_from_specific_collection( + self, submit_mock, api_client, url, generate_auth_header, db, privacy_requests + ): + auth_header = generate_auth_header(scopes=[PRIVACY_REQUEST_CALLBACK_RESUME]) + data = [privacy_requests[0].id] + privacy_requests[0].status = PrivacyRequestStatus.error + privacy_requests[0].save(db) + + privacy_requests[0].cache_failed_checkpoint_details( + step=CurrentStep.access, + collection=CollectionAddress("test_dataset", "test_collection"), + ) + + response = api_client.post(url, json=data, headers=auth_header) + assert response.status_code == 200 + + db.refresh(privacy_requests[0]) + assert privacy_requests[0].status == PrivacyRequestStatus.in_processing + assert response.json()["failed"] == [] + + succeeded_ids = [x["id"] for x in response.json()["succeeded"]] + + assert privacy_requests[0].id in succeeded_ids + + submit_mock.assert_called_with( + privacy_request_id=privacy_requests[0].id, + from_step=CurrentStep.access.value, + from_webhook_id=None, + ) + + @mock.patch( + "fides.api.ops.service.privacy_request.request_runner_service.run_privacy_request.delay" + ) + def test_restart_from_failure_outside_graph( + self, submit_mock, api_client, url, generate_auth_header, db, privacy_requests + ): + auth_header = generate_auth_header(scopes=[PRIVACY_REQUEST_CALLBACK_RESUME]) + data = [privacy_requests[0].id] + privacy_requests[0].status = PrivacyRequestStatus.error + privacy_requests[0].save(db) + + privacy_requests[0].cache_failed_checkpoint_details( + step=CurrentStep.erasure_email_post_send, + collection=None, + ) + + response = api_client.post(url, json=data, headers=auth_header) + assert response.status_code == 200 + + db.refresh(privacy_requests[0]) + assert privacy_requests[0].status == PrivacyRequestStatus.in_processing + assert response.json()["failed"] == [] + + succeeded_ids = [x["id"] for x in response.json()["succeeded"]] + + assert privacy_requests[0].id in succeeded_ids + + submit_mock.assert_called_with( + privacy_request_id=privacy_requests[0].id, + from_step=CurrentStep.erasure_email_post_send.value, + from_webhook_id=None, + ) + + @mock.patch( + "fides.api.ops.service.privacy_request.request_runner_service.run_privacy_request.delay" + ) + def test_mixed_result( + self, submit_mock, api_client, url, generate_auth_header, db, privacy_requests + ): + auth_header = generate_auth_header(scopes=[PRIVACY_REQUEST_CALLBACK_RESUME]) + data = [privacy_requests[0].id, privacy_requests[1].id] + privacy_requests[0].status = PrivacyRequestStatus.error + privacy_requests[0].save(db) + + privacy_requests[0].cache_failed_checkpoint_details( + step=CurrentStep.access, + collection=CollectionAddress("test_dataset", "test_collection"), + ) + + privacy_requests[1].status = PrivacyRequestStatus.error + privacy_requests[1].save(db) + + response = api_client.post(url, json=data, headers=auth_header) + assert response.status_code == 200 + + db.refresh(privacy_requests[0]) + assert privacy_requests[0].status == PrivacyRequestStatus.in_processing + + succeeded_ids = [x["id"] for x in response.json()["succeeded"]] + failed_ids = [ + x["data"]["privacy_request_id"] for x in response.json()["failed"] + ] + + assert privacy_requests[0].id in succeeded_ids + assert privacy_requests[1].id in failed_ids + + submit_mock.assert_called_with( + privacy_request_id=privacy_requests[0].id, + from_step=CurrentStep.access.value, + from_webhook_id=None, + ) + + class TestRestartFromFailure: @pytest.fixture(scope="function") def url(self, db, privacy_request): From 68a8fa17ce5ebf239d95c83acb519f302bbdec13 Mon Sep 17 00:00:00 2001 From: Paul Sanders Date: Mon, 17 Oct 2022 17:33:04 -0400 Subject: [PATCH 04/58] Fix mypy error --- .../authentication_strategy_doordash.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/fides/api/ops/service/saas_request/override_implementations/authentication_strategy_doordash.py b/src/fides/api/ops/service/saas_request/override_implementations/authentication_strategy_doordash.py index 6752b7ed6f2..7b9d13e5b68 100644 --- a/src/fides/api/ops/service/saas_request/override_implementations/authentication_strategy_doordash.py +++ b/src/fides/api/ops/service/saas_request/override_implementations/authentication_strategy_doordash.py @@ -1,5 +1,6 @@ import math import time +from typing import Any, Dict, Optional import jwt.utils from requests import PreparedRequest @@ -42,13 +43,15 @@ def add_authentication( Generate a Doordash JWT and add it as bearer auth """ - secrets = connection_config.secrets + secrets: Optional[Dict[str, Any]] = connection_config.secrets token = jwt.encode( { "aud": "doordash", - "iss": assign_placeholders(self.developer_id, secrets), - "kid": assign_placeholders(self.key_id, secrets), + "iss": assign_placeholders(self.developer_id, secrets) + if secrets + else None, + "kid": assign_placeholders(self.key_id, secrets) if secrets else None, "exp": str(math.floor(time.time() + 60)), "iat": str(math.floor(time.time())), }, From 776e79f864fdc465d4839ecf06e3a8ae3dd6edbe Mon Sep 17 00:00:00 2001 From: chriscalhoun1974 <68459950+chriscalhoun1974@users.noreply.github.com> Date: Tue, 18 Oct 2022 23:16:49 -0400 Subject: [PATCH 05/58] 1205 - Bulk select and reprocess DSRs that have errored --- .../privacy-requests/ReprocessButton.tsx | 62 --------- .../features/privacy-requests/RequestRow.tsx | 24 +++- .../privacy-requests/RequestTable.tsx | 100 +++++++++++--- .../buttons/ActionButtons.tsx | 19 +++ .../buttons/ReprocessButton.tsx | 87 ++++++++++++ .../privacy-requests.slice.ts | 125 ++++++++++-------- .../subject-request/RequestDetails.tsx | 7 +- clients/admin-ui/src/pages/index.tsx | 13 +- 8 files changed, 287 insertions(+), 150 deletions(-) delete mode 100644 clients/admin-ui/src/features/privacy-requests/ReprocessButton.tsx create mode 100644 clients/admin-ui/src/features/privacy-requests/buttons/ActionButtons.tsx create mode 100644 clients/admin-ui/src/features/privacy-requests/buttons/ReprocessButton.tsx diff --git a/clients/admin-ui/src/features/privacy-requests/ReprocessButton.tsx b/clients/admin-ui/src/features/privacy-requests/ReprocessButton.tsx deleted file mode 100644 index f4b977ef12a..00000000000 --- a/clients/admin-ui/src/features/privacy-requests/ReprocessButton.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { Button, ButtonProps } from "@fidesui/react"; -import React, { useState } from "react"; - -import { useAlert, useAPIHelper } from "../common/hooks"; -import { useRetryMutation } from "./privacy-requests.slice"; -import { PrivacyRequest } from "./types"; - -type ReprocessButtonProps = { - buttonProps?: ButtonProps; - ref?: React.LegacyRef; - subjectRequest: PrivacyRequest; -}; - -const ReprocessButton: React.FC = ({ - buttonProps, - ref, - subjectRequest, -}) => { - const { successAlert } = useAlert(); - const { handleError } = useAPIHelper(); - const [retry] = useRetryMutation(); - const [isReprocessing, setIsReprocessing] = useState(false); - - const handleReprocessClick = () => { - setIsReprocessing(true); - retry(subjectRequest) - .unwrap() - .then(() => { - successAlert(`Data subject request is now being reprocessed.`); - }) - .catch((error) => { - handleError(error); - }) - .finally(() => { - setIsReprocessing(false); - }); - }; - - return ( - - ); -}; - -export default ReprocessButton; diff --git a/clients/admin-ui/src/features/privacy-requests/RequestRow.tsx b/clients/admin-ui/src/features/privacy-requests/RequestRow.tsx index 642ba7ef89a..aa1de67c572 100644 --- a/clients/admin-ui/src/features/privacy-requests/RequestRow.tsx +++ b/clients/admin-ui/src/features/privacy-requests/RequestRow.tsx @@ -3,6 +3,7 @@ import { AlertTitle, Button, ButtonGroup, + Checkbox, Menu, MenuButton, MenuItem, @@ -13,7 +14,7 @@ import { Text, Tr, useClipboard, - useToast + useToast, } from "@fidesui/react"; import DaysLeftTag from "common/DaysLeftTag"; import { formatDate } from "common/utils"; @@ -23,12 +24,12 @@ import React, { useRef, useState } from "react"; import { MoreIcon } from "../common/Icon"; import PII from "../common/PII"; import RequestStatusBadge from "../common/RequestStatusBadge"; +import ReprocessButton from "./buttons/ReprocessButton"; import DenyPrivacyRequestModal from "./DenyPrivacyRequestModal"; import { useApproveRequestMutation, - useDenyRequestMutation + useDenyRequestMutation, } from "./privacy-requests.slice"; -import ReprocessButton from "./ReprocessButton"; import { PrivacyRequest } from "./types"; const useRequestRow = (request: PrivacyRequest) => { @@ -114,7 +115,11 @@ const useRequestRow = (request: PrivacyRequest) => { }; }; -const RequestRow: React.FC<{ request: PrivacyRequest }> = ({ request }) => { +const RequestRow: React.FC<{ + isChecked: boolean; + onCheckChange: (id: string, checked: boolean) => void; + request: PrivacyRequest; +}> = ({ isChecked, onCheckChange, request }) => { const { hovered, handleMenuOpen, @@ -150,6 +155,14 @@ const RequestRow: React.FC<{ request: PrivacyRequest }> = ({ request }) => { onMouseLeave={handleMouseLeave} height="36px" > + + onCheckChange(request.id, e.target.checked)} + /> + @@ -221,8 +234,7 @@ const RequestRow: React.FC<{ request: PrivacyRequest }> = ({ request }) => { case "error": return ( ); diff --git a/clients/admin-ui/src/features/privacy-requests/RequestTable.tsx b/clients/admin-ui/src/features/privacy-requests/RequestTable.tsx index f80115d1899..1cb87e716e6 100644 --- a/clients/admin-ui/src/features/privacy-requests/RequestTable.tsx +++ b/clients/admin-ui/src/features/privacy-requests/RequestTable.tsx @@ -1,11 +1,14 @@ -import { Flex, Table, Tbody, Th, Thead, Tr } from "@fidesui/react"; +import { Checkbox, Flex, Table, Tbody, Th, Thead, Tr } from "@fidesui/react"; import { debounce } from "common/utils"; -import React, { useEffect, useRef, useState } from "react"; -import { useDispatch, useSelector } from "react-redux"; +import React, { useCallback, useEffect, useRef, useState } from "react"; + +import { useAppDispatch, useAppSelector } from "~/app/hooks"; import PaginationFooter from "../common/PaginationFooter"; import { + selectErrorRequests, selectPrivacyRequestFilters, + setErrorRequests, setPage, useGetAllPrivacyRequestsQuery, } from "./privacy-requests.slice"; @@ -13,14 +16,15 @@ import RequestRow from "./RequestRow"; import SortRequestButton from "./SortRequestButton"; import { PrivacyRequest, PrivacyRequestParams } from "./types"; -interface RequestTableProps { +type RequestTableProps = { requests?: PrivacyRequest[]; -} +}; const useRequestTable = () => { - const dispatch = useDispatch(); - const filters = useSelector(selectPrivacyRequestFilters); + const dispatch = useAppDispatch(); + const filters = useAppSelector(selectPrivacyRequestFilters); const [cachedFilters, setCachedFilters] = useState(filters); + const [isSelectAll, setIsSelectAll] = useState(false); const updateCachedFilters = useRef( debounce( (updatedFilters: React.SetStateAction) => @@ -28,9 +32,30 @@ const useRequestTable = () => { 250 ) ); - useEffect(() => { - updateCachedFilters.current(filters); - }, [setCachedFilters, filters]); + + const errorRequests = useAppSelector(selectErrorRequests); + const { data, isFetching } = useGetAllPrivacyRequestsQuery(cachedFilters); + const { items: requests, total } = data || { items: [], total: 0 }; + + const getErrorRequests = useCallback( + () => requests.filter((r) => r.status === "error").map((r) => r.id), + [requests] + ); + + const handleCheckChange = (id: string, checked: boolean) => { + if (!checked && isSelectAll) { + setIsSelectAll(false); + } + let list: string[]; + if (checked) { + list = [...errorRequests, id]; + } else { + list = [...errorRequests]; + const index = list.findIndex((value) => value === id); + list.splice(index, 1); + } + dispatch(setErrorRequests(list)); + }; const handlePreviousPage = () => { dispatch(setPage(filters.page - 1)); @@ -40,29 +65,43 @@ const useRequestTable = () => { dispatch(setPage(filters.page + 1)); }; - const { data, isLoading, isFetching } = - useGetAllPrivacyRequestsQuery(cachedFilters); - const { items: requests, total } = data || { items: [], total: 0 }; + const handleSelectAll = () => { + const value = !isSelectAll; + setIsSelectAll(value); + dispatch(setErrorRequests(value ? getErrorRequests() : [])); + }; + + useEffect(() => { + updateCachedFilters.current(filters); + }, [filters]); + return { ...filters, - isLoading, + errorRequests, + handleCheckChange, + handleNextPage, + handlePreviousPage, + handleSelectAll, isFetching, + isSelectAll, requests, total, - handleNextPage, - handlePreviousPage, }; }; const RequestTable: React.FC = () => { const { - requests, - total, - page, - size, + errorRequests, + handleCheckChange, handleNextPage, handlePreviousPage, + handleSelectAll, isFetching, + isSelectAll, + page, + requests, + size, + total, } = useRequestTable(); return ( @@ -70,6 +109,18 @@ const RequestTable: React.FC = () => { + - {requests.map((request: any) => ( - + {requests.map((request: PrivacyRequest) => ( + ))}
+ r.status === "error") !== -1 + ? "auto" + : "none" + } + /> + Status @@ -89,8 +140,13 @@ const RequestTable: React.FC = () => {
diff --git a/clients/admin-ui/src/features/privacy-requests/buttons/ActionButtons.tsx b/clients/admin-ui/src/features/privacy-requests/buttons/ActionButtons.tsx new file mode 100644 index 00000000000..c1237491bbd --- /dev/null +++ b/clients/admin-ui/src/features/privacy-requests/buttons/ActionButtons.tsx @@ -0,0 +1,19 @@ +import { ButtonGroup } from "@fidesui/react"; +import React from "react"; + +import { useAppSelector } from "~/app/hooks"; + +import { selectErrorRequests } from "../privacy-requests.slice"; +import ReprocessButton from "./ReprocessButton"; + +const ActionButtons: React.FC = () => { + const errorRequests = useAppSelector(selectErrorRequests); + + return errorRequests.length > 0 ? ( + + + + ) : null; +}; + +export default ActionButtons; diff --git a/clients/admin-ui/src/features/privacy-requests/buttons/ReprocessButton.tsx b/clients/admin-ui/src/features/privacy-requests/buttons/ReprocessButton.tsx new file mode 100644 index 00000000000..32d1be3766d --- /dev/null +++ b/clients/admin-ui/src/features/privacy-requests/buttons/ReprocessButton.tsx @@ -0,0 +1,87 @@ +import { Button, ButtonProps, forwardRef } from "@fidesui/react"; +import React, { useState } from "react"; + +import { useAppDispatch, useAppSelector } from "~/app/hooks"; + +import { useAlert, useAPIHelper } from "../../common/hooks"; +import { + selectErrorRequests, + setErrorRequests, + useBulkRetryMutation, + useRetryMutation, +} from "../privacy-requests.slice"; +import { PrivacyRequest } from "../types"; + +type ReprocessButtonProps = { + buttonProps?: ButtonProps; + subjectRequest?: PrivacyRequest; +}; + +const ReprocessButton = forwardRef( + ({ buttonProps, subjectRequest }: ReprocessButtonProps, ref) => { + const dispatch = useAppDispatch(); + const [isReprocessing, setIsReprocessing] = useState(false); + const { handleError } = useAPIHelper(); + const { successAlert } = useAlert(); + + const errorRequests = useAppSelector(selectErrorRequests); + const [bulkRetry] = useBulkRetryMutation(); + const [retry] = useRetryMutation(); + + const handleBulkReprocessClick = () => { + setIsReprocessing(true); + bulkRetry(errorRequests!) + .unwrap() + .then(() => { + successAlert(`Data subject request(s) are now being reprocessed.`); + }) + .catch((error) => { + handleError(error); + }) + .finally(() => { + dispatch(setErrorRequests([])); + setIsReprocessing(false); + }); + }; + + const handleSingleReprocessClick = () => { + setIsReprocessing(true); + retry(subjectRequest!) + .unwrap() + .then(() => { + successAlert(`Data subject request is now being reprocessed.`); + }) + .catch((error) => { + handleError(error); + }) + .finally(() => { + setIsReprocessing(false); + }); + }; + + return ( + + ); + } +); + +export default ReprocessButton; diff --git a/clients/admin-ui/src/features/privacy-requests/privacy-requests.slice.ts b/clients/admin-ui/src/features/privacy-requests/privacy-requests.slice.ts index aa0d2773799..930b68c3e0a 100644 --- a/clients/admin-ui/src/features/privacy-requests/privacy-requests.slice.ts +++ b/clients/admin-ui/src/features/privacy-requests/privacy-requests.slice.ts @@ -79,6 +79,14 @@ export const privacyRequestApi = createApi({ }), invalidatesTags: ["Request"], }), + bulkRetry: build.mutation({ + query: (values) => ({ + url: `privacy-request/bulk/retry`, + method: "POST", + body: values, + }), + invalidatesTags: ["Request"], + }), denyRequest: build.mutation({ query: ({ id, reason }) => ({ url: "privacy-request/administrate/deny", @@ -138,6 +146,7 @@ export const privacyRequestApi = createApi({ export const { useApproveRequestMutation, + useBulkRetryMutation, useDenyRequestMutation, useGetAllPrivacyRequestsQuery, useGetUploadedManualWebhookDataQuery, @@ -191,123 +200,131 @@ export const requestCSVDownload = async ({ }; // Subject requests state (filters, etc.) -interface SubjectRequestsState { - revealPII: boolean; - status?: PrivacyRequestStatus[]; - id: string; +type SubjectRequestsState = { + errorRequests: string[]; from: string; - to: string; + id: string; page: number; + revealPII: boolean; size: number; - verbose?: boolean; - sort_field?: string; sort_direction?: string; -} + sort_field?: string; + status?: PrivacyRequestStatus[]; + to: string; + verbose?: boolean; +}; const initialState: SubjectRequestsState = { - revealPII: false, - id: "", + errorRequests: [], from: "", - to: "", + id: "", page: 1, + revealPII: false, size: 25, + to: "", }; export const subjectRequestsSlice = createSlice({ name: "subjectRequests", initialState, reducers: { - setRevealPII: (state, action: PayloadAction) => ({ + clearAllFilters: ({ revealPII }) => ({ + ...initialState, + revealPII, + }), + clearSortFields: (state) => ({ ...state, - revealPII: action.payload, + sort_direction: undefined, + sort_field: undefined, }), - setRequestStatus: ( - state, - action: PayloadAction - ) => ({ + setErrorRequests: (state, action: PayloadAction) => ({ + ...state, + errorRequests: action.payload, + }), + setPage: (state, action: PayloadAction) => ({ + ...state, + page: action.payload, + }), + setRequestFrom: (state, action: PayloadAction) => ({ ...state, page: initialState.page, - status: action.payload, + from: action.payload, }), setRequestId: (state, action: PayloadAction) => ({ ...state, page: initialState.page, id: action.payload, }), - setRequestFrom: (state, action: PayloadAction) => ({ + setRequestStatus: ( + state, + action: PayloadAction + ) => ({ ...state, page: initialState.page, - from: action.payload, + status: action.payload, }), setRequestTo: (state, action: PayloadAction) => ({ ...state, page: initialState.page, to: action.payload, }), - clearAllFilters: ({ revealPII }) => ({ - ...initialState, - revealPII, - }), - setPage: (state, action: PayloadAction) => ({ - ...state, - page: action.payload, - }), - setSize: (state, action: PayloadAction) => ({ + setRevealPII: (state, action: PayloadAction) => ({ ...state, - page: initialState.page, - size: action.payload, + revealPII: action.payload, }), - setVerbose: (state, action: PayloadAction) => ({ + setSortDirection: (state, action: PayloadAction) => ({ ...state, - verbose: action.payload, + sort_direction: action.payload, }), setSortField: (state, action: PayloadAction) => ({ ...state, sort_field: action.payload, }), - setSortDirection: (state, action: PayloadAction) => ({ + setSize: (state, action: PayloadAction) => ({ ...state, - sort_direction: action.payload, + page: initialState.page, + size: action.payload, }), - clearSortFields: (state) => ({ + setVerbose: (state, action: PayloadAction) => ({ ...state, - sort_direction: undefined, - sort_field: undefined, + verbose: action.payload, }), }, }); export const { - setRevealPII, + clearAllFilters, + clearSortFields, + setErrorRequests, + setPage, + setRequestFrom, setRequestId, setRequestStatus, - setRequestFrom, setRequestTo, - setPage, - setVerbose, - setSortField, + setRevealPII, setSortDirection, - clearAllFilters, - clearSortFields, + setSortField, + setVerbose, } = subjectRequestsSlice.actions; -export const selectRevealPII = (state: RootState) => - state.subjectRequests.revealPII; -export const selectRequestStatus = (state: RootState) => - state.subjectRequests.status; - +export const selectErrorRequests = (state: RootState) => + state.subjectRequests.errorRequests; export const selectPrivacyRequestFilters = ( state: RootState ): PrivacyRequestParams => ({ - status: state.subjectRequests.status, - id: state.subjectRequests.id, from: state.subjectRequests.from, - to: state.subjectRequests.to, + id: state.subjectRequests.id, page: state.subjectRequests.page, size: state.subjectRequests.size, - verbose: state.subjectRequests.verbose, sort_direction: state.subjectRequests.sort_direction, sort_field: state.subjectRequests.sort_field, + status: state.subjectRequests.status, + to: state.subjectRequests.to, + verbose: state.subjectRequests.verbose, }); +export const selectRequestStatus = (state: RootState) => + state.subjectRequests.status; +export const selectRevealPII = (state: RootState) => + state.subjectRequests.revealPII; export const { reducer } = subjectRequestsSlice; diff --git a/clients/admin-ui/src/features/subject-request/RequestDetails.tsx b/clients/admin-ui/src/features/subject-request/RequestDetails.tsx index 8dd9cf01bd1..07e1fe0c9ff 100644 --- a/clients/admin-ui/src/features/subject-request/RequestDetails.tsx +++ b/clients/admin-ui/src/features/subject-request/RequestDetails.tsx @@ -5,7 +5,7 @@ import { PrivacyRequest } from "privacy-requests/types"; import ClipboardButton from "../common/ClipboardButton"; import RequestStatusBadge from "../common/RequestStatusBadge"; import RequestType from "../common/RequestType"; -import ReprocessButton from "../privacy-requests/ReprocessButton"; +import ReprocessButton from "../privacy-requests/buttons/ReprocessButton"; type RequestDetailsProps = { subjectRequest: PrivacyRequest; @@ -50,7 +50,10 @@ const RequestDetails = ({ subjectRequest }: RequestDetailsProps) => { {status === "error" && ( - + )} diff --git a/clients/admin-ui/src/pages/index.tsx b/clients/admin-ui/src/pages/index.tsx index 0badca5e16e..fd5b9045a72 100644 --- a/clients/admin-ui/src/pages/index.tsx +++ b/clients/admin-ui/src/pages/index.tsx @@ -1,4 +1,4 @@ -import { Heading } from "@fidesui/react"; +import { Flex, Heading, Spacer } from "@fidesui/react"; import type { NextPage } from "next"; import RequestFilters from "privacy-requests/RequestFilters"; import RequestTable from "privacy-requests/RequestTable"; @@ -6,13 +6,18 @@ import RequestTable from "privacy-requests/RequestTable"; import { LOGIN_ROUTE } from "~/constants"; import ProtectedRoute from "~/features/auth/ProtectedRoute"; import Layout from "~/features/common/Layout"; +import ActionButtons from "~/features/privacy-requests/buttons/ActionButtons"; const Home: NextPage = () => ( - - Subject Requests - + + + Subject Requests + + + + From 9af2ae31640684bf99662c423009ac9043a1135b Mon Sep 17 00:00:00 2001 From: chriscalhoun1974 <68459950+chriscalhoun1974@users.noreply.github.com> Date: Wed, 19 Oct 2022 03:10:41 -0400 Subject: [PATCH 06/58] 1205 - Bulk select and reprocess DSRs that have errored --- .../privacy-requests/RequestTable.tsx | 46 +-- .../buttons/ActionButtons.tsx | 4 +- .../buttons/ReprocessButton.tsx | 15 +- .../privacy-requests.slice.ts | 269 ++++++++++-------- .../src/features/privacy-requests/types.ts | 5 + 5 files changed, 183 insertions(+), 156 deletions(-) diff --git a/clients/admin-ui/src/features/privacy-requests/RequestTable.tsx b/clients/admin-ui/src/features/privacy-requests/RequestTable.tsx index 1cb87e716e6..21de69dd614 100644 --- a/clients/admin-ui/src/features/privacy-requests/RequestTable.tsx +++ b/clients/admin-ui/src/features/privacy-requests/RequestTable.tsx @@ -6,10 +6,10 @@ import { useAppDispatch, useAppSelector } from "~/app/hooks"; import PaginationFooter from "../common/PaginationFooter"; import { - selectErrorRequests, selectPrivacyRequestFilters, - setErrorRequests, + selectRetryRequests, setPage, + setRetryRequests, useGetAllPrivacyRequestsQuery, } from "./privacy-requests.slice"; import RequestRow from "./RequestRow"; @@ -24,7 +24,6 @@ const useRequestTable = () => { const dispatch = useAppDispatch(); const filters = useAppSelector(selectPrivacyRequestFilters); const [cachedFilters, setCachedFilters] = useState(filters); - const [isSelectAll, setIsSelectAll] = useState(false); const updateCachedFilters = useRef( debounce( (updatedFilters: React.SetStateAction) => @@ -33,7 +32,7 @@ const useRequestTable = () => { ) ); - const errorRequests = useAppSelector(selectErrorRequests); + const { checkAll, errorRequests } = useAppSelector(selectRetryRequests); const { data, isFetching } = useGetAllPrivacyRequestsQuery(cachedFilters); const { items: requests, total } = data || { items: [], total: 0 }; @@ -43,9 +42,6 @@ const useRequestTable = () => { ); const handleCheckChange = (id: string, checked: boolean) => { - if (!checked && isSelectAll) { - setIsSelectAll(false); - } let list: string[]; if (checked) { list = [...errorRequests, id]; @@ -54,7 +50,12 @@ const useRequestTable = () => { const index = list.findIndex((value) => value === id); list.splice(index, 1); } - dispatch(setErrorRequests(list)); + dispatch( + setRetryRequests({ + checkAll: !(!checked && checkAll), + errorRequests: list, + }) + ); }; const handlePreviousPage = () => { @@ -65,25 +66,32 @@ const useRequestTable = () => { dispatch(setPage(filters.page + 1)); }; - const handleSelectAll = () => { - const value = !isSelectAll; - setIsSelectAll(value); - dispatch(setErrorRequests(value ? getErrorRequests() : [])); + const handleCheckAll = () => { + const value = !checkAll; + dispatch( + setRetryRequests({ + checkAll: value, + errorRequests: value ? getErrorRequests() : [], + }) + ); }; useEffect(() => { updateCachedFilters.current(filters); - }, [filters]); + if (isFetching && filters.status?.includes("error")) { + dispatch(setRetryRequests({ checkAll: false, errorRequests: [] })); + } + }, [dispatch, filters, isFetching]); return { ...filters, + checkAll, errorRequests, handleCheckChange, handleNextPage, handlePreviousPage, - handleSelectAll, + handleCheckAll, isFetching, - isSelectAll, requests, total, }; @@ -91,13 +99,13 @@ const useRequestTable = () => { const RequestTable: React.FC = () => { const { + checkAll, errorRequests, handleCheckChange, handleNextPage, handlePreviousPage, - handleSelectAll, + handleCheckAll, isFetching, - isSelectAll, page, requests, size, @@ -112,8 +120,8 @@ const RequestTable: React.FC = () => { r.status === "error") !== -1 ? "auto" diff --git a/clients/admin-ui/src/features/privacy-requests/buttons/ActionButtons.tsx b/clients/admin-ui/src/features/privacy-requests/buttons/ActionButtons.tsx index c1237491bbd..96094ead587 100644 --- a/clients/admin-ui/src/features/privacy-requests/buttons/ActionButtons.tsx +++ b/clients/admin-ui/src/features/privacy-requests/buttons/ActionButtons.tsx @@ -3,11 +3,11 @@ import React from "react"; import { useAppSelector } from "~/app/hooks"; -import { selectErrorRequests } from "../privacy-requests.slice"; +import { selectRetryRequests } from "../privacy-requests.slice"; import ReprocessButton from "./ReprocessButton"; const ActionButtons: React.FC = () => { - const errorRequests = useAppSelector(selectErrorRequests); + const { errorRequests } = useAppSelector(selectRetryRequests); return errorRequests.length > 0 ? ( diff --git a/clients/admin-ui/src/features/privacy-requests/buttons/ReprocessButton.tsx b/clients/admin-ui/src/features/privacy-requests/buttons/ReprocessButton.tsx index 32d1be3766d..805dd55712f 100644 --- a/clients/admin-ui/src/features/privacy-requests/buttons/ReprocessButton.tsx +++ b/clients/admin-ui/src/features/privacy-requests/buttons/ReprocessButton.tsx @@ -1,14 +1,12 @@ import { Button, ButtonProps, forwardRef } from "@fidesui/react"; -import React, { useState } from "react"; +import { useState } from "react"; -import { useAppDispatch, useAppSelector } from "~/app/hooks"; +import { useAppSelector } from "~/app/hooks"; import { useAlert, useAPIHelper } from "../../common/hooks"; import { - selectErrorRequests, - setErrorRequests, - useBulkRetryMutation, - useRetryMutation, + selectRetryRequests, useBulkRetryMutation, + useRetryMutation } from "../privacy-requests.slice"; import { PrivacyRequest } from "../types"; @@ -19,12 +17,11 @@ type ReprocessButtonProps = { const ReprocessButton = forwardRef( ({ buttonProps, subjectRequest }: ReprocessButtonProps, ref) => { - const dispatch = useAppDispatch(); const [isReprocessing, setIsReprocessing] = useState(false); const { handleError } = useAPIHelper(); const { successAlert } = useAlert(); - const errorRequests = useAppSelector(selectErrorRequests); + const { errorRequests } = useAppSelector(selectRetryRequests); const [bulkRetry] = useBulkRetryMutation(); const [retry] = useRetryMutation(); @@ -39,7 +36,6 @@ const ReprocessButton = forwardRef( handleError(error); }) .finally(() => { - dispatch(setErrorRequests([])); setIsReprocessing(false); }); }; @@ -62,6 +58,7 @@ const ReprocessButton = forwardRef( return ( - - { - setDenialReason(e.target.value); - }} - /> - - ); - default: - return null; - } - })()} - {/* Hamburger menu */} + {request.status === "error" && ( + + )} + {request.status === "pending" && ( + <> + + + { + setDenialReason(e.target.value); + }} + /> + + )} { if (checked) { list = [...errorRequests, id]; } else { - list = [...errorRequests]; - const index = list.findIndex((value) => value === id); - list.splice(index, 1); + errorRequests.filter(value => value !== id) + list = [...errorRequests.filter((value) => value !== id)]; } dispatch( setRetryRequests({ From e412d0e5cfcf84cd1ab145a93254e1d6f891c143 Mon Sep 17 00:00:00 2001 From: chriscalhoun1974 <68459950+chriscalhoun1974@users.noreply.github.com> Date: Fri, 21 Oct 2022 13:36:23 -0400 Subject: [PATCH 12/58] Code review feedback --- clients/admin-ui/src/features/privacy-requests/RequestTable.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/clients/admin-ui/src/features/privacy-requests/RequestTable.tsx b/clients/admin-ui/src/features/privacy-requests/RequestTable.tsx index 18eab1c747d..66844b2be50 100644 --- a/clients/admin-ui/src/features/privacy-requests/RequestTable.tsx +++ b/clients/admin-ui/src/features/privacy-requests/RequestTable.tsx @@ -46,7 +46,7 @@ const useRequestTable = () => { if (checked) { list = [...errorRequests, id]; } else { - errorRequests.filter(value => value !== id) + errorRequests.filter((value) => value !== id); list = [...errorRequests.filter((value) => value !== id)]; } dispatch( From 4231e29f97ee945363c6c4c3051de7bf40df610c Mon Sep 17 00:00:00 2001 From: chriscalhoun1974 <68459950+chriscalhoun1974@users.noreply.github.com> Date: Fri, 21 Oct 2022 13:40:29 -0400 Subject: [PATCH 13/58] Code review feedback --- .../src/features/privacy-requests/buttons/ReprocessButton.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/clients/admin-ui/src/features/privacy-requests/buttons/ReprocessButton.tsx b/clients/admin-ui/src/features/privacy-requests/buttons/ReprocessButton.tsx index 5c449203d10..ed0b5c36f6f 100644 --- a/clients/admin-ui/src/features/privacy-requests/buttons/ReprocessButton.tsx +++ b/clients/admin-ui/src/features/privacy-requests/buttons/ReprocessButton.tsx @@ -8,8 +8,8 @@ import { import { useState } from "react"; import { useAppDispatch, useAppSelector } from "~/app/hooks"; +import { useAlert, useAPIHelper } from "~/features/common/hooks"; -import { useAlert, useAPIHelper } from "../../common/hooks"; import { selectRetryRequests, setRetryRequests, From 460298a99f6b6fac35ef91a1fa591d3329271004 Mon Sep 17 00:00:00 2001 From: chriscalhoun1974 <68459950+chriscalhoun1974@users.noreply.github.com> Date: Fri, 21 Oct 2022 16:26:33 -0400 Subject: [PATCH 14/58] Code review feedback --- .../src/features/privacy-requests/buttons/ActionButtons.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/clients/admin-ui/src/features/privacy-requests/buttons/ActionButtons.tsx b/clients/admin-ui/src/features/privacy-requests/buttons/ActionButtons.tsx index 96094ead587..a749e55814a 100644 --- a/clients/admin-ui/src/features/privacy-requests/buttons/ActionButtons.tsx +++ b/clients/admin-ui/src/features/privacy-requests/buttons/ActionButtons.tsx @@ -9,7 +9,7 @@ import ReprocessButton from "./ReprocessButton"; const ActionButtons: React.FC = () => { const { errorRequests } = useAppSelector(selectRetryRequests); - return errorRequests.length > 0 ? ( + return errorRequests?.length > 0 ? ( From 89de341b23f3d434484741e7dcdf848922a61113 Mon Sep 17 00:00:00 2001 From: chriscalhoun1974 <68459950+chriscalhoun1974@users.noreply.github.com> Date: Fri, 21 Oct 2022 17:54:36 -0400 Subject: [PATCH 15/58] Code review feedback: Refactoring --- clients/admin-ui/package-lock.json | 27 ----- clients/admin-ui/package.json | 2 - .../src/features/common/hooks/useAlert.tsx | 45 +++----- .../features/privacy-requests/RequestRow.tsx | 2 +- .../buttons/ReprocessButton.tsx | 105 ++++++++---------- 5 files changed, 62 insertions(+), 119 deletions(-) diff --git a/clients/admin-ui/package-lock.json b/clients/admin-ui/package-lock.json index c237108e65c..350f2d3a9ce 100644 --- a/clients/admin-ui/package-lock.json +++ b/clients/admin-ui/package-lock.json @@ -35,7 +35,6 @@ "react-feature-flags": "^1.0.0", "react-redux": "^7.2.6", "redux-persist": "^6.0.0", - "uuid": "^9.0.0", "whatwg-fetch": "^3.6.2", "yup": "^0.32.11" }, @@ -48,7 +47,6 @@ "@types/node": "17.0.10", "@types/react": "17.0.38", "@types/react-redux": "^7.1.24", - "@types/uuid": "^8.3.4", "@typescript-eslint/eslint-plugin": "^5.12.0", "@typescript-eslint/parser": "^5.12.0", "babel-jest": "^27.5.1", @@ -4424,12 +4422,6 @@ "@types/jest": "*" } }, - "node_modules/@types/uuid": { - "version": "8.3.4", - "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz", - "integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==", - "dev": true - }, "node_modules/@types/warning": { "version": "3.0.0", "license": "MIT" @@ -12000,14 +11992,6 @@ "version": "1.0.2", "license": "MIT" }, - "node_modules/uuid": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", - "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/v8-compile-cache": { "version": "2.3.0", "dev": true, @@ -15617,12 +15601,6 @@ "@types/jest": "*" } }, - "@types/uuid": { - "version": "8.3.4", - "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz", - "integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==", - "dev": true - }, "@types/warning": { "version": "3.0.0" }, @@ -20373,11 +20351,6 @@ "util-deprecate": { "version": "1.0.2" }, - "uuid": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", - "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==" - }, "v8-compile-cache": { "version": "2.3.0", "dev": true diff --git a/clients/admin-ui/package.json b/clients/admin-ui/package.json index 1abd4424f3e..343a8419058 100644 --- a/clients/admin-ui/package.json +++ b/clients/admin-ui/package.json @@ -54,7 +54,6 @@ "react-feature-flags": "^1.0.0", "react-redux": "^7.2.6", "redux-persist": "^6.0.0", - "uuid": "^9.0.0", "whatwg-fetch": "^3.6.2", "yup": "^0.32.11" }, @@ -67,7 +66,6 @@ "@types/node": "17.0.10", "@types/react": "17.0.38", "@types/react-redux": "^7.1.24", - "@types/uuid": "^8.3.4", "@typescript-eslint/eslint-plugin": "^5.12.0", "@typescript-eslint/parser": "^5.12.0", "babel-jest": "^27.5.1", diff --git a/clients/admin-ui/src/features/common/hooks/useAlert.tsx b/clients/admin-ui/src/features/common/hooks/useAlert.tsx index 3bf6eaaedde..6d4ee00fb53 100644 --- a/clients/admin-ui/src/features/common/hooks/useAlert.tsx +++ b/clients/admin-ui/src/features/common/hooks/useAlert.tsx @@ -6,9 +6,8 @@ import { Box, CloseButton, useToast, + UseToastOptions, } from "@fidesui/react"; -import { CSSProperties } from "react"; -import { v4 as uuid } from "uuid"; /** * Custom hook for toast notifications @@ -24,20 +23,13 @@ export const useAlert = () => { */ const errorAlert = ( description: string | JSX.Element, - id: string = uuid(), title?: string, - duration?: number | null | undefined, - containerStyle?: CSSProperties + options?: UseToastOptions ) => { - if (toast.isActive(id)) { - return; - } toast({ - id, - containerStyle, - duration, - position: DEFAULT_POSITION, - render: () => ( + ...options, + position: options?.position || DEFAULT_POSITION, + render: ({ onClose }) => ( @@ -46,13 +38,11 @@ export const useAlert = () => { { - toast.close(id); - }} /> ), @@ -65,20 +55,13 @@ export const useAlert = () => { */ const successAlert = ( description: string, - id: string = uuid(), title?: string, - duration?: number | null | undefined, - containerStyle?: CSSProperties + options?: UseToastOptions ) => { - if (toast.isActive(id)) { - return; - } toast({ - id, - containerStyle, - duration, - position: DEFAULT_POSITION, - render: () => ( + ...options, + position: options?.position || DEFAULT_POSITION, + render: ({ onClose }) => ( @@ -87,13 +70,11 @@ export const useAlert = () => { { - toast.close(id); - }} /> ), diff --git a/clients/admin-ui/src/features/privacy-requests/RequestRow.tsx b/clients/admin-ui/src/features/privacy-requests/RequestRow.tsx index a3026cfe213..74e1291cf24 100644 --- a/clients/admin-ui/src/features/privacy-requests/RequestRow.tsx +++ b/clients/admin-ui/src/features/privacy-requests/RequestRow.tsx @@ -159,7 +159,7 @@ const RequestRow: React.FC<{ onCheckChange(request.id, e.target.checked)} /> diff --git a/clients/admin-ui/src/features/privacy-requests/buttons/ReprocessButton.tsx b/clients/admin-ui/src/features/privacy-requests/buttons/ReprocessButton.tsx index ed0b5c36f6f..80f227f2389 100644 --- a/clients/admin-ui/src/features/privacy-requests/buttons/ReprocessButton.tsx +++ b/clients/admin-ui/src/features/privacy-requests/buttons/ReprocessButton.tsx @@ -1,10 +1,4 @@ -import { - Button, - ButtonProps, - forwardRef, - ListItem, - UnorderedList, -} from "@fidesui/react"; +import { Box, Button, ButtonProps, forwardRef, Text } from "@fidesui/react"; import { useState } from "react"; import { useAppDispatch, useAppSelector } from "~/app/hooks"; @@ -34,63 +28,60 @@ const ReprocessButton = forwardRef( const [bulkRetry] = useBulkRetryMutation(); const [retry] = useRetryMutation(); - const handleBulkReprocessClick = () => { + const handleBulkReprocessClick = async () => { setIsReprocessing(true); - bulkRetry(errorRequests!) - .unwrap() - .then((response) => { - if (response.failed.length > 0) { - errorAlert( - - {response.failed.map((f, index) => ( - - Request Id: {f.data.privacy_request_id} -
- Message: {f.message} -
- ))} -
, - undefined, - `DSR automation has failed for the following request(s):`, - null, - { - maxWidth: "max-content", - } - ); - } - if (response.succeeded.length > 0) { - successAlert(`Data subject request(s) are now being reprocessed.`); - } - }) - .catch((error) => { - dispatch(setRetryRequests({ checkAll: false, errorRequests: [] })); + try { + const payload = await bulkRetry(errorRequests).unwrap(); + if (payload.failed.length > 0) { errorAlert( - error, + + DSR automation has failed for{" "} + + {payload.failed.length} + {" "} + subject request(s). Please review the event log for further + details. + , undefined, - `DSR batch automation has failed due to the following:` + { containerStyle: { maxWidth: "max-content" }, duration: null } ); - }) - .finally(() => { - setIsReprocessing(false); - }); + } + if (payload.succeeded.length > 0) { + successAlert(`Data subject request(s) are now being reprocessed.`); + } + } catch (error) { + dispatch(setRetryRequests({ checkAll: false, errorRequests: [] })); + errorAlert( + error as string, + `DSR batch automation has failed due to the following:`, + { duration: null } + ); + } finally { + setIsReprocessing(false); + } }; - const handleSingleReprocessClick = () => { + const handleSingleReprocessClick = async () => { + if (!subjectRequest) { + return; + } setIsReprocessing(true); - retry(subjectRequest!) - .unwrap() - .then(() => { - successAlert(`Data subject request is now being reprocessed.`); - }) - .catch((error) => { - handleError(error); - }) - .finally(() => { - setIsReprocessing(false); - }); + try { + await retry(subjectRequest).unwrap(); + successAlert(`Data subject request is now being reprocessed.`); + } catch (error) { + errorAlert( + + DSR automation has failed for this subject request. Please review + the event log for further details. + , + undefined, + { containerStyle: { maxWidth: "max-content" }, duration: null } + ); + handleError(error); + } finally { + setIsReprocessing(false); + } }; return ( From 6ee717b45b580e32e757e5d39ad65edd7f707a71 Mon Sep 17 00:00:00 2001 From: chriscalhoun1974 <68459950+chriscalhoun1974@users.noreply.github.com> Date: Fri, 21 Oct 2022 18:10:34 -0400 Subject: [PATCH 16/58] Update to userAlert hook --- clients/admin-ui/src/features/common/hooks/useAlert.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/clients/admin-ui/src/features/common/hooks/useAlert.tsx b/clients/admin-ui/src/features/common/hooks/useAlert.tsx index 6d4ee00fb53..696e142ef1b 100644 --- a/clients/admin-ui/src/features/common/hooks/useAlert.tsx +++ b/clients/admin-ui/src/features/common/hooks/useAlert.tsx @@ -37,10 +37,9 @@ export const useAlert = () => { {description} @@ -69,10 +68,9 @@ export const useAlert = () => { {description} From 596d446122856671919d909c3fe4a474fb28f9d2 Mon Sep 17 00:00:00 2001 From: chriscalhoun1974 <68459950+chriscalhoun1974@users.noreply.github.com> Date: Mon, 24 Oct 2022 08:58:16 -0400 Subject: [PATCH 17/58] Rollback package-lock.json file commit --- clients/admin-ui/package-lock.json | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/clients/admin-ui/package-lock.json b/clients/admin-ui/package-lock.json index 350f2d3a9ce..18cfc4a03f8 100644 --- a/clients/admin-ui/package-lock.json +++ b/clients/admin-ui/package-lock.json @@ -2341,15 +2341,6 @@ "node": ">=0.8" } }, - "node_modules/@cypress/request/node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "dev": true, - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/@cypress/xvfb": { "version": "1.2.4", "dev": true, @@ -11992,6 +11983,14 @@ "version": "1.0.2", "license": "MIT" }, + "node_modules/uuid": { + "version": "8.3.2", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/v8-compile-cache": { "version": "2.3.0", "dev": true, @@ -14151,12 +14150,6 @@ "psl": "^1.1.28", "punycode": "^2.1.1" } - }, - "uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "dev": true } } }, @@ -20351,6 +20344,10 @@ "util-deprecate": { "version": "1.0.2" }, + "uuid": { + "version": "8.3.2", + "dev": true + }, "v8-compile-cache": { "version": "2.3.0", "dev": true @@ -20645,4 +20642,4 @@ } } } -} +} \ No newline at end of file From d03e980343e6049ca5df57ec3325f57089b3e055 Mon Sep 17 00:00:00 2001 From: chriscalhoun1974 <68459950+chriscalhoun1974@users.noreply.github.com> Date: Mon, 24 Oct 2022 09:05:49 -0400 Subject: [PATCH 18/58] Revert "Merge remote-tracking branch 'origin/ps-bulk-reprocess' into 1203-individually-select-and-reprocess-DSRs-that-have-errored" This reverts commit d446fe76c3f2c849bf4058574d6fa84e6ff2c8a5, reversing changes made to 28d3e33a69dce6271f0833ab1b70dbd5888b4d63. --- .../v1/endpoints/privacy_request_endpoints.py | 110 ++---------- src/fides/api/ops/api/v1/urn_registry.py | 1 - .../authentication_strategy_doordash.py | 9 +- .../test_privacy_request_endpoints.py | 163 ------------------ 4 files changed, 22 insertions(+), 261 deletions(-) diff --git a/src/fides/api/ops/api/v1/endpoints/privacy_request_endpoints.py b/src/fides/api/ops/api/v1/endpoints/privacy_request_endpoints.py index b178a3229bb..58f0df68da0 100644 --- a/src/fides/api/ops/api/v1/endpoints/privacy_request_endpoints.py +++ b/src/fides/api/ops/api/v1/endpoints/privacy_request_endpoints.py @@ -48,7 +48,6 @@ from fides.api.ops.api.v1.urn_registry import ( PRIVACY_REQUEST_ACCESS_MANUAL_WEBHOOK_INPUT, PRIVACY_REQUEST_APPROVE, - PRIVACY_REQUEST_BULK_RETRY, PRIVACY_REQUEST_DENY, PRIVACY_REQUEST_MANUAL_ERASURE, PRIVACY_REQUEST_MANUAL_INPUT, @@ -997,68 +996,6 @@ async def resume_with_erasure_confirmation( ) -@router.post( - PRIVACY_REQUEST_BULK_RETRY, - status_code=HTTP_200_OK, - response_model=BulkPostPrivacyRequests, - dependencies=[ - Security(verify_oauth_client, scopes=[PRIVACY_REQUEST_CALLBACK_RESUME]) - ], -) -async def bulk_restart_privacy_request_from_failure( - privacy_request_ids: List[str], - *, - db: Session = Depends(deps.get_db), -) -> BulkPostPrivacyRequests: - """Bulk restart a of privacy request from failure.""" - - succeeded: List[PrivacyRequestResponse] = [] - failed: List[Dict[str, Any]] = [] - - # privacy_request = PrivacyRequest.get(db, object_id=request_id) - - for privacy_request_id in privacy_request_ids: - privacy_request = PrivacyRequest.get(db, object_id=privacy_request_id) - - if not privacy_request: - failed.append( - { - "message": f"No privacy request found with id '{privacy_request_id}'", - "data": {"privacy_request_id": privacy_request_id}, - } - ) - continue - - if privacy_request.status != PrivacyRequestStatus.error: - failed.append( - { - "message": f"Cannot restart privacy request from failure: privacy request '{privacy_request.id}' status = {privacy_request.status.value}.", - "data": {"privacy_request_id": privacy_request_id}, - } - ) - continue - - failed_details: Optional[ - CheckpointActionRequired - ] = privacy_request.get_failed_checkpoint_details() - if not failed_details: - failed.append( - { - "message": f"Cannot restart privacy request from failure '{privacy_request.id}'; no failed step or collection.", - "data": {"privacy_request_id": privacy_request_id}, - } - ) - continue - - succeeded.append( - _process_privacy_request_restart( - privacy_request, failed_details.step, failed_details.collection, db - ) - ) - - return BulkPostPrivacyRequests(succeeded=succeeded, failed=failed) - - @router.post( PRIVACY_REQUEST_RETRY, status_code=HTTP_200_OK, @@ -1092,10 +1029,27 @@ async def restart_privacy_request_from_failure( detail=f"Cannot restart privacy request from failure '{privacy_request.id}'; no failed step or collection.", ) - return _process_privacy_request_restart( - privacy_request, failed_details.step, failed_details.collection, db + failed_step: CurrentStep = failed_details.step + failed_collection: Optional[CollectionAddress] = failed_details.collection + + logger.info( + "Restarting failed privacy request '%s' from '%s step, 'collection '%s'", + privacy_request_id, + failed_step, + failed_collection, ) + privacy_request.status = PrivacyRequestStatus.in_processing + privacy_request.save(db=db) + queue_privacy_request( + privacy_request_id=privacy_request.id, + from_step=failed_step.value, + ) + + privacy_request.cache_failed_checkpoint_details() # Reset failed step and collection to None + + return privacy_request + def review_privacy_request( db: Session, @@ -1480,29 +1434,3 @@ async def resume_privacy_request_from_requires_input( ) return privacy_request - - -def _process_privacy_request_restart( - privacy_request: PrivacyRequest, - failed_step: CurrentStep, - failed_collection: Optional[CollectionAddress], - db: Session, -) -> PrivacyRequestResponse: - - logger.info( - "Restarting failed privacy request '%s' from '%s step, 'collection '%s'", - privacy_request.id, - failed_step, - failed_collection, - ) - - privacy_request.status = PrivacyRequestStatus.in_processing - privacy_request.save(db=db) - queue_privacy_request( - privacy_request_id=privacy_request.id, - from_step=failed_step.value, - ) - - privacy_request.cache_failed_checkpoint_details() # Reset failed step and collection to None - - return privacy_request diff --git a/src/fides/api/ops/api/v1/urn_registry.py b/src/fides/api/ops/api/v1/urn_registry.py index df1242dbd22..cee67929362 100644 --- a/src/fides/api/ops/api/v1/urn_registry.py +++ b/src/fides/api/ops/api/v1/urn_registry.py @@ -49,7 +49,6 @@ # Privacy request URLs PRIVACY_REQUESTS = "/privacy-request" PRIVACY_REQUEST_APPROVE = "/privacy-request/administrate/approve" -PRIVACY_REQUEST_BULK_RETRY = "/privacy-request/bulk/retry" PRIVACY_REQUEST_DENY = "/privacy-request/administrate/deny" REQUEST_STATUS_LOGS = "/privacy-request/{privacy_request_id}/log" PRIVACY_REQUEST_VERIFY_IDENTITY = "/privacy-request/{privacy_request_id}/verify" diff --git a/src/fides/api/ops/service/saas_request/override_implementations/authentication_strategy_doordash.py b/src/fides/api/ops/service/saas_request/override_implementations/authentication_strategy_doordash.py index 7b9d13e5b68..6752b7ed6f2 100644 --- a/src/fides/api/ops/service/saas_request/override_implementations/authentication_strategy_doordash.py +++ b/src/fides/api/ops/service/saas_request/override_implementations/authentication_strategy_doordash.py @@ -1,6 +1,5 @@ import math import time -from typing import Any, Dict, Optional import jwt.utils from requests import PreparedRequest @@ -43,15 +42,13 @@ def add_authentication( Generate a Doordash JWT and add it as bearer auth """ - secrets: Optional[Dict[str, Any]] = connection_config.secrets + secrets = connection_config.secrets token = jwt.encode( { "aud": "doordash", - "iss": assign_placeholders(self.developer_id, secrets) - if secrets - else None, - "kid": assign_placeholders(self.key_id, secrets) if secrets else None, + "iss": assign_placeholders(self.developer_id, secrets), + "kid": assign_placeholders(self.key_id, secrets), "exp": str(math.floor(time.time() + 60)), "iat": str(math.floor(time.time())), }, diff --git a/tests/ops/api/v1/endpoints/test_privacy_request_endpoints.py b/tests/ops/api/v1/endpoints/test_privacy_request_endpoints.py index e7d31e07669..6c1e7d32f81 100644 --- a/tests/ops/api/v1/endpoints/test_privacy_request_endpoints.py +++ b/tests/ops/api/v1/endpoints/test_privacy_request_endpoints.py @@ -38,7 +38,6 @@ DATASETS, PRIVACY_REQUEST_ACCESS_MANUAL_WEBHOOK_INPUT, PRIVACY_REQUEST_APPROVE, - PRIVACY_REQUEST_BULK_RETRY, PRIVACY_REQUEST_DENY, PRIVACY_REQUEST_MANUAL_ERASURE, PRIVACY_REQUEST_MANUAL_INPUT, @@ -2575,168 +2574,6 @@ def test_resume_with_manual_count( privacy_request.delete(db) -class TestBulkRestartFromFailure: - @pytest.fixture(scope="function") - def url(self): - return f"{V1_URL_PREFIX}{PRIVACY_REQUEST_BULK_RETRY}" - - def test_restart_from_failure_not_authenticated(self, api_client, url): - data = ["1234", "5678"] - response = api_client.post(url, json=data, headers={}) - assert response.status_code == 401 - - def test_restart_from_failure_wrong_scope( - self, api_client, url, generate_auth_header - ): - auth_header = generate_auth_header(scopes=[PRIVACY_REQUEST_READ]) - data = ["1234", "5678"] - - response = api_client.post(url, json=data, headers=auth_header) - assert response.status_code == 403 - - @pytest.mark.usefixtures("privacy_requests") - def test_restart_from_failure_not_errored( - self, api_client, url, generate_auth_header - ): - auth_header = generate_auth_header(scopes=[PRIVACY_REQUEST_CALLBACK_RESUME]) - data = ["1234", "5678"] - - response = api_client.post(url, json=data, headers=auth_header) - assert response.status_code == 200 - - assert response.json()["succeeded"] == [] - - failed_ids = [ - x["data"]["privacy_request_id"] for x in response.json()["failed"] - ] - assert sorted(failed_ids) == sorted(data) - - def test_restart_from_failure_no_stopped_step( - self, api_client, url, generate_auth_header, db, privacy_requests - ): - auth_header = generate_auth_header(scopes=[PRIVACY_REQUEST_CALLBACK_RESUME]) - data = [privacy_requests[0].id] - - privacy_requests[0].status = PrivacyRequestStatus.error - privacy_requests[0].save(db) - - response = api_client.post(url, json=data, headers=auth_header) - - assert response.status_code == 200 - assert response.json()["succeeded"] == [] - - failed_ids = [ - x["data"]["privacy_request_id"] for x in response.json()["failed"] - ] - - assert privacy_requests[0].id in failed_ids - - @mock.patch( - "fides.api.ops.service.privacy_request.request_runner_service.run_privacy_request.delay" - ) - def test_restart_from_failure_from_specific_collection( - self, submit_mock, api_client, url, generate_auth_header, db, privacy_requests - ): - auth_header = generate_auth_header(scopes=[PRIVACY_REQUEST_CALLBACK_RESUME]) - data = [privacy_requests[0].id] - privacy_requests[0].status = PrivacyRequestStatus.error - privacy_requests[0].save(db) - - privacy_requests[0].cache_failed_checkpoint_details( - step=CurrentStep.access, - collection=CollectionAddress("test_dataset", "test_collection"), - ) - - response = api_client.post(url, json=data, headers=auth_header) - assert response.status_code == 200 - - db.refresh(privacy_requests[0]) - assert privacy_requests[0].status == PrivacyRequestStatus.in_processing - assert response.json()["failed"] == [] - - succeeded_ids = [x["id"] for x in response.json()["succeeded"]] - - assert privacy_requests[0].id in succeeded_ids - - submit_mock.assert_called_with( - privacy_request_id=privacy_requests[0].id, - from_step=CurrentStep.access.value, - from_webhook_id=None, - ) - - @mock.patch( - "fides.api.ops.service.privacy_request.request_runner_service.run_privacy_request.delay" - ) - def test_restart_from_failure_outside_graph( - self, submit_mock, api_client, url, generate_auth_header, db, privacy_requests - ): - auth_header = generate_auth_header(scopes=[PRIVACY_REQUEST_CALLBACK_RESUME]) - data = [privacy_requests[0].id] - privacy_requests[0].status = PrivacyRequestStatus.error - privacy_requests[0].save(db) - - privacy_requests[0].cache_failed_checkpoint_details( - step=CurrentStep.erasure_email_post_send, - collection=None, - ) - - response = api_client.post(url, json=data, headers=auth_header) - assert response.status_code == 200 - - db.refresh(privacy_requests[0]) - assert privacy_requests[0].status == PrivacyRequestStatus.in_processing - assert response.json()["failed"] == [] - - succeeded_ids = [x["id"] for x in response.json()["succeeded"]] - - assert privacy_requests[0].id in succeeded_ids - - submit_mock.assert_called_with( - privacy_request_id=privacy_requests[0].id, - from_step=CurrentStep.erasure_email_post_send.value, - from_webhook_id=None, - ) - - @mock.patch( - "fides.api.ops.service.privacy_request.request_runner_service.run_privacy_request.delay" - ) - def test_mixed_result( - self, submit_mock, api_client, url, generate_auth_header, db, privacy_requests - ): - auth_header = generate_auth_header(scopes=[PRIVACY_REQUEST_CALLBACK_RESUME]) - data = [privacy_requests[0].id, privacy_requests[1].id] - privacy_requests[0].status = PrivacyRequestStatus.error - privacy_requests[0].save(db) - - privacy_requests[0].cache_failed_checkpoint_details( - step=CurrentStep.access, - collection=CollectionAddress("test_dataset", "test_collection"), - ) - - privacy_requests[1].status = PrivacyRequestStatus.error - privacy_requests[1].save(db) - - response = api_client.post(url, json=data, headers=auth_header) - assert response.status_code == 200 - - db.refresh(privacy_requests[0]) - assert privacy_requests[0].status == PrivacyRequestStatus.in_processing - - succeeded_ids = [x["id"] for x in response.json()["succeeded"]] - failed_ids = [ - x["data"]["privacy_request_id"] for x in response.json()["failed"] - ] - - assert privacy_requests[0].id in succeeded_ids - assert privacy_requests[1].id in failed_ids - - submit_mock.assert_called_with( - privacy_request_id=privacy_requests[0].id, - from_step=CurrentStep.access.value, - from_webhook_id=None, - ) - - class TestRestartFromFailure: @pytest.fixture(scope="function") def url(self, db, privacy_request): From 156c6026b671047790c6635ef78477135abaf390 Mon Sep 17 00:00:00 2001 From: chriscalhoun1974 <68459950+chriscalhoun1974@users.noreply.github.com> Date: Mon, 24 Oct 2022 09:10:31 -0400 Subject: [PATCH 19/58] Resolve high security vulnerability NPM dependency (npm audit fix) --- clients/admin-ui/package-lock.json | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/clients/admin-ui/package-lock.json b/clients/admin-ui/package-lock.json index 18cfc4a03f8..daa11dd8a47 100644 --- a/clients/admin-ui/package-lock.json +++ b/clients/admin-ui/package-lock.json @@ -9772,9 +9772,10 @@ } }, "node_modules/minimatch": { - "version": "3.0.4", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, - "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -19010,7 +19011,9 @@ "dev": true }, "minimatch": { - "version": "3.0.4", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "requires": { "brace-expansion": "^1.1.7" @@ -20642,4 +20645,4 @@ } } } -} \ No newline at end of file +} From 0c7335966ef87decd2e1b1eb91780b38f13ba9ff Mon Sep 17 00:00:00 2001 From: chriscalhoun1974 <68459950+chriscalhoun1974@users.noreply.github.com> Date: Mon, 24 Oct 2022 09:29:07 -0400 Subject: [PATCH 20/58] Changed the casing on the Create new connection button --- .../add-connection/AddConnectionButton.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/clients/admin-ui/src/features/datastore-connections/add-connection/AddConnectionButton.tsx b/clients/admin-ui/src/features/datastore-connections/add-connection/AddConnectionButton.tsx index 81a03bd8f3e..d347dc4f443 100644 --- a/clients/admin-ui/src/features/datastore-connections/add-connection/AddConnectionButton.tsx +++ b/clients/admin-ui/src/features/datastore-connections/add-connection/AddConnectionButton.tsx @@ -15,7 +15,7 @@ const AddConnectionButton: React.FC = () => ( _hover={{ bg: "primary.400" }} _active={{ bg: "primary.500" }} > - Create New Connection + Create new connection ); From 97dee204413a06089b7b9dc624892988c8dd0a9c Mon Sep 17 00:00:00 2001 From: chriscalhoun1974 <68459950+chriscalhoun1974@users.noreply.github.com> Date: Mon, 24 Oct 2022 09:30:40 -0400 Subject: [PATCH 21/58] Changed the casing on the Delete connection button --- .../features/datastore-connections/DeleteConnectionModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/clients/admin-ui/src/features/datastore-connections/DeleteConnectionModal.tsx b/clients/admin-ui/src/features/datastore-connections/DeleteConnectionModal.tsx index 4173d0aa74b..ec1585247d1 100644 --- a/clients/admin-ui/src/features/datastore-connections/DeleteConnectionModal.tsx +++ b/clients/admin-ui/src/features/datastore-connections/DeleteConnectionModal.tsx @@ -95,7 +95,7 @@ const DeleteConnectionModal: React.FC = ({ color: "gray.600", }} > - Delete Connection + Delete connection From 3fb26718e237827baff34e387a391a6e1cb31f98 Mon Sep 17 00:00:00 2001 From: chriscalhoun1974 <68459950+chriscalhoun1974@users.noreply.github.com> Date: Mon, 24 Oct 2022 21:13:30 -0400 Subject: [PATCH 22/58] Added More button and Configure Alerts drawer --- .../buttons/ActionButtons.tsx | 8 +- .../privacy-requests/buttons/MoreButton.tsx | 46 ++++++ .../drawers/ConfigureAlerts.tsx | 139 ++++++++++++++++++ clients/admin-ui/src/pages/index.tsx | 7 +- 4 files changed, 196 insertions(+), 4 deletions(-) create mode 100644 clients/admin-ui/src/features/privacy-requests/buttons/MoreButton.tsx create mode 100644 clients/admin-ui/src/features/privacy-requests/drawers/ConfigureAlerts.tsx diff --git a/clients/admin-ui/src/features/privacy-requests/buttons/ActionButtons.tsx b/clients/admin-ui/src/features/privacy-requests/buttons/ActionButtons.tsx index a749e55814a..9d3c3f455b9 100644 --- a/clients/admin-ui/src/features/privacy-requests/buttons/ActionButtons.tsx +++ b/clients/admin-ui/src/features/privacy-requests/buttons/ActionButtons.tsx @@ -4,16 +4,18 @@ import React from "react"; import { useAppSelector } from "~/app/hooks"; import { selectRetryRequests } from "../privacy-requests.slice"; +import MoreButton from "./MoreButton"; import ReprocessButton from "./ReprocessButton"; const ActionButtons: React.FC = () => { const { errorRequests } = useAppSelector(selectRetryRequests); - return errorRequests?.length > 0 ? ( + return ( - + {errorRequests?.length > 0 && } + - ) : null; + ); }; export default ActionButtons; diff --git a/clients/admin-ui/src/features/privacy-requests/buttons/MoreButton.tsx b/clients/admin-ui/src/features/privacy-requests/buttons/MoreButton.tsx new file mode 100644 index 00000000000..dd8c866919e --- /dev/null +++ b/clients/admin-ui/src/features/privacy-requests/buttons/MoreButton.tsx @@ -0,0 +1,46 @@ +import { + Box, + Button, + Menu, + MenuButton, + MenuList, + MenuProps, +} from "@fidesui/react"; + +import { ArrowDownLineIcon } from "~/features/common/Icon"; + +import ConfigureAlerts from "../drawers/ConfigureAlerts"; + +type MoreButtonProps = { + menuProps?: MenuProps; +}; + +const MoreButton: React.FC = ({ + menuProps, +}: MoreButtonProps) => ( + + + } + size="sm" + variant="outline" + _active={{ + bg: "none", + }} + _hover={{ + bg: "none", + }} + > + More + + + {/* MenuItems are not rendered unless Menu is open */} + + + + +); + +export default MoreButton; diff --git a/clients/admin-ui/src/features/privacy-requests/drawers/ConfigureAlerts.tsx b/clients/admin-ui/src/features/privacy-requests/drawers/ConfigureAlerts.tsx new file mode 100644 index 00000000000..1853c35a804 --- /dev/null +++ b/clients/admin-ui/src/features/privacy-requests/drawers/ConfigureAlerts.tsx @@ -0,0 +1,139 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { + Box, + Button, + ButtonGroup, + Drawer, + DrawerBody, + DrawerCloseButton, + DrawerContent, + DrawerFooter, + DrawerHeader, + DrawerOverlay, + FormControl, + FormLabel, + HStack, + MenuItem, + Switch, + Text, + useDisclosure, + VStack, +} from "@fidesui/react"; +import { Form, Formik, FormikProps } from "formik"; +import { ChangeEvent, useRef } from "react"; +import * as Yup from "yup"; + +type ConfigureAlertsProps = {}; + +// eslint-disable-next-line no-empty-pattern +const ConfigureAlerts: React.FC = ({}) => { + const { isOpen, onOpen, onClose } = useDisclosure(); + const firstField = useRef(); + + const handleSubmit = async (values: any, _actions: any) => { + onClose(); + }; + + const handleToggle = (event: ChangeEvent) => { + + } + + + return ( + <> + + Configure alerts + + + {/* @ts-ignore */} + {(_props: FormikProps) => ( + + + + + + + Configure alerts and notifications + + + + Setup your alerts to send you a notification when there are + any processing failures. You can also setup a threshold for + connector failures for Fides to notify you after X amount of + failures have occurred. + + + + +
+ Contact details + + + + + Notify me immediately if there are any DSR processing + errors + + + + + + + If selected, then Fides will notify you by your chosen + method of communication every time the system encounters a + data subject request processing error. You can turn this off + anytime and setup a more suitable notification method below + if you wish. + +
+
+ + + + + + +
+
+ )} +
+ + ); +}; + +export default ConfigureAlerts; diff --git a/clients/admin-ui/src/pages/index.tsx b/clients/admin-ui/src/pages/index.tsx index fd5b9045a72..15462395a2c 100644 --- a/clients/admin-ui/src/pages/index.tsx +++ b/clients/admin-ui/src/pages/index.tsx @@ -1,12 +1,17 @@ import { Flex, Heading, Spacer } from "@fidesui/react"; import type { NextPage } from "next"; +import dynamic from "next/dynamic"; import RequestFilters from "privacy-requests/RequestFilters"; import RequestTable from "privacy-requests/RequestTable"; import { LOGIN_ROUTE } from "~/constants"; import ProtectedRoute from "~/features/auth/ProtectedRoute"; import Layout from "~/features/common/Layout"; -import ActionButtons from "~/features/privacy-requests/buttons/ActionButtons"; + +const ActionButtons = dynamic( + () => import("~/features/privacy-requests/buttons/ActionButtons"), + { loading: () =>
Loading...
} +); const Home: NextPage = () => ( From ffc67aec6c0c7122a6bb791d3943fdc5280ae03a Mon Sep 17 00:00:00 2001 From: chriscalhoun1974 <68459950+chriscalhoun1974@users.noreply.github.com> Date: Tue, 25 Oct 2022 09:26:13 -0400 Subject: [PATCH 23/58] Code review feedback --- .../src/features/privacy-requests/RequestTable.tsx | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/clients/admin-ui/src/features/privacy-requests/RequestTable.tsx b/clients/admin-ui/src/features/privacy-requests/RequestTable.tsx index 66844b2be50..ca830565082 100644 --- a/clients/admin-ui/src/features/privacy-requests/RequestTable.tsx +++ b/clients/admin-ui/src/features/privacy-requests/RequestTable.tsx @@ -46,12 +46,11 @@ const useRequestTable = () => { if (checked) { list = [...errorRequests, id]; } else { - errorRequests.filter((value) => value !== id); - list = [...errorRequests.filter((value) => value !== id)]; + list = errorRequests.filter((value) => value !== id); } dispatch( setRetryRequests({ - checkAll: !!(checked && checkAll), + checkAll: checked && checkAll, errorRequests: list, }) ); @@ -120,12 +119,8 @@ const RequestTable: React.FC = () => { r.status === "error")} onChange={handleCheckAll} - pointerEvents={ - requests.findIndex((r) => r.status === "error") !== -1 - ? "auto" - : "none" - } /> Status From ee1b4b403d6900cf2b7cb83d3fd9274e0a2dabaf Mon Sep 17 00:00:00 2001 From: chriscalhoun1974 <68459950+chriscalhoun1974@users.noreply.github.com> Date: Tue, 25 Oct 2022 10:33:48 -0400 Subject: [PATCH 24/58] Updated ConfigureAlerts component --- .../drawers/ConfigureAlerts.tsx | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/clients/admin-ui/src/features/privacy-requests/drawers/ConfigureAlerts.tsx b/clients/admin-ui/src/features/privacy-requests/drawers/ConfigureAlerts.tsx index 1853c35a804..df774e53060 100644 --- a/clients/admin-ui/src/features/privacy-requests/drawers/ConfigureAlerts.tsx +++ b/clients/admin-ui/src/features/privacy-requests/drawers/ConfigureAlerts.tsx @@ -13,7 +13,9 @@ import { FormControl, FormLabel, HStack, + Input, MenuItem, + Stack, Switch, Text, useDisclosure, @@ -22,6 +24,7 @@ import { import { Form, Formik, FormikProps } from "formik"; import { ChangeEvent, useRef } from "react"; import * as Yup from "yup"; +import { CustomSwitch } from "~/features/common/form/inputs"; type ConfigureAlertsProps = {}; @@ -84,7 +87,25 @@ const ConfigureAlerts: React.FC = ({}) => {
Contact details - + + + + + Email + + + + From 831d7868e99362ed875e1883cdb75165904afe99 Mon Sep 17 00:00:00 2001 From: chriscalhoun1974 <68459950+chriscalhoun1974@users.noreply.github.com> Date: Sun, 30 Oct 2022 21:48:40 -0400 Subject: [PATCH 25/58] 1492 - DSR configure alters (FE) --- .../drawers/ConfigureAlerts.tsx | 173 +++++++++++++----- .../ManualProcessingDetail.tsx | 3 +- clients/admin-ui/src/pages/index.tsx | 1 - 3 files changed, 128 insertions(+), 49 deletions(-) diff --git a/clients/admin-ui/src/features/privacy-requests/drawers/ConfigureAlerts.tsx b/clients/admin-ui/src/features/privacy-requests/drawers/ConfigureAlerts.tsx index df774e53060..886b8e64c48 100644 --- a/clients/admin-ui/src/features/privacy-requests/drawers/ConfigureAlerts.tsx +++ b/clients/admin-ui/src/features/privacy-requests/drawers/ConfigureAlerts.tsx @@ -11,37 +11,55 @@ import { DrawerHeader, DrawerOverlay, FormControl, + FormErrorMessage, FormLabel, HStack, Input, MenuItem, - Stack, + NumberDecrementStepper, + NumberIncrementStepper, + NumberInput, + NumberInputField, + NumberInputStepper, Switch, Text, useDisclosure, VStack, } from "@fidesui/react"; -import { Form, Formik, FormikProps } from "formik"; +import { + Field, + FieldInputProps, + FieldMetaProps, + Form, + Formik, + FormikProps, +} from "formik"; import { ChangeEvent, useRef } from "react"; import * as Yup from "yup"; -import { CustomSwitch } from "~/features/common/form/inputs"; -type ConfigureAlertsProps = {}; +const initialValues = { + email: "", + notify: false, + minErrorCount: 0, +}; + +type FormValues = typeof initialValues; + +const validationSchema = Yup.object().shape({ + email: Yup.string() + .email("Must be a valid email format") + .required("Email is required"), +}); -// eslint-disable-next-line no-empty-pattern -const ConfigureAlerts: React.FC = ({}) => { +const ConfigureAlerts: React.FC = () => { const { isOpen, onOpen, onClose } = useDisclosure(); - const firstField = useRef(); + const firstField = useRef(null); - const handleSubmit = async (values: any, _actions: any) => { + const handleSubmit = async (values: FormValues, _actions: any) => { + console.log(values); onClose(); }; - const handleToggle = (event: ChangeEvent) => { - - } - - return ( <> = ({}) => { - {/* @ts-ignore */} - {(_props: FormikProps) => ( + {(props: FormikProps) => ( - + @@ -89,34 +105,65 @@ const ConfigureAlerts: React.FC = ({}) => { Contact details - - - Email - - - + + {({ + field, + meta, + }: { + field: FieldInputProps; + meta: FieldMetaProps; + }) => ( + + + Email + + + + + {props.errors.email} + + + + )} + - - - Notify me immediately if there are any DSR processing - errors - - - + + {({ field }: { field: FieldInputProps }) => ( + + + Notify me immediately if there are any DSR + processing errors + + + ) => { + field.onChange(event); + props.setFieldValue("minErrorCount", 0); + }} + /> + + )} + @@ -126,6 +173,40 @@ const ConfigureAlerts: React.FC = ({}) => { anytime and setup a more suitable notification method below if you wish. + {props.values.notify && ( + + + {({ field }: { field: FieldInputProps }) => ( + + Notify me after + { + props.setFieldValue( + "minErrorCount", + valueAsNumber + ); + }} + size="sm" + w="80px" + > + + + + + + + DSR processing errors + + )} + + + )}
@@ -137,7 +218,7 @@ const ConfigureAlerts: React.FC = ({}) => { bg="primary.800" color="white" form="configure-alerts-form" - // isLoading={isSubmitting} + isLoading={props.isSubmitting} loadingText="Submitting" size="sm" variant="solid" diff --git a/clients/admin-ui/src/features/subject-request/manual-processing/ManualProcessingDetail.tsx b/clients/admin-ui/src/features/subject-request/manual-processing/ManualProcessingDetail.tsx index 8f683911756..42ffa56ab6f 100644 --- a/clients/admin-ui/src/features/subject-request/manual-processing/ManualProcessingDetail.tsx +++ b/clients/admin-ui/src/features/subject-request/manual-processing/ManualProcessingDetail.tsx @@ -40,7 +40,7 @@ const ManualProcessingDetail: React.FC = ({ onSaveClick, }) => { const { isOpen, onOpen, onClose } = useDisclosure(); - const firstField = useRef(); + const firstField = useRef(null); const handleSubmit = async (values: any, _actions: any) => { const params: PatchUploadManualWebhookDataRequest = { @@ -93,7 +93,6 @@ const ManualProcessingDetail: React.FC = ({ import("~/features/privacy-requests/buttons/ActionButtons"), From 0e0972534efcb603eb148ddafa5f6631933bdef0 Mon Sep 17 00:00:00 2001 From: chriscalhoun1974 <68459950+chriscalhoun1974@users.noreply.github.com> Date: Mon, 31 Oct 2022 19:11:56 -0400 Subject: [PATCH 26/58] Added configureAlertsFlag feature flag --- clients/admin-ui/src/flags.json | 4 ++-- clients/admin-ui/src/pages/index.tsx | 8 +++++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/clients/admin-ui/src/flags.json b/clients/admin-ui/src/flags.json index 6d3a9e4d256..2fb5a308621 100644 --- a/clients/admin-ui/src/flags.json +++ b/clients/admin-ui/src/flags.json @@ -1,7 +1,7 @@ [ { - "name": "createNewConnection", - "isActive": true + "name": "configureAlertsFlag", + "isActive": false }, { "name": "configWizardFlag", diff --git a/clients/admin-ui/src/pages/index.tsx b/clients/admin-ui/src/pages/index.tsx index 664c95ed60d..1ed7afbbfb4 100644 --- a/clients/admin-ui/src/pages/index.tsx +++ b/clients/admin-ui/src/pages/index.tsx @@ -3,6 +3,9 @@ import type { NextPage } from "next"; import dynamic from "next/dynamic"; import RequestFilters from "privacy-requests/RequestFilters"; import RequestTable from "privacy-requests/RequestTable"; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +import { Flags } from "react-feature-flags"; import { LOGIN_ROUTE } from "~/constants"; import ProtectedRoute from "~/features/auth/ProtectedRoute"; @@ -21,7 +24,10 @@ const Home: NextPage = () => ( Privacy Requests - + } + /> From d04e9906d86c8fb90b4ba51259c068885e498136 Mon Sep 17 00:00:00 2001 From: chriscalhoun1974 <68459950+chriscalhoun1974@users.noreply.github.com> Date: Tue, 1 Nov 2022 10:11:27 -0400 Subject: [PATCH 27/58] Updates to ConfigureAlerts component --- .../drawers/ConfigureAlerts.tsx | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/clients/admin-ui/src/features/privacy-requests/drawers/ConfigureAlerts.tsx b/clients/admin-ui/src/features/privacy-requests/drawers/ConfigureAlerts.tsx index 886b8e64c48..f058a89d2b1 100644 --- a/clients/admin-ui/src/features/privacy-requests/drawers/ConfigureAlerts.tsx +++ b/clients/admin-ui/src/features/privacy-requests/drawers/ConfigureAlerts.tsx @@ -32,6 +32,7 @@ import { FieldMetaProps, Form, Formik, + FormikHelpers, FormikProps, } from "formik"; import { ChangeEvent, useRef } from "react"; @@ -55,8 +56,11 @@ const ConfigureAlerts: React.FC = () => { const { isOpen, onOpen, onClose } = useDisclosure(); const firstField = useRef(null); - const handleSubmit = async (values: FormValues, _actions: any) => { - console.log(values); + const handleSubmit = async ( + values: FormValues, + helpers: FormikHelpers + ) => { + helpers.setSubmitting(false); onClose(); }; @@ -211,13 +215,20 @@ const ConfigureAlerts: React.FC = () => { -