)
+ }
);
@@ -76,12 +94,28 @@ class Question extends Component {
* Little quiz component - roperites: questionList, title, onSubmit
*/
class Quiz extends Component {
+ static propTypes = {
+ questionList: PropTypes.arrayOf(
+ PropTypes.shape(
+ {
+ question: PropTypes.string,
+ id: PropTypes.string,
+ options: PropTypes.arrayOf(PropTypes.string),
+ answer: PropTypes.number,
+ hint: PropTypes.string,
+ },
+ ),
+ ).isRequired,
+ onUpdateForm: PropTypes.func.isRequired,
+ onSubmitForm: PropTypes.func.isRequired,
+ title: PropTypes.string.isRequired,
+ };
+
constructor(props) {
super(props);
- this.state = { display_error: false };
- // required to be able to pass to child
- this.hideError = this.hideError.bind(this);
+ this.state = { displayError: false };
}
+
validateForm(values) {
// update redux store
if (Object.keys(values).length > 0) {
@@ -95,16 +129,20 @@ class Quiz extends Component {
}
// hide errors when user is updating the form
hideError() {
- this.setState({ display_error: false });
+ if (this.state.displayError) {
+ this.setState({ displayError: false });
+ }
}
// show errors when user hits submit button
showError() {
- this.setState({ display_error: true });
+ if (!this.state.displayError) {
+ this.setState({ displayError: true });
+ }
}
+
render() {
const { questionList, title } = this.props;
-
return (
{title}
@@ -121,10 +159,10 @@ class Quiz extends Component {
(item, i) =>
( this.hideError()}
index={i}
key={i}
- showErrors={this.state.display_error}
+ showErrors={this.state.displayError}
/>),
)
}
diff --git a/src/Certificate/ReduxQuiz.js b/src/Certificate/ReduxQuiz.js
index 59535ec022..82fa1052bc 100644
--- a/src/Certificate/ReduxQuiz.js
+++ b/src/Certificate/ReduxQuiz.js
@@ -1,24 +1,23 @@
import { connect } from 'react-redux';
import Quiz from './Quiz';
import { userapiPath } from '../configs';
-import browserHistory from '../history';
import { fetchWrapper } from '../actions';
/**
* Redux action triggered by quiz form update
* @method updateForm
- * @param {*} data
+ * @param {*} data
*/
export const updateForm = data => ({
type: 'UPDATE_CERTIFICATE_FORM',
data,
});
-export const receiveSubmitCert = ({ status }) => {
+export const receiveSubmitCert = ({ status }, history) => {
switch (status) {
case 201:
- browserHistory.push('/');
+ history.push('/');
return {
type: 'RECEIVE_CERT_SUBMIT',
};
@@ -31,12 +30,12 @@ export const receiveSubmitCert = ({ status }) => {
/**
* Redux action triggered by quiz submit
- * @param {*} data
- * @param {*} questionList
+ * @param {*} data
+ * @param {*} questionList
*/
-export const submitForm = (data, questionList) => fetchWrapper({
+export const submitForm = (data, questionList, history) => fetchWrapper({
path: `${userapiPath}/user/cert/security_quiz?extension=txt`,
- handler: receiveSubmitCert,
+ handler: (result) => { receiveSubmitCert(result, history); },
body: JSON.stringify({ answers: data, certificate_form: questionList }, null, '\t'),
method: 'PUT',
});
@@ -44,7 +43,7 @@ export const submitForm = (data, questionList) => fetchWrapper({
/**
* answer is the index of the correct option
- */
+ */
const questionList = [
{
question: 'As a registered user, I can:',
@@ -72,9 +71,10 @@ const mapStateToProps = state => ({
title,
});
-const mapDispatchToProps = dispatch => ({
+const mapDispatchToProps = (dispatch, ownProps) => ({
onUpdateForm: data => dispatch(updateForm(data)),
- onSubmitForm: data => dispatch(submitForm(data, questionList)),
+ // ownProps.history from react-router
+ onSubmitForm: data => dispatch(submitForm(data, questionList, ownProps.history)),
});
const ReduxQuiz = connect(mapStateToProps, mapDispatchToProps)(Quiz);
diff --git a/src/Certificate/reducers.js b/src/Certificate/reducers.js
index 6b3cbdb479..fafa446aa1 100644
--- a/src/Certificate/reducers.js
+++ b/src/Certificate/reducers.js
@@ -1,4 +1,4 @@
-export const certificate = (state = {}, action) => {
+const certificate = (state = {}, action) => {
switch (action.type) {
case 'UPDATE_CERTIFICATE_FORM':
return { ...state, certificate_result: action.data };
@@ -6,3 +6,5 @@ export const certificate = (state = {}, action) => {
return state;
}
};
+
+export default certificate;
diff --git a/src/DataDictionary/DataDictionary.jsx b/src/DataDictionary/DataDictionary.jsx
index 50b80ebe6b..2e57e01661 100644
--- a/src/DataDictionary/DataDictionary.jsx
+++ b/src/DataDictionary/DataDictionary.jsx
@@ -1,5 +1,5 @@
import React from 'react';
-import { Link } from 'react-router';
+import { Link } from 'react-router-dom';
import PropTypes from 'prop-types';
import { Table, TableData, TableRow, TableHead } from '../theme';
@@ -73,8 +73,8 @@ CategoryTable.propTypes = {
* Just exported for testing
* Little helper that extacts a mapping of category-name to
* the list of nodes in that category given a dictionary definition object
- *
- * @param {Object} dictionary
+ *
+ * @param {Object} dictionary
* @return {} mapping from category to node list
*/
export function category2NodeList(dictionary) {
@@ -96,8 +96,8 @@ export function category2NodeList(dictionary) {
/**
* Little components presents an overview of the types in a dictionary organized by category
- *
- * @param {dictionary} params
+ *
+ * @param {dictionary} params
*/
const DataDictionary = ({ dictionary }) => {
const c2nl = category2NodeList(dictionary);
diff --git a/src/DataDictionary/DataDictionary.test.jsx b/src/DataDictionary/DataDictionary.test.jsx
index b17c83cae7..6df8886c42 100644
--- a/src/DataDictionary/DataDictionary.test.jsx
+++ b/src/DataDictionary/DataDictionary.test.jsx
@@ -1,5 +1,7 @@
import React from 'react';
import { mount } from 'enzyme';
+import { StaticRouter } from 'react-router-dom';
+
import DataDictionary, { category2NodeList } from './DataDictionary';
describe('the DataDictionary component', () => {
@@ -45,7 +47,11 @@ describe('the DataDictionary component', () => {
});
it('renders category tables', () => {
- const ux = mount();
+ const ux = mount(
+
+
+ ,
+ );
expect(ux.find('table').length).toBe(2);
});
});
diff --git a/src/DataDictionary/DataDictionaryNode.jsx b/src/DataDictionary/DataDictionaryNode.jsx
index 6b2a5276f0..1abd8699de 100644
--- a/src/DataDictionary/DataDictionaryNode.jsx
+++ b/src/DataDictionary/DataDictionaryNode.jsx
@@ -1,6 +1,6 @@
import React from 'react';
import styled, { css } from 'styled-components';
-import { Link } from 'react-router';
+import { Link } from 'react-router-dom';
import PropTypes from 'prop-types';
import { Table, TableData, TableRow, TableHead, Bullet } from '../theme';
@@ -57,7 +57,7 @@ const LinkTable = ({ links }) => {
* @param {Object} property one of the properties of a dictionary node
* @return {String|Array} string for scalar types, array for enums
* and other listish types or 'UNDEFINED' if no
- * type information availabale
+ * type information availabale
*/
export const getType = (property) => {
let type = 'UNDEFINED';
@@ -116,7 +116,7 @@ const NodeTable = ({ node }) => (
(key) => {
const compoundKey = key.join(', ');
return {compoundKey};
- }
+ },
)
}
@@ -246,8 +246,8 @@ const DownloadButton = styled.a`
/**
* Component renders a view with details of a particular dictionary type (node - /dd/typename) or
* of the whole dictionary (/dd/graph).
- *
- * @param {*} param0
+ *
+ * @param {*} param0
*/
const DataDictionaryNode = ({ params, submission }) => {
const node = params.node;
@@ -301,12 +301,12 @@ const DataDictionaryNode = ({ params, submission }) => {
DataDictionaryNode.propTypes = {
params: PropTypes.shape({
- dictionary: PropTypes.object.isRequired,
node: PropTypes.string.isRequired,
}).isRequired,
submission: PropTypes.shape(
{
counts_search: PropTypes.objectOf(PropTypes.number),
+ dictionary: PropTypes.object.isRequired,
links_search: PropTypes.objectOf(PropTypes.number),
},
),
diff --git a/src/DataDictionary/DictionaryGraph.jsx b/src/DataDictionary/DictionaryGraph.jsx
index fede514602..5de393b54a 100644
--- a/src/DataDictionary/DictionaryGraph.jsx
+++ b/src/DataDictionary/DictionaryGraph.jsx
@@ -1,5 +1,5 @@
import React from 'react';
-import { Link } from 'react-router';
+import { Link } from 'react-router-dom';
import { assignNodePositions, createNodesAndEdges } from '../DataModelGraph/utils';
import { createFullGraph, createAbridgedGraph } from './GraphCreator';
@@ -63,7 +63,7 @@ class DictionaryGraph extends React.Component {
};
// Note: svg#data_model_graph is popuplated by createFull|AbridedGraph above
return (
-
+
Explore dictionary as a table
Bold, italicized properties are required
diff --git a/src/DataDictionary/DictionaryGraph.test.jsx b/src/DataDictionary/DictionaryGraph.test.jsx
index 443c3ee23a..581d8f914d 100644
--- a/src/DataDictionary/DictionaryGraph.test.jsx
+++ b/src/DataDictionary/DictionaryGraph.test.jsx
@@ -1,5 +1,6 @@
import React from 'react';
import { mount } from 'enzyme';
+import { StaticRouter } from 'react-router-dom';
import DictionaryGraph from './DictionaryGraph';
import { buildTestData } from '../DataModelGraph/testData';
@@ -10,21 +11,23 @@ describe('the DictionaryGraph', () => {
const data = buildTestData();
// Material-UI components require the Mui theme ...
const $dom = mount(
- ,
+
+
+ ,
);
- const $graph = $dom; // .find('DataModelGraph');
+ const $graph = $dom.find(DictionaryGraph);
return { ...data, $graph, $dom };
}
it('boots to a full view', () => {
const { $graph } = buildTest();
expect($graph.length).toBe(1);
- expect($graph.state('fullToggle')).toBe(true);
+ expect(!!$graph.find('div[data-toggle="full"]')).toBe(true);
});
it('toggles between full and compact views', () => {
@@ -32,7 +35,7 @@ describe('the DictionaryGraph', () => {
const $toggleButton = $dom.find('a#toggle_button');
expect($toggleButton.length).toBe(1);
$toggleButton.simulate('click');
- expect($graph.state('fullToggle')).toBe(false);
+ expect(!!$graph.find('div[data-toggle="abridged"]')).toBe(true);
expect(document.querySelector('#data_model_graph')).toBeDefined();
// jsdom does not yet support svg
// const ellipseList = document.querySelectorAll('ellipse');
diff --git a/src/DataDictionary/GraphCreator.js b/src/DataDictionary/GraphCreator.js
index bf754d75d1..164c1c652e 100644
--- a/src/DataDictionary/GraphCreator.js
+++ b/src/DataDictionary/GraphCreator.js
@@ -12,7 +12,7 @@ const d3 = {
/**
* createDDGraph: Creates a Data Dictionary graph (rectangular nodes).
- * Needs position as property of each node (as fraction of 1 e.g. [0.5, 0.1]
+ * Needs position as property of each node (as fraction of 1 e.g. [0.5, 0.1]
* for placement at (0.5*svgWidth, 0.1*svgHeight))
*/
function createDDGraph(nodesIn, edges, radius = 60, boxHeightMult, boxWidthMult, svgHeightMult) {
@@ -148,7 +148,7 @@ function createDDGraph(nodesIn, edges, radius = 60, boxHeightMult, boxWidthMult,
}
/**
- * formatField: Recurisvely inserts newline characters into strings that are
+ * formatField: Recurisvely inserts newline characters into strings that are
* too long after underscores
*/
function formatField(name) {
@@ -171,8 +171,8 @@ function formatField(name) {
return name;
}
-/**
- * formatType: Turn different ways used to represent type in data dictionary
+/**
+ * formatType: Turn different ways used to represent type in data dictionary
* into a string
*/
function formatType(type) {
diff --git a/src/DataModelGraph/DataModelGraph.test.jsx b/src/DataModelGraph/DataModelGraph.test.jsx
index 9638dd14da..3748f31c9a 100644
--- a/src/DataModelGraph/DataModelGraph.test.jsx
+++ b/src/DataModelGraph/DataModelGraph.test.jsx
@@ -38,7 +38,7 @@ describe('the DataModelGraph', () => {
expect($graph.state('fullToggle')).toBe(true);
expect(document.querySelector('#data_model_graph')).toBeDefined();
// Not sure why this doesn't work ...?
- // Could be jsdom does not support svg properly.
+ // Could be jsdom does not support svg properly.
// expect(d3.selectAll('ellipse').size()).toBe(nodes.length);
});
diff --git a/src/DataModelGraph/ReduxDataModelGraph.js b/src/DataModelGraph/ReduxDataModelGraph.js
index 1663af739a..381be1e067 100644
--- a/src/DataModelGraph/ReduxDataModelGraph.js
+++ b/src/DataModelGraph/ReduxDataModelGraph.js
@@ -11,9 +11,9 @@ export const clearCounts = {
/**
- * Compose and send a single graphql query to get a count of how
+ * Compose and send a single graphql query to get a count of how
* many of each node and edge are in the current state
- *
+ *
* @method getCounts
* @param {Array} typeList
* @param {string} project
@@ -46,7 +46,7 @@ export const getCounts = (typeList, project, dictionary) => {
// Add links to query
Object.keys(dictionary).filter(
name => (!name.startsWith('_' && dictionary[name].links)),
- ).reduce( // extract links from each node
+ ).reduce( // extract links from each node
(linkList, name) => {
const node = dictionary[name];
const newLinks = node.links;
diff --git a/src/DataModelGraph/SvgGraph.jsx b/src/DataModelGraph/SvgGraph.jsx
index 2aac8333e6..01845dfbee 100644
--- a/src/DataModelGraph/SvgGraph.jsx
+++ b/src/DataModelGraph/SvgGraph.jsx
@@ -11,12 +11,12 @@ const d3 = {
};
/**
- * createSvgGraph: builds an SVG graph (oval nodes) in the SVG DOM
+ * createSvgGraph: builds an SVG graph (oval nodes) in the SVG DOM
* node with selector: svg#data_model_graph.
- * Needs position as property of each node (as fraction of 1 e.g. [0.5, 0.1]
+ * Needs position as property of each node (as fraction of 1 e.g. [0.5, 0.1]
* for placement at (0.5*svg_width, 0.1*svg_height))
* Side effect - decorates each node in 'nodes' with a 'position' property
- *
+ *
* @param nodes
* @param edges
*/
diff --git a/src/DataModelGraph/utils.js b/src/DataModelGraph/utils.js
index b948e3956d..3ceca0f811 100644
--- a/src/DataModelGraph/utils.js
+++ b/src/DataModelGraph/utils.js
@@ -4,10 +4,10 @@
* and edges, returns the nodes and edges in correct format
*
* @method createNodesAndEdges
- * @param props: Object (normally taken from redux state) that includes dictionary
- * property defining the dictionary as well as other optional properties
+ * @param props: Object (normally taken from redux state) that includes dictionary
+ * property defining the dictionary as well as other optional properties
* such as counts_search and links_search (created by getCounts) with
- * information about the number of each type (node) and link (between
+ * information about the number of each type (node) and link (between
* nodes with a link's source and target types) that actually
* exist in the data
* @param createAll: Include all nodes and edges or only those that are populated in
@@ -40,7 +40,7 @@ export function createNodesAndEdges(props, createAll, nodesToHide = ['program'])
const edges = nodes.filter(
node => node.links && node.links.length > 0,
- ).reduce( // add each node's links to the edge list
+ ).reduce( // add each node's links to the edge list
(list, node) => {
const newLinks = node.links.map(
link => ({ source: node.name, target: link.target_type, exists: 1, ...link }),
@@ -62,12 +62,12 @@ export function createNodesAndEdges(props, createAll, nodesToHide = ['program'])
return result;
}, [],
).filter(
- // target type exist and is not in hide list
+ // target type exist and is not in hide list
link => (link.target && nameToNode[link.target] && !hideDb[link.target]),
)
.map(
(link) => {
- // decorate each link with its "exists" count if available
+ // decorate each link with its "exists" count if available
// (number of instances of link between source and target types in the data)
link.exists = props.links_search ? props.links_search[`${link.source}_${link.name}_to_${link.target}_link`] : undefined;
return link;
@@ -95,7 +95,7 @@ export function createNodesAndEdges(props, createAll, nodesToHide = ['program'])
export function findRoot(nodes, edges) {
const couldBeRoot = edges.reduce(
(db, edge) => {
- // At some point the d3 force layout converts
+ // At some point the d3 force layout converts
// edge.source and edge.target into node references ...
const sourceName = typeof edge.source === 'object' ? edge.source.name : edge.source;
if (db[sourceName]) {
@@ -103,7 +103,7 @@ export function findRoot(nodes, edges) {
}
return db;
},
- // initialize emptyDb - any node could be the root
+ // initialize emptyDb - any node could be the root
nodes.reduce((emptyDb, node) => { emptyDb[node.name] = true; return emptyDb; }, {}),
);
const rootNode = nodes.find(n => couldBeRoot[n.name]);
@@ -114,13 +114,13 @@ export function findRoot(nodes, edges) {
* Arrange nodes in dictionary graph breadth first, and build level database.
* If a node links to multiple parents, then place it under the highest parent ...
* Exported for testing.
- *
- * @param {Array} nodes
+ *
+ * @param {Array} nodes
* @param {Array} edges
- * @return { nodesBreadthFirst, treeLevel2Names, name2Level } where
+ * @return { nodesBreadthFirst, treeLevel2Names, name2Level } where
* nodesBreadthFirst is array of node names, and
- * treeLevel2Names is an array of arrays of node names,
- * and name2Level is a mapping of node name to level
+ * treeLevel2Names is an array of arrays of node names,
+ * and name2Level is a mapping of node name to level
*/
export function nodesBreadthFirst(nodes, edges) {
const result = {
@@ -132,7 +132,7 @@ export function nodesBreadthFirst(nodes, edges) {
// mapping of node name to edges that point into that node
const name2EdgesIn = edges.reduce(
(db, edge) => {
- // At some point the d3 force layout converts edge.source
+ // At some point the d3 force layout converts edge.source
// and edge.target into node references ...
const targetName = typeof edge.target === 'object' ? edge.target.name : edge.target;
if (db[targetName]) {
@@ -142,7 +142,7 @@ export function nodesBreadthFirst(nodes, edges) {
}
return db;
},
- // initialize emptyDb - include nodes that have no incoming edges (leaves)
+ // initialize emptyDb - include nodes that have no incoming edges (leaves)
nodes.reduce((emptyDb, node) => { emptyDb[node.name] = []; return emptyDb; }, {}),
);
@@ -163,7 +163,7 @@ export function nodesBreadthFirst(nodes, edges) {
}
// queue.shift is O(n), so just keep pushing, and move the head
- for (let head = 0; head < queue.length; head++) {
+ for (let head = 0; head < queue.length; head += 1) {
const { query, level } = queue[head]; // breadth first
result.bfOrder.push(query);
processedNodes.add(query);
@@ -174,7 +174,7 @@ export function nodesBreadthFirst(nodes, edges) {
result.name2Level[query] = level;
name2EdgesIn[query].forEach(
(edge) => {
- // At some point the d3 force layout converts edge.source
+ // At some point the d3 force layout converts edge.source
// and edge.target into node references ...
const sourceName = typeof edge.source === 'object' ? edge.source.name : edge.source;
if (name2EdgesIn[sourceName]) {
@@ -200,13 +200,13 @@ export function nodesBreadthFirst(nodes, edges) {
/**
* Decorate the nodes of a graph with a position based on the node's position in the graph
* Exported for testing. Decorates nodes with position property array [x,y] on a [0,1) space
- *
+ *
* @method assignNodePositions
* @param nodes
* @param edges
- * @param opts {breadthFirstInfo,numPerRow} breadthFirstInfo is output
+ * @param opts {breadthFirstInfo,numPerRow} breadthFirstInfo is output
* from nodesBreadthFirst - otherwise call it ourselves,
- * numPerRow specifies number of nodes per row if we want a
+ * numPerRow specifies number of nodes per row if we want a
* grid under the root rather than the tree structure
*/
export function assignNodePositions(nodes, edges, opts) {
diff --git a/src/Explorer/ExplorerComponent.jsx b/src/Explorer/ExplorerComponent.jsx
index b69ecd7ac7..66206ee77b 100644
--- a/src/Explorer/ExplorerComponent.jsx
+++ b/src/Explorer/ExplorerComponent.jsx
@@ -1,17 +1,16 @@
import React, { Component } from 'react';
import { createRefetchContainer } from 'react-relay';
-import { withAuthTimeout, withBoxAndNav, computeLastPageSizes } from '../utils';
+import { computeLastPageSizes } from '../utils';
import { GQLHelper } from '../gqlHelper';
import { getReduxStore } from '../reduxStore';
import { ReduxExplorerTabPanel, ReduxSideBar } from './ReduxExplorer';
-import { BodyBackground } from './style';
const gqlHelper = GQLHelper.getGQLHelper();
class ExplorerComponent extends Component {
/**
* Subscribe to Redux updates at mount time
- */
+ */
componentWillMount() {
getReduxStore().then(
(store) => {
@@ -145,11 +144,11 @@ class ExplorerComponent extends Component {
render() {
this.updateFilesMap();
- const flexBox = {
+ const style = {
display: 'flex',
};
return (
-
+
@@ -158,7 +157,7 @@ class ExplorerComponent extends Component {
}
export const RelayExplorerComponent = createRefetchContainer(
- withBoxAndNav(withAuthTimeout(ExplorerComponent), BodyBackground),
+ ExplorerComponent,
{
viewer: gqlHelper.explorerPageFragment,
},
diff --git a/src/Explorer/ExplorerSideBar.jsx b/src/Explorer/ExplorerSideBar.jsx
index bebfc92b5b..086f9eb75f 100644
--- a/src/Explorer/ExplorerSideBar.jsx
+++ b/src/Explorer/ExplorerSideBar.jsx
@@ -55,21 +55,21 @@ class ExplorerSideBar extends Component {
listItems={projects}
title="Projects"
selectedItems={this.props.selectedFilters.projects}
- group_name="projects"
+ groupName="projects"
onChange={state => this.props.onChange({ ...this.props.selectedFilters, ...state })}
/>
this.props.onChange({ ...this.props.selectedFilters, ...state })}
/>
this.props.onChange({ ...this.props.selectedFilters, ...state })}
/>
diff --git a/src/Explorer/ExplorerTable.jsx b/src/Explorer/ExplorerTable.jsx
index 0efb19ddf9..34f10b651f 100644
--- a/src/Explorer/ExplorerTable.jsx
+++ b/src/Explorer/ExplorerTable.jsx
@@ -1,6 +1,6 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
-import { Link } from 'react-router';
+import { Link } from 'react-router-dom';
import styled from 'styled-components';
import { TableRow, TableHead } from '../theme';
import { TableData, TableHeadCell,
diff --git a/src/Explorer/ReduxExplorer.test.jsx b/src/Explorer/ReduxExplorer.test.jsx
index bd72fe9130..5f53c1e205 100644
--- a/src/Explorer/ReduxExplorer.test.jsx
+++ b/src/Explorer/ReduxExplorer.test.jsx
@@ -3,6 +3,7 @@ import { mount } from 'enzyme';
import { Provider } from 'react-redux';
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
import { ThemeProvider } from 'styled-components';
+import { StaticRouter } from 'react-router-dom';
import { getReduxStore } from '../reduxStore';
import { theme } from '../theme';
@@ -18,7 +19,9 @@ function renderComponent(ComponentClass, props) {
-
+
+
+ ,
diff --git a/src/Explorer/style.js b/src/Explorer/style.js
index dbfc2ca057..53ad51de90 100644
--- a/src/Explorer/style.js
+++ b/src/Explorer/style.js
@@ -114,8 +114,6 @@ export const ExplorerTabBox = styled.div`
display:${props => (props.active ? 'block' : 'none')};
`;
-export const BodyBackground = '#ecebeb';
-
export const ExplorerTableRow = styled.tr`
${TableRow};
overflow: visible;
diff --git a/src/GraphQLEditor/ReduxGqlEditor.js b/src/GraphQLEditor/ReduxGqlEditor.js
index 644e744342..046ed9b67e 100644
--- a/src/GraphQLEditor/ReduxGqlEditor.js
+++ b/src/GraphQLEditor/ReduxGqlEditor.js
@@ -1,30 +1,8 @@
import { connect } from 'react-redux';
-import { graphqlSchemaUrl } from '../localconf';
-import { fetchJsonOrText, connectionError } from '../actions';
import GqlEditor from './GqlEditor';
-/**
- * Fetch the schema for graphi, and stuff it into redux -
- * handled by router
- */
-export const fetchSchema = dispatch => fetchJsonOrText({ path: graphqlSchemaUrl, dispatch })
- .then(
- ({ status, data }) => {
- switch (status) {
- case 200:
- return dispatch(
- {
- type: 'RECEIVE_SCHEMA_LOGIN',
- schema: data,
- },
- );
- }
- },
- );
-
-
const mapStateToProps = state => ({
schema: state.graphiql.schema,
});
diff --git a/src/Homepage/AmbiHomepage.jsx b/src/Homepage/AmbiHomepage.jsx
index 7f58de61ea..4d5ea34255 100644
--- a/src/Homepage/AmbiHomepage.jsx
+++ b/src/Homepage/AmbiHomepage.jsx
@@ -1,7 +1,6 @@
import React from 'react';
import { RelayProjectDashboard } from './RelayHomepage';
import ReduxProjectDashboard from './ReduxProjectDashboard';
-import { withAuthTimeout, withBoxAndNav } from '../utils';
import { getReduxStore } from '../reduxStore';
@@ -43,7 +42,7 @@ class AmbidextrousDashboard extends React.Component {
/**
* Ambidextrous homepage
*/
-const AmbiHomepage = withBoxAndNav(withAuthTimeout(AmbidextrousDashboard));
+const AmbiHomepage = AmbidextrousDashboard;
export default AmbiHomepage;
diff --git a/src/Homepage/ProjectBarChart.jsx b/src/Homepage/ProjectBarChart.jsx
index 228f9db0bc..d6eee14b54 100644
--- a/src/Homepage/ProjectBarChart.jsx
+++ b/src/Homepage/ProjectBarChart.jsx
@@ -2,7 +2,7 @@ import { ResponsiveContainer, Legend, Tooltip, BarChart, Bar, XAxis, YAxis } fro
import styled from 'styled-components';
import PropTypes from 'prop-types'; // see https://github.com/facebook/prop-types#prop-types
import React from 'react';
-import { browserHistory } from 'react-router';
+import { browserHistory } from 'react-router-dom';
import Translator from './translate';
@@ -20,7 +20,7 @@ const FloatBox = styled.div`
/**
* Component shows stacked-bars - one stacked-bar for each project in props.projectList -
* where experiments are stacked on top of cases. projectList looks like:
- *
+ *
* const data = [
* {name: 'bpa-test', experimentCount: 4000, caseCount: 2400, aliquotCount: 2400},
* ...
diff --git a/src/Homepage/ProjectDashboard.jsx b/src/Homepage/ProjectDashboard.jsx
index abef17b448..9250509180 100644
--- a/src/Homepage/ProjectDashboard.jsx
+++ b/src/Homepage/ProjectDashboard.jsx
@@ -91,15 +91,15 @@ class CountCard extends React.Component {
* props { caseCount, experimnentCount, fileCount, aliquoteCount, projectList
* }
* where
- *
- * const projectList = [
+ *
+ * const projectList = [
* {name: 'bpa-test', experiments: 4000, cases: 2400, amt: 2400},
* {name: 'ProjectB', experiments: 3000, cases: 1398, amt: 2210},
* {name: 'ProjectC', experiments: 2000, cases: 9800, amt: 2290},
* {name: 'ProjectD', experiments: 2780, cases: 3908, amt: 2000},
* {name: 'ProjectE', experiments: 1890, cases: 4800, amt: 2181},
* {name: 'ProjectRye', experiments: 2390, cases: 3800, amt: 2500},
- *
+ *
* ];
*/
export class LittleProjectDashboard extends React.Component {
diff --git a/src/Homepage/ProjectTable.jsx b/src/Homepage/ProjectTable.jsx
index f66a3b25d2..e73e91c1e3 100644
--- a/src/Homepage/ProjectTable.jsx
+++ b/src/Homepage/ProjectTable.jsx
@@ -1,6 +1,6 @@
import React from 'react';
import FlatButton from 'material-ui/FlatButton';
-import { Link } from 'react-router';
+import { Link } from 'react-router-dom';
import styled from 'styled-components';
import { TableBarColor } from '../theme';
import Translator from './translate';
@@ -90,9 +90,9 @@ export class ProjectTR extends React.Component {
*/
/**
- * Table of projects.
+ * Table of projects.
* Has projectList property where each entry has the properties
- * for a project detail, and a summaryCounts property with
+ * for a project detail, and a summaryCounts property with
* prefetched totals (property details may be fetched lazily via Relay, whatever ...)
*/
export class ProjectTable extends React.Component {
diff --git a/src/Homepage/ProjectTable.test.jsx b/src/Homepage/ProjectTable.test.jsx
index c3e1767fac..fbbee45084 100644
--- a/src/Homepage/ProjectTable.test.jsx
+++ b/src/Homepage/ProjectTable.test.jsx
@@ -1,7 +1,9 @@
import React from 'react';
-import { ProjectTable } from './ProjectTable';
import { mount } from 'enzyme';
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
+import { StaticRouter } from 'react-router-dom';
+
+import { ProjectTable } from './ProjectTable';
test('Project table renders', () => {
@@ -22,7 +24,9 @@ test('Project table renders', () => {
// Material-UI components require the Mui theme ...
const table = mount(
-
+
+
+ ,
);
console.log(`ProjectTable looks like this: ${table.html()}`);
diff --git a/src/Homepage/ReduxProjectBarChart.js b/src/Homepage/ReduxProjectBarChart.js
index 21a957b1c1..88e2250ed7 100644
--- a/src/Homepage/ReduxProjectBarChart.js
+++ b/src/Homepage/ReduxProjectBarChart.js
@@ -5,7 +5,7 @@ import Translator from './translate';
const tor = Translator.getTranslator();
-// Map state.homepage.projectsByName to projectList
+// Map state.homepage.projectsByName to projectList
const mapStateToProps = (state) => {
if (state.homepage && state.homepage.projectsByName) {
const projectList = Object.values(state.homepage.projectsByName)
diff --git a/src/Homepage/ReduxProjectDashboard.js b/src/Homepage/ReduxProjectDashboard.js
index 2f43cd4d37..cbd775c293 100644
--- a/src/Homepage/ReduxProjectDashboard.js
+++ b/src/Homepage/ReduxProjectDashboard.js
@@ -3,7 +3,7 @@ import { DashboardWith } from './ProjectDashboard';
import { ProjectTable } from './ProjectTable';
-// Map state.homepage.projectsByName to projectList
+// Map state.homepage.projectsByName to projectList
const mapStateToProps = (state) => {
if (state.homepage && state.homepage.projectsByName) {
const projectList = Object.values(state.homepage.projectsByName);
diff --git a/src/Homepage/RelayHomepage.jsx b/src/Homepage/RelayHomepage.jsx
index 31c68a1cc1..969af90988 100644
--- a/src/Homepage/RelayHomepage.jsx
+++ b/src/Homepage/RelayHomepage.jsx
@@ -3,7 +3,6 @@ import { QueryRenderer } from 'react-relay';
import environment from '../environment';
import { DashboardWith } from './ProjectDashboard';
import { RelayProjectTable } from './RelayProjectTable';
-import { withAuthTimeout, withBoxAndNav } from '../utils';
import { GQLHelper } from '../gqlHelper';
import { getReduxStore } from '../reduxStore';
import Spinner from '../components/Spinner';
@@ -17,7 +16,7 @@ const DashboardWithRelayTable = DashboardWith(RelayProjectTable);
* Relay modern QueryRenderer rendered ProjectDashboard.
* Note - this is exported to support testing - it's really an module-private class
* that corrdinates data-collection on the homepage.
- *
+ *
* @see https://medium.com/@ven_korolyov/relay-modern-refetch-container-c886296448c7
* @see https://facebook.github.io/relay/docs/query-renderer.html
*/
@@ -27,8 +26,8 @@ export class RelayProjectDashboard extends React.Component {
* The ReduxProjectBarChart renders a graph with project-details
* as data flows into redux from Relay (RelayProjectTable supplements
* redux with per-project details).
- *
- * @param {Array} projectList
+ *
+ * @param {Array} projectList
*/
static async updateRedux({ projectList, summaryCounts }) {
// Update redux store if data is not already there
@@ -51,7 +50,7 @@ export class RelayProjectDashboard extends React.Component {
/**
* Translate relay properties to {summaryCounts, projectList} structure
* that is friendly to underlying components.
- *
+ *
* @param relayProps
* @return {projectList, summaryCounts}
*/
@@ -107,7 +106,7 @@ export class RelayProjectDashboard extends React.Component {
}
-const RelayHomepage = withBoxAndNav(withAuthTimeout(RelayProjectDashboard));
+const RelayHomepage = RelayProjectDashboard;
/**
diff --git a/src/Homepage/RelayProjectTable.jsx b/src/Homepage/RelayProjectTable.jsx
index dab656ed53..0b02f870ed 100644
--- a/src/Homepage/RelayProjectTable.jsx
+++ b/src/Homepage/RelayProjectTable.jsx
@@ -18,13 +18,13 @@ const gqlHelper = GQLHelper.getGQLHelper();
* Not a normal relay fragment container.
* Overrides rowRender in ProjectTable parent class to fetch row data via Relay QueryRender.
* Assumes higher level container injects the original undetailed list of projects.
- *
+ *
*/
export class RelayProjectTable extends ProjectTable {
/**
* Overrides rowRender in ProjectTable parent class to fetch row data via Relay QueryRender.
- *
- * @param {Object} proj
+ *
+ * @param {Object} proj
*/
rowRender(proj) {
return ( {
let next = basename;
- if (Object.keys(props.location.query).length !== 0) {
- next = basename === '/' ? props.location.query.next : basename + props.location.query.next;
+ const location = props.location; // this is the react-router "location"
+ const queryParams = querystring.parse(location.search ? location.search.replace(/^\?+/, '') : '');
+ if (queryParams.next) {
+ next = basename === '/' ? queryParams.next : basename + queryParams.next;
}
return (
{appname}
- {login.title}
+ {login.title}
diff --git a/src/Login/ProtectedContent.jsx b/src/Login/ProtectedContent.jsx
new file mode 100644
index 0000000000..8693afbe3e
--- /dev/null
+++ b/src/Login/ProtectedContent.jsx
@@ -0,0 +1,284 @@
+import React from 'react';
+import { Redirect } from 'react-router-dom';
+import styled from 'styled-components';
+import PropTypes from 'prop-types';
+
+import { fetchUser, fetchOAuthURL, fetchJsonOrText, fetchProjects } from '../actions';
+import Spinner from '../components/Spinner';
+import { getReduxStore } from '../reduxStore';
+import { requiredCerts, submissionApiOauthPath } from '../configs';
+import ReduxAuthTimeoutPopup from '../Popup/ReduxAuthTimeoutPopup';
+
+let lastAuthMs = 0;
+let lastTokenRefreshMs = 0;
+
+const Body = styled.div`
+ background: ${props => props.background};
+ padding: ${props => props.padding || '50px 100px'};
+`;
+
+/**
+ * Redux listener - just clears auth-cache on logout
+ */
+export function logoutListener(state = {}, action) {
+ switch (action.type) {
+ case 'RECEIVE_API_LOGOUT':
+ lastAuthMs = 0;
+ lastTokenRefreshMs = 0;
+ break;
+ default: // noop
+ }
+ return state;
+}
+
+/**
+ * Avoid importing underscore just for this ... export for testing
+ * @method intersection
+ * @param aList {Array}
+ * @param bList {Array}
+ * @return list of intersecting elements
+ */
+export function intersection(aList, bList) {
+ const key2Count = aList.concat(bList).reduce(
+ (db, it) => { if (db[it]) { db[it] += 1; } else { db[it] = 1; } return db; },
+ {},
+ );
+ return Object.entries(key2Count)
+ .filter(kv => kv[1] > 1)
+ .map(([k]) => k);
+}
+
+/**
+ * Container for components that require authentication to access.
+ * Takes a few properties
+ * @param component required child component
+ * @param location from react-router
+ * @param history from react-router
+ * @param match from react-router.match
+ * @param public default false - set true to disable auth-guard
+ * @param background passed through to wrapper for page-level background
+ * @param filter {() => Promise} optional filter to apply before rendering the child component
+ */
+class ProtectedContent extends React.Component {
+ static propTypes = {
+ component: PropTypes.func.isRequired,
+ location: PropTypes.object.isRequired,
+ history: PropTypes.object.isRequired,
+ match: PropTypes.shape(
+ {
+ params: PropTypes.object,
+ path: PropTypes.string,
+ },
+ ).isRequired,
+ public: PropTypes.bool,
+ background: PropTypes.string,
+ filter: PropTypes.func,
+ };
+
+ static defaultProps = {
+ public: false,
+ background: null,
+ filter: () => Promise.resolve('ok'),
+ };
+
+ constructor(props, context) {
+ super(props, context);
+ this.state = {
+ authenticated: false,
+ redirectTo: null,
+ };
+ }
+
+
+ /**
+ * We start out in an unauthenticatd state - after mount do
+ * the checks to see if the current session is authenticated
+ * in the various ways we want it to be.
+ */
+ componentDidMount() {
+ if (!this.props.public) {
+ getReduxStore().then(
+ store =>
+ Promise.all(
+ [
+ store.dispatch({ type: 'CLEAR_COUNTS' }), // clear some counters
+ store.dispatch({ type: 'CLEAR_QUERY_NODES' }),
+ ],
+ ).then(
+ () => this.checkLoginStatus(store)
+ .then(newState => this.checkQuizStatus(newState))
+ .then(newState => this.checkApiToken(store, newState)),
+ ).then(
+ (newState) => {
+ const filterPromise = (newState.authenticated && typeof this.props.filter === 'function') ? this.props.filter() : Promise.resolve('ok');
+ const finish = () => this.setState(newState); // finally update the component state
+ return filterPromise.then(finish, finish);
+ },
+ ),
+ );
+ }
+ }
+
+ /**
+ * Start filter the 'newState' for the checkLoginStatus component.
+ * Check if the user is logged in, and update state accordingly.
+ * @method checkLoginStatus
+ * @param {ReduxStore} store
+ * @return Promise<{redirectTo, authenticated, user}>
+ */
+ checkLoginStatus = (store) => {
+ const nowMs = Date.now();
+ const newState = {
+ authenticated: true,
+ redirectTo: null,
+ user: store.getState().user,
+ };
+
+ if (nowMs - lastAuthMs < 60000) {
+ // assume we're still logged in after 1 minute ...
+ return Promise.resolve(newState);
+ }
+
+ return store.dispatch(fetchUser) // make an API call to see if we're still logged in ...
+ .then(
+ () => {
+ const { user } = store.getState();
+ newState.user = user;
+ if (!user.username) { // not authenticated
+ newState.redirectTo = '/login';
+ newState.authenticated = false;
+ } else { // auth ok - cache it
+ lastAuthMs = Date.now();
+ }
+ return newState;
+ },
+ );
+ };
+
+ /**
+ * Filter refreshes the gdc-api token (acquired via oauth with user-api) if necessary.
+ * @method checkApiToken
+ * @param store Redux store
+ * @param initialState
+ * @return newState passed through
+ */
+ checkApiToken = (store, initialState) => {
+ const nowMs = Date.now();
+ const newState = Object.assign({}, initialState);
+
+ if (!newState.authenticated) {
+ return Promise.resolve(newState);
+ }
+ if (nowMs - lastTokenRefreshMs < 41000) {
+ return Promise.resolve(newState);
+ }
+ return store.dispatch(fetchProjects())
+ .then(() => {
+ //
+ // The assumption here is that fetchProjects either succeeds or fails.
+ // If it fails (we won't have any project data), then we need to refresh our api token ...
+ //
+ const projects = store.getState().submission.projects;
+ if (projects) {
+ // user already has a valid token
+ return Promise.resolve(newState);
+ }
+ // else do the oauth dance
+ return store.dispatch(fetchOAuthURL(submissionApiOauthPath))
+ .then(
+ oauthUrl => fetchJsonOrText({ path: oauthUrl, dispatch: store.dispatch.bind(store) }))
+ .then(
+ ({ status, data }) => {
+ switch (status) {
+ case 200:
+ return {
+ type: 'RECEIVE_SUBMISSION_LOGIN',
+ result: true,
+ };
+ default: {
+ return {
+ type: 'RECEIVE_SUBMISSION_LOGIN',
+ result: false,
+ error: data,
+ };
+ }
+ }
+ },
+ )
+ .then(
+ msg => store.dispatch(msg),
+ )
+ .then(
+ // refetch the projects - since the earlier call failed with an invalid token ...
+ () => store.dispatch(fetchProjects()),
+ )
+ .then(
+ () => {
+ lastTokenRefreshMs = Date.now();
+ return newState;
+ },
+ () => {
+ // something went wront - better just re-login
+ newState.authenticated = false;
+ newState.redirectTo = '/login';
+ return newState;
+ },
+ );
+ });
+ };
+
+ /**
+ * Filter the 'newState' for the ProtectedComponent.
+ * User needs to take a security quiz before he/she can acquire tokens ...
+ * something like that
+ */
+ checkQuizStatus = (initialState) => {
+ const newState = Object.assign(initialState);
+
+ if (!(newState.authenticated && newState.user && newState.user.username)) {
+ return newState; // NOOP for unauthenticated session
+ }
+ const { user } = newState;
+ // user is authenticated - now check if he has certs
+ const isMissingCerts =
+ intersection(requiredCerts, user.certificates_uploaded).length !== requiredCerts.length;
+ // take quiz if this user doesn't have required certificate
+ if (this.props.match.path !== '/quiz' && isMissingCerts) {
+ newState.redirectTo = '/quiz';
+ // do not update lastAuthMs (indicates time of last successful auth)
+ } else if (this.props.match.path === '/quiz' && !isMissingCerts) {
+ newState.redirectTo = '/';
+ }
+ return newState;
+ };
+
+ render() {
+ const Component = this.props.component;
+ let params = {}; // router params
+ if (this.props.match) {
+ params = this.props.match.params || {};
+ }
+
+ window.scrollTo(0, 0);
+ if (this.state.redirectTo) {
+ return ();
+ } else if (this.props.public) {
+ return (
+
+
+
+ );
+ } else if (this.state.authenticated) {
+ return (
+
+
+
+
+ );
+ }
+ return ();
+ }
+}
+
+export default ProtectedContent;
+
diff --git a/src/Login/ProtectedContent.test.jsx b/src/Login/ProtectedContent.test.jsx
new file mode 100644
index 0000000000..33ca1c0b5b
--- /dev/null
+++ b/src/Login/ProtectedContent.test.jsx
@@ -0,0 +1,7 @@
+import { intersection } from './ProtectedContent';
+
+describe('the ProtectedContent container', () => {
+ it('can compute the intersection of 2 lists', () => {
+ expect(intersection(['a', 'b', 'c', 'd', 'e'], ['c', 'd', 'e', 'f', 'g'])).toEqual(['c', 'd', 'e']);
+ });
+});
diff --git a/src/Popup/ReduxAuthTimeoutPopup.jsx b/src/Popup/ReduxAuthTimeoutPopup.jsx
index 3fe1abfdc0..54043b9821 100644
--- a/src/Popup/ReduxAuthTimeoutPopup.jsx
+++ b/src/Popup/ReduxAuthTimeoutPopup.jsx
@@ -1,30 +1,39 @@
import React from 'react';
import { connect } from 'react-redux';
-import { browserHistory } from 'react-router';
+import { withRouter } from 'react-router-dom';
import Popup from './Popup';
+const goToLogin = (history) => {
+ history.push('/login');
+ // Refresh the page.
+ window.location.reload(false);
+};
+
+
+const AuthPopup = withRouter(
+ ({ history }) =>
+ ( { goToLogin(history); }}
+ />),
+);
+
const timeoutPopupMapState = state => ({
- auth_popup: state.popups.authPopup,
+ authPopup: state.popups.authPopup,
});
const timeoutPopupMapDispatch = () => ({});
-const goToLogin = () => {
- browserHistory.push('/login');
- // Refresh the page.
- window.location.reload(false);
-};
const ReduxAuthTimeoutPopup = connect(timeoutPopupMapState, timeoutPopupMapDispatch)(
({ authPopup }) => {
if (authPopup) {
- return ;
+ return ();
}
- return (null);
+ return null;
},
);
-
export default ReduxAuthTimeoutPopup;
-
diff --git a/src/QueryNode/QueryNode.jsx b/src/QueryNode/QueryNode.jsx
index 16d0749559..88c992e787 100644
--- a/src/QueryNode/QueryNode.jsx
+++ b/src/QueryNode/QueryNode.jsx
@@ -1,8 +1,8 @@
import React from 'react';
-import { Link } from 'react-router';
+import { Link } from 'react-router-dom';
import styled, { css } from 'styled-components';
-import { reduxForm } from 'redux-form';
import Select from 'react-select';
+import PropTypes from 'prop-types';
import { jsonToString, getSubmitPath } from '../utils';
import Popup from '../Popup/Popup';
@@ -65,6 +65,10 @@ const Dropdown = styled(Select)`
`;
class QueryForm extends React.Component {
+ static propTypes = {
+ project: PropTypes.string.isRequired,
+ };
+
constructor(props) {
super(props);
this.state = {
@@ -78,16 +82,16 @@ class QueryForm extends React.Component {
event.preventDefault();
const form = event.target;
const data = { project: this.props.project };
- const query_param = [];
+ const queryParam = [];
- for (let i = 0; i < form.length; i++) {
+ for (let i = 0; i < form.length; i += 1) {
const input = form[i];
if (input.name && input.value) {
- query_param.push(`${input.name}=${input.value}`);
+ queryParam.push(`${input.name}=${input.value}`);
data[input.name] = input.value;
}
}
- const url = `/${this.props.project}/search?${query_param.join('&')}`;
+ const url = `/${this.props.project}/search?${queryParam.join('&')}`;
this.props.onSearchFormSubmit(data, url);
}
@@ -99,8 +103,8 @@ class QueryForm extends React.Component {
}
render() {
- const nodes_for_query = this.props.nodeTypes.filter(nt => !['program', 'project'].includes(nt));
- const options = nodes_for_query.map(node_type => ({ value: node_type, label: node_type }));
+ const nodesForQuery = this.props.nodeTypes.filter(nt => !['program', 'project'].includes(nt));
+ const options = nodesForQuery.map(nodeType => ({ value: nodeType, label: nodeType }));
const state = this.state || {};
return (