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

Deploy v9.4.11 to production #4145

Merged
merged 26 commits into from
Sep 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
3d55af6
Bump @babel/runtime from 7.25.4 to 7.25.6 in /app/assets
dependabot[bot] Aug 29, 2024
35b3d6d
Bump @testing-library/react from 16.0.0 to 16.0.1 in /app/assets
dependabot[bot] Aug 29, 2024
99d1235
Bump honeybadger from 0.21.0 to 0.22.0 in /app
dependabot[bot] Aug 30, 2024
e508561
Bump sass from 1.77.8 to 1.78.0 in /app/assets
dependabot[bot] Sep 4, 2024
032a25d
Bump @apollo/client from 3.11.4 to 3.11.8 in /app/assets
dependabot[bot] Sep 5, 2024
91814bb
Bump excoveralls from 0.18.2 to 0.18.3 in /app
dependabot[bot] Sep 6, 2024
d697d48
Bump @nulib/dcapi-types from 2.3.1 to 2.5.0 in /app/assets
dependabot[bot] Sep 9, 2024
4c4576a
Bump typescript from 5.5.4 to 5.6.2 in /app/assets
dependabot[bot] Sep 10, 2024
3ad01e9
Bump tzdata from 1.1.1 to 1.1.2 in /app
dependabot[bot] Sep 10, 2024
262602e
Bump plug_cowboy from 2.7.1 to 2.7.2 in /app
dependabot[bot] Sep 10, 2024
a1cdf2b
Bump version to 9.4.11
github-actions[bot] Sep 10, 2024
ed5ee6b
Update sitemapper to v0.9.0
mbklein Sep 10, 2024
66e94b9
Fix Gettext warnings
mbklein Sep 10, 2024
57ad199
Merge dependabot/hex/app/deploy/staging/plug_cowboy-2.7.2 into combin…
github-actions[bot] Sep 10, 2024
8eb49f7
Merge dependabot/hex/app/deploy/staging/tzdata-1.1.2 into combined-de…
github-actions[bot] Sep 10, 2024
aa9e069
Merge dependabot/npm_and_yarn/app/assets/deploy/staging/typescript-5.…
github-actions[bot] Sep 10, 2024
7483493
Merge dependabot/npm_and_yarn/app/assets/deploy/staging/nulib/dcapi-t…
github-actions[bot] Sep 10, 2024
a31a3cc
Merge dependabot/hex/app/deploy/staging/excoveralls-0.18.3 into combi…
github-actions[bot] Sep 10, 2024
1fb4d56
Merge dependabot/npm_and_yarn/app/assets/deploy/staging/apollo/client…
github-actions[bot] Sep 10, 2024
fc9eed7
Merge dependabot/npm_and_yarn/app/assets/deploy/staging/sass-1.78.0 i…
github-actions[bot] Sep 10, 2024
bc9bf0a
Merge dependabot/hex/app/deploy/staging/honeybadger-0.22.0 into combi…
github-actions[bot] Sep 10, 2024
eb77d1f
Merge dependabot/npm_and_yarn/app/assets/deploy/staging/testing-libra…
github-actions[bot] Sep 10, 2024
13ec9e5
Merge dependabot/npm_and_yarn/app/assets/deploy/staging/babel/runtime…
github-actions[bot] Sep 10, 2024
9c235f1
Merge pull request #4146 from nulib/combined-dependencies
mbklein Sep 10, 2024
cf05230
Use Chonky file browser with S3 data provided via GraphQL in the S3Ob…
mbklein Sep 11, 2024
0b15b74
Merge pull request #4149 from nulib/5198-s3-file-browser
mbklein Sep 18, 2024
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
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
Loading