Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improved privacy request error handling #5500

Merged
merged 18 commits into from
Nov 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I broke out the TimelineEntry and LogDrawer into separate components. I also removed the EventDetails component since it added unnecessary click before opening the LogDrawer

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
Loading