Skip to content

Commit

Permalink
[ethyca#396] Frontend for Privacy Request denial flow (ethyca#480)
Browse files Browse the repository at this point in the history
  • Loading branch information
TheAndrewJackson authored and Adam Sachs committed May 17, 2022
1 parent 5e4367b commit a11b367
Show file tree
Hide file tree
Showing 4 changed files with 171 additions and 40 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import {
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalFooter,
ModalBody,
Textarea,
Button,
} from '@fidesui/react';
import React from 'react';

type DenyModalProps = {
isOpen: boolean;
isLoading: boolean;
handleMenuClose: () => void;
handleDenyRequest: (reason: string) => Promise<any>;
denialReason: string;
onChange: (e: any) => void;
};

const closeModal = (
handleMenuClose: () => void,
handleDenyRequest: (reason: string) => Promise<any>,
denialReason: string
) => {
handleDenyRequest(denialReason).then(() => {
handleMenuClose();
});
};

const DenyPrivacyRequestModal = ({
isOpen,
isLoading,
handleMenuClose,
denialReason,
onChange,
handleDenyRequest,
}: DenyModalProps) => (
<Modal
isOpen={isOpen}
onClose={handleMenuClose}
isCentered
returnFocusOnClose={false}
>
<ModalOverlay />
<ModalContent width='100%' maxWidth='456px'>
<ModalHeader>Data subject request denial</ModalHeader>
<ModalBody color='gray.500' fontSize='14px'>
Please enter a reason for denying this data subject request
</ModalBody>
<ModalBody>
<Textarea
focusBorderColor='primary.600'
value={denialReason}
onChange={onChange}
disabled={isLoading}
/>
</ModalBody>
<ModalFooter>
<Button
size='sm'
width='100%'
maxWidth='198px'
colorScheme='gray.200'
mr={3}
disabled={isLoading}
onClick={handleMenuClose}
>
Close
</Button>
<Button
size='sm'
width='100%'
maxWidth='198px'
colorScheme='primary'
variant='solid'
isLoading={isLoading}
onClick={() => {
closeModal(handleMenuClose, handleDenyRequest, denialReason);
}}
>
Confirm
</Button>
</ModalFooter>
</ModalContent>
</Modal>
);

export default DenyPrivacyRequestModal;
104 changes: 69 additions & 35 deletions clients/admin-ui/src/features/privacy-requests/RequestRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
useApproveRequestMutation,
useDenyRequestMutation,
} from './privacy-requests.slice';
import DenyPrivacyRequestModal from './DenyPrivacyRequestModal';

const PII: React.FC<{ data: string }> = ({ data }) => (
<>{useObscuredPII(data)}</>
Expand All @@ -38,6 +39,8 @@ const useRequestRow = (request: PrivacyRequest) => {
const [hovered, setHovered] = useState(false);
const [focused, setFocused] = useState(false);
const [menuOpen, setMenuOpen] = useState(false);
const [modalOpen, setModalOpen] = useState(false);
const [denialReason, setDenialReason] = useState('');
const [approveRequest, approveRequestResult] = useApproveRequestMutation();
const [denyRequest, denyRequestResult] = useDenyRequestMutation();
const handleMenuOpen = () => setMenuOpen(true);
Expand All @@ -52,17 +55,28 @@ const useRequestRow = (request: PrivacyRequest) => {
const handleFocus = () => setFocused(true);
const handleBlur = () => setFocused(false);
const handleApproveRequest = () => approveRequest(request);
const handleDenyRequest = () => denyRequest(request);
const handleDenyRequest = (reason: string) =>
denyRequest({ id: request.id, reason });
const { onCopy } = useClipboard(request.id);
const handleModalOpen = () => setModalOpen(true);
const handleModalClose = () => {
setModalOpen(false);
setFocused(false);
setHovered(false);
setMenuOpen(false);
if (!denyRequestResult.isLoading) {
setDenialReason('');
}
};
const handleIdCopy = () => {
onCopy();
if (typeof window !== 'undefined') {
toast({
title: 'Request ID copied',
duration: 5000,
render: () => (
<Alert bg="gray.600" borderRadius="6px" display="flex">
<AlertTitle color="white">Request ID copied</AlertTitle>
<Alert bg='gray.600' borderRadius='6px' display='flex'>
<AlertTitle color='white'>Request ID copied</AlertTitle>
</Alert>
),
containerStyle: {
Expand All @@ -77,6 +91,9 @@ const useRequestRow = (request: PrivacyRequest) => {
hovered,
focused,
menuOpen,
modalOpen,
handleModalOpen,
handleModalClose,
handleMenuClose,
handleMenuOpen,
handleMouseEnter,
Expand All @@ -88,6 +105,8 @@ const useRequestRow = (request: PrivacyRequest) => {
handleDenyRequest,
hoverButtonRef,
shiftFocusToHoverMenu,
denialReason,
setDenialReason,
};
};

Expand All @@ -105,10 +124,15 @@ const RequestRow: React.FC<{ request: PrivacyRequest }> = ({ request }) => {
approveRequestResult,
denyRequestResult,
hoverButtonRef,
modalOpen,
handleModalClose,
handleModalOpen,
shiftFocusToHoverMenu,
handleFocus,
handleBlur,
focused,
denialReason,
setDenialReason,
} = useRequestRow(request);
const showMenu = hovered || menuOpen || focused;

Expand All @@ -119,25 +143,25 @@ const RequestRow: React.FC<{ request: PrivacyRequest }> = ({ request }) => {
bg={showMenu ? 'gray.50' : 'white'}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
height="36px"
height='36px'
>
<Td pl={0} py={1}>
<RequestBadge status={request.status} />
</Td>
<Td py={1}>
<Tag
color="white"
bg="primary.400"
color='white'
bg='primary.400'
px={2}
py={0.5}
size="sm"
fontWeight="medium"
size='sm'
fontWeight='medium'
>
{request.policy.name}
</Tag>
</Td>
<Td py={1}>
<Text fontSize="xs">
<Text fontSize='xs'>
<PII
data={
request.identity
Expand All @@ -148,50 +172,50 @@ const RequestRow: React.FC<{ request: PrivacyRequest }> = ({ request }) => {
</Text>
</Td>
<Td py={1}>
<Text fontSize="xs">
<Text fontSize='xs'>
{format(new Date(request.created_at), 'MMMM d, Y, KK:mm:ss z')}
</Text>
</Td>
<Td py={1}>
<Text fontSize="xs">
<Text fontSize='xs'>
<PII data={request.reviewer ? request.reviewer.username : ''} />
</Text>
</Td>
<Td py={1}>
<Text isTruncated fontSize="xs" maxWidth="87px">
<Text isTruncated fontSize='xs' maxWidth='87px'>
{request.id}
</Text>
</Td>
<Td pr={0} py={1} textAlign="end" position="relative">
<Td pr={0} py={1} textAlign='end' position='relative'>
<Button
size="xs"
variant="ghost"
size='xs'
variant='ghost'
mr={2.5}
onFocus={shiftFocusToHoverMenu}
tabIndex={showMenu ? -1 : 0}
>
<MoreIcon color="gray.700" w={18} h={18} />
<MoreIcon color='gray.700' w={18} h={18} />
</Button>
<ButtonGroup
isAttached
variant="outline"
position="absolute"
variant='outline'
position='absolute'
right={2.5}
top="50%"
transform="translate(1px, -50%)"
top='50%'
transform='translate(1px, -50%)'
opacity={showMenu ? 1 : 0}
pointerEvents={showMenu ? 'auto' : 'none'}
onFocus={handleFocus}
onBlur={handleBlur}
shadow="base"
borderRadius="md"
shadow='base'
borderRadius='md'
>
{request.status === 'pending' ? (
<>
<Button
size="xs"
mr="-px"
bg="white"
size='xs'
mr='-px'
bg='white'
onClick={handleApproveRequest}
isLoading={approveRequestResult.isLoading}
_loading={{
Expand All @@ -206,11 +230,10 @@ const RequestRow: React.FC<{ request: PrivacyRequest }> = ({ request }) => {
Approve
</Button>
<Button
size="xs"
mr="-px"
bg="white"
onClick={handleDenyRequest}
isLoading={denyRequestResult.isLoading}
size='xs'
mr='-px'
bg='white'
onClick={handleModalOpen}
_loading={{
opacity: 1,
div: { opacity: 0.4 },
Expand All @@ -221,24 +244,35 @@ const RequestRow: React.FC<{ request: PrivacyRequest }> = ({ request }) => {
>
Deny
</Button>
<DenyPrivacyRequestModal
isOpen={modalOpen}
isLoading={denyRequestResult.isLoading}
handleMenuClose={handleModalClose}
handleDenyRequest={handleDenyRequest}
denialReason={denialReason}
onChange={(e) => {
setDenialReason(e.target.value);
}}
/>
</>
) : null}

<Menu onOpen={handleMenuOpen} onClose={handleMenuClose}>
<MenuButton
as={Button}
size="xs"
bg="white"
size='xs'
bg='white'
ref={request.status !== 'pending' ? hoverButtonRef : null}
>
<MoreIcon color="gray.700" w={18} h={18} />
<MoreIcon color='gray.700' w={18} h={18} />
</MenuButton>
<Portal>
<MenuList shadow="xl">
<MenuList shadow='xl'>
<MenuItem
_focus={{ color: 'complimentary.500', bg: 'gray.100' }}
onClick={handleIdCopy}
>
<Text fontSize="sm">Copy Request ID</Text>
<Text fontSize='sm'>Copy Request ID</Text>
</MenuItem>
</MenuList>
</Portal>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
PrivacyRequestParams,
PrivacyRequestResponse,
PrivacyRequestStatus,
DenyPrivacyRequest,
} from './types';

// Helpers
Expand Down Expand Up @@ -81,15 +82,13 @@ export const privacyRequestApi = createApi({
}),
invalidatesTags: ['Request'],
}),
denyRequest: build.mutation<
PrivacyRequest,
Partial<PrivacyRequest> & Pick<PrivacyRequest, 'id'>
>({
query: ({ id }) => ({
denyRequest: build.mutation<PrivacyRequest, DenyPrivacyRequest>({
query: ({ id, reason }) => ({
url: 'privacy-request/administrate/deny',
method: 'PATCH',
body: {
request_ids: [id],
reason,
},
}),
invalidatesTags: ['Request'],
Expand Down
8 changes: 8 additions & 0 deletions clients/admin-ui/src/features/privacy-requests/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import {string} from "prop-types";

export type PrivacyRequestStatus =
| 'approved'
| 'complete'
Expand All @@ -7,6 +9,12 @@ export type PrivacyRequestStatus =
| 'paused'
| 'pending';


export interface DenyPrivacyRequest{
id:string,
reason: string
}

export interface PrivacyRequest {
status: PrivacyRequestStatus;
identity: {
Expand Down

0 comments on commit a11b367

Please sign in to comment.