Skip to content

Commit 0232509

Browse files
committed
feat: add file storage actions log
1 parent f010ceb commit 0232509

File tree

13 files changed

+515
-52
lines changed

13 files changed

+515
-52
lines changed

api/models/file-storage-user-action.js

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
const ACTION_TYPES = [
2+
// make sure to keep in sync with file storage history messages in
23
'CREATE_SITE_FILE_STORAGE_SERVICE',
34
'CREATE_ORGANIZATION_FILE_STORAGE_SERVICE',
45
'CREATE_DIRECTORY',

frontend/hooks/useFileHistory.js

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { useQuery } from '@tanstack/react-query';
2+
import api from '../util/federalistApi';
3+
4+
const INITIAL_DATA = {
5+
data: [],
6+
currentPage: 1,
7+
totalPages: 1,
8+
totalItems: 0,
9+
};
10+
11+
export default function useFileHistory(fileStorageServiceId, page = 1) {
12+
const { data, error, isPending } = useQuery({
13+
queryKey: ['fileHistory', fileStorageServiceId, page],
14+
queryFn: async () => {
15+
const response = await api.fetchAllPublicFilesHistory(fileStorageServiceId, page);
16+
return response || INITIAL_DATA;
17+
},
18+
enabled: !!fileStorageServiceId,
19+
onError: () => {
20+
throw new Error('Unable to load file history. Please try again later.');
21+
},
22+
});
23+
24+
return {
25+
...data,
26+
isPending,
27+
error,
28+
};
29+
}
+89
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { waitFor, renderHook } from '@testing-library/react';
2+
import nock from 'nock';
3+
import { createTestQueryClient } from '@support/queryClient';
4+
import { getFileHistory, getFileHistoryError } from '@support/nocks/fileStorage';
5+
import useFileHistory from './useFileHistory';
6+
import { getFileHistoryData } from '../../test/frontend/support/data/fileStorageData';
7+
8+
const createWrapper = createTestQueryClient();
9+
10+
const props = {
11+
fileStorageServiceId: 123,
12+
page: 1,
13+
};
14+
15+
describe('useFileHistory', () => {
16+
beforeEach(() => nock.cleanAll());
17+
afterAll(() => nock.restore());
18+
19+
it('should fetch file history successfully', async () => {
20+
const expectedData = getFileHistoryData(props.page);
21+
await getFileHistory({ ...props });
22+
23+
const { result } = renderHook(
24+
() => useFileHistory(props.fileStorageServiceId, props.page),
25+
{ wrapper: createWrapper() },
26+
);
27+
28+
await waitFor(() => expect(!result.current.isPending).toBe(true));
29+
30+
expect(result.current.data).toEqual(expectedData.data);
31+
expect(result.current.currentPage).toEqual(expectedData.currentPage);
32+
expect(result.current.totalPages).toEqual(expectedData.totalPages);
33+
expect(result.current.totalItems).toEqual(expectedData.totalItems);
34+
});
35+
36+
it('should fetch the second page of results correctly', async () => {
37+
const pageNumber = 2;
38+
const expectedData = getFileHistoryData(pageNumber);
39+
await getFileHistory({ ...props, page: pageNumber });
40+
41+
const { result } = renderHook(
42+
() => useFileHistory(props.fileStorageServiceId, pageNumber),
43+
{ wrapper: createWrapper() },
44+
);
45+
46+
await waitFor(() => expect(!result.current.isPending).toBe(true));
47+
48+
expect(result.current.data).toEqual(expectedData.data);
49+
expect(result.current.data.length).toEqual(expectedData.data.length);
50+
expect(result.current.currentPage).toEqual(expectedData.currentPage);
51+
});
52+
53+
it('should return an error when fetching history fails', async () => {
54+
await getFileHistoryError({ ...props });
55+
56+
const { result } = renderHook(() => useFileHistory(props.fileStorageServiceId), {
57+
wrapper: createWrapper(),
58+
});
59+
60+
await waitFor(() => expect(result.current.error).toBeInstanceOf(Error));
61+
expect(result.current.error.message).toBe('Failed to fetch storage history');
62+
});
63+
64+
it('should not fetch when fileStorageServiceId is not provided', async () => {
65+
const { result } = renderHook(() => useFileHistory(), {
66+
wrapper: createWrapper(),
67+
});
68+
expect(result.current.isPending).toBe(true);
69+
expect(result.current.error).toBe(null);
70+
});
71+
72+
it('should refetch when page changes', async () => {
73+
await getFileHistory({ ...props });
74+
const { result, rerender } = renderHook(
75+
({ page }) => useFileHistory(props.fileStorageServiceId, page),
76+
{
77+
wrapper: createWrapper(),
78+
initialProps: { page: 1 },
79+
},
80+
);
81+
82+
await waitFor(() => expect(!result.current.isPending).toBe(true));
83+
84+
await getFileHistory({ ...props, page: 2 });
85+
rerender({ page: 2 });
86+
87+
await waitFor(() => expect(result.current.currentPage).toBe(2));
88+
});
89+
});

frontend/pages/sites/$siteId/storage/NewFileOrFolder.jsx

+53-26
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import React, { useState } from 'react';
22
import PropTypes from 'prop-types';
33
import FileUpload from '@shared/FileUpload';
4-
import { IconFolder } from '@shared/icons';
5-
4+
import { IconFolder, IconAttachment } from '@shared/icons';
5+
import AlertBanner from '@shared/alertBanner';
6+
import { Link } from 'react-router-dom';
67
const NewFileOrFolder = ({ onUpload, onCreateFolder }) => {
78
const [folderName, setFolderName] = useState('');
89
const [creatingFolder, setCreatingFolder] = useState(false);
@@ -30,16 +31,36 @@ const NewFileOrFolder = ({ onUpload, onCreateFolder }) => {
3031

3132
setCreatingFolder(false);
3233
};
33-
34+
const docsLink = 'https://cloud.gov/pages/documentation'; // TODO: update link
35+
const message = (
36+
<>
37+
Before uploading, carefully review your file to ensure it does not contain any
38+
Personally Identifiable Information (PII) or sensitive data. We{' '}
39+
<Link to="./storage/logs"> log every file upload and deletion </Link> for security
40+
tracking. For more about Public File Storage, review the{' '}
41+
<a target="_blank" rel="noreferrer" className="usa-link" href={docsLink}>
42+
documentation
43+
</a>
44+
.
45+
</>
46+
);
3447
return (
3548
<div className="new-file-or-folder">
3649
{errorMessage && <div className="usa-error-message">{errorMessage}</div>}
3750
{showFileDropZone && (
38-
<FileUpload
39-
onUpload={onUpload}
40-
onCancel={() => setShowFileDropZone(false)}
41-
triggerOnMount
42-
/>
51+
<>
52+
<AlertBanner
53+
className="margin-top-1"
54+
status="warning"
55+
message={message}
56+
alertRole={false}
57+
/>
58+
<FileUpload
59+
onUpload={onUpload}
60+
onCancel={() => setShowFileDropZone(false)}
61+
triggerOnMount
62+
/>
63+
</>
4364
)}
4465
{showFolderNameField && (
4566
<div className="new-folder grid-row flex-align-center margin-y-1">
@@ -71,24 +92,30 @@ const NewFileOrFolder = ({ onUpload, onCreateFolder }) => {
7192
)}
7293
<div className="margin-y-1">
7394
{!showFolderNameField && !showFileDropZone && (
74-
<button
75-
type="button"
76-
className="usa-button usa-button--outline"
77-
onClick={() => setShowFileDropZone(true)}
78-
>
79-
<IconFolder className="usa-icon" />
80-
Upload files
81-
</button>
82-
)}
83-
{!showFileDropZone && !showFolderNameField && (
84-
<button
85-
type="button"
86-
className="usa-button usa-button--outline"
87-
onClick={() => setShowFolderNameField(true)}
88-
>
89-
<IconFolder className="usa-icon" />
90-
New folder
91-
</button>
95+
<>
96+
<button
97+
type="button"
98+
className="usa-button"
99+
onClick={() => setShowFileDropZone(true)}
100+
>
101+
<IconAttachment className="usa-icon" />
102+
Upload files
103+
</button>
104+
<button
105+
type="button"
106+
className="usa-button usa-button--outline"
107+
onClick={() => setShowFolderNameField(true)}
108+
>
109+
<IconFolder className="usa-icon" />
110+
New folder
111+
</button>
112+
<Link
113+
className="usa-button usa-button--unstyled text-top padding-105"
114+
to="./logs"
115+
>
116+
View file history logs
117+
</Link>
118+
</>
92119
)}
93120
</div>
94121
</div>

frontend/pages/sites/$siteId/storage/NewFileOrFolder.test.jsx

+12-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React from 'react';
22
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
33
import '@testing-library/jest-dom';
4+
import { MemoryRouter } from 'react-router-dom';
45
import userEvent from '@testing-library/user-event';
56
import NewFileOrFolder from './NewFileOrFolder';
67
import prettyBytes from 'pretty-bytes';
@@ -9,13 +10,19 @@ jest.mock('pretty-bytes', () => jest.fn());
910
const mockOnUpload = jest.fn();
1011
const mockOnCreateFolder = jest.fn();
1112

13+
jest.mock('@shared/alertBanner', () => {
14+
return jest.fn(() => null);
15+
});
16+
1217
const renderComponent = (props = {}) => {
1318
return render(
14-
<NewFileOrFolder
15-
onUpload={mockOnUpload}
16-
onCreateFolder={mockOnCreateFolder}
17-
{...props}
18-
/>,
19+
<MemoryRouter>
20+
<NewFileOrFolder
21+
onUpload={mockOnUpload}
22+
onCreateFolder={mockOnCreateFolder}
23+
{...props}
24+
/>
25+
</MemoryRouter>,
1926
);
2027
};
2128

0 commit comments

Comments
 (0)