diff --git a/data/config/default.json b/data/config/default.json index 26e6cca406..98c3b06ddd 100644 --- a/data/config/default.json +++ b/data/config/default.json @@ -284,5 +284,6 @@ ] } }, + "useArboristUI": false, "componentToResourceMapping": {} } diff --git a/src/Homepage/reduxer.js b/src/Homepage/reduxer.js index 7e8f2d4493..8bee0bdc6a 100644 --- a/src/Homepage/reduxer.js +++ b/src/Homepage/reduxer.js @@ -34,10 +34,10 @@ export const ReduxProjectDashboard = (() => { export const ReduxTransaction = (() => { const mapStateToProps = (state) => { if (state.homepage && state.homepage.transactions) { - return { log: state.homepage.transactions }; + return { log: state.homepage.transactions, userAuthMapping: state.userAuthMapping }; } - return { log: [] }; + return { log: [], userAuthMapping: state.userAuthMapping }; }; // Table does not dispatch anything diff --git a/src/Index/page.jsx b/src/Index/page.jsx index 3ec83d2161..801a04a7d9 100644 --- a/src/Index/page.jsx +++ b/src/Index/page.jsx @@ -1,8 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import MediaQuery from 'react-responsive'; -import Introduction from '../components/Introduction'; -import { ReduxIndexButtonBar, ReduxIndexBarChart, ReduxIndexCounts } from './reduxer'; +import { ReduxIndexButtonBar, ReduxIndexBarChart, ReduxIndexCounts, ReduxIntroduction } from './reduxer'; import dictIcons from '../img/icons'; import { components } from '../params'; import getProjectNodeCounts from './utils'; @@ -24,7 +23,7 @@ class IndexPageComponent extends React.Component {
- + diff --git a/src/Index/reduxer.jsx b/src/Index/reduxer.jsx index 1dc3c19609..4fb90ba292 100644 --- a/src/Index/reduxer.jsx +++ b/src/Index/reduxer.jsx @@ -5,6 +5,7 @@ import { setActive } from '../Layout/reduxer'; import IndexBarChart from '../components/charts/IndexBarChart/.'; import IndexCounts from '../components/cards/IndexCounts/.'; import IndexButtonBar from '../components/IndexButtonBar'; +import Introduction from '../components/Introduction'; import { components } from '../params'; export const ReduxIndexBarChart = (() => { @@ -61,3 +62,11 @@ export const ReduxIndexButtonBar = (() => { return connect(mapStateToProps, mapDispatchToProps)(IndexButtonBar); })(); + +export const ReduxIntroduction = (() => { + const mapStateToProps = state => ({ + userAuthMapping: state.userAuthMapping, + }); + + return connect(mapStateToProps)(Introduction); +})(); diff --git a/src/Layout/reduxer.js b/src/Layout/reduxer.js index a2a5517a33..a970d71ce1 100644 --- a/src/Layout/reduxer.js +++ b/src/Layout/reduxer.js @@ -43,6 +43,7 @@ export const ReduxTopBar = (() => { topItems: components.topBar.items, activeTab: state.bar.active, user: state.user, + userAuthMapping: state.userAuthMapping, isFullWidth: isPageFullScreen(state.bar.active), }); diff --git a/src/QueryNode/QueryNode.jsx b/src/QueryNode/QueryNode.jsx index 536f9532ee..90ba155671 100644 --- a/src/QueryNode/QueryNode.jsx +++ b/src/QueryNode/QueryNode.jsx @@ -5,8 +5,9 @@ import { jsonToString, getSubmitPath } from '../utils'; import Popup from '../components/Popup'; import QueryForm from './QueryForm'; import './QueryNode.less'; +import { useArboristUI } from '../configs'; -const Entity = ({ value, project, onUpdatePopup, onStoreNodeInfo, tabindexStart }) => { +const Entity = ({ value, project, onUpdatePopup, onStoreNodeInfo, tabindexStart, showDelete }) => { const onDelete = () => { onStoreNodeInfo({ project, id: value.id }).then( () => onUpdatePopup({ nodedelete_popup: true }), @@ -20,7 +21,9 @@ const Entity = ({ value, project, onUpdatePopup, onStoreNodeInfo, tabindexStart {value.submitter_id} Download View - Delete + { + showDelete ? Delete : null + } ); }; @@ -31,6 +34,7 @@ Entity.propTypes = { tabindexStart: PropTypes.number.isRequired, onUpdatePopup: PropTypes.func, onStoreNodeInfo: PropTypes.func, + showDelete: PropTypes.bool.isRequired, }; Entity.defaultProps = { @@ -40,7 +44,7 @@ Entity.defaultProps = { onSearchFormSubmit: null, }; -const Entities = ({ value, project, onUpdatePopup, onStoreNodeInfo }) => ( +const Entities = ({ value, project, onUpdatePopup, onStoreNodeInfo, showDelete }) => (
    { value.map( @@ -51,6 +55,7 @@ const Entities = ({ value, project, onUpdatePopup, onStoreNodeInfo }) => ( key={v.submitter_id} value={v} tabindexStart={i * 3} + showDelete={showDelete} />), ) } @@ -62,6 +67,7 @@ Entities.propTypes = { project: PropTypes.string.isRequired, onUpdatePopup: PropTypes.func, onStoreNodeInfo: PropTypes.func, + showDelete: PropTypes.bool.isRequired, }; Entities.defaultProps = { @@ -188,6 +194,16 @@ class QueryNode extends React.Component { return popup; } + userHasDeleteOnProject = () => { + var split = this.props.params.project.split('-'); + var program = split[0] + var project = split.slice(1).join('-') + var resourcePath = ["/programs", program, "projects", project].join('/') + var actions = this.props.userAuthMapping[resourcePath] + + return actions !== undefined && actions.some(x => x["method"] === "delete") + } + render() { const queryNodesList = this.props.queryNodes.search_status === 'succeed: 200' ? Object.entries(this.props.queryNodes.search_result.data) @@ -226,15 +242,23 @@ class QueryNode extends React.Component { />

    most recent 20:

    { queryNodesList.map( - value => ( - ), + value => { + var showDelete = true + if (useArboristUI) { + showDelete = this.userHasDeleteOnProject() + } + return ( + + ) + } ) }
@@ -253,6 +277,7 @@ QueryNode.propTypes = { onClearDeleteSession: PropTypes.func.isRequired, onDeleteNode: PropTypes.func.isRequired, onStoreNodeInfo: PropTypes.func.isRequired, + userAuthMapping: PropTypes.object.isRequired, }; QueryNode.defaultProps = { diff --git a/src/QueryNode/ReduxQueryNode.js b/src/QueryNode/ReduxQueryNode.js index 06ba39f0ba..e5e1441d56 100644 --- a/src/QueryNode/ReduxQueryNode.js +++ b/src/QueryNode/ReduxQueryNode.js @@ -117,6 +117,7 @@ const mapStateToProps = (state, ownProps) => { ownProps, queryNodes: state.queryNodes, popups: Object.assign({}, state.popups), + userAuthMapping: state.userAuthMapping, }; return result; }; diff --git a/src/Submission/ProjectDashboard.jsx b/src/Submission/ProjectDashboard.jsx index 542a47ca18..d58f50c567 100644 --- a/src/Submission/ProjectDashboard.jsx +++ b/src/Submission/ProjectDashboard.jsx @@ -1,6 +1,6 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import ProjectTable from '../components/tables/ProjectTable'; +import ReduxProjectTable from '../components/tables/reduxer'; import ReduxSubmissionHeader from './ReduxSubmissionHeader'; import './ProjectDashboard.less'; @@ -13,7 +13,11 @@ class ProjectDashboard extends Component { Data Submission
- +
); } diff --git a/src/Submission/ProjectSubmission.jsx b/src/Submission/ProjectSubmission.jsx index a40824550c..b73cb27b49 100644 --- a/src/Submission/ProjectSubmission.jsx +++ b/src/Submission/ProjectSubmission.jsx @@ -6,6 +6,7 @@ import DataModelGraph from '../DataModelGraph/DataModelGraph'; import SubmitForm from './SubmitForm'; import Spinner from '../components/Spinner'; import './ProjectSubmission.less'; +import { useArboristUI } from '../configs'; const ProjectSubmission = (props) => { // hack to detect if dictionary data is available, and to trigger fetch if not @@ -28,14 +29,32 @@ const ProjectSubmission = (props) => { return ; }; + const userHasCreateOrUpdateForThisProject = () => { + const actionHasCreateOrUpdate = x => { return x['method'] === 'create' || x['method'] === 'update' } + + var split = props.project.split('-'); + var program = split[0] + var project = split.slice(1).join('-') + var resourcePath = ["/programs", program, "projects", project].join('/') + + var resource = props.userAuthMapping[resourcePath] + return resource !== undefined && resource.some(actionHasCreateOrUpdate) + } + return (

{props.project}

{ browse nodes } - - + { + (useArboristUI && !userHasCreateOrUpdateForThisProject()) ? null : + + } + { + (useArboristUI && !userHasCreateOrUpdateForThisProject()) ? null : + + } { displayData() }
); @@ -50,6 +69,7 @@ ProjectSubmission.propTypes = { dataModelGraph: PropTypes.func, onGetCounts: PropTypes.func.isRequired, typeList: PropTypes.array, + userAuthMapping: PropTypes.object.isRequired, }; ProjectSubmission.defaultProps = { diff --git a/src/Submission/ReduxProjectSubmission.js b/src/Submission/ReduxProjectSubmission.js index cf767e34b8..b978f163ee 100644 --- a/src/Submission/ReduxProjectSubmission.js +++ b/src/Submission/ReduxProjectSubmission.js @@ -145,6 +145,7 @@ const ReduxProjectSubmission = (() => { submitTSV: ReduxSubmitTSV, dataModelGraph: ReduxDataModelGraph, project: ownProps.params.project, + userAuthMapping: state.userAuthMapping, }); const mapDispatchToProps = dispatch => ({ diff --git a/src/Submission/ReduxSubmissionHeader.js b/src/Submission/ReduxSubmissionHeader.js index 87b0b0306f..5e4797f915 100644 --- a/src/Submission/ReduxSubmissionHeader.js +++ b/src/Submission/ReduxSubmissionHeader.js @@ -47,6 +47,7 @@ const ReduxSubmissionHeader = (() => { unmappedFileCount: state.submission.unmappedFileCount, unmappedFileSize: state.submission.unmappedFileSize, user: state.user, + userAuthMapping: state.userAuthMapping, }); const mapDispatchToProps = dispatch => ({ diff --git a/src/Submission/SubmissionHeader.jsx b/src/Submission/SubmissionHeader.jsx index f018e08cf7..130c428b72 100644 --- a/src/Submission/SubmissionHeader.jsx +++ b/src/Submission/SubmissionHeader.jsx @@ -5,6 +5,7 @@ import Gen3ClientSvg from '../img/gen3client.svg'; import MapFilesSvg from '../img/mapfiles.svg'; import { humanFileSize } from '../utils.js'; import './SubmissionHeader.less'; +import { useArboristUI } from '../configs'; class SubmissionHeader extends React.Component { componentDidMount = () => { @@ -19,6 +20,13 @@ class SubmissionHeader extends React.Component { window.open('https://gen3.org/resources/user/gen3-client/', '_blank'); } + userHasDataUpload = () => { + //data_upload policy is resource data_file, method file_upload, service fence + const actionIsFileUpload = x => { return x['method'] === 'file_upload' && x['service'] === 'fence' } + var resource = this.props.userAuthMapping['/data_file'] + return resource !== undefined && resource.some(actionIsFileUpload) + } + render() { const totalFileSize = humanFileSize(this.props.unmappedFileSize); @@ -52,27 +60,30 @@ class SubmissionHeader extends React.Component { /> -
-
- -
-
-
Map My Files
-
- {this.props.unmappedFileCount} files | {totalFileSize} + { + (useArboristUI && !this.userHasDataUpload()) ? null : +
+
+
-
- Mapping files to metadata in order to create medical meaning. +
+
Map My Files
+
+ {this.props.unmappedFileCount} files | {totalFileSize} +
+
+ Mapping files to metadata in order to create medical meaning. +
+
-
-
+ }
); } @@ -83,6 +94,7 @@ SubmissionHeader.propTypes = { unmappedFileCount: PropTypes.number, fetchUnmappedFileStats: PropTypes.func.isRequired, user: PropTypes.object.isRequired, + userAuthMapping: PropTypes.object.isRequired, }; SubmissionHeader.defaultProps = { diff --git a/src/actions.js b/src/actions.js index f1b5de8b25..a442218cd5 100644 --- a/src/actions.js +++ b/src/actions.js @@ -12,6 +12,7 @@ import { graphqlSchemaUrl, useGuppyForExplorer, authzPath, + authzMappingPath, } from './configs'; import { config } from './params'; import sessionMonitor from './SessionMonitor'; @@ -453,3 +454,30 @@ export const fetchUserAccess = async (dispatch) => { data: userAccess, }); }; + +// asks arborist for the user's auth mapping if Arborist UI enabled +export const fetchUserAuthMapping = async (dispatch) => { + if (!config.useArboristUI) { + return; + } + + // Arborist will get the username from the jwt + const authMapping = await fetch( + `${authzMappingPath}`, + ).then((fetchRes) => { + switch (fetchRes.status) { + case 200: + return fetchRes.json(); + default: + // This is dispatched on app init and on user login. + // Could be not logged in -> no username -> 404; this is ok + // There may be plans to update Arborist to return anonymous access when username not found + return {}; + } + }); + + dispatch({ + type: 'RECEIVE_USER_AUTH_MAPPING', + data: authMapping, + }); +}; diff --git a/src/components/Introduction.jsx b/src/components/Introduction.jsx index 3f78345378..9e27178394 100644 --- a/src/components/Introduction.jsx +++ b/src/components/Introduction.jsx @@ -2,6 +2,7 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import IconicLink from './buttons/IconicLink'; import './Introduction.less'; +import { useArboristUI } from '../configs'; class Introduction extends Component { static propTypes = { @@ -9,7 +10,24 @@ class Introduction extends Component { dictIcons: PropTypes.object.isRequired, }; + userHasCreateForAnyProject = () => { + const actionHasCreate = x => { return (x["method"] === "create") } + //array of arrays of { service: x, method: y } + var actionArrays = Object.values(this.props.userAuthMapping) + var hasCreate = actionArrays.some(x => { return x.some(actionHasCreate) }) + return hasCreate + } + render() { + var buttonText = 'Submit Data' + if (useArboristUI) { + if (this.userHasCreateForAnyProject()) { + buttonText = 'Submit/Browse Data' + } else { + buttonText = 'Browse Data' + } + } + return (
{this.props.data.heading}
@@ -20,11 +38,15 @@ class Introduction extends Component { className='introduction__icon' icon='upload' iconColor='#' - caption='Submit Data' + caption={buttonText} />
); } } +Introduction.propTypes = { + userAuthMapping: PropTypes.object.isRequired, +}; + export default Introduction; diff --git a/src/components/layout/TopBar.jsx b/src/components/layout/TopBar.jsx index eb5e595525..f050243ad0 100644 --- a/src/components/layout/TopBar.jsx +++ b/src/components/layout/TopBar.jsx @@ -3,6 +3,7 @@ import { Link } from 'react-router-dom'; import PropTypes from 'prop-types'; import TopIconButton from './TopIconButton'; import './TopBar.less'; +import { useArboristUI } from '../../configs'; /** * NavBar renders row of nav-items of form { name, icon, link } @@ -10,6 +11,14 @@ import './TopBar.less'; class TopBar extends Component { isActive = id => this.props.activeTab === id; + userHasCreateForAnyProject = () => { + const actionHasCreate = x => { return (x["method"] === "create") } + //array of arrays of { service: x, method: y } + var actionArrays = Object.values(this.props.userAuthMapping) + var hasCreate = actionArrays.some(x => { return x.some(actionHasCreate) }) + return hasCreate + } + render() { return (
@@ -17,8 +26,16 @@ class TopBar extends Component {