Skip to content

Commit

Permalink
Merge pull request #273 from bento-platform/fix/drs-download
Browse files Browse the repository at this point in the history
fix: authorization for DRS object download
  • Loading branch information
davidlougheed authored Jul 24, 2023
2 parents c481a1f + df37290 commit 67b1c03
Show file tree
Hide file tree
Showing 3 changed files with 184 additions and 134 deletions.
44 changes: 44 additions & 0 deletions src/components/manager/DownloadButton.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { useSelector } from "react-redux";
import React, { useCallback } from "react";
import { Button } from "antd";
import PropTypes from "prop-types";

const DownloadButton = ({ disabled, uri, children }) => {
const { accessToken } = useSelector((state) => state.auth);

const onClick = useCallback(() => {
if (!uri) return;

const form = document.createElement("form");
form.method = "post";
form.target = "_blank";
form.action = uri;
form.innerHTML = `<input type="hidden" name="token" value="${accessToken}" />`;
document.body.appendChild(form);
try {
form.submit();
} finally {
// Even if submit raises for some reason, we still need to clean this up; it has a token in it!
document.body.removeChild(form);
}
}, [uri, accessToken]);

return (
<Button key="download" icon="download" disabled={disabled} onClick={onClick}>
{children}
</Button>
);
};

DownloadButton.defaultProps = {
disabled: false,
children: "Download",
};

DownloadButton.propTypes = {
disabled: PropTypes.bool,
uri: PropTypes.string,
children: PropTypes.oneOfType([PropTypes.object, PropTypes.string]),
};

export default DownloadButton;
38 changes: 4 additions & 34 deletions src/components/manager/ManagerDropBoxContent.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,10 @@ import {
} from "antd";

import {LAYOUT_CONTENT_STYLE} from "../../styles/layoutContent";
import DownloadButton from "./DownloadButton";
import DropBoxTreeSelect from "./DropBoxTreeSelect";
import TableSelectionModal from "./TableSelectionModal";
import JsonDisplay from "../JsonDisplay";
import TableSelectionModal from "./TableSelectionModal";

import {BENTO_DROP_BOX_FS_BASE_PATH} from "../../config";
import {STEP_INPUT} from "./workflowCommon";
Expand Down Expand Up @@ -417,37 +418,6 @@ FileContentsModal.propTypes = {
};


const InfoDownloadButton = ({disabled, uri}) => {
const {accessToken} = useSelector(state => state.auth);

const onClick = useCallback(() => {
if (!uri) return;

const form = document.createElement("form");
form.method = "post";
form.target = "_blank";
form.action = uri;
form.innerHTML = `<input type="hidden" name="token" value="${accessToken}" />`;
document.body.appendChild(form);
try {
form.submit();
} finally {
// Even if submit raises for some reason, we still need to clean this up; it has a token in it!
document.body.removeChild(form);
}
}, [uri, accessToken]);

return <Button key="download" icon="download" disabled={disabled} onClick={onClick}>Download</Button>;
};
InfoDownloadButton.defaultProps = {
disabled: false,
};
InfoDownloadButton.propTypes = {
disabled: PropTypes.bool,
uri: PropTypes.string,
};


const DropBoxInformation = () => (
<Alert type="info" showIcon={true} message="About the drop box" description={`
The drop box contains files which are not yet ingested into this Bento instance. They are not
Expand Down Expand Up @@ -688,7 +658,7 @@ const ManagerDropBoxContent = () => {
<Modal visible={fileInfoModal}
title={`${fileForInfo.split("/").at(-1)} - information`}
width={960}
footer={[<InfoDownloadButton key="download" uri={filesByPath[fileForInfo]?.uri} />]}
footer={[<DownloadButton key="download" uri={filesByPath[fileForInfo]?.uri} />]}
onCancel={hideFileInfoModal}>
<Descriptions bordered={true}>
<Descriptions.Item label="Name" span={3}>
Expand Down Expand Up @@ -731,7 +701,7 @@ const ManagerDropBoxContent = () => {
<Button icon="file-text" onClick={handleViewFile} disabled={!selectedFileViewable}>
View
</Button>
<InfoDownloadButton disabled={!selectedFileInfoAvailable} uri={filesByPath[fileForInfo]?.uri} />
<DownloadButton disabled={!selectedFileInfoAvailable} uri={filesByPath[fileForInfo]?.uri} />
</Button.Group>

<Button type="danger"
Expand Down
236 changes: 136 additions & 100 deletions src/components/manager/drs/ManagerDRSContent.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,24 @@
import React, {useCallback, useState} from "react";
import {useSelector} from "react-redux";
import {filesize} from "filesize";
import {throttle} from "lodash";
import React, { useCallback, useMemo, useState } from "react";
import { useSelector } from "react-redux";
import { filesize } from "filesize";
import { throttle } from "lodash";

import {Layout, Input, Table, Button, Descriptions, message} from "antd";
import { Layout, Input, Table, Descriptions, message } from "antd";

import {LAYOUT_CONTENT_STYLE} from "../../../styles/layoutContent";
import {makeAuthorizationHeader, useAuthorizationHeader} from "../../../lib/auth/utils";
import { LAYOUT_CONTENT_STYLE } from "../../../styles/layoutContent";
import { makeAuthorizationHeader, useAuthorizationHeader } from "../../../lib/auth/utils";

import DownloadButton from "../DownloadButton";

const SEARCH_CONTAINER_STYLE = {
maxWidth: 800,
marginBottom: "1rem",
};

const TABLE_NESTED_DESCRIPTIONS_STYLE = {
backgroundColor: "white",
borderRadius: 3,
};

const DRS_COLUMNS = [
{
Expand All @@ -23,119 +35,143 @@ const DRS_COLUMNS = [
title: "Size",
dataIndex: "size",
key: "size",
render: size => filesize(size),
render: (size) => filesize(size),
},
{
title: "Actions",
dataIndex: "",
key: "actions",
render: record => {
render: (record) => {
const url = record.access_methods[0]?.access_url?.url;
return <Button disabled={!url} icon="download" onClick={() => {
console.debug(`Opening ${url} for download`);
window.open(url);
}}>Download</Button>;
return <DownloadButton disabled={!url} uri={url} />;
},
},
];

const ManagerDRSContent = () => {
const drsUrl = useSelector(state => state.services.drsService?.url);
const drsUrl = useSelector((state) => state.services.drsService?.url);

const authHeader = useAuthorizationHeader();

const [searchResults, setSearchResults] = useState([]);
const [doneSearch, setDoneSearch] = useState(false);
const [loading, setLoading] = useState(false);

const onSearch = useCallback(throttle(v => {
if (!drsUrl) return;

// Extract value from either the native HTML event or the AntDesign event
const sv = (v.target?.value ?? v ?? "").trim();
if (!sv) {
setDoneSearch(false); // Behave as if we have never searched before
setSearchResults([]);
return;
}

setLoading(true);

const authHeaders = makeAuthorizationHeader(authHeader);
fetch(`${drsUrl}/search?` + new URLSearchParams({q: sv}), {method: "GET", headers: authHeaders})
.then(r => Promise.all([Promise.resolve(r.ok), r.json()]))
.then(([ok, data]) => {
if (ok) {
console.debug("received DRS objects:", data);
setSearchResults(data);
} else {
message.error(`Encountered error while fetching DRS objects: ${data.message}`);
console.error(data);
const onSearch = useCallback(
throttle(
(v) => {
if (!drsUrl) return;

// Extract value from either the native HTML event or the AntDesign event
const sv = (v.target?.value ?? v ?? "").trim();
if (!sv) {
setDoneSearch(false); // Behave as if we have never searched before
setSearchResults([]);
return;
}
setLoading(false);
setDoneSearch(true);
})
.catch(e => {
message.error(`Encountered error while fetching DRS objects: ${e}`);
console.error(e);
});
}, 250, {leading: true, trailing: true}), [drsUrl, authHeader]);

return <Layout>
<Layout.Content style={LAYOUT_CONTENT_STYLE}>
<div style={{maxWidth: 800, marginBottom: "1rem"}}>
<Input.Search
placeholder="Search DRS objects by name."
loading={loading || !drsUrl}
disabled={!drsUrl}
onChange={onSearch}
onSearch={onSearch}
size="large"
/>
</div>
<Table
rowKey="id"
columns={DRS_COLUMNS}
dataSource={searchResults}
loading={loading}
bordered={true}
expandedRowRender={({id, description, checksums, access_methods: accessMethods, size}) => (
<div style={{backgroundColor: "white", borderRadius: 3}} className="table-nested-ant-descriptions">
<Descriptions bordered={true}>
<Descriptions.Item label="ID" span={2}>
<span style={{fontFamily: "monospace"}}>{id}</span></Descriptions.Item>
<Descriptions.Item label="Size" span={1}>{filesize(size)}</Descriptions.Item>
<Descriptions.Item label="Checksums" span={3}>
{checksums.map(({type, checksum}) =>
<div key={type} style={{display: "flex", gap: "0.8em", alignItems: "baseline"}}>
<span style={{fontWeight: "bold"}}>{type.toLocaleUpperCase()}:</span>
<span style={{fontFamily: "monospace"}}>{checksum}</span>
</div>,
)}
</Descriptions.Item>
<Descriptions.Item label="Access Methods" span={3}>
{accessMethods.map(({type, access_url: url}, i) =>
<div key={i} style={{display: "flex", gap: "0.8em", alignItems: "baseline"}}>
<span style={{fontWeight: "bold"}}>{type.toLocaleUpperCase()}:</span>
<span style={{fontFamily: "monospace"}}>
{type === "http"
? <a href={url?.url} target="_blank" rel="noreferrer">{url?.url}</a>
: url?.url}
</span>
</div>,

setLoading(true);

const authHeaders = makeAuthorizationHeader(authHeader);
fetch(`${drsUrl}/search?` + new URLSearchParams({ q: sv }), { method: "GET", headers: authHeaders })
.then((r) => Promise.all([Promise.resolve(r.ok), r.json()]))
.then(([ok, data]) => {
if (ok) {
console.debug("received DRS objects:", data);
setSearchResults(data);
} else {
message.error(`Encountered error while fetching DRS objects: ${data.message}`);
console.error(data);
}
setLoading(false);
setDoneSearch(true);
})
.catch((e) => {
message.error(`Encountered error while fetching DRS objects: ${e}`);
console.error(e);
});
},
250,
{ leading: true, trailing: true },
),
[drsUrl, authHeader],
);

const tableLocale = useMemo(
() => ({
emptyText: doneSearch ? "No matching objects" : "Search to see matching objects",
}),
[doneSearch],
);

return (
<Layout>
<Layout.Content style={LAYOUT_CONTENT_STYLE}>
<div style={SEARCH_CONTAINER_STYLE}>
<Input.Search
placeholder="Search DRS objects by name."
loading={loading || !drsUrl}
disabled={!drsUrl}
onChange={onSearch}
onSearch={onSearch}
size="large"
/>
</div>
<Table
rowKey="id"
columns={DRS_COLUMNS}
dataSource={searchResults}
loading={loading}
bordered={true}
expandedRowRender={({ id, description, checksums, access_methods: accessMethods, size }) => (
<div style={TABLE_NESTED_DESCRIPTIONS_STYLE} className="table-nested-ant-descriptions">
<Descriptions bordered={true}>
<Descriptions.Item label="ID" span={2}>
<span style={{ fontFamily: "monospace" }}>{id}</span>
</Descriptions.Item>
<Descriptions.Item label="Size" span={1}>
{filesize(size)}
</Descriptions.Item>
<Descriptions.Item label="Checksums" span={3}>
{checksums.map(({ type, checksum }) => (
<div
key={type}
style={{ display: "flex", gap: "0.8em", alignItems: "baseline" }}
>
<span style={{ fontWeight: "bold" }}>{type.toLocaleUpperCase()}:</span>
<span style={{ fontFamily: "monospace" }}>{checksum}</span>
</div>
))}
</Descriptions.Item>
<Descriptions.Item label="Access Methods" span={3}>
{accessMethods.map(({ type, access_url: url }, i) => (
<div key={i} style={{ display: "flex", gap: "0.8em", alignItems: "baseline" }}>
<span style={{ fontWeight: "bold" }}>{type.toLocaleUpperCase()}:</span>
<span style={{ fontFamily: "monospace" }}>
{type === "http" ? (
<a href={url?.url} target="_blank" rel="noreferrer">
{url?.url}
</a>
) : (
url?.url
)}
</span>
</div>
))}
</Descriptions.Item>
{description && (
<Descriptions.Item label="Description" span={3}>
{description}
</Descriptions.Item>
)}
</Descriptions.Item>
{description && <Descriptions.Item label="Description" span={3}>
{description}</Descriptions.Item>}
</Descriptions>
</div>
)}
locale={{
emptyText: doneSearch ? "No matching objects" : "Search to see matching objects",
}}
/>
</Layout.Content>
</Layout>;
</Descriptions>
</div>
)}
locale={tableLocale}
/>
</Layout.Content>
</Layout>
);
};

export default ManagerDRSContent;

0 comments on commit 67b1c03

Please sign in to comment.