Changed PDF file preview behavior (#3144)

Signed-off-by: Benjamin Perez <benjamin@bexsoft.net>
This commit is contained in:
Alex
2023-12-12 14:20:06 -06:00
committed by GitHub
parent f4a3f46bcf
commit f0d4dddacd
14 changed files with 62904 additions and 345 deletions

View File

@@ -6,17 +6,20 @@
"private": true,
"dependencies": {
"@reduxjs/toolkit": "^1.9.6",
"@types/pdfjs-dist": "^2.10.378",
"kbar": "^0.1.0-beta.39",
"local-storage-fallback": "^4.1.1",
"lodash": "^4.17.21",
"luxon": "^3.4.3",
"mds": "https://github.com/minio/mds.git#v0.13.3",
"pdfjs-dist": "3.11.174",
"react": "^18.1.0",
"react-component-export-image": "^1.0.6",
"react-copy-to-clipboard": "^5.0.2",
"react-dom": "^18.1.0",
"react-dropzone": "^14.2.3",
"react-markdown": "8.0.7",
"react-pdf": "7.5.1",
"react-redux": "^8.1.3",
"react-router-dom": "6.20.1",
"react-virtualized": "^9.22.5",

61779
portal-ui/public/scripts/pdf.worker.min.js vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -950,11 +950,16 @@ const ListObjects = () => {
bucketName={bucketName}
/>
)}
{previewOpen && (
{previewOpen && selectedPreview && (
<PreviewFileModal
open={previewOpen}
bucketName={bucketName}
object={selectedPreview}
actualInfo={{
name: selectedPreview.name || "",
last_modified: "",
version_id: selectedPreview.version_id || "",
size: selectedPreview.size || 0,
}}
onClosePreview={closePreviewWindow}
/>
)}

View File

@@ -570,13 +570,7 @@ const ObjectDetailPanel = ({
<PreviewFileModal
open={previewOpen}
bucketName={bucketName}
object={{
name: actualInfo.name || "",
version_id: actualInfo.version_id || "null",
size: actualInfo.size || 0,
content_type: "",
last_modified: actualInfo.last_modified || "",
}}
actualInfo={actualInfo}
onClosePreview={() => {
setPreviewOpen(false);
}}

View File

@@ -330,7 +330,7 @@ const VersionsNavigator = ({
<PreviewFileModal
open={previewOpen}
bucketName={bucketName}
object={{
actualInfo={{
name: actualInfo.name || "",
version_id:
objectToShare && objectToShare.version_id

View File

@@ -17,28 +17,33 @@
import React, { Fragment, useCallback, useEffect, useState } from "react";
import { ProgressBar, Grid, Box, InformativeMessage } from "mds";
import get from "lodash/get";
import { BucketObjectItem } from "../ListObjects/types";
import { AllowedPreviews, previewObjectType } from "../utils";
import { encodeURLString } from "../../../../../../common/utils";
import { api } from "../../../../../../api";
import PreviewPDF from "./PreviewPDF";
import { downloadObject } from "../../../../ObjectBrowser/utils";
import { useAppDispatch } from "../../../../../../store";
import { BucketObject } from "../../../../../../api/consoleApi";
interface IPreviewFileProps {
bucketName: string;
object: BucketObjectItem | null;
actualInfo: BucketObject;
isFullscreen?: boolean;
}
const PreviewFile = ({
bucketName,
object,
actualInfo,
isFullscreen = false,
}: IPreviewFileProps) => {
const dispatch = useAppDispatch();
const [loading, setLoading] = useState<boolean>(true);
const [metaData, setMetaData] = useState<any>(null);
const [isMetaDataLoaded, setIsMetaDataLoaded] = useState(false);
const objectName = object?.name || "";
const objectName = actualInfo?.name || "";
const fetchMetadata = useCallback(() => {
if (!isMetaDataLoaded) {
@@ -71,12 +76,12 @@ const PreviewFile = ({
let path = "";
if (object) {
const encodedPath = encodeURLString(object.name);
if (actualInfo) {
const encodedPath = encodeURLString(actualInfo.name || "");
let basename = document.baseURI.replace(window.location.origin, "");
path = `${window.location.origin}${basename}api/v1/buckets/${bucketName}/objects/download?preview=true&prefix=${encodedPath}`;
if (object.version_id) {
path = path.concat(`&version_id=${object.version_id}`);
if (actualInfo.version_id) {
path = path.concat(`&version_id=${actualInfo.version_id}`);
}
}
@@ -174,6 +179,18 @@ const PreviewFile = ({
onLoad={iframeLoaded}
/>
)}
{objectType === "pdf" && (
<Fragment>
<PreviewPDF
path={path}
onLoad={iframeLoaded}
loading={loading}
downloadFile={() =>
downloadObject(dispatch, bucketName, path, actualInfo)
}
/>
</Fragment>
)}
{objectType === "none" && (
<div>
<InformativeMessage
@@ -188,7 +205,8 @@ const PreviewFile = ({
{objectType !== "none" &&
objectType !== "video" &&
objectType !== "audio" &&
objectType !== "image" && (
objectType !== "image" &&
objectType !== "pdf" && (
<div className={`iframeBase ${loading ? "iframeHidden" : ""}`}>
<iframe
src={path}

View File

@@ -17,32 +17,32 @@
import React, { Fragment } from "react";
import ModalWrapper from "../../../../Common/ModalWrapper/ModalWrapper";
import PreviewFileContent from "./PreviewFileContent";
import { BucketObjectItem } from "../ListObjects/types";
import { ObjectPreviewIcon } from "mds";
import { BucketObject } from "../../../../../../api/consoleApi";
interface IPreviewFileProps {
open: boolean;
bucketName: string;
object: BucketObjectItem | null;
actualInfo: BucketObject;
onClosePreview: () => void;
}
const PreviewFileModal = ({
open,
bucketName,
object,
actualInfo,
onClosePreview,
}: IPreviewFileProps) => {
return (
<Fragment>
<ModalWrapper
modalOpen={open}
title={`Preview - ${object?.name}`}
title={`Preview - ${actualInfo?.name}`}
onClose={onClosePreview}
wideLimit={false}
titleIcon={<ObjectPreviewIcon />}
>
<PreviewFileContent bucketName={bucketName} object={object} />
<PreviewFileContent bucketName={bucketName} actualInfo={actualInfo} />
</ModalWrapper>
</Fragment>
);

View File

@@ -0,0 +1,143 @@
// This file is part of MinIO Console Server
// Copyright (c) 2023 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { Fragment, useState } from "react";
import { Document, Page, pdfjs } from "react-pdf";
import { Box, Button, InformativeMessage } from "mds";
pdfjs.GlobalWorkerOptions.workerSrc = "./scripts/pdf.worker.min.js";
interface IPreviewPDFProps {
path: string;
loading: boolean;
onLoad: () => void;
downloadFile: () => void;
}
const PreviewPDF = ({
path,
loading,
onLoad,
downloadFile,
}: IPreviewPDFProps) => {
const [errorState, setErrorState] = useState<boolean>(false);
const [totalPages, setTotalPages] = useState<number>(0);
if (!path) {
return null;
}
const renderPages = totalPages > 5 ? 5 : totalPages;
const arrayCreate = Array.from(Array(renderPages).keys());
return (
<Fragment>
{errorState && totalPages === 0 && (
<InformativeMessage
variant={"error"}
title={"Error"}
message={
<Fragment>
File preview couldn't be displayed, Please try Download instead.
<Box
sx={{
display: "flex",
justifyContent: "center",
marginTop: 12,
}}
>
<Button
id={"download-preview"}
onClick={downloadFile}
variant={"callAction"}
>
Download File
</Button>
</Box>
</Fragment>
}
sx={{ marginBottom: 10 }}
/>
)}
{!loading && !errorState && (
<InformativeMessage
variant={"warning"}
title={"File Preview"}
message={
<Fragment>
This is a File Preview for the first {arrayCreate.length} pages of
the document, if you wish to work with the full document please
download instead.
<Box
sx={{
display: "flex",
justifyContent: "center",
marginTop: 12,
}}
>
<Button
id={"download-preview"}
onClick={downloadFile}
variant={"callAction"}
>
Download File
</Button>
</Box>
</Fragment>
}
sx={{ marginBottom: 10 }}
/>
)}
{!errorState && (
<Box
sx={{
overflowY: "auto",
"& .react-pdf__Page__canvas": {
margin: "0 auto",
backgroundColor: "transparent",
},
}}
>
<Document
file={path}
onLoadSuccess={({ _pdfInfo }) => {
setTotalPages(_pdfInfo.numPages || 0);
setErrorState(false);
onLoad();
}}
onLoadError={(error) => {
setErrorState(true);
onLoad();
console.error(error);
}}
>
{arrayCreate.map((item) => (
<Page
pageNumber={item + 1}
key={`render-page-${item}`}
renderAnnotationLayer={false}
renderTextLayer={false}
renderForms={false}
/>
))}
</Document>
</Box>
)}
</Fragment>
);
};
export default PreviewPDF;

View File

@@ -191,7 +191,13 @@ class BrowserDownload {
}
}
export type AllowedPreviews = "image" | "text" | "audio" | "video" | "none";
export type AllowedPreviews =
| "image"
| "pdf"
| "text"
| "audio"
| "video"
| "none";
export const contentTypePreview = (contentType: string): AllowedPreviews => {
if (contentType) {
const mimeObjectType = (contentType || "").toLowerCase();
@@ -199,6 +205,9 @@ export const contentTypePreview = (contentType: string): AllowedPreviews => {
if (mimeObjectType.includes("image")) {
return "image";
}
if (mimeObjectType.includes("pdf")) {
return "pdf";
}
if (mimeObjectType.includes("text")) {
return "text";
}
@@ -231,7 +240,8 @@ export const extensionPreview = (fileName: string): AllowedPreviews => {
"png",
"heic",
];
const textExtensions = ["pdf"];
const textExtensions = ["txt"];
const pdfExtensions = ["pdf"];
const audioExtensions = ["wav", "mp3", "alac", "aiff", "dsd", "pcm"];
const videoExtensions = [
"mp4",
@@ -258,6 +268,10 @@ export const extensionPreview = (fileName: string): AllowedPreviews => {
return "image";
}
if (pdfExtensions.includes(fileExtension)) {
return "pdf";
}
if (textExtensions.includes(fileExtension)) {
return "text";
}

View File

@@ -0,0 +1,108 @@
// This file is part of MinIO Console Server
// Copyright (c) 2023 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import * as roles from "../utils/roles";
import { Selector } from "testcafe";
import * as functions from "../utils/functions";
import { namedTestBucketBrowseButtonFor } from "../utils/functions";
fixture("Test Preview page in Console").page("http://localhost:9090/");
const bucketName = "preview";
export const file = Selector(".ReactVirtualized__Table__rowColumn").withText(
"internode.png",
);
export const fileScript = Selector(
".ReactVirtualized__Table__rowColumn",
).withText("filescript.pdf");
export const pdfFile = Selector(".ReactVirtualized__Table__rowColumn").withText(
"file1.pdf",
);
const bucketNameAction = namedTestBucketBrowseButtonFor(bucketName);
test
.before(async (t) => {
await functions.setUpNamedBucket(t, bucketName);
await functions.uploadNamedObjectToBucket(
t,
bucketName,
"internode.png",
"portal-ui/tests/uploads/internode.png",
);
})("File can be previewed", async (t) => {
await t
.useRole(roles.admin)
.navigateTo(`http://localhost:9090/browser`)
.click(bucketNameAction)
.click(file)
.click(Selector(".objectActions button").withText("Preview"))
.expect(Selector(".dialogContent > div > img").exists)
.ok();
})
.after(async (t) => {
await functions.cleanUpNamedBucketAndUploads(t, bucketName);
});
test
.before(async (t) => {
await functions.setUpNamedBucket(t, bucketName);
await functions.uploadNamedObjectToBucket(
t,
bucketName,
"file1.pdf",
"portal-ui/tests/uploads/file1.pdf",
);
})("PDF File can be previewed", async (t) => {
await t
.useRole(roles.admin)
.navigateTo(`http://localhost:9090/browser`)
.click(bucketNameAction)
.click(pdfFile)
.click(Selector(".objectActions button").withText("Preview"))
.expect(Selector(".react-pdf__Page__canvas").exists)
.ok();
})
.after(async (t) => {
await functions.cleanUpNamedBucketAndUploads(t, bucketName);
});
test
.before(async (t) => {
await functions.setUpNamedBucket(t, bucketName);
await functions.uploadNamedObjectToBucket(
t,
bucketName,
"filescript.pdf",
"portal-ui/tests/uploads/filescript.pdf",
);
})("PDF with Alert doesn't execute script", async (t) => {
await t
.useRole(roles.admin)
.navigateTo(`http://localhost:9090/browser`)
.click(bucketNameAction)
.click(fileScript)
.click(Selector(".objectActions button").withText("Preview"))
.setNativeDialogHandler(() => false);
const history = await t.getNativeDialogHistory();
await t.expect(history.length).eql(0);
})
.after(async (t) => {
await functions.cleanUpNamedBucketAndUploads(t, bucketName);
});

Binary file not shown.

View File

@@ -0,0 +1,254 @@
%PDF-1.3
%<25>߬<EFBFBD>
3 0 obj
<</Type /Page
/Parent 1 0 R
/Resources 2 0 R
/MediaBox [0 0 595.2799999999999727 841.8899999999999864]
/Contents 4 0 R
>>
endobj
4 0 obj
<<
/Length 126
>>
stream
0.5670000000000001 w
0 G
BT
/F1 16 Tf
18.3999999999999986 TL
0 g
56.6929133858267775 785.1970866141732586 Td
(Some text) Tj
ET
endstream
endobj
1 0 obj
<</Type /Pages
/Kids [3 0 R ]
/Count 1
>>
endobj
5 0 obj
<<
/Type /Font
/BaseFont /Helvetica
/Subtype /Type1
/Encoding /WinAnsiEncoding
/FirstChar 32
/LastChar 255
>>
endobj
6 0 obj
<<
/Type /Font
/BaseFont /Helvetica-Bold
/Subtype /Type1
/Encoding /WinAnsiEncoding
/FirstChar 32
/LastChar 255
>>
endobj
7 0 obj
<<
/Type /Font
/BaseFont /Helvetica-Oblique
/Subtype /Type1
/Encoding /WinAnsiEncoding
/FirstChar 32
/LastChar 255
>>
endobj
8 0 obj
<<
/Type /Font
/BaseFont /Helvetica-BoldOblique
/Subtype /Type1
/Encoding /WinAnsiEncoding
/FirstChar 32
/LastChar 255
>>
endobj
9 0 obj
<<
/Type /Font
/BaseFont /Courier
/Subtype /Type1
/Encoding /WinAnsiEncoding
/FirstChar 32
/LastChar 255
>>
endobj
10 0 obj
<<
/Type /Font
/BaseFont /Courier-Bold
/Subtype /Type1
/Encoding /WinAnsiEncoding
/FirstChar 32
/LastChar 255
>>
endobj
11 0 obj
<<
/Type /Font
/BaseFont /Courier-Oblique
/Subtype /Type1
/Encoding /WinAnsiEncoding
/FirstChar 32
/LastChar 255
>>
endobj
12 0 obj
<<
/Type /Font
/BaseFont /Courier-BoldOblique
/Subtype /Type1
/Encoding /WinAnsiEncoding
/FirstChar 32
/LastChar 255
>>
endobj
13 0 obj
<<
/Type /Font
/BaseFont /Times-Roman
/Subtype /Type1
/Encoding /WinAnsiEncoding
/FirstChar 32
/LastChar 255
>>
endobj
14 0 obj
<<
/Type /Font
/BaseFont /Times-Bold
/Subtype /Type1
/Encoding /WinAnsiEncoding
/FirstChar 32
/LastChar 255
>>
endobj
15 0 obj
<<
/Type /Font
/BaseFont /Times-Italic
/Subtype /Type1
/Encoding /WinAnsiEncoding
/FirstChar 32
/LastChar 255
>>
endobj
16 0 obj
<<
/Type /Font
/BaseFont /Times-BoldItalic
/Subtype /Type1
/Encoding /WinAnsiEncoding
/FirstChar 32
/LastChar 255
>>
endobj
17 0 obj
<<
/Type /Font
/BaseFont /ZapfDingbats
/Subtype /Type1
/FirstChar 32
/LastChar 255
>>
endobj
18 0 obj
<<
/Type /Font
/BaseFont /Symbol
/Subtype /Type1
/FirstChar 32
/LastChar 255
>>
endobj
2 0 obj
<<
/ProcSet [/PDF /Text /ImageB /ImageC /ImageI]
/Font <<
/F1 5 0 R
/F2 6 0 R
/F3 7 0 R
/F4 8 0 R
/F5 9 0 R
/F6 10 0 R
/F7 11 0 R
/F8 12 0 R
/F9 13 0 R
/F10 14 0 R
/F11 15 0 R
/F12 16 0 R
/F13 17 0 R
/F14 18 0 R
>>
/XObject <<
>>
>>
endobj
19 0 obj
<<
/Names [(EmbeddedJS) 20 0 R]
>>
endobj
20 0 obj
<<
/S /JavaScript
/JS (app.alert(1);)
>>
endobj
21 0 obj
<<
/Producer (jsPDF 2.5.1)
/CreationDate (D:20231120150708-06'00')
>>
endobj
22 0 obj
<<
/Type /Catalog
/Pages 1 0 R
/OpenAction [3 0 R /FitH null]
/PageLayout /OneColumn
/Names <</JavaScript 19 0 R>>
>>
endobj
xref
0 23
0000000000 65535 f
0000000329 00000 n
0000002146 00000 n
0000000015 00000 n
0000000152 00000 n
0000000386 00000 n
0000000511 00000 n
0000000641 00000 n
0000000774 00000 n
0000000911 00000 n
0000001034 00000 n
0000001163 00000 n
0000001295 00000 n
0000001431 00000 n
0000001559 00000 n
0000001686 00000 n
0000001815 00000 n
0000001948 00000 n
0000002050 00000 n
0000002394 00000 n
0000002445 00000 n
0000002502 00000 n
0000002588 00000 n
trailer
<<
/Size 23
/Root 22 0 R
/Info 21 0 R
/ID [ <B6B01304F38DFFEC893A6499A048ED16> <B6B01304F38DFFEC893A6499A048ED16> ]
>>
startxref
2722
%%EOF

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

File diff suppressed because it is too large Load Diff