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

Arborist UI integration #592

Merged
merged 13 commits into from
Oct 14, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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