Skip to content

Commit

Permalink
Improved privacy request error handling (#5500)
Browse files Browse the repository at this point in the history
  • Loading branch information
galvana authored Nov 20, 2024
1 parent 0a7a2b2 commit 97edac0
Show file tree
Hide file tree
Showing 35 changed files with 870 additions and 374 deletions.
20 changes: 10 additions & 10 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,28 +22,28 @@ The types of changes are:
- Added support for field-level masking overrides [#5446](https://github.com/ethyca/fides/pull/5446)
- Added BigQuery Enterprise access request integration test [#5504](https://github.com/ethyca/fides/pull/5504)

### Developer Experience
- Migrated several instances of Chakra's Select component to use Ant's Select component [#5475](https://github.com/ethyca/fides/pull/5475)

### Fixed
- Fixed deletion of ConnectionConfigs that have related MonitorConfigs [#5478](https://github.com/ethyca/fides/pull/5478)
- Fixed extra delete icon on Domains page [#5513](https://github.com/ethyca/fides/pull/5513)
- Fixed incorrect display names on some D&D resources [#5498](https://github.com/ethyca/fides/pull/5498)
- Fixed position of "Integration" button on system detail page [#5497](https://github.com/ethyca/fides/pull/5497)
- Fixing issue where "privacy request received" emails would not be sent if the request had custom identities [#5518](https://github.com/ethyca/fides/pull/5518)

### Changed
- Allow hiding systems via a `hidden` parameter and add two flags on the `/system` api endpoint; `show_hidden` and `dnd_relevant`, to display only systems with integrations [#5484](https://github.com/ethyca/fides/pull/5484)
- The CMP override `fides_privacy_policy_url` will now apply even if the `fides_override_language` doesn't match [#5515](https://github.com/ethyca/fides/pull/5515)
- Updated POST taxonomy endpoints to handle creating resources without specifying fides_key [#5468](https://github.com/ethyca/fides/pull/5468)
- Disabled connection pooling for task workers and added retries and keep-alive configurations for database connections [#5448](https://github.com/ethyca/fides/pull/5448)

### Developer Experience
- Migrated several instances of Chakra's Select component to use Ant's Select component [#5475](https://github.com/ethyca/fides/pull/5475)
- Fixing BigQuery integration tests [#5491](https://github.com/ethyca/fides/pull/5491)
- Enhanced logging for privacy requests [#5500](https://github.com/ethyca/fides/pull/5500)

### Docs
- Added docs for PrivacyNoticeRegion type [#5488](https://github.com/ethyca/fides/pull/5488)

### Fixed
- Fixed deletion of ConnectionConfigs that have related MonitorConfigs [#5478](https://github.com/ethyca/fides/pull/5478)
- Fixed extra delete icon on Domains page [#5513](https://github.com/ethyca/fides/pull/5513)
- Fixed incorrect display names on some D&D resources [#5498](https://github.com/ethyca/fides/pull/5498)
- Fixed position of "Integration" button on system detail page [#5497](https://github.com/ethyca/fides/pull/5497)
- Fixing issue where "privacy request received" emails would not be sent if the request had custom identities [#5518](https://github.com/ethyca/fides/pull/5518)
- Fixed issue with long-running privacy request tasks losing their connection to the database [#5500](https://github.com/ethyca/fides/pull/5500)

### Security
- Password Policy is now Enforced in Accept Invite API [CVE-2024-52008](https://github.com/ethyca/fides/security/advisories/GHSA-v7vm-rhmg-8j2r)

Expand Down
15 changes: 13 additions & 2 deletions clients/admin-ui/src/features/common/RequestStatusBadge.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Badge, BadgeProps } from "fidesui";
import { Badge, BadgeProps, Spinner } from "fidesui";

import { PrivacyRequestStatus } from "~/types/api";

Expand Down Expand Up @@ -65,7 +65,18 @@ const RequestStatusBadge = ({ status }: RequestBadgeProps) => (
textAlign="center"
data-testid="request-status-badge"
>
{statusPropMap[status].label}
<span
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
{statusPropMap[status].label}
{status === PrivacyRequestStatus.IN_PROCESSING && (
<Spinner size="xs" color="white" ml={2} />
)}
</span>
</Badge>
);

Expand Down
59 changes: 43 additions & 16 deletions clients/admin-ui/src/features/privacy-requests/PrivacyRequest.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { Box, VStack } from "fidesui";
import { useMemo } from "react";

import { useGetAllPrivacyRequestsQuery } from "~/features/privacy-requests";
import { PrivacyRequestStatus } from "~/types/api";

import EventsAndLogs from "./events-and-logs/EventsAndLogs";
import ManualProcessingList from "./manual-processing/ManualProcessingList";
Expand All @@ -10,23 +14,46 @@ type PrivacyRequestProps = {
data: PrivacyRequestEntity;
};

const PrivacyRequest = ({ data: subjectRequest }: PrivacyRequestProps) => (
<VStack align="stretch" display="flex-start" spacing={6}>
<Box data-testid="privacy-request-details">
<RequestDetails subjectRequest={subjectRequest} />
</Box>
<Box>
<SubjectIdentities subjectRequest={subjectRequest} />
</Box>
{subjectRequest.status === "requires_input" && (
const PrivacyRequest = ({ data: initialData }: PrivacyRequestProps) => {
const queryOptions = useMemo(
() => ({
id: initialData.id,
verbose: true,
}),
[initialData.id],
);

// Poll for the latest privacy request data while the status is approved or in processing
const { data: latestData } = useGetAllPrivacyRequestsQuery(queryOptions, {
pollingInterval:
initialData.status === PrivacyRequestStatus.APPROVED ||
initialData.status === PrivacyRequestStatus.IN_PROCESSING
? 2000
: 0,
skip: !initialData.id,
});

// Use latest data if available, otherwise use initial data
const subjectRequest = latestData?.items[0] ?? initialData;

return (
<VStack align="stretch" display="flex-start" spacing={6}>
<Box data-testid="privacy-request-details">
<RequestDetails subjectRequest={subjectRequest} />
</Box>
<Box>
<SubjectIdentities subjectRequest={subjectRequest} />
</Box>
{subjectRequest.status === PrivacyRequestStatus.REQUIRES_INPUT && (
<Box>
<ManualProcessingList subjectRequest={subjectRequest} />
</Box>
)}
<Box>
<ManualProcessingList subjectRequest={subjectRequest} />
<EventsAndLogs subjectRequest={subjectRequest} />
</Box>
)}
<Box>
<EventsAndLogs subjectRequest={subjectRequest} />
</Box>
</VStack>
);
</VStack>
);
};

export default PrivacyRequest;
Original file line number Diff line number Diff line change
@@ -1,104 +1,78 @@
import {
Box,
ErrorWarningIcon,
Flex,
GreenCheckCircleIcon,
Text,
} from "fidesui";
import { Box, Text, useDisclosure } from "fidesui";
import { ExecutionLog, PrivacyRequestEntity } from "privacy-requests/types";
import React from "react";
import React, { useEffect, useState } from "react";

import { ExecutionLogStatus } from "~/types/api";

import { EventData } from "./EventDetails";
import LogDrawer from "./LogDrawer";
import TimelineEntry from "./TimelineEntry";

type ActivityTimelineProps = {
subjectRequest: PrivacyRequestEntity;
setEventDetails: (d: EventData) => void;
};

const hasUnresolvedError = (entries: ExecutionLog[]) => {
const groupedByCollection: {
[key: string]: ExecutionLog;
} = {};
const ActivityTimeline = ({ subjectRequest }: ActivityTimelineProps) => {
const { isOpen, onOpen, onClose } = useDisclosure();
const [currentLogs, setCurrentLogs] = useState<ExecutionLog[]>([]);
const [currentKey, setCurrentKey] = useState<string>("");
const [isViewingError, setViewingError] = useState(false);
const [errorMessage, setErrorMessage] = useState("");

// Group the entries by collection_name and keep only the latest entry for each collection.
entries.forEach((entry) => {
const { collection_name: collectionName, updated_at: updatedAt } = entry;
if (
!groupedByCollection[collectionName] ||
new Date(groupedByCollection[collectionName].updated_at) <
new Date(updatedAt)
) {
groupedByCollection[collectionName] = entry;
}
});
const { results } = subjectRequest;
const resultKeys = results ? Object.keys(results) : [];

// Check if any of the latest entries for the collections have an error status.
return Object.values(groupedByCollection).some(
(entry) => entry.status === ExecutionLogStatus.ERROR,
);
};
// Update currentLogs when results change and we have a selected key
useEffect(() => {
if (currentKey && results && results[currentKey]) {
setCurrentLogs(results[currentKey]);
}
}, [results, currentKey]);

const ActivityTimeline = ({
subjectRequest,
setEventDetails,
}: ActivityTimelineProps) => {
const { results } = subjectRequest;
const openErrorPanel = (message: string) => {
setErrorMessage(message);
setViewingError(true);
};

const resultKeys = results ? Object.keys(results) : [];
const closeErrorPanel = () => {
setViewingError(false);
};

const timelineEntries = resultKeys.map((key, index) => (
<Box key={key}>
<Flex alignItems="center" height={23} position="relative">
<Box zIndex={1}>
{hasUnresolvedError(results![key]) ? (
<ErrorWarningIcon />
) : (
<GreenCheckCircleIcon />
)}
</Box>
{index === resultKeys.length - 1 ? null : (
<Box
width="2px"
height="63px"
backgroundColor="gray.700"
position="absolute"
top="16px"
left="6px"
zIndex={0}
/>
)}
const closeDrawer = () => {
if (isViewingError) {
closeErrorPanel();
}
setCurrentKey("");
onClose();
};

<Text color="gray.600" fontWeight="500" fontSize="sm" ml={2}>
{key}
</Text>
</Flex>
<Text
cursor="pointer"
color="complimentary.500"
fontWeight="500"
fontSize="sm"
ml={6}
mb={7}
onClick={() => {
setEventDetails({
key,
logs: results![key],
});
}}
>
View Details
</Text>
</Box>
));
const showLogs = (key: string, logs: ExecutionLog[]) => {
setCurrentKey(key);
setCurrentLogs(logs);
onOpen();
};

return (
<Box width="100%">
<Text color="gray.900" fontSize="md" fontWeight="500" mb={1}>
Activity timeline
</Text>
{timelineEntries}
{results &&
resultKeys.map((key, index) => (
<TimelineEntry
key={key}
entryKey={key}
logs={results[key]}
isLast={index === resultKeys.length - 1}
onViewLog={() => showLogs(key, results[key])}
/>
))}
<LogDrawer
isOpen={isOpen}
onClose={closeDrawer}
currentLogs={currentLogs}
isViewingError={isViewingError}
errorMessage={errorMessage}
onOpenErrorPanel={openErrorPanel}
onCloseErrorPanel={closeErrorPanel}
/>
</Box>
);
};
Expand Down
Loading

0 comments on commit 97edac0

Please sign in to comment.