Skip to content

Commit

Permalink
Merge pull request #4145 from nulib/deploy/staging
Browse files Browse the repository at this point in the history
Deploy v9.4.11 to production
  • Loading branch information
kdid authored Sep 24, 2024
2 parents d1bf739 + 0b15b74 commit 4a10a9e
Show file tree
Hide file tree
Showing 18 changed files with 4,776 additions and 2,060 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,7 @@ function WorkTabsPreservationFileSetModal({
<div className="box">
<h3>Option 2: Choose from S3 Ingest Bucket</h3>
<S3ObjectPicker
onFiles={console.log}
onFileSelect={handleSelectS3Object}
fileSetRole={watchRole}
workTypeId={workTypeId}
Expand Down
212 changes: 80 additions & 132 deletions app/assets/js/components/Work/Tabs/Preservation/S3ObjectPicker.jsx
Original file line number Diff line number Diff line change
@@ -1,144 +1,92 @@
import useAcceptedMimeTypes from "@js/hooks/useAcceptedMimeTypes";
import { Button } from "@nulib/design-system";
import {
LIST_INGEST_BUCKET_OBJECTS,
} from "@js/components/Work/work.gql.js";
import React, { useState } from "react";
/** @jsx jsx */
import { css, jsx } from "@emotion/react";
import { useQuery } from "@apollo/client";
import { FaSpinner } from "react-icons/fa";
import { formatBytes } from "@js/services/helpers";

import Error from "@js/components/UI/Error";
import UIFormInput from "@js/components/UI/Form/Input.jsx";

const tableContainerCss = css`
max-height: 30vh;
overflow-y: auto;
`;

const fileRowCss = css`
cursor: pointer;
`;

const selectedRowCss = css`
background-color: #f0f8ff !important;
`;
import React, { useEffect, useRef, useState } from "react";
import S3ObjectProvider from './S3ObjectProvider';
import { styled } from '@stitches/react';

const colHeaders = ["File Key", "Size", "Mime Type"];

const S3ObjectPicker = ({ onFileSelect, fileSetRole, workTypeId, defaultPrefix = "" }) => {
import {
ChonkyActions,
FileBrowser,
FileList,
FileNavbar,
FileToolbar,
} from "chonky";
import { ChonkyIconFA } from "chonky-icon-fontawesome";

const StyledFilePicker = styled('div', {
"& .chonky-toolbarRight": {
display: "none"
}
});

const S3ObjectPicker = ({
onFileSelect,
fileSetRole,
workTypeId,
defaultPrefix = "",
}) => {
const [prefix, setPrefix] = useState(defaultPrefix);
const [selectedFile, setSelectedFile] = useState(null);
const [error, _setError] = useState(null);
const [uploadProgress, setUploadProgress] = useState(0);
const [isUploading, setIsUploading] = useState(false);

const { isFileValid } = useAcceptedMimeTypes();

const { loading: queryLoading, error: queryError, data, refetch } = useQuery(LIST_INGEST_BUCKET_OBJECTS, {
variables: { prefix }
});

const handleClear = () => {
setPrefix(defaultPrefix);
refetch({ prefix: defaultPrefix });
};

const handlePrefixChange = async (e) => {
const inputValue = e.target.value;
const newPrefix = inputValue.startsWith(defaultPrefix) ? inputValue : defaultPrefix + inputValue;
setPrefix(newPrefix);
await refetch({ prefix: newPrefix });
};

const handleRefresh = async () => {
await refetch({ prefix: prefix });
};

const handleFileClick = (fileSet) => {
setSelectedFile(fileSet.key);
onFileSelect(fileSet);
// Reset upload progress and isUploading state when selecting an S3 object
setUploadProgress(0);
setIsUploading(false);
};
const [error, setError] = useState(null);

const fileBrowserRef = useRef(null);
const providerRef = useRef(null);

useEffect(() => {
const fileSet = providerRef?.current?.findFileSetByUri(selectedFile);
fileSet && onFileSelect && onFileSelect(fileSet);
}, [selectedFile]);

const handleFileAction = (action) => {
switch (action.id) {
case ChonkyActions.OpenFiles.id:
const { targetFile } = action.payload;
if (targetFile.isDir) {
setPrefix(action.payload.targetFile.id);
}
break;

case ChonkyActions.ChangeSelection.id:
if (
action.payload.selection.size == 0 &&
files.find(({ id }) => selectedFile == id)
) {
fileBrowserRef.current.setFileSelection(new Set([selectedFile]));
return;
}

const handleDragAndDrop = (file) => {
// Simulating file upload process
setIsUploading(true);
setUploadProgress(0);
const interval = setInterval(() => {
setUploadProgress((prevProgress) => {
if (prevProgress >= 100) {
clearInterval(interval);
setIsUploading(false);
return 100;
const selectedFiles = [...action.payload.selection];
const clicked = selectedFiles[selectedFiles.length - 1];

if (selectedFiles.length > 1) {
// Reject multiselect
fileBrowserRef.current.setFileSelection(new Set([clicked]));
} else if (
clicked &&
clicked.match(/^s3:/) &&
selectedFile != clicked
) {
setSelectedFile(clicked);
}
return prevProgress + 10;
});
}, 500);
break;
}
};

if (queryLoading) return <FaSpinner className="spinner" />;
if (queryError) return <Error error={queryError} />;

return (
<div className="file-picker">
<div className="drag-drop-area" onDrop={handleDragAndDrop}>
{/* Drag and drop area */}
<p>Drag 'n' drop a file here, or click to select file</p>
{isUploading && (
<div className="progress-bar">
<div className="progress" style={{ width: `${uploadProgress}%` }}></div>
</div>
)}
</div>
<UIFormInput
placeholder="Enter prefix"
name="prefixSearch"
label="Prefix Search"
onChange={handlePrefixChange}
value={prefix}
/>
<div className="buttons mt-2">
<Button onClick={handleClear}>Clear</Button>
<Button onClick={handleRefresh}>Refresh</Button>
</div>
<StyledFilePicker className="file-picker" data-testid="file-picker">
{error && <div className="error">{error}</div>}
{data && data.ListIngestBucketObjects && (
<div className="table-container" css={tableContainerCss}>
<table className="table is-striped is-fullwidth">
<thead>
<tr>
{colHeaders.map((col) => (
<th key={col}>{col}</th>
))}
</tr>
</thead>
<tbody>
{data.ListIngestBucketObjects.filter(file => {
const { isValid } = isFileValid(fileSetRole, workTypeId, file.mimeType);
return isValid;
}).map((fileSet, index) => (
<tr
key={index}
onClick={() => handleFileClick(fileSet)}
className={selectedFile === fileSet.key ? "selected" : ""}
css={[fileRowCss, selectedFile === fileSet.key && selectedRowCss]}
>
<td>{fileSet.key}</td>
<td>{formatBytes(fileSet.size)}</td>
<td>{fileSet.mimeType}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
<S3ObjectProvider fileSetRole={fileSetRole} workTypeId={workTypeId} prefix={prefix} ref={providerRef}>
<FileBrowser
ref={fileBrowserRef}
defaultFileViewActionId={ChonkyActions.EnableListView.id}
onFileAction={handleFileAction}
iconComponent={ChonkyIconFA}
>
<FileNavbar />
<FileToolbar />
<FileList/>
</FileBrowser>
</S3ObjectProvider>
</StyledFilePicker>
);
};

export default S3ObjectPicker;
export default S3ObjectPicker;
Original file line number Diff line number Diff line change
@@ -1,49 +1,37 @@
import React from "react";
import { render, fireEvent, waitFor } from "@testing-library/react";
import { render } from "@testing-library/react";
import { MockedProvider } from "@apollo/client/testing";
import S3ObjectPicker from "@js/components/Work/Tabs/Preservation/S3ObjectPicker";
import S3ObjectPicker from "./S3ObjectPicker";
import { LIST_INGEST_BUCKET_OBJECTS } from "@js/components/Work/work.gql.js";

const mocks = [
{
request: {
query: LIST_INGEST_BUCKET_OBJECTS,
variables: { prefix: "file_sets/" },
},
result: {
data: {
ListIngestBucketObjects: [
{ key: "file_sets/file3", size: 1000, mimeType: "image/jpeg" },
{ key: "file_sets/file4", size: 2000, mimeType: "image/png" },
],
},
},
},
{
request: {
query: LIST_INGEST_BUCKET_OBJECTS,
variables: { prefix: "" },
},
result: {
data: {
ListIngestBucketObjects: [
{ key: "file1", size: 1000, mimeType: "image/jpeg" },
{ key: "file2", size: 2000, mimeType: "image/png" },
{ key: "file_sets/file3", size: 1000, mimeType: "image/jpeg" },
{ key: "file_sets/file4", size: 2000, mimeType: "image/png" },
],
ListIngestBucketObjects: {
objects: [
{ uri: "s3://bucket/file1.jpg", key: "file1", size: 1000, mimeType: "image/jpeg", storageClass: "STANDARD", lastModified: new Date().toISOString() },
{ uri: "s3://bucket/file2.png", key: "file2", size: 2000, mimeType: "image/png", storageClass: "STANDARD", lastModified: new Date().toISOString() },
],
folders: ["file_sets"],
},
},
},
},
];

describe("S3ObjectPicker component", () => {
it("renders without crashing", () => {
render(
it("renders without crashing", async () => {
const { findByTestId } = render(
<MockedProvider mocks={mocks} addTypename={false}>
<S3ObjectPicker onFileSelect={() => { }} fileSetRole="A" workTypeId="IMAGE" />
</MockedProvider>
);
expect(await findByTestId("file-picker")).toBeInTheDocument();
});

it("renders an error message when there is a query error", async () => {
Expand All @@ -59,54 +47,8 @@ describe("S3ObjectPicker component", () => {
const { findByText } = render(
<MockedProvider mocks={errorMock} addTypename={false}>
<S3ObjectPicker onFileSelect={() => { }} fileSetRole="A" workTypeId="IMAGE" />
</MockedProvider>
</MockedProvider>,
);
expect(await findByText("An error occurred")).toBeInTheDocument();
});

it("renders the Clear and Refresh buttons", async () => {
const { findByText } = render(
<MockedProvider mocks={mocks} addTypename={false}>
<S3ObjectPicker onFileSelect={() => { }} fileSetRole="A" workTypeId="IMAGE" />
</MockedProvider>
);
expect(await findByText("Clear")).toBeInTheDocument();
expect(await findByText("Refresh")).toBeInTheDocument();
});

it("renders the table when data is available", async () => {
const { findByText } = render(
<MockedProvider mocks={mocks} addTypename={false}>
<S3ObjectPicker onFileSelect={() => { }} fileSetRole="A" workTypeId="IMAGE" />
</MockedProvider>
);
expect(await findByText("file1")).toBeInTheDocument();
expect(await findByText("file2")).toBeInTheDocument();
});

it("handles prefixed search", async () => {
const { findByText, getByPlaceholderText, queryByText } = render(
<MockedProvider mocks={mocks} addTypename={false}>
<S3ObjectPicker onFileSelect={() => { }} fileSetRole="A" workTypeId="IMAGE" />
</MockedProvider>
);

await findByText("file1");

const input = getByPlaceholderText("Enter prefix");
fireEvent.change(input, { target: { value: "file_sets/" } });

await waitFor(() => {
expect(input.value).toBe("file_sets/");
});

// Check that the prefixed files are present
expect(await findByText("file_sets/file3")).toBeInTheDocument();
expect(await findByText("file_sets/file4")).toBeInTheDocument();

// Check that the non-prefixed files are not present
expect(queryByText("file1")).not.toBeInTheDocument();
expect(queryByText("file2")).not.toBeInTheDocument();
});

});
Loading

0 comments on commit 4a10a9e

Please sign in to comment.