Skip to content

Commit

Permalink
Arborist UI integration (#592)
Browse files Browse the repository at this point in the history
* feat(auth-mapping): Get auth mapping from arborist

* feat(auth-mapping): Don't pass username to /auth/mapping; no longer needed

* feat(auth-mapping): dispatch and reduce fetchUserAuthMapping

* feat(auth-mapping): add useArboristUI config var

* feat(auth-ui): change per-project submit data buttons per authz

* feat(auth-ui): toggle Recent Submissions table per authz

* feat(auth-ui): toggle topbar submit data button text per authz

* feat(auth-ui): toggle Map My Files card per authz

* feat(auth-ui): toggle form submission and upload file components per authz

* feat(auth-ui): toggle node browser delete button per authz

* feat(auth-ui): toggle intro submit data button text per authz

* feat(auth-ui): add css for project table submit buttons

* feat(auth-ui): rm console print
  • Loading branch information
vpsx authored Oct 14, 2019
1 parent d0fc466 commit d6ad331
Show file tree
Hide file tree
Showing 22 changed files with 264 additions and 51 deletions.
1 change: 1 addition & 0 deletions data/config/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -284,5 +284,6 @@
]
}
},
"useArboristUI": false,
"componentToResourceMapping": {}
}
4 changes: 2 additions & 2 deletions src/Homepage/reduxer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 2 additions & 3 deletions src/Index/page.jsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -24,7 +23,7 @@ class IndexPageComponent extends React.Component {
<div className='index-page'>
<div className='index-page__top'>
<div className='index-page__introduction'>
<Introduction data={components.index.introduction} dictIcons={dictIcons} />
<ReduxIntroduction data={components.index.introduction} dictIcons={dictIcons} />
<MediaQuery query={`(max-width: ${breakpoints.tablet}px)`}>
<ReduxIndexCounts />
</MediaQuery>
Expand Down
9 changes: 9 additions & 0 deletions src/Index/reduxer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = (() => {
Expand Down Expand Up @@ -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);
})();
1 change: 1 addition & 0 deletions src/Layout/reduxer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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),
});

Expand Down
49 changes: 37 additions & 12 deletions src/QueryNode/QueryNode.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 }),
Expand All @@ -20,7 +21,9 @@ const Entity = ({ value, project, onUpdatePopup, onStoreNodeInfo, tabindexStart
<span>{value.submitter_id}</span>
<a role='button' tabIndex={tabindexStart} className='query-node__button query-node__button--download' href={`${getSubmitPath(project)}/export?format=json&ids=${value.id}`}>Download</a>
<a role='button' tabIndex={tabindexStart + 1} className='query-node__button query-node__button--view' onClick={onView}>View</a>
<a role='button' tabIndex={tabindexStart + 2} className='query-node__button query-node__button--delete' onClick={onDelete}>Delete</a>
{
showDelete ? <a role='button' tabIndex={tabindexStart + 2} className='query-node__button query-node__button--delete' onClick={onDelete}>Delete</a> : null
}
</li>
);
};
Expand All @@ -31,6 +34,7 @@ Entity.propTypes = {
tabindexStart: PropTypes.number.isRequired,
onUpdatePopup: PropTypes.func,
onStoreNodeInfo: PropTypes.func,
showDelete: PropTypes.bool.isRequired,
};

Entity.defaultProps = {
Expand All @@ -40,7 +44,7 @@ Entity.defaultProps = {
onSearchFormSubmit: null,
};

const Entities = ({ value, project, onUpdatePopup, onStoreNodeInfo }) => (
const Entities = ({ value, project, onUpdatePopup, onStoreNodeInfo, showDelete }) => (
<ul>
{
value.map(
Expand All @@ -51,6 +55,7 @@ const Entities = ({ value, project, onUpdatePopup, onStoreNodeInfo }) => (
key={v.submitter_id}
value={v}
tabindexStart={i * 3}
showDelete={showDelete}
/>),
)
}
Expand All @@ -62,6 +67,7 @@ Entities.propTypes = {
project: PropTypes.string.isRequired,
onUpdatePopup: PropTypes.func,
onStoreNodeInfo: PropTypes.func,
showDelete: PropTypes.bool.isRequired,
};

Entities.defaultProps = {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -226,15 +242,23 @@ class QueryNode extends React.Component {
/>
<h4>most recent 20:</h4>
{ queryNodesList.map(
value => (<Entities
project={project}
onStoreNodeInfo={this.props.onStoreNodeInfo}
onUpdatePopup={this.props.onUpdatePopup}
node_type={value[0]}
key={value[0]}
value={value[1]}
/>
),
value => {
var showDelete = true
if (useArboristUI) {
showDelete = this.userHasDeleteOnProject()
}
return (
<Entities
project={project}
onStoreNodeInfo={this.props.onStoreNodeInfo}
onUpdatePopup={this.props.onUpdatePopup}
node_type={value[0]}
key={value[0]}
value={value[1]}
showDelete={showDelete}
/>
)
}
)
}
</div>
Expand All @@ -253,6 +277,7 @@ QueryNode.propTypes = {
onClearDeleteSession: PropTypes.func.isRequired,
onDeleteNode: PropTypes.func.isRequired,
onStoreNodeInfo: PropTypes.func.isRequired,
userAuthMapping: PropTypes.object.isRequired,
};

QueryNode.defaultProps = {
Expand Down
1 change: 1 addition & 0 deletions src/QueryNode/ReduxQueryNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ const mapStateToProps = (state, ownProps) => {
ownProps,
queryNodes: state.queryNodes,
popups: Object.assign({}, state.popups),
userAuthMapping: state.userAuthMapping,
};
return result;
};
Expand Down
8 changes: 6 additions & 2 deletions src/Submission/ProjectDashboard.jsx
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -13,7 +13,11 @@ class ProjectDashboard extends Component {
Data Submission
</div>
<ReduxSubmissionHeader />
<ProjectTable projectList={projectList} summaries={this.props.details} {...this.props} />
<ReduxProjectTable
projectList={projectList}
summaries={this.props.details}
{...this.props}
/>
</div>
);
}
Expand Down
24 changes: 22 additions & 2 deletions src/Submission/ProjectSubmission.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -28,14 +29,32 @@ const ProjectSubmission = (props) => {
return <MyDataModelGraph project={props.project} />;
};

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 (
<div className='project-submission'>
<h2 className='project-submission__title'>{props.project}</h2>
{
<Link className='project-submission__link' to={`/${props.project}/search`}>browse nodes</Link>
}
<MySubmitForm />
<MySubmitTSV project={props.project} />
{
(useArboristUI && !userHasCreateOrUpdateForThisProject()) ? null :
<MySubmitForm />
}
{
(useArboristUI && !userHasCreateOrUpdateForThisProject()) ? null :
<MySubmitTSV project={props.project} />
}
{ displayData() }
</div>
);
Expand All @@ -50,6 +69,7 @@ ProjectSubmission.propTypes = {
dataModelGraph: PropTypes.func,
onGetCounts: PropTypes.func.isRequired,
typeList: PropTypes.array,
userAuthMapping: PropTypes.object.isRequired,
};

ProjectSubmission.defaultProps = {
Expand Down
1 change: 1 addition & 0 deletions src/Submission/ReduxProjectSubmission.js
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ const ReduxProjectSubmission = (() => {
submitTSV: ReduxSubmitTSV,
dataModelGraph: ReduxDataModelGraph,
project: ownProps.params.project,
userAuthMapping: state.userAuthMapping,
});

const mapDispatchToProps = dispatch => ({
Expand Down
1 change: 1 addition & 0 deletions src/Submission/ReduxSubmissionHeader.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ const ReduxSubmissionHeader = (() => {
unmappedFileCount: state.submission.unmappedFileCount,
unmappedFileSize: state.submission.unmappedFileSize,
user: state.user,
userAuthMapping: state.userAuthMapping,
});

const mapDispatchToProps = dispatch => ({
Expand Down
48 changes: 30 additions & 18 deletions src/Submission/SubmissionHeader.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () => {
Expand All @@ -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);

Expand Down Expand Up @@ -52,27 +60,30 @@ class SubmissionHeader extends React.Component {
/>
</div>
</div>
<div className='submission-header__section'>
<div className='submission-header__section-image'>
<MapFilesSvg />
</div>
<div className='submission-header__section-info'>
<div className='h3-typo'>Map My Files</div>
<div className='h4-typo'>
{this.props.unmappedFileCount} files | {totalFileSize}
{
(useArboristUI && !this.userHasDataUpload()) ? null :
<div className='submission-header__section'>
<div className='submission-header__section-image'>
<MapFilesSvg />
</div>
<div className='body-typo'>
Mapping files to metadata in order to create medical meaning.
<div className='submission-header__section-info'>
<div className='h3-typo'>Map My Files</div>
<div className='h4-typo'>
{this.props.unmappedFileCount} files | {totalFileSize}
</div>
<div className='body-typo'>
Mapping files to metadata in order to create medical meaning.
</div>
<Button
onClick={() => { window.location.href = `${window.location.href}/files`; }}
className='submission-header__section-button'
label='Map My Files'
buttonType='primary'
enabled
/>
</div>
<Button
onClick={() => { window.location.href = `${window.location.href}/files`; }}
className='submission-header__section-button'
label='Map My Files'
buttonType='primary'
enabled
/>
</div>
</div>
}
</div>
);
}
Expand All @@ -83,6 +94,7 @@ SubmissionHeader.propTypes = {
unmappedFileCount: PropTypes.number,
fetchUnmappedFileStats: PropTypes.func.isRequired,
user: PropTypes.object.isRequired,
userAuthMapping: PropTypes.object.isRequired,
};

SubmissionHeader.defaultProps = {
Expand Down
28 changes: 28 additions & 0 deletions src/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
graphqlSchemaUrl,
useGuppyForExplorer,
authzPath,
authzMappingPath,
} from './configs';
import { config } from './params';
import sessionMonitor from './SessionMonitor';
Expand Down Expand Up @@ -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,
});
};
Loading

0 comments on commit d6ad331

Please sign in to comment.