From a3181dd5347c43ab661c5e973ba7407ebe33cc88 Mon Sep 17 00:00:00 2001 From: David Masselink Date: Sat, 22 Dec 2018 16:13:13 -0600 Subject: [PATCH 01/15] extend-graph-navigation - initial extension of graph/graphcontainer comps to use redux store/action flow --- app/Dockerfile | 5 +- app/src/actions/actionTypes.js | 1 + app/src/actions/entityActions.js | 55 ++++++++ app/src/actions/queryActions.js | 4 +- app/src/components/graph/Graph.js | 28 +++- app/src/components/graph/GraphContainer.js | 62 ++++++--- app/src/components/home/Home.js | 4 +- app/src/reducers/entityReducer.js | 30 +++++ app/src/reducers/initialState.js | 6 +- .../services/{apiService.js => ApiService.js} | 14 +- .../{httpService.js => HttpService.js} | 0 app/src/utils/graph.js | 127 +++++++++--------- 12 files changed, 233 insertions(+), 103 deletions(-) rename app/src/services/{apiService.js => ApiService.js} (56%) rename app/src/services/{httpService.js => HttpService.js} (100%) diff --git a/app/Dockerfile b/app/Dockerfile index 265c433..f6d153c 100644 --- a/app/Dockerfile +++ b/app/Dockerfile @@ -1,10 +1,11 @@ FROM node:8.12.0 as build-stage WORKDIR /app -#initially copy over just package.json (and locks, if present) for dependency installation step +#initially copy over just package.json (and locks, if present) for dependency installation COPY package*.json /app/ RUN yarn install -#then copy over everything, including our code, for the build step +#then copy over our code for test+build COPY ./ /app/ +#RUN yarn test RUN yarn build #once the build-stage is complete, we can build an image for our deployment diff --git a/app/src/actions/actionTypes.js b/app/src/actions/actionTypes.js index d6593c5..51dade3 100644 --- a/app/src/actions/actionTypes.js +++ b/app/src/actions/actionTypes.js @@ -2,5 +2,6 @@ export const CHANGE_QUERY = 'CHANGE_QUERY'; export const SUBMIT_QUERY = 'SUBMIT_QUERY'; export const FETCH_QUERY = 'FETCH_QUERY'; export const RECEIVE_QUERY = 'RECEIVE_QUERY'; +export const ADD_ENTITY_WATCH = 'ADD_ENTITY_WATCH'; export const FETCH_ENTITY = 'FETCH_ENTITY'; export const RECEIVE_ENTITY = 'RECEIVE_ENTITY'; \ No newline at end of file diff --git a/app/src/actions/entityActions.js b/app/src/actions/entityActions.js index e69de29..28bb7ec 100644 --- a/app/src/actions/entityActions.js +++ b/app/src/actions/entityActions.js @@ -0,0 +1,55 @@ +import * as types from './actionTypes'; +import ApiService from "../services/ApiService"; + +export const addEntityWatch = uid => ({ + type: types.ADD_ENTITY_WATCH, + uid +}); + +export const fetchEntity = uid => { + return dispatch => { + return ApiService.getEntity(uid) + .then(handleResponse) + .then(json => dispatch(receiveEntity(json[0])));//index into JSON arr to grab single entity object + }; +}; + +export const receiveEntity = json => ({ + type: types.RECEIVE_ENTITY, + results: json +}); + +function handleResponse(json) { + let results = []; + let existingUids = {}; + for (let objKey in json){ + let objArr = json[objKey]; + if (objArr.length) { + objArr.forEach(obj => { + //screen out duplicate UID entries + if(!existingUids[obj.uid]){ + results.push(obj); + existingUids[obj.uid] = true; + } + }); + } + } + return results; +} + +//only update state if the objects fail lodash equality check AND +// //the component is still mounted. usually, the lifecycle methods should +// //be used directly for such things that, but in testing we're getting +// //intermittent errors that setState is being called on unmounted +// //components, without this check +// if(!_.isEqual(this.state.data, json) && this._isMounted) { +// this.setState({ +// data: json, +// waitingOnReq: false +// }); + +//logic here to transform API raw data to something usable by graph, or does that make more sense in actions? + +//also trigger periodic re-requests of data here with knowledge of period, max reqs/sec, num objs tracked? +//no, I think it still makes sense to do that in a component, probably GraphContainer, where we easily have access to +//store to see num of requests needed and can compute interval ms based on limit and num entities diff --git a/app/src/actions/queryActions.js b/app/src/actions/queryActions.js index a64c6ba..faf5b31 100644 --- a/app/src/actions/queryActions.js +++ b/app/src/actions/queryActions.js @@ -1,5 +1,5 @@ import * as types from './actionTypes'; -import {apiService} from "../services/apiService"; +import ApiService from "../services/ApiService"; export const changeQuery = str => ({ type: types.CHANGE_QUERY, @@ -10,7 +10,7 @@ export const submitQuery = () => ({type: types.SUBMIT_QUERY}); export function fetchQuery(query) { return dispatch => { - return apiService.getKeyword(query) + return ApiService.getKeyword(query) .then(handleResponse) .then(json => dispatch(receiveQuery(json))); }; diff --git a/app/src/components/graph/Graph.js b/app/src/components/graph/Graph.js index 5e3609c..6c0fec4 100644 --- a/app/src/components/graph/Graph.js +++ b/app/src/components/graph/Graph.js @@ -1,14 +1,18 @@ import React, { Component } from 'react'; +import { connect } from "react-redux"; +import { bindActionCreators } from "redux"; import { withRouter } from 'react-router-dom'; import _ from 'lodash'; import vis from 'vis'; import { Grid } from '@material-ui/core'; -import {options} from "../../config/visjsConfig"; -import {getVisData, clearVisData, colorMixer, getLegends} from "../../utils/graph"; -import {NodeStatusPulseColors} from "../../config/appConfig"; +import * as entityActions from "../../actions/entityActions"; +import { options } from "../../config/visjsConfig"; +import { getVisData, clearVisData, colorMixer, getLegends } from "../../utils/graph"; +import { NodeStatusPulseColors } from "../../config/appConfig"; import './Graph.css'; + class Graph extends Component { constructor(props) { super(props); @@ -65,6 +69,7 @@ class Graph extends Component { } render() { + //TODO: this is a bit awkward to have to explicitly call a render "helper" function, ideally anything critical would either be done directly in render() or earlier in lifecycle methods, etc. this.renderGraphExpanded(this.state.data); return (
@@ -210,10 +215,10 @@ class Graph extends Component { //configure custom behaviors for network object configNetwork(network) { - network.on("doubleClick", params => { + network.on("doubleClick", element => { //ensure the double click was on a graph node - if (params.nodes.length > 0) { - const targetNodeUid = params.nodes[0]; + if (element.nodes.length > 0) { + const targetNodeUid = element.nodes[0]; const pathComponents = this.props.location.pathname.split('/'); const currentNodeUid = pathComponents[pathComponents.length - 1]; //only update props if target node is not current node @@ -226,4 +231,13 @@ class Graph extends Component { } } -export default withRouter(Graph); \ No newline at end of file +const mapStoreToProps = store => ({entity: store.entity}); + +const mapDispatchToProps = dispatch => ({ + entityActions: bindActionCreators(entityActions, dispatch) +}); + +export default connect( + mapStoreToProps, + mapDispatchToProps +)(withRouter(Graph)); \ No newline at end of file diff --git a/app/src/components/graph/GraphContainer.js b/app/src/components/graph/GraphContainer.js index 06f2261..565cee8 100644 --- a/app/src/components/graph/GraphContainer.js +++ b/app/src/components/graph/GraphContainer.js @@ -1,4 +1,6 @@ import React, { Component } from 'react'; +import {connect} from 'react-redux'; +import {bindActionCreators} from 'redux'; import PropTypes from 'prop-types'; import { withRouter } from 'react-router-dom'; import _ from 'lodash'; @@ -7,9 +9,11 @@ import Grid from '@material-ui/core/Grid'; import CircularProgress from '@material-ui/core/CircularProgress'; import EntityDetails from '../entityDetails/EntityDetails'; -import {apiService} from "../../services/apiService"; +import ApiService from "../../services/ApiService"; import Graph from './Graph'; import "./Graph.css"; +import * as entityActions from "../../actions/entityActions"; + const DATA_FETCH_PERIOD_MS = 5000; @@ -66,20 +70,25 @@ class GraphContainer extends Component { const pathComponents = this.props.location.pathname.split('/'); const uidParam = pathComponents[pathComponents.length - 1]; - apiService.getEntity(uidParam) - .then(json => { - //only update state if the objects fail lodash equality check AND - //the component is still mounted. usually, the lifecycle methods should - //be used directly for such things that, but in testing we're getting - //intermittent errors that setState is being called on unmounted - //components, without this check - if(!_.isEqual(this.state.data, json) && this._isMounted) { - this.setState({ - data: json, - waitingOnReq: false - }); - } - }); + //TODO:DM - adding to watch will be redundant, but the UIDs set will enforce uniqueness, should I instead check here? + this.props.entityActions.addEntityWatch(uidParam); + this.props.entityActions.fetchEntity(uidParam); + + //TODO:DM - will I need to do something similar to the use of _isMounted still, for testing? + // ApiService.getEntity(uidParam) + // .then(json => { + // //only update state if the objects fail lodash equality check AND + // //the component is still mounted. usually, the lifecycle methods should + // //be used directly for such things that, but in testing we're getting + // //intermittent errors that setState is being called on unmounted + // //components, without this check + // if(!_.isEqual(this.state.data, json) && this._isMounted) { + // this.setState({ + // data: json, + // waitingOnReq: false + // }); + // } + // }); }; render() { @@ -95,14 +104,14 @@ class GraphContainer extends Component { } - +
- ); + );//get rid of [0] idx ^ } } @@ -110,4 +119,21 @@ GraphContainer.propTypes = { classes: PropTypes.object.isRequired, }; -export default withRouter(withStyles(styles)(GraphContainer)); \ No newline at end of file +const mapStoreToProps = store => ({entity: store.entity}); + +const mapDispatchToProps = dispatch => ({ + entityActions: bindActionCreators(entityActions, dispatch) +}); + +export default connect( + mapStoreToProps, + mapDispatchToProps +)( + withRouter( + withStyles(styles)(GraphContainer) + ) +); + +//also trigger periodic re-requests of data here with knowledge of period, max reqs/sec, num objs tracked? +//no, I think it still makes sense to do that in a component, probably GraphContainer, where we easily have access to +//store to see num of requests needed and can compute interval ms based on limit and num entities \ No newline at end of file diff --git a/app/src/components/home/Home.js b/app/src/components/home/Home.js index 3daf3c9..a7ba8b2 100644 --- a/app/src/components/home/Home.js +++ b/app/src/components/home/Home.js @@ -83,14 +83,14 @@ Home.propTypes = { query: PropTypes.object }; -const mapStateToProps = state => ({query: state.query}); +const mapStoreToProps = store => ({query: store.query}); const mapDispatchToProps = dispatch => ({ queryActions: bindActionCreators(queryActions, dispatch) }); export default connect( - mapStateToProps, + mapStoreToProps, mapDispatchToProps )( withStyles(styles)( diff --git a/app/src/reducers/entityReducer.js b/app/src/reducers/entityReducer.js index e69de29..2caa21d 100644 --- a/app/src/reducers/entityReducer.js +++ b/app/src/reducers/entityReducer.js @@ -0,0 +1,30 @@ +import initialState from './initialState'; +import {ADD_ENTITY_WATCH, FETCH_ENTITY, RECEIVE_ENTITY} from '../actions/actionTypes'; + +export default function entity(state = initialState.entity, action) { + let newState, now; + switch (action.type) { + case ADD_ENTITY_WATCH: + //start with a copy of existing state + newState = { ...state }; + //extend the uidsObj which is otherwise difficult to with variable key + //name action.uid in statement above + newState.uidsObj[action.uid] = true; + return newState; + case FETCH_ENTITY: + return action; + case RECEIVE_ENTITY: + now = +new Date(); + //project changes on top of existing state attrs + newState = { ...state, + //results: action.results, // how to add to existing results? + latestTimestamp: now, + }; + //TODO:DM - technically I could use just 1 obj to maintain UIDs to watch as well as results, just make null until new obj arrives {val: null, timestamp: ...} + newState.results[action.results.uid] = action.results; + return newState; + default: + return state; + } +} + diff --git a/app/src/reducers/initialState.js b/app/src/reducers/initialState.js index 1540c91..aeaa06c 100644 --- a/app/src/reducers/initialState.js +++ b/app/src/reducers/initialState.js @@ -6,5 +6,9 @@ export default { isWaiting: false, results: [] }, - entity: {} + entity: { + uidsObj: {}, + results: {}, + latestTimestamp: 0 + } }; \ No newline at end of file diff --git a/app/src/services/apiService.js b/app/src/services/ApiService.js similarity index 56% rename from app/src/services/apiService.js rename to app/src/services/ApiService.js index 1cb1a87..5c5db04 100644 --- a/app/src/services/apiService.js +++ b/app/src/services/ApiService.js @@ -1,27 +1,31 @@ -import {HttpService} from './httpService'; +import {HttpService} from './HttpService'; const ALL_SERVICE_CONTEXT = '/v1'; const QUERY_SERVICE_PATH = '/query'; const ENTITY_SERVICE_PATH = '/entity/uid/'; -export class apiService { +export default class ApiService { static getKeyword(input) { const params = { keyword: input }; + //load env provided URL at query time to allow conf.js to load it in time + //in testing const ALL_SERVICE_URL = window.envConfig.KATLAS_API_URL; - return request_helper(ALL_SERVICE_URL + ALL_SERVICE_CONTEXT + + return requestHelper(ALL_SERVICE_URL + ALL_SERVICE_CONTEXT + QUERY_SERVICE_PATH, params); } static getEntity(uid) { + //load env provided URL at query time to allow conf.js to load it in time + //in testing const ALL_SERVICE_URL = window.envConfig.KATLAS_API_URL; - return request_helper(ALL_SERVICE_URL + ALL_SERVICE_CONTEXT + + return requestHelper(ALL_SERVICE_URL + ALL_SERVICE_CONTEXT + ENTITY_SERVICE_PATH + uid); } } -const request_helper = (url, params) => { +const requestHelper = (url, params) => { return HttpService.get({ url, params diff --git a/app/src/services/httpService.js b/app/src/services/HttpService.js similarity index 100% rename from app/src/services/httpService.js rename to app/src/services/HttpService.js diff --git a/app/src/utils/graph.js b/app/src/utils/graph.js index 4673b71..c175b9f 100644 --- a/app/src/utils/graph.js +++ b/app/src/utils/graph.js @@ -37,28 +37,25 @@ let edges, nodes; let legendTypesObj = {}; let legendStatusesObj = {}; -function parseDgraphJson(jsonData) { - if (jsonData === null || jsonData === undefined ) { - console.error("Empty json data"); +function parseDgraphData(data) { + if (data === null || data === undefined ) { + console.error("Empty data"); return; } - for (let outerObj of jsonData){ - //TODO:DM - clean up anon fn dependent on closure for outerObj - _.forOwn(outerObj, (innerObj, key) => { - let uid = ""; - if (key === uidPropNameDgraph) { - uid = innerObj; - //Store entire block in map, so we can use to create edges later - //Edges cannot be created here as we may not have got the uid still - //As json from dgraph has randomized order and uid may be after Array elements - uidMap.set(uid, outerObj); - } - if (EdgeLabels.indexOf(key) > -1) { - parseDgraphJson(innerObj); - - } - }); - } + _.forOwn(data, (val, key) => { + let uid = ""; + if (key === uidPropNameDgraph) { + uid = val; + //Store entire block in map, so we can use to create edges later + //Edges cannot be created here as we may not have got the uid still + //As json from dgraph has randomized order and uid may be after Array elements + uidMap.set(uid, data); + } + //if this key is a relationship type (as defined in EdgeLabels) recurse to get children nodes + if (EdgeLabels.indexOf(key) > -1) { + parseDgraphData(val); + } + }); } @@ -160,57 +157,55 @@ function validateJSONData(uid, nodeName, nodeObjtype) { } } -export function getVisData(jsonData) { +export function getVisData(data) { clearVisData(); - if (jsonData && jsonData.objects) { - parseDgraphJson(jsonData.objects); + _.forOwn(data, (entity, key) => { + parseDgraphData(entity); printUidMap(); for (const [uid, v] of uidMap.entries()) { - - let nodeName = "", nodeObjtype = "", nodeStatus = ""; - - const block = v; - - for (const prop in block) { - if (!block.hasOwnProperty(prop)) { - continue; - } - const val = block[prop]; - //determine whether we are looking at a property for this node OR a set of child nodes - if ( - Array.isArray(val) && - val.length > 0 && - typeof val[0] === "object" - ) { - // These are child nodes - for (let i = 0; i < val.length; i++) { - const fromUid = uid; //key for this map entry - const toUid = val[i].uid; - - const e = getVisFormatEdge(fromUid, toUid, prop); - edges.push(e); - } - } else { - //get properties which we need to feed to Visjs Node - if (prop === namePropNameDgraphApp) { - nodeName = val; - } - if (prop === objtypePropNameDgraphApp) { - nodeObjtype = val; - } - if (prop === nodeStatusProp || - prop === nodePhaseProp) { - nodeStatus = val; - } - } - } - validateJSONData(uid, nodeName, nodeObjtype); - let n = getVisFormatNode(uid, nodeName, nodeObjtype, nodeStatus); - nodes.push(n); + let nodeName = "", nodeObjtype = "", nodeStatus = ""; + const block = v; + + for (const prop in block) { + if (!block.hasOwnProperty(prop)) { + continue; + } + const val = block[prop]; + //determine whether we are looking at a property for this node OR a set of child nodes + if ( + Array.isArray(val) && + val.length > 0 && + typeof val[0] === "object" + ) { + // These are child nodes + for (let i = 0; i < val.length; i++) { + const fromUid = uid; //key for this map entry + const toUid = val[i].uid; + + const e = getVisFormatEdge(fromUid, toUid, prop); + edges.push(e); + } + } else { + //get properties which we need to feed to Visjs Node + if (prop === namePropNameDgraphApp) { + nodeName = val; + } + if (prop === objtypePropNameDgraphApp) { + nodeObjtype = val; + } + if (prop === nodeStatusProp || + prop === nodePhaseProp) { + nodeStatus = val; + } + } + } + validateJSONData(uid, nodeName, nodeObjtype); + let n = getVisFormatNode(uid, nodeName, nodeObjtype, nodeStatus); + nodes.push(n); } - return {nodes, edges}; - } + }); + return {nodes, edges}; } From c7ff270b8c21a77dcc23192b9787c7c01f01f763 Mon Sep 17 00:00:00 2001 From: David Masselink Date: Wed, 26 Dec 2018 14:59:24 -0500 Subject: [PATCH 02/15] extend-graph-navigation - fix merge conflict, need to get upstream master in yet too --- app/src/history.js | 10 ++ app/src/index.js | 13 ++- app/src/utils/graph.js | 213 ++++++++++++++++++++--------------------- 3 files changed, 118 insertions(+), 118 deletions(-) create mode 100644 app/src/history.js diff --git a/app/src/history.js b/app/src/history.js new file mode 100644 index 0000000..06f5ba0 --- /dev/null +++ b/app/src/history.js @@ -0,0 +1,10 @@ +import { createBrowserHistory } from 'history'; + +export default createBrowserHistory({ + //this value is not needed or used during local dev or testing scenarios + //instead, it's required in deployed scenarios where the application is + //deployed on a subdirectory of a host (ex: 'example.com/ui'). the value is + //provided by "homepage" attribute in package.json at webpack build time + //further info here: https://www.npmjs.com/package/history#using-a-base-url + basename: process.env.PUBLIC_URL +}); \ No newline at end of file diff --git a/app/src/index.js b/app/src/index.js index 997a290..a8659e9 100644 --- a/app/src/index.js +++ b/app/src/index.js @@ -1,13 +1,14 @@ import React from 'react'; import { Provider } from 'react-redux'; import { render } from 'react-dom'; -import { BrowserRouter } from 'react-router-dom'; +import { Router } from 'react-router-dom'; import { MuiThemeProvider, createMuiTheme } from "@material-ui/core"; import WebFont from 'webfontloader'; -import './index.css'; -import configureStore from './store/configureStore'; import App from './components/app/App'; +import store from './store'; +import history from './history'; +import './index.css'; WebFont.load({ custom: { @@ -31,14 +32,12 @@ const theme = createMuiTheme({ }, }); -const store = configureStore(); - render( - + - + , document.getElementById('root') diff --git a/app/src/utils/graph.js b/app/src/utils/graph.js index c175b9f..bcbdf6f 100644 --- a/app/src/utils/graph.js +++ b/app/src/utils/graph.js @@ -58,103 +58,96 @@ function parseDgraphData(data) { }); } - - function printUidMap() { - console.debug("Printing uidMap"); - for (const [k, v] of uidMap.entries()) { - console.debug(k, v); - } + console.debug("Printing uidMap"); + for (const [k, v] of uidMap.entries()) { + console.debug(k, v); + } } function getNodeIcon(nodeObjtype) { - - if (nodeObjtype === undefined || nodeObjtype === null) { - return NodeIconMap.get("default"); - } - if (NodeIconMap.has(nodeObjtype)) { - return NodeIconMap.get(nodeObjtype); - } else { - return NodeIconMap.get("default"); - } + if (nodeObjtype === undefined || nodeObjtype === null) { + return NodeIconMap.get("default"); + } + if (NodeIconMap.has(nodeObjtype)) { + return NodeIconMap.get(nodeObjtype); + } else { + return NodeIconMap.get("default"); + } } function getEdgeLabelShortHand(prop) { - - let edgeLabel = ""; - if (prop !== "") { - //edgeLabel = prop.substring(0, 1); - edgeLabel = prop; - //edgeLabelMap.set(edgeLabel, prop); - } - return edgeLabel; + let edgeLabel = ""; + if (prop !== "") { + edgeLabel = prop; + } + return edgeLabel; } function getVisFormatEdge(fromUid, toUid, relation) { - - const e = { - from: fromUid, - to: toUid, - label: getEdgeLabelShortHand(relation), - color: { - color: EdgeColorMap.get(NODE_DEFAULT_STR), - inherit: false - }, - font: { - size: 8 - }, - arrows: "to" - }; - return e; + const e = { + from: fromUid, + to: toUid, + label: getEdgeLabelShortHand(relation), + color: { + color: EdgeColorMap.get(NODE_DEFAULT_STR), + inherit: false + }, + font: { + size: 8 + }, + arrows: "to" + }; + return e; } function getVisFormatNode(uid, nodeName, nodeObjtype, nodeStatus) { - let idParam = uid; - let titleParam = ""; + let idParam = uid; + let titleParam = ""; - if (nodeObjtype !== undefined && nodeObjtype !== null) { - titleParam = nodeObjtype; - } else { - titleParam = nodeName; - } + if (nodeObjtype !== undefined && nodeObjtype !== null) { + titleParam = nodeObjtype; + } else { + titleParam = nodeName; + } - //some names are too large to render correctly in hierarchy, split them by middle dash across 2 lines of text - nodeName = nameSplitter(nodeName); + //some names are too large to render correctly in hierarchy, split them by middle dash across 2 lines of text + nodeName = nameSplitter(nodeName); - const color = NodeStatusColorMap.get(nodeStatus || NODE_DEFAULT_STR); + const color = NodeStatusColorMap.get(nodeStatus || NODE_DEFAULT_STR); - const n = { - id: idParam, - uid: uid, - label: nodeName, - icon:{ - face: NODE_ICON_FONT, - code: getNodeIcon(nodeObjtype), - size: NODE_ICON_FONT_SIZE, - color: color, - }, - name: nodeName, - title: titleParam, - status: nodeStatus, - }; - legendTypesObj[nodeObjtype] = { + const n = { + id: idParam, + uid: uid, + label: nodeName, + icon:{ + face: NODE_ICON_FONT, code: getNodeIcon(nodeObjtype), - color: NODE_DEFAULT_COLOR, - }; - legendStatusesObj[nodeStatus] = { - code: getNodeIcon(NODE_DEFAULT_STR), + size: NODE_ICON_FONT_SIZE, color: color, - }; - return n; + }, + name: nodeName, + title: titleParam, + status: nodeStatus, + }; + legendTypesObj[nodeObjtype] = { + code: getNodeIcon(nodeObjtype), + color: NODE_DEFAULT_COLOR, + }; + legendStatusesObj[nodeStatus] = { + code: getNodeIcon(NODE_DEFAULT_STR), + color: color, + }; + return n; } function validateJSONData(uid, nodeName, nodeObjtype) { - if (nodeName === "") { - console.error(`JSON Error - Attribute ${namePropNameDgraphApp} missing for uid = ${uid}`); - } - if (nodeObjtype === "") { - console.error(`JSON Error - Attribute ${objtypePropNameDgraphApp} missing for uid = ${uid}`); - } + if (nodeName === "") { + console.error(`JSON Error - Attribute ${namePropNameDgraphApp} missing for uid = ${uid}`); + } + if (nodeObjtype === "") { + console.error(`JSON Error - Attribute ${objtypePropNameDgraphApp} missing for uid = ${uid}`); + } } export function getVisData(data) { @@ -165,40 +158,38 @@ export function getVisData(data) { for (const [uid, v] of uidMap.entries()) { let nodeName = "", nodeObjtype = "", nodeStatus = ""; + const block = v; for (const prop in block) { - if (!block.hasOwnProperty(prop)) { - continue; + if (!block.hasOwnProperty(prop)) { + continue; + } + const val = block[prop]; + //determine whether we are looking at a property for this node OR a set of child nodes + if (Array.isArray(val) && val.length > 0 && + typeof val[0] === "object") { + // These are child nodes + for (let i = 0; i < val.length; i++) { + const fromUid = uid; //key for this map entry + const toUid = val[i].uid; + + const e = getVisFormatEdge(fromUid, toUid, prop); + edges.push(e); } - const val = block[prop]; - //determine whether we are looking at a property for this node OR a set of child nodes - if ( - Array.isArray(val) && - val.length > 0 && - typeof val[0] === "object" - ) { - // These are child nodes - for (let i = 0; i < val.length; i++) { - const fromUid = uid; //key for this map entry - const toUid = val[i].uid; - - const e = getVisFormatEdge(fromUid, toUid, prop); - edges.push(e); - } - } else { - //get properties which we need to feed to Visjs Node - if (prop === namePropNameDgraphApp) { - nodeName = val; - } - if (prop === objtypePropNameDgraphApp) { - nodeObjtype = val; - } - if (prop === nodeStatusProp || - prop === nodePhaseProp) { - nodeStatus = val; - } + } else { + //get properties which we need to feed to Visjs Node + if (prop === namePropNameDgraphApp) { + nodeName = val; + } + if (prop === objtypePropNameDgraphApp) { + nodeObjtype = val; + } + if (prop === nodeStatusProp || + prop === nodePhaseProp) { + nodeStatus = val; } + } } validateJSONData(uid, nodeName, nodeObjtype); let n = getVisFormatNode(uid, nodeName, nodeObjtype, nodeStatus); @@ -226,17 +217,17 @@ export function clearVisData(){ //colorChannelA and colorChannelB are ints ranging from 0 to 255 function colorChannelMixer(colorChannelA, colorChannelB, amountToMix){ - let channelA = colorChannelA*amountToMix; - let channelB = colorChannelB*(1-amountToMix); - return parseInt(channelA+channelB); + let channelA = colorChannelA*amountToMix; + let channelB = colorChannelB*(1-amountToMix); + return parseInt(channelA+channelB); } //rgbA and rgbB are arrays, amountToMix ranges from 0.0 to 1.0 //example (red): rgbA = [255,0,0] export function colorMixer(rgbA, rgbB, amountToMix){ - let r = colorChannelMixer(rgbA[0],rgbB[0],amountToMix); - let g = colorChannelMixer(rgbA[1],rgbB[1],amountToMix); - let b = colorChannelMixer(rgbA[2],rgbB[2],amountToMix); - return "rgb("+r+","+g+","+b+")"; + let r = colorChannelMixer(rgbA[0],rgbB[0],amountToMix); + let g = colorChannelMixer(rgbA[1],rgbB[1],amountToMix); + let b = colorChannelMixer(rgbA[2],rgbB[2],amountToMix); + return "rgb("+r+","+g+","+b+")"; } //Function to split long label names. If too long, name is split by its middle From c298c24bf8272b2f28dd77c909431a8e118133d0 Mon Sep 17 00:00:00 2001 From: David Masselink Date: Tue, 1 Jan 2019 20:24:41 -0800 Subject: [PATCH 03/15] extend-graph-navigation - add new entity related actions --- app/Dockerfile | 4 +-- app/src/actions/actionTypes.js | 4 +++ app/src/actions/entityActions.js | 54 ++++++++++---------------------- app/src/actions/queryActions.js | 4 +-- 4 files changed, 24 insertions(+), 42 deletions(-) diff --git a/app/Dockerfile b/app/Dockerfile index f6d153c..191d5f0 100644 --- a/app/Dockerfile +++ b/app/Dockerfile @@ -3,9 +3,8 @@ WORKDIR /app #initially copy over just package.json (and locks, if present) for dependency installation COPY package*.json /app/ RUN yarn install -#then copy over our code for test+build +#then copy over our code for build COPY ./ /app/ -#RUN yarn test RUN yarn build #once the build-stage is complete, we can build an image for our deployment @@ -16,4 +15,5 @@ COPY --from=build-stage /app/build /usr/share/nginx/html COPY --from=build-stage /app/nginx.conf /etc/nginx/conf.d/default.conf COPY --from=build-stage /app/entrypoint.sh /etc/nginx/entrypoint.sh RUN chmod 777 /etc/nginx/entrypoint.sh +EXPOSE 80 CMD /etc/nginx/entrypoint.sh \ No newline at end of file diff --git a/app/src/actions/actionTypes.js b/app/src/actions/actionTypes.js index d189bb2..b32b7d3 100644 --- a/app/src/actions/actionTypes.js +++ b/app/src/actions/actionTypes.js @@ -2,7 +2,11 @@ export const CHANGE_QUERY = 'CHANGE_QUERY'; export const SUBMIT_QUERY = 'SUBMIT_QUERY'; export const FETCH_QUERY = 'FETCH_QUERY'; export const RECEIVE_QUERY = 'RECEIVE_QUERY'; + +export const SET_ROOT_ENTITY = 'SET_ROOT_ENTITY'; export const ADD_ENTITY_WATCH = 'ADD_ENTITY_WATCH'; export const FETCH_ENTITY = 'FETCH_ENTITY'; +export const FETCH_ENTITIES = 'FETCH_ENTITIES'; export const RECEIVE_ENTITY = 'RECEIVE_ENTITY'; + export const SHOW_NOTIFY = 'SHOW_NOTIFY'; \ No newline at end of file diff --git a/app/src/actions/entityActions.js b/app/src/actions/entityActions.js index 28bb7ec..b80c912 100644 --- a/app/src/actions/entityActions.js +++ b/app/src/actions/entityActions.js @@ -1,6 +1,11 @@ import * as types from './actionTypes'; import ApiService from "../services/ApiService"; +export const setRootEntity = uid => ({ + type: types.SET_ROOT_ENTITY, + uid +}); + export const addEntityWatch = uid => ({ type: types.ADD_ENTITY_WATCH, uid @@ -9,47 +14,20 @@ export const addEntityWatch = uid => ({ export const fetchEntity = uid => { return dispatch => { return ApiService.getEntity(uid) - .then(handleResponse) - .then(json => dispatch(receiveEntity(json[0])));//index into JSON arr to grab single entity object + //TODO:DM - can this hard-coded idx be cleaned up? + .then(json => dispatch(receiveEntity(json.objects[0]))); + }; +}; + +export const fetchEntities = uids => { + return dispatch => { + return Promise.all(uids.map(uid => + dispatch(fetchEntity(uid)) + )); }; }; export const receiveEntity = json => ({ type: types.RECEIVE_ENTITY, results: json -}); - -function handleResponse(json) { - let results = []; - let existingUids = {}; - for (let objKey in json){ - let objArr = json[objKey]; - if (objArr.length) { - objArr.forEach(obj => { - //screen out duplicate UID entries - if(!existingUids[obj.uid]){ - results.push(obj); - existingUids[obj.uid] = true; - } - }); - } - } - return results; -} - -//only update state if the objects fail lodash equality check AND -// //the component is still mounted. usually, the lifecycle methods should -// //be used directly for such things that, but in testing we're getting -// //intermittent errors that setState is being called on unmounted -// //components, without this check -// if(!_.isEqual(this.state.data, json) && this._isMounted) { -// this.setState({ -// data: json, -// waitingOnReq: false -// }); - -//logic here to transform API raw data to something usable by graph, or does that make more sense in actions? - -//also trigger periodic re-requests of data here with knowledge of period, max reqs/sec, num objs tracked? -//no, I think it still makes sense to do that in a component, probably GraphContainer, where we easily have access to -//store to see num of requests needed and can compute interval ms based on limit and num entities +}); \ No newline at end of file diff --git a/app/src/actions/queryActions.js b/app/src/actions/queryActions.js index 88c55b0..cd77a91 100644 --- a/app/src/actions/queryActions.js +++ b/app/src/actions/queryActions.js @@ -34,9 +34,9 @@ export function fetchQuery(query) { let requestPromise; if (query.includes(QSL_TAG)) { - requestPromise = ApiService.getQueryResult(query); - } else { requestPromise = ApiService.getQSLResult(query); + } else { + requestPromise = ApiService.getQueryResult(query); } return requestPromise From ecdf7db7a6e3ed046ad83318c0115f6fb118d894 Mon Sep 17 00:00:00 2001 From: David Masselink Date: Tue, 1 Jan 2019 20:45:33 -0800 Subject: [PATCH 04/15] extend-graph-navigation - addl changes related to entity related reducer and initial state --- app/src/config/visjsConfig.js | 1 + app/src/reducers/entityReducer.js | 61 +++++++++++++++++++++++++------ app/src/reducers/initialState.js | 6 ++- app/src/utils/validate.test.js | 1 + 4 files changed, 55 insertions(+), 14 deletions(-) diff --git a/app/src/config/visjsConfig.js b/app/src/config/visjsConfig.js index bb0b9f0..6755cd6 100644 --- a/app/src/config/visjsConfig.js +++ b/app/src/config/visjsConfig.js @@ -31,6 +31,7 @@ export const options = { zoomView: true }, layout: { + randomSeed: 42, improvedLayout: true, }, }; \ No newline at end of file diff --git a/app/src/reducers/entityReducer.js b/app/src/reducers/entityReducer.js index 2caa21d..8dbe3f3 100644 --- a/app/src/reducers/entityReducer.js +++ b/app/src/reducers/entityReducer.js @@ -1,30 +1,67 @@ +import _ from 'lodash'; + import initialState from './initialState'; -import {ADD_ENTITY_WATCH, FETCH_ENTITY, RECEIVE_ENTITY} from '../actions/actionTypes'; +import { SET_ROOT_ENTITY, ADD_ENTITY_WATCH, FETCH_ENTITY, + FETCH_ENTITIES, RECEIVE_ENTITY} from '../actions/actionTypes'; +import { EdgeLabels } from '../config/appConfig'; export default function entity(state = initialState.entity, action) { - let newState, now; + let newState, now, potentialResults; switch (action.type) { + case SET_ROOT_ENTITY: + newState = { + ...state, //start with a copy of existing state + rootUid: action.uid, //and apply changes on top of existing state attrs + }; + return newState; case ADD_ENTITY_WATCH: - //start with a copy of existing state - newState = { ...state }; - //extend the uidsObj which is otherwise difficult to with variable key - //name action.uid in statement above - newState.uidsObj[action.uid] = true; + newState = { + ...state, + isWaiting: true, + }; + //extend the entity obj separately since we only want to change part of it + newState.entitiesByUid[action.uid] = {}; return newState; case FETCH_ENTITY: return action; + case FETCH_ENTITIES: + return action; case RECEIVE_ENTITY: now = +new Date(); - //project changes on top of existing state attrs - newState = { ...state, - //results: action.results, // how to add to existing results? + newState = { + ...state, latestTimestamp: now, + isWaiting: false, }; - //TODO:DM - technically I could use just 1 obj to maintain UIDs to watch as well as results, just make null until new obj arrives {val: null, timestamp: ...} - newState.results[action.results.uid] = action.results; + //extend the entity obj separately since we only want to change part of it + newState.entitiesByUid[action.results.uid] = action.results; + //build a new result obj but only update newState if it's different + potentialResults = entityWalk(newState.rootUid, newState.entitiesByUid); + if (!_.isEqual(state.results, potentialResults)) { + newState.results = potentialResults; + } return newState; default: return state; } } +const entityWalk = (rootUid, entityObj) => { + // start with root obj + let results = entityObj[rootUid]; + //walk it (recursing into all arrs) + //TODO:DM - this may not actually be recursing as deep as I want... more than 1 hop from root + for (let key in results) { + let candidate = results[key]; + //ensure that the key is an expected relationship and dealing with an array + if ((EdgeLabels.indexOf(key) > -1) && _.isArray(candidate)) { + for (let idx in candidate) { + let node = candidate[idx]; + if (entityObj[node.uid]) { + _.merge(node, entityObj[node.uid]); + } + } + } + } + return results; +}; \ No newline at end of file diff --git a/app/src/reducers/initialState.js b/app/src/reducers/initialState.js index 9110c4f..08344a4 100644 --- a/app/src/reducers/initialState.js +++ b/app/src/reducers/initialState.js @@ -4,12 +4,14 @@ export default { lastSubmitted: '', submitted: false, isWaiting: false, - results: [] + results: [], }, entity: { - uidsObj: {}, + rootUid: '', + entitiesByUid: {}, results: {}, latestTimestamp: 0, + isWaiting: false, }, notify: { msg: '', diff --git a/app/src/utils/validate.test.js b/app/src/utils/validate.test.js index 59d23d7..5e6d04f 100644 --- a/app/src/utils/validate.test.js +++ b/app/src/utils/validate.test.js @@ -1,3 +1,4 @@ +//TODO:DM - looks like the validate module isn't currently being used in the app, therefore these tests shouldn't really count UNLESS we plan to use it again import { validateIPaddress } from './validate'; it('should correctly recognize an IP addr', () => { From b21dfe14e01b43ae9198886b22d35b03c4ad0710 Mon Sep 17 00:00:00 2001 From: David Masselink Date: Tue, 1 Jan 2019 21:10:25 -0800 Subject: [PATCH 05/15] extend-graph-navigation - graph and graph container related changes --- app/src/components/graph/Graph.js | 82 +++++++----------- app/src/components/graph/GraphContainer.js | 93 ++++++++------------ app/src/utils/graph.js | 98 ++++++++++------------ 3 files changed, 105 insertions(+), 168 deletions(-) diff --git a/app/src/components/graph/Graph.js b/app/src/components/graph/Graph.js index 6c0fec4..2782f03 100644 --- a/app/src/components/graph/Graph.js +++ b/app/src/components/graph/Graph.js @@ -12,6 +12,7 @@ import { getVisData, clearVisData, colorMixer, getLegends } from "../../utils/gr import { NodeStatusPulseColors } from "../../config/appConfig"; import './Graph.css'; +const PULSE_TIME_STEP_MS = 100; class Graph extends Component { constructor(props) { @@ -35,11 +36,9 @@ class Graph extends Component { statuses: {} }; - - this.setDetailsTab = this.setDetailsTab.bind(this); + //TODO:DM - can I mitigate need for these hard binds with => fns or something else? this.validateInputs = this.validateInputs.bind(this); this.renderGraph = this.renderGraph.bind(this); - this.renderGraphExpanded = this.renderGraphExpanded.bind(this); this.renderVisGraph = this.renderVisGraph.bind(this); this.clearNetwork = this.clearNetwork.bind(this); } @@ -49,9 +48,8 @@ class Graph extends Component { } componentWillReceiveProps(nextProps){ - if(!_.isEqual(nextProps, this.props)){ - this.clearNetwork(); - this.setState({data: nextProps.dataSet}); + if(!_.isEqual(nextProps.dataSet, this.props.dataSet)){ + this.renderGraph(nextProps.dataSet) } } @@ -69,8 +67,6 @@ class Graph extends Component { } render() { - //TODO: this is a bit awkward to have to explicitly call a render "helper" function, ideally anything critical would either be done directly in render() or earlier in lifecycle methods, etc. - this.renderGraphExpanded(this.state.data); return (
{/*Graph Visualization*/} @@ -120,54 +116,43 @@ class Graph extends Component { } renderGraph(jsonData) { - console.debug(`Got data ${JSON.stringify(jsonData)}`); - - const {nodes, edges} = getVisData(jsonData); - this._nodes = nodes; - this._edges = edges; - - this.renderVisGraph(); - } - - renderGraphExpanded(jsonData) { if (_.isEmpty(jsonData)) return; - this.nodesDataset.clear(); - this.edgesDataset.clear(); - this._nodes = []; - this._edges = []; - const {nodes, edges} = getVisData(jsonData); - const nodesNew = nodes; - const edgesNew = edges; + this._edges = edges; this._legends = getLegends(); - //Avoid adding duplicate selected node, as vis.js does not allow duplicates. - for (let i = 0; i < nodesNew.length; i++) { - this._nodes.push(nodesNew[i]); + for (let i = 0; i < nodes.length; i++) { + this._nodes.push(nodes[i]); } - this._edges.push(edgesNew); - this.renderVisGraph(); } renderVisGraph() { - this.nodesDataset = new vis.DataSet(); - for (let i = 0; i < this._nodes.length; i++) { + //determine if we have any newnodes to add to the graph dataset + for (let i = 0; i < this._nodes.length; i++) { + if(!this.nodesDataset.get(this._nodes[i].uid)){ this.nodesDataset.add(this._nodes[i]); } - this.edgesDataset = new vis.DataSet(); - for (let i = 0; i < this._edges.length; i++) { + } + //determine if we have any new edges to add to the graph dataset + for (let i = 0; i < this._edges.length; i++) { + //edge IDs are a concatenation of from and to IDs + if(!this.edgesDataset.get(this._edges[i].from + this._edges[i].to)){ this.edgesDataset.add(this._edges[i]); } - this._data = {nodes: this.nodesDataset, edges: this.edgesDataset}; + } + this._data = {nodes: this.nodesDataset, edges: this.edgesDataset}; - const container = document.getElementById("graph"); //id of div container for graph. - this._network = new vis.Network(container, this._data, options); + //id of div container for graph + const container = document.getElementById("graph"); + this._network = new vis.Network(container, this._data, options); - this.configNetwork(this._network); - //must bind call to handleColorPulse since it'll be called by browser otherwise with Window as "this" context - this.pulseIntervalHandle = setInterval(this.handleColorPulse.bind(this), 100);//TODO:DM-constantize time! + this.configNetwork(this._network); + //must bind call to handleColorPulse since it'll be called by browser + //otherwise with window as "this" context + this.pulseIntervalHandle = setInterval(this.handleColorPulse.bind(this), + PULSE_TIME_STEP_MS); } handleColorPulse() { @@ -188,17 +173,6 @@ class Graph extends Component { }); } - setDetailsTab(attributesMap) { - const attributes = []; - for (const [k, v] of attributesMap.entries()) { - if (v !== undefined && v !== "") { - const entry = ` ${k} : ${v}`; - attributes.push(entry); - } - } - this.setState({detailsTab: attributes}); - } - clearNetwork() { //cancel node pulse timers, before this._data is cleared clearInterval(this.pulseIntervalHandle); @@ -221,10 +195,10 @@ class Graph extends Component { const targetNodeUid = element.nodes[0]; const pathComponents = this.props.location.pathname.split('/'); const currentNodeUid = pathComponents[pathComponents.length - 1]; - //only update props if target node is not current node + //only add node data if target node is not current node + //TODO:DM - rather than just current node, could skip any already watched UIDs if (targetNodeUid !== currentNodeUid) { - this.clearNetwork(); - this.props.history.push('/graph/'+targetNodeUid); + this.props.entityActions.addEntityWatch(targetNodeUid); } } }); diff --git a/app/src/components/graph/GraphContainer.js b/app/src/components/graph/GraphContainer.js index 565cee8..8287190 100644 --- a/app/src/components/graph/GraphContainer.js +++ b/app/src/components/graph/GraphContainer.js @@ -1,21 +1,18 @@ import React, { Component } from 'react'; -import {connect} from 'react-redux'; -import {bindActionCreators} from 'redux'; +import { connect } from 'react-redux'; +import { bindActionCreators } from 'redux'; import PropTypes from 'prop-types'; import { withRouter } from 'react-router-dom'; -import _ from 'lodash'; import { withStyles } from '@material-ui/core/styles'; import Grid from '@material-ui/core/Grid'; import CircularProgress from '@material-ui/core/CircularProgress'; -import EntityDetails from '../entityDetails/EntityDetails'; -import ApiService from "../../services/ApiService"; -import Graph from './Graph'; import "./Graph.css"; +import Graph from './Graph'; +import EntityDetails from '../entityDetails/EntityDetails'; import * as entityActions from "../../actions/entityActions"; - -const DATA_FETCH_PERIOD_MS = 5000; +const FETCH_PERIOD_PER_ENTITY_MS = 2000; const styles = theme => ({ container: { @@ -35,83 +32,69 @@ const styles = theme => ({ }); class GraphContainer extends Component { - constructor(props) { - super(props); - - this.state = { - data: {}, - waitingOnReq: false - }; - this._isMounted = false; - } - componentDidMount() { - this._isMounted = true; - this.setState({waitingOnReq: true}); - this.intervalHandle = setInterval(() => this.getData(), DATA_FETCH_PERIOD_MS); - //Delay the very first call by just one scheduling cycle so that the webfont can load first + this.setRootNode(); + this.intervalHandle = setTimeout(() => this.getData(), + FETCH_PERIOD_PER_ENTITY_MS); + //delay the first call by no ms (therefore one scheduling cycle) which + //allows for webfont to be loaded before data inserted to graph setTimeout(() => this.getData(), 0); } componentDidUpdate(prevProps) { //recognize change in URL and re-issue API request as necessary if (this.props.location !== prevProps.location){ - this.setState({waitingOnReq: true}); + this.setRootNode(); this.getData(); } } componentWillUnmount() { clearInterval(this.intervalHandle); - this._isMounted = false; } - getData = () => { + setRootNode = () => { const pathComponents = this.props.location.pathname.split('/'); - const uidParam = pathComponents[pathComponents.length - 1]; + //TODO:DM - simply grabbing last param after '/' feels fragile, how to more safely verify as UID? + //could be empty string... a better default to use, if so? + const uid = pathComponents[pathComponents.length - 1]; + + this.props.entityActions.setRootEntity(uid); + this.props.entityActions.addEntityWatch(uid); - //TODO:DM - adding to watch will be redundant, but the UIDs set will enforce uniqueness, should I instead check here? - this.props.entityActions.addEntityWatch(uidParam); - this.props.entityActions.fetchEntity(uidParam); + }; - //TODO:DM - will I need to do something similar to the use of _isMounted still, for testing? - // ApiService.getEntity(uidParam) - // .then(json => { - // //only update state if the objects fail lodash equality check AND - // //the component is still mounted. usually, the lifecycle methods should - // //be used directly for such things that, but in testing we're getting - // //intermittent errors that setState is being called on unmounted - // //components, without this check - // if(!_.isEqual(this.state.data, json) && this._isMounted) { - // this.setState({ - // data: json, - // waitingOnReq: false - // }); - // } - // }); + getData = () => { + //reschedule next automatic data request while computing time value based + //on number of entities and a min time between fetches + const NUM_ENTITIES = Object.keys(this.props.entity.entitiesByUid).length; + this.intervalHandle = setTimeout(() => this.getData(), NUM_ENTITIES * FETCH_PERIOD_PER_ENTITY_MS); + //fetch all entities currently represented as keys in the store + this.props.entityActions.fetchEntities(Object.keys( + this.props.entity.entitiesByUid)); }; render() { - const { classes } = this.props; + const { classes, entity } = this.props; return (
{//selectively show the progress indicator when we're waiting for an outstanding request - this.state.waitingOnReq ? ( + entity.isWaiting ? (
- ) : null //TODO:DM - ideally want to select between spinner and graph here, but loading graph only after spinner disappears leads to graph not being correctly populated; understand why this is + ) : null } - + - +
- );//get rid of [0] idx ^ + ); } } @@ -129,11 +112,5 @@ export default connect( mapStoreToProps, mapDispatchToProps )( - withRouter( - withStyles(styles)(GraphContainer) - ) -); - -//also trigger periodic re-requests of data here with knowledge of period, max reqs/sec, num objs tracked? -//no, I think it still makes sense to do that in a component, probably GraphContainer, where we easily have access to -//store to see num of requests needed and can compute interval ms based on limit and num entities \ No newline at end of file + withRouter(withStyles(styles)(GraphContainer)) +); \ No newline at end of file diff --git a/app/src/utils/graph.js b/app/src/utils/graph.js index bcbdf6f..870c5a2 100644 --- a/app/src/utils/graph.js +++ b/app/src/utils/graph.js @@ -1,13 +1,7 @@ import _ from 'lodash'; //TODO:DM - determine what from this file can be isolated into pure util fns and what makes most sense to incorporate directly into Graph component -//This Library Includes utilities for processing dgraph json and converting to components required by vis.js. - -//NOTE- -//Below are mandatory fields for data node in the json- -//"uid", "name", "objtype", "assetid" -//JSON parsing errors are handled passively- -//Logs will show errors when issues with graph display are due to invalid JSON format. +//This Library Includes utilities for processing dgraph json and converting to components required by vis.js import { NodeIconMap, @@ -53,18 +47,11 @@ function parseDgraphData(data) { } //if this key is a relationship type (as defined in EdgeLabels) recurse to get children nodes if (EdgeLabels.indexOf(key) > -1) { - parseDgraphData(val); + _.forEach(val, (item) => parseDgraphData(item)); } }); } -function printUidMap() { - console.debug("Printing uidMap"); - for (const [k, v] of uidMap.entries()) { - console.debug(k, v); - } -} - function getNodeIcon(nodeObjtype) { if (nodeObjtype === undefined || nodeObjtype === null) { return NodeIconMap.get("default"); @@ -85,7 +72,8 @@ function getEdgeLabelShortHand(prop) { } function getVisFormatEdge(fromUid, toUid, relation) { - const e = { + return { + id: fromUid + toUid, from: fromUid, to: toUid, label: getEdgeLabelShortHand(relation), @@ -98,7 +86,6 @@ function getVisFormatEdge(fromUid, toUid, relation) { }, arrows: "to" }; - return e; } function getVisFormatNode(uid, nodeName, nodeObjtype, nodeStatus) { @@ -111,7 +98,7 @@ function getVisFormatNode(uid, nodeName, nodeObjtype, nodeStatus) { titleParam = nodeName; } - //some names are too large to render correctly in hierarchy, split them by middle dash across 2 lines of text + //some names are too large to render elegantly, split them by middle dash, if present, across 2 lines of text nodeName = nameSplitter(nodeName); const color = NodeStatusColorMap.get(nodeStatus || NODE_DEFAULT_STR); @@ -151,53 +138,52 @@ function validateJSONData(uid, nodeName, nodeObjtype) { } export function getVisData(data) { + let existingUids = {}; clearVisData(); - _.forOwn(data, (entity, key) => { - parseDgraphData(entity); - printUidMap(); - - for (const [uid, v] of uidMap.entries()) { - let nodeName = "", nodeObjtype = "", nodeStatus = ""; + parseDgraphData(data); - const block = v; + for (const [uid, v] of uidMap.entries()) { + let nodeName = "", nodeObjtype = "", nodeStatus = ""; + const block = v; - for (const prop in block) { - if (!block.hasOwnProperty(prop)) { - continue; + for (const prop in block) { + if (!block.hasOwnProperty(prop)) { + continue; + } + const val = block[prop]; + //determine whether we are looking at a property for this node OR a set of child nodes + if (Array.isArray(val) && val.length > 0 && + typeof val[0] === "object") { + // These are child nodes + for (let i = 0; i < val.length; i++) { + const fromUid = uid; //key for this map entry + const toUid = val[i].uid; + + const e = getVisFormatEdge(fromUid, toUid, prop); + edges.push(e); } - const val = block[prop]; - //determine whether we are looking at a property for this node OR a set of child nodes - if (Array.isArray(val) && val.length > 0 && - typeof val[0] === "object") { - // These are child nodes - for (let i = 0; i < val.length; i++) { - const fromUid = uid; //key for this map entry - const toUid = val[i].uid; - - const e = getVisFormatEdge(fromUid, toUid, prop); - edges.push(e); - } - } else { - //get properties which we need to feed to Visjs Node - if (prop === namePropNameDgraphApp) { - nodeName = val; - } - if (prop === objtypePropNameDgraphApp) { - nodeObjtype = val; - } - if (prop === nodeStatusProp || - prop === nodePhaseProp) { - nodeStatus = val; - } + } else { + //get properties which we need to feed to Visjs Node + if (prop === namePropNameDgraphApp) { + nodeName = val; + } + if (prop === objtypePropNameDgraphApp) { + nodeObjtype = val; + } + if (prop === nodeStatusProp || + prop === nodePhaseProp) { + nodeStatus = val; } } - validateJSONData(uid, nodeName, nodeObjtype); - let n = getVisFormatNode(uid, nodeName, nodeObjtype, nodeStatus); + } + validateJSONData(uid, nodeName, nodeObjtype); + let n = getVisFormatNode(uid, nodeName, nodeObjtype, nodeStatus); + if(!existingUids[n.uid]){ nodes.push(n); + existingUids[n.uid] = true; } - }); + } return {nodes, edges}; - } export function getLegends(){ From cedc7f33fd1568930425b180ef0efea2cf12e737 Mon Sep 17 00:00:00 2001 From: David Masselink Date: Tue, 1 Jan 2019 21:17:55 -0800 Subject: [PATCH 06/15] extend-graph-navigation - updating tests so as to wrap GraphContainer with store provider --- app/src/components/app/App.test.js | 2 +- .../components/graph/GraphContainer.test.js | 37 +++++++++++-------- 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/app/src/components/app/App.test.js b/app/src/components/app/App.test.js index 46f9dee..f30f625 100644 --- a/app/src/components/app/App.test.js +++ b/app/src/components/app/App.test.js @@ -4,8 +4,8 @@ import { render, unmountComponentAtNode } from 'react-dom'; import { MemoryRouter } from 'react-router-dom'; import { mount, shallow } from 'enzyme'; -import configureStore from '../../store/configureStore'; import App from './App'; +import configureStore from '../../store/configureStore'; //next import will load envVars from local override of app/public/conf.js import '../../../public/conf'; diff --git a/app/src/components/graph/GraphContainer.test.js b/app/src/components/graph/GraphContainer.test.js index acc47d7..df549b5 100644 --- a/app/src/components/graph/GraphContainer.test.js +++ b/app/src/components/graph/GraphContainer.test.js @@ -1,25 +1,26 @@ import React from 'react'; -import {render, unmountComponentAtNode} from 'react-dom'; -import {MemoryRouter} from 'react-router-dom'; -import {mount, shallow} from 'enzyme'; +import { Provider } from 'react-redux'; +import { render, unmountComponentAtNode } from 'react-dom'; +import { MemoryRouter } from 'react-router-dom'; +import { mount } from 'enzyme'; import GraphContainer from './GraphContainer'; +import configureStore from '../../store/configureStore'; //next import will load envVars from local override of app/public/conf.js import '../../../public/conf'; jest.useFakeTimers(); const div = document.createElement('div'); - -it('shallow renders graph container', () => { - shallow(); -}); +const store = configureStore(); it('deep renders graph container', () => { render( - - - , div); + + + + + , div); unmountComponentAtNode(div); }); @@ -29,9 +30,11 @@ it('deep renders graph container while making async request', (done) => { Promise.resolve(new Response(JSON.stringify(MOCK_RESP), {status:200}))); //render component with path which will cause data fetch render( - - - , div); + + + + + , div); //force timers to complete, so as to trigger request jest.runOnlyPendingTimers(); //ensure that fetch was called at least once @@ -42,9 +45,11 @@ it('deep renders graph container while making async request', (done) => { it('shows a spinner during outstanding request', () => { const wrapper = mount( - - - ); + + + + + ); wrapper.setState({waitingOnReq: true}); expect(wrapper.find('CircularProgress').length).toBe(1); }); From b09ace217c52eec514f5ec4db6a0ae1f65f0bd0b Mon Sep 17 00:00:00 2001 From: David Masselink Date: Wed, 2 Jan 2019 14:07:31 -0800 Subject: [PATCH 07/15] extend-graph-navigation - adding use of splitter to graph container, modeled on recent changes to results/results list --- app/src/components/graph/GraphContainer.js | 14 +++++--------- app/src/components/results/Results.js | 14 +++++--------- 2 files changed, 10 insertions(+), 18 deletions(-) diff --git a/app/src/components/graph/GraphContainer.js b/app/src/components/graph/GraphContainer.js index 412022e..a95a71d 100644 --- a/app/src/components/graph/GraphContainer.js +++ b/app/src/components/graph/GraphContainer.js @@ -4,8 +4,8 @@ import { bindActionCreators } from 'redux'; import PropTypes from 'prop-types'; import { withRouter } from 'react-router-dom'; import { withStyles } from '@material-ui/core/styles'; -import Grid from '@material-ui/core/Grid'; import CircularProgress from '@material-ui/core/CircularProgress'; +import SplitterLayout from 'react-splitter-layout'; import "./Graph.css"; import Graph from './Graph'; @@ -92,14 +92,10 @@ class GraphContainer extends Component {
) : null } - - - - - - - - + + + + ); } diff --git a/app/src/components/results/Results.js b/app/src/components/results/Results.js index 48f3a50..7934dcc 100644 --- a/app/src/components/results/Results.js +++ b/app/src/components/results/Results.js @@ -3,16 +3,14 @@ import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import PropTypes from 'prop-types'; import { withStyles } from '@material-ui/core/styles'; -import Grid from '@material-ui/core/Grid'; import CircularProgress from '@material-ui/core/CircularProgress'; import Typography from '@material-ui/core/Typography'; import SplitterLayout from 'react-splitter-layout'; +import ResultList from './ResultList'; import EntityDetails from '../entityDetails/EntityDetails'; import * as apiCfg from '../../config/apiConfig'; import * as queryActions from '../../actions/queryActions'; -import ResultList from './ResultList'; - const styles = theme => ({ progress: { @@ -81,12 +79,10 @@ class Results extends Component { ) : ( - - - - - - + + + + )} ); From 8fbaf88f4f7529dda2f421d9dbd0adba696d5c2e Mon Sep 17 00:00:00 2001 From: David Masselink Date: Wed, 2 Jan 2019 14:24:38 -0800 Subject: [PATCH 08/15] extend-graph-navigation - clean-up home component, re-center headers --- app/src/components/home/Home.js | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/app/src/components/home/Home.js b/app/src/components/home/Home.js index 2c11210..96c30ce 100644 --- a/app/src/components/home/Home.js +++ b/app/src/components/home/Home.js @@ -6,10 +6,10 @@ import { withRouter } from 'react-router-dom'; import { withStyles } from '@material-ui/core/styles'; import TextField from '@material-ui/core/TextField'; +import './Home.css'; +import logo from './map.png'; import { ENTER_KEYCODE, ENTER_KEYSTR } from "../../config/appConfig"; import * as queryActions from '../../actions/queryActions'; -import logo from './map.png'; -import './Home.css'; const styles = theme => ({ container: { @@ -42,20 +42,19 @@ class Home extends Component { }; render() { + const { classes, query } = this.props; return (
-
-

Welcome to Kubernetes Application Topology Browser

-

K-Atlas Browser

-
-
+

Welcome to Kubernetes Application Topology Browser

+

K-Atlas Browser

+
From 8a5f5c670f6a327122c2509565763e9f98d370c0 Mon Sep 17 00:00:00 2001 From: David Masselink Date: Mon, 7 Jan 2019 14:33:32 -0800 Subject: [PATCH 09/15] extend-graph-navigation - incorporating review feedback and some general clean-up --- app/src/actions/entityActions.js | 4 +--- app/src/actions/queryActions.js | 28 +++++++++++----------- app/src/components/graph/GraphContainer.js | 26 ++++++++++---------- app/src/reducers/entityReducer.js | 28 ++++++++++++++-------- app/src/reducers/queryReducer.js | 11 +++------ 5 files changed, 50 insertions(+), 47 deletions(-) diff --git a/app/src/actions/entityActions.js b/app/src/actions/entityActions.js index b80c912..3cc5314 100644 --- a/app/src/actions/entityActions.js +++ b/app/src/actions/entityActions.js @@ -21,9 +21,7 @@ export const fetchEntity = uid => { export const fetchEntities = uids => { return dispatch => { - return Promise.all(uids.map(uid => - dispatch(fetchEntity(uid)) - )); + uids.map(uid => dispatch(fetchEntity(uid))); }; }; diff --git a/app/src/actions/queryActions.js b/app/src/actions/queryActions.js index cd77a91..830951b 100644 --- a/app/src/actions/queryActions.js +++ b/app/src/actions/queryActions.js @@ -51,19 +51,19 @@ export const receiveQuery = json => ({ }); function handleResponse(json) { - let results = []; - let existingUids = {}; - for (let objKey in json){ - let objArr = json[objKey]; - if (objArr.length) { - objArr.forEach(obj => { - //screen out duplicate UID entries - if(!existingUids[obj.uid]){ - results.push(obj); - existingUids[obj.uid] = true; - } - }); - } + let results = []; + let existingUids = {}; + for (let objKey in json){ + let objArr = json[objKey]; + if (objArr.length) { + objArr.forEach(obj => { + //screen out duplicate UID entries + if(!existingUids[obj.uid]){ + results.push(obj); + existingUids[obj.uid] = true; + } + }); } - return results; } + return results; +} diff --git a/app/src/components/graph/GraphContainer.js b/app/src/components/graph/GraphContainer.js index a95a71d..01883e4 100644 --- a/app/src/components/graph/GraphContainer.js +++ b/app/src/components/graph/GraphContainer.js @@ -41,15 +41,13 @@ const styles = theme => ({ class GraphContainer extends Component { componentDidMount() { this.setRootNode(); - this.intervalHandle = setTimeout(() => this.getData(), - FETCH_PERIOD_PER_ENTITY_MS); - //delay the first call by no ms (therefore one scheduling cycle) which - //allows for webfont to be loaded before data inserted to graph - setTimeout(() => this.getData(), 0); + //run first data acquisition event immediately, maintain handle for + //cancellation purpose + this.intervalHandle = setTimeout(() => this.getDataInterval(), 0); } componentDidUpdate(prevProps) { - //recognize change in URL and re-issue API request as necessary + //recognize change in URL and re-issue API request in that case if (this.props.location !== prevProps.location){ this.setRootNode(); this.getData(); @@ -60,7 +58,7 @@ class GraphContainer extends Component { clearInterval(this.intervalHandle); } - setRootNode = () => { + setRootNode() { const pathComponents = this.props.location.pathname.split('/'); //TODO:DM - simply grabbing last param after '/' feels fragile, how to more safely verify as UID? //could be empty string... a better default to use, if so? @@ -68,18 +66,22 @@ class GraphContainer extends Component { this.props.entityActions.setRootEntity(uid); this.props.entityActions.addEntityWatch(uid); + } - }; - - getData = () => { + getDataInterval() { + this.getData(); //reschedule next automatic data request while computing time value based //on number of entities and a min time between fetches const NUM_ENTITIES = Object.keys(this.props.entity.entitiesByUid).length; - this.intervalHandle = setTimeout(() => this.getData(), NUM_ENTITIES * FETCH_PERIOD_PER_ENTITY_MS); + this.intervalHandle = setTimeout(() => this.getDataInterval(), + NUM_ENTITIES * FETCH_PERIOD_PER_ENTITY_MS); + } + + getData() { //fetch all entities currently represented as keys in the store this.props.entityActions.fetchEntities(Object.keys( this.props.entity.entitiesByUid)); - }; + } render() { const { classes, entity } = this.props; diff --git a/app/src/reducers/entityReducer.js b/app/src/reducers/entityReducer.js index 8dbe3f3..cc9a71c 100644 --- a/app/src/reducers/entityReducer.js +++ b/app/src/reducers/entityReducer.js @@ -51,17 +51,25 @@ const entityWalk = (rootUid, entityObj) => { let results = entityObj[rootUid]; //walk it (recursing into all arrs) //TODO:DM - this may not actually be recursing as deep as I want... more than 1 hop from root - for (let key in results) { + _.forOwn(results, (val, key) => { let candidate = results[key]; - //ensure that the key is an expected relationship and dealing with an array - if ((EdgeLabels.indexOf(key) > -1) && _.isArray(candidate)) { - for (let idx in candidate) { - let node = candidate[idx]; - if (entityObj[node.uid]) { - _.merge(node, entityObj[node.uid]); - } - } + if ((EdgeLabels.indexOf(key) > -1)){ + entityWalkHelper(candidate, entityObj); } - } + }); return results; +}; + +const entityWalkHelper = (candidate, entityObj) => { + //ensure that the key is an expected relationship and the val is an array + if (_.isArray(candidate)) { + _.forEach(candidate, (node) => { + if (entityObj[node.uid]) { + _.merge(node, entityObj[node.uid]); + } + }); + } else { + //object or string + //how to recurse here? + } }; \ No newline at end of file diff --git a/app/src/reducers/queryReducer.js b/app/src/reducers/queryReducer.js index 4e84e67..056c74b 100644 --- a/app/src/reducers/queryReducer.js +++ b/app/src/reducers/queryReducer.js @@ -6,17 +6,14 @@ export default function query(state = initialState.query, action) { switch (action.type) { case CHANGE_QUERY: newState = { + ...state, current: action.query, - lastSubmitted: state.lastSubmitted, submitted: false, - isWaiting: state.isWaiting, - results: state.results, }; return newState; case SUBMIT_QUERY: newState = { - current: state.current, - lastSubmitted: state.current, + ...state, submitted: true, isWaiting: true, results: [], //new array to clear out old results upon new submission @@ -26,9 +23,7 @@ export default function query(state = initialState.query, action) { return action; case RECEIVE_QUERY: newState = { - current: state.current, - lastSubmitted: state.lastSubmitted, - submitted: state.submitted, + ...state, isWaiting: false, results: action.results }; From efc670be284f95e3b5d23b590e7a18770431acd0 Mon Sep 17 00:00:00 2001 From: David Masselink Date: Mon, 7 Jan 2019 14:44:04 -0800 Subject: [PATCH 10/15] extend-graph-navigation - merge fixes, updated alpha channel in logo png, refined relationships rendered --- app/src/actions/queryActions.js | 4 ++-- app/src/components/home/map.png | Bin 21123 -> 22813 bytes app/src/config/appConfig.js | 7 ++++--- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/app/src/actions/queryActions.js b/app/src/actions/queryActions.js index c7083e9..f5e17af 100644 --- a/app/src/actions/queryActions.js +++ b/app/src/actions/queryActions.js @@ -1,9 +1,9 @@ -import ApiService from "../services/ApiService"; import * as types from './actionTypes'; import * as notifyActions from './notifyActions'; //Use of app history here so that route navigations can be occur in actions, //which are otherwise not wrappable withRouter import history from '../history'; +import ApiService from "../services/ApiService"; import { QUERY_LEN_ERR } from '../utils/errors'; //TODO:DM - is there a better place to define router related consts? @@ -20,7 +20,7 @@ export const changeQuery = str => ({ const submitQueryAction = () => ({ type: types.SUBMIT_QUERY, -}) +}); export function submitQuery(query) { return dispatch => { diff --git a/app/src/components/home/map.png b/app/src/components/home/map.png index 18a56620bf0ea8483863e30ee761916c26d29956..20dd78ba400acbebc20e5ca918aa89d7caf458a0 100644 GIT binary patch literal 22813 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 diff --git a/app/src/config/appConfig.js b/app/src/config/appConfig.js index 5522d41..7b767b1 100644 --- a/app/src/config/appConfig.js +++ b/app/src/config/appConfig.js @@ -34,7 +34,7 @@ export const NodeStatusColorMap = new Map([ ['Running', '#25e293'], ['Bound', '#25e293'], //concerning, risky statuses - yellow - ['Terminating', '#e2cf25'], //this color doesn't matter since it will be overwritten thru pulsing... but probably good for the legend + ['Terminating', '#e2cf25'], //this color will be overwritten thru pulsing, but is still needed for the legend //error, fault statuses - red ['Stopped', '#e2254e'], ]); @@ -50,5 +50,6 @@ export const EdgeColorMap = new Map([ export const EdgeLabels = ['belongs_to', 'binds', 'claims', 'contains', 'controlled_by', 'has', 'is_bound_to', 'routes_traffic', 'runs', - //relationship types used after dgraph metadata changes - 'cluster', 'namespace', 'owner', 'nodename', '~cluster', '~owner', '~namespace']; + //relationship types used after dgraph metadata changes, not currently using + //'~cluster', '~namesapce' since they often have hundreds of children + 'cluster', 'namespace', 'owner', 'nodename', '~owner']; From 4fca129ff03436d568fd6a78786bcf2c1698c5bd Mon Sep 17 00:00:00 2001 From: David Masselink Date: Mon, 7 Jan 2019 16:43:44 -0800 Subject: [PATCH 11/15] extend-graph-navigation - get recursive navigation working with entity reucer; clean up a notifier test --- app/src/components/graph/Graph.js | 2 ++ app/src/components/notifier/Notifier.test.js | 2 -- app/src/reducers/entityReducer.js | 38 ++++++++++---------- 3 files changed, 20 insertions(+), 22 deletions(-) diff --git a/app/src/components/graph/Graph.js b/app/src/components/graph/Graph.js index 2782f03..31a980a 100644 --- a/app/src/components/graph/Graph.js +++ b/app/src/components/graph/Graph.js @@ -199,6 +199,8 @@ class Graph extends Component { //TODO:DM - rather than just current node, could skip any already watched UIDs if (targetNodeUid !== currentNodeUid) { this.props.entityActions.addEntityWatch(targetNodeUid); + //and immediately attempt to fetch for the indicated UID + this.props.entityActions.fetchEntity(targetNodeUid); } } }); diff --git a/app/src/components/notifier/Notifier.test.js b/app/src/components/notifier/Notifier.test.js index 847d6cb..5d7d32c 100644 --- a/app/src/components/notifier/Notifier.test.js +++ b/app/src/components/notifier/Notifier.test.js @@ -133,8 +133,6 @@ it('Notifier can open snackbar for errors returned from httpService', () => { //check for expected state of the store const nowStore = store.getState(); - console.log('nowstore.notify.type=' + nowStore.notify.type); - console.log('nowstore.notify.msg=' + nowStore.notify.msg); expect(nowStore.notify.msg).not.toEqual(''); expect(nowStore.notify.timestamp).not.toEqual(0); let timeDiff = +new Date() - nowStore.notify.timestamp; diff --git a/app/src/reducers/entityReducer.js b/app/src/reducers/entityReducer.js index cc9a71c..70f0d42 100644 --- a/app/src/reducers/entityReducer.js +++ b/app/src/reducers/entityReducer.js @@ -49,27 +49,25 @@ export default function entity(state = initialState.entity, action) { const entityWalk = (rootUid, entityObj) => { // start with root obj let results = entityObj[rootUid]; - //walk it (recursing into all arrs) - //TODO:DM - this may not actually be recursing as deep as I want... more than 1 hop from root - _.forOwn(results, (val, key) => { - let candidate = results[key]; - if ((EdgeLabels.indexOf(key) > -1)){ - entityWalkHelper(candidate, entityObj); - } - }); + let encounteredUids = {}; + encounteredUids[rootUid] = true; + + entityWalkHelper(results, entityObj, encounteredUids); return results; }; -const entityWalkHelper = (candidate, entityObj) => { - //ensure that the key is an expected relationship and the val is an array - if (_.isArray(candidate)) { - _.forEach(candidate, (node) => { - if (entityObj[node.uid]) { - _.merge(node, entityObj[node.uid]); - } - }); - } else { - //object or string - //how to recurse here? - } +const entityWalkHelper = (results, entityObj, encounteredUids) => { + _.forOwn(results, (childrenCandidate, key) => { + if ((EdgeLabels.indexOf(key) > -1) && _.isArray(childrenCandidate)){ + _.forEach(childrenCandidate, node => { + if (node.uid && entityObj[node.uid] && !encounteredUids[node.uid]) { + _.assign(node, entityObj[node.uid]); + encounteredUids[node.uid] = true; + } + //recurse thru object if children are present, important to do this + //after this object is augmented so we won't later overwrite + entityWalkHelper(node, entityObj, encounteredUids); + }); + } + }); }; \ No newline at end of file From e460a0fe351e02d26eba5fa2105ef6b952ce47ff Mon Sep 17 00:00:00 2001 From: David Masselink Date: Mon, 7 Jan 2019 17:00:55 -0800 Subject: [PATCH 12/15] extend-graph-navigation - fixing broken test cases which somehow still ran locally --- app/src/components/notifier/Notifier.test.js | 5 +---- .../services/{httpService.test.js => HttpService.test.js} | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) rename app/src/services/{httpService.test.js => HttpService.test.js} (97%) diff --git a/app/src/components/notifier/Notifier.test.js b/app/src/components/notifier/Notifier.test.js index 5d7d32c..dd19792 100644 --- a/app/src/components/notifier/Notifier.test.js +++ b/app/src/components/notifier/Notifier.test.js @@ -4,17 +4,14 @@ import { MemoryRouter } from 'react-router-dom'; import { mount } from 'enzyme'; import fetchMock from 'fetch-mock'; -import configureStore from '../../store/configureStore'; import {AutoHideDuration} from './Notifier'; import App from '../app/App'; -import {HttpService} from '../../services/httpService'; +import {HttpService} from '../../services/HttpService'; import {QUERY_LEN_ERR} from "../../utils/errors"; //Use the real store rather than mock Store to keep consistent with the same that is used by HttpService. import store from '../../store.js'; -const div = document.createElement('div'); - function sleep (time) { return new Promise((resolve) => setTimeout(resolve, time)); } diff --git a/app/src/services/httpService.test.js b/app/src/services/HttpService.test.js similarity index 97% rename from app/src/services/httpService.test.js rename to app/src/services/HttpService.test.js index 6a42410..cfee660 100644 --- a/app/src/services/httpService.test.js +++ b/app/src/services/HttpService.test.js @@ -1,5 +1,5 @@ import fetchMock from 'fetch-mock'; -import {HttpService} from './httpService'; +import {HttpService} from './HttpService'; it('Returns 200 for GET request, then response is as expected', () => { From b4ce6145fc8d6bf4792811c31f70be9c92a6c8bb Mon Sep 17 00:00:00 2001 From: David Masselink Date: Fri, 11 Jan 2019 11:19:22 -0800 Subject: [PATCH 13/15] fix-notifier-tests by changing from looking for an autogenerated CSS class to a node ID under our control * also leaving a TODO comment for @ssmails --- app/src/components/notifier/Notifier.test.js | 44 ++++++++++---------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/app/src/components/notifier/Notifier.test.js b/app/src/components/notifier/Notifier.test.js index dd19792..2a0ad67 100644 --- a/app/src/components/notifier/Notifier.test.js +++ b/app/src/components/notifier/Notifier.test.js @@ -16,9 +16,6 @@ function sleep (time) { return new Promise((resolve) => setTimeout(resolve, time)); } -//Using hostNodes hack instead of wrapper.find('.Notifier-root-120').length - refer https://github.com/airbnb/enzyme/issues/1253 -//expect(wrapper.find('.Notifier-root-120').hostNodes().length).toEqual(0); - it('Notifier does not open snackbar for valid query', () => { const SEARCH_STR = 'foobar'; @@ -30,8 +27,8 @@ it('Notifier does not open snackbar for valid query', () => { ); - //Check for class name of Notifier Snack Bar. Initially not present. - expect(wrapper.find('.Notifier-root-120').hostNodes().length).toEqual(0); + //Check for id of Notifier Snack Bar. Initially not present. + expect(wrapper.find('#snackbar-message-id').length).toEqual(0); let input = wrapper.find('input').last(); input.simulate('change', { target: { value: SEARCH_STR}}); @@ -42,8 +39,8 @@ it('Notifier does not open snackbar for valid query', () => { const nowStore = store.getState(); expect(nowStore.notify.msg).toEqual(''); - //Check for class name of Notifier Snack Bar. Present now. - expect(wrapper.find('.Notifier-root-120').hostNodes().length).toEqual(0); + //Check for id of Notifier Snack Bar. Present now. + expect(wrapper.find('#snackbar-message-id').length).toEqual(0); }); it('Notifier can open snackbar for empty query', () => { @@ -57,8 +54,8 @@ it('Notifier can open snackbar for empty query', () => { ); - //Check for class name of Notifier Snack Bar. Initially not present. - expect(wrapper.find('.Notifier-root-120').hostNodes().length).toEqual(0); + //Check for id of Notifier Snack Bar. Initially not present. + expect(wrapper.find('#snackbar-message-id').length).toEqual(0); let input = wrapper.find('input').last(); input.simulate('change', { target: { value: SEARCH_STR}}); @@ -69,8 +66,8 @@ it('Notifier can open snackbar for empty query', () => { const nowStore = store.getState(); expect(nowStore.notify.msg).toEqual(QUERY_LEN_ERR); - //Check for class name of Notifier Snack Bar. Present now. - expect(wrapper.find('.Notifier-root-120').hostNodes().length).toEqual(1); + //Check for id of Notifier Snack Bar. Present now. + expect(wrapper.find('#snackbar-message-id').length).toEqual(1); }); it('Notifier can open snackbar for short query', () => { @@ -84,8 +81,8 @@ it('Notifier can open snackbar for short query', () => { ); - //Check for class name of Notifier Snack Bar. Initially not present. - expect(wrapper.find('.Notifier-root-120').hostNodes().length).toEqual(0); + //Check for id of Notifier Snack Bar. Initially not present. + expect(wrapper.find('#snackbar-message-id').length).toEqual(0); let input = wrapper.find('input').last(); input.simulate('change', { target: { value: SEARCH_STR}}); @@ -96,8 +93,8 @@ it('Notifier can open snackbar for short query', () => { const nowStore = store.getState(); expect(nowStore.notify.msg).toEqual(QUERY_LEN_ERR); - //Check for class name of Notifier Snack Bar. Present now. - expect(wrapper.find('.Notifier-root-120').hostNodes().length).toEqual(1); + //Check for id of Notifier Snack Bar. Present now. + expect(wrapper.find('#snackbar-message-id').length).toEqual(1); }); it('Notifier can open snackbar for errors returned from httpService', () => { @@ -109,8 +106,8 @@ it('Notifier can open snackbar for errors returned from httpService', () => { ); - //Check for class name of Notifier Snack Bar. Initially not present. - expect(wrapper.find('.Notifier-root-120').hostNodes().length).toEqual(0); + //Check for id of Notifier Snack Bar. Initially not present. + expect(wrapper.find('#snackbar-message-id').length).toEqual(0); let dummyUrl = "http://katlas.com/v1/qsl"; let dummyParams = {qslstring: 'Cluster'}; @@ -152,8 +149,8 @@ it('Notifier can close snackbar after AutoHideDuration', () => { ); - //Check for class name of Notifier Snack Bar. Initially not present. - expect(wrapper.find('.Notifier-root-120').hostNodes().length).toEqual(0); + //Check for id of Notifier Snack Bar. Initially not present. + expect(wrapper.find('#snackbar-message-id').length).toEqual(0); input = wrapper.find('input').last(); input.simulate('change', { target: { value: SEARCH_STR}}); @@ -164,12 +161,13 @@ it('Notifier can close snackbar after AutoHideDuration', () => { const nowStore = store.getState(); expect(nowStore.notify.msg).toEqual(QUERY_LEN_ERR); - //Check for class name of Notifier Snack Bar. Present now. - expect(wrapper.find('.Notifier-root-120').hostNodes().length).toEqual(1); + //Check for id of Notifier Snack Bar. Present now. + expect(wrapper.find('#snackbar-message-id').length).toEqual(1); sleep(AutoHideDuration).then(() => { - //Check for class name of Notifier Snack Bar. Not present now as we are over the AutoHideDuration. - expect(wrapper.find('.Notifier-root-120').hostNodes().length).toEqual(0); + //Check for id of Notifier Snack Bar. Not present now as we are over the AutoHideDuration. + //TODO:SS - this expectation isn't actually being executed during the lifespan of the test, if I introduce the "done()" async feature of the test, it exceeds jest timeout. overall, I think you'll need to fix the test to simulate the time rather than actually sleep for it + expect(wrapper.find('#snackbar-message-id').length).toEqual(0); }); }); From 5649993f388c1f5605e83cf587e7b05ae9b93307 Mon Sep 17 00:00:00 2001 From: David Masselink Date: Tue, 26 Mar 2019 17:18:43 -0700 Subject: [PATCH 14/15] #153 - fix issue of incorrectly styled splitter handle --- app/package.json | 2 +- app/public/manifest.json | 2 +- app/src/components/graph/GraphContainer.js | 1 + app/src/components/home/Home.js | 2 +- app/src/components/results/Results.css | 6 - app/src/components/results/Results.js | 3 +- app/yarn.lock | 493 +++++++++++---------- 7 files changed, 269 insertions(+), 240 deletions(-) delete mode 100644 app/src/components/results/Results.css diff --git a/app/package.json b/app/package.json index 939d97f..7243769 100644 --- a/app/package.json +++ b/app/package.json @@ -21,7 +21,7 @@ "react-redux": "^5.1.1", "react-router-dom": "^4.3.1", "react-scripts": "2.1.8", - "react-splitter-layout": "^3.0.1", + "react-splitter-layout": "davemasselink/react-splitter-layout", "react-test-renderer": "^16.6.3", "redux": "^4.0.1", "redux-mock-store": "^1.5.3", diff --git a/app/public/manifest.json b/app/public/manifest.json index 50ef275..5f7ad04 100644 --- a/app/public/manifest.json +++ b/app/public/manifest.json @@ -4,7 +4,7 @@ "icons": [ { "src": "favicon.ico", - "sizes": "64x64 32x32 24x24 16x16", + "sizes": "32x32 24x24 16x16", "type": "image/x-icon" } ], diff --git a/app/src/components/graph/GraphContainer.js b/app/src/components/graph/GraphContainer.js index 752588d..88d5e42 100644 --- a/app/src/components/graph/GraphContainer.js +++ b/app/src/components/graph/GraphContainer.js @@ -6,6 +6,7 @@ import { withRouter } from 'react-router-dom'; import { withStyles } from '@material-ui/core/styles'; import CircularProgress from '@material-ui/core/CircularProgress'; import SplitterLayout from 'react-splitter-layout'; +import 'react-splitter-layout/lib/index.css'; import "./Graph.css"; import Graph from './Graph'; diff --git a/app/src/components/home/Home.js b/app/src/components/home/Home.js index 750b6c2..c384299 100644 --- a/app/src/components/home/Home.js +++ b/app/src/components/home/Home.js @@ -83,7 +83,7 @@ class Home extends React.Component {
.layout-splitter { - width: 12px; - background-image: url("./grabIcon.png"); - background-position: center; - background-repeat: no-repeat; -} \ No newline at end of file diff --git a/app/src/components/results/Results.js b/app/src/components/results/Results.js index 94a5541..ba445a9 100644 --- a/app/src/components/results/Results.js +++ b/app/src/components/results/Results.js @@ -9,6 +9,7 @@ import SearchIcon from '@material-ui/icons/Search'; import IconButton from '@material-ui/core/IconButton'; import InputAdornment from '@material-ui/core/InputAdornment'; import SplitterLayout from 'react-splitter-layout'; +import 'react-splitter-layout/lib/index.css'; import { ENTER_KEYCODE } from '../../config/appConfig'; import ResultList from './ResultList'; @@ -16,8 +17,6 @@ import EntityDetails from '../entityDetails/EntityDetails'; import * as queryActions from '../../actions/queryActions'; import { getQueryParam } from '../../utils/url'; -import './Results.css'; - const styles = theme => ({ progress: { margin: theme.spacing.unit * 2 diff --git a/app/yarn.lock b/app/yarn.lock index 22ea589..eb7aebe 100644 --- a/app/yarn.lock +++ b/app/yarn.lock @@ -30,17 +30,17 @@ source-map "^0.5.0" "@babel/core@^7.1.6": - version "7.3.4" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.3.4.tgz#921a5a13746c21e32445bf0798680e9d11a6530b" - integrity sha512-jRsuseXBo9pN197KnDwhhaaBzyZr2oIcLHHTt2oDdQrej5Qp57dCCJafWx5ivU8/alEYDpssYqv1MUqcxwQlrA== + version "7.4.0" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.4.0.tgz#248fd6874b7d755010bfe61f557461d4f446d9e9" + integrity sha512-Dzl7U0/T69DFOTwqz/FJdnOSWS57NpjNfCwMKHABr589Lg8uX1RrlBIJ7L5Dubt/xkLsx0xH5EBFzlBVes1ayA== dependencies: "@babel/code-frame" "^7.0.0" - "@babel/generator" "^7.3.4" - "@babel/helpers" "^7.2.0" - "@babel/parser" "^7.3.4" - "@babel/template" "^7.2.2" - "@babel/traverse" "^7.3.4" - "@babel/types" "^7.3.4" + "@babel/generator" "^7.4.0" + "@babel/helpers" "^7.4.0" + "@babel/parser" "^7.4.0" + "@babel/template" "^7.4.0" + "@babel/traverse" "^7.4.0" + "@babel/types" "^7.4.0" convert-source-map "^1.1.0" debug "^4.1.0" json5 "^2.1.0" @@ -49,12 +49,12 @@ semver "^5.4.1" source-map "^0.5.0" -"@babel/generator@^7.2.2", "@babel/generator@^7.3.4": - version "7.3.4" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.3.4.tgz#9aa48c1989257877a9d971296e5b73bfe72e446e" - integrity sha512-8EXhHRFqlVVWXPezBW5keTiQi/rJMQTg/Y9uVCEZ0CAF3PKtCCaVRnp64Ii1ujhkoDhhF1fVsImoN4yJ2uz4Wg== +"@babel/generator@^7.2.2", "@babel/generator@^7.4.0": + version "7.4.0" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.4.0.tgz#c230e79589ae7a729fd4631b9ded4dc220418196" + integrity sha512-/v5I+a1jhGSKLgZDcmAUZ4K/VePi43eRkUs3yePW1HB1iANOD5tqJXwGSG4BZhSksP8J9ejSlwGeTiiOFZOrXQ== dependencies: - "@babel/types" "^7.3.4" + "@babel/types" "^7.4.0" jsesc "^2.5.1" lodash "^4.17.11" source-map "^0.5.0" @@ -83,35 +83,35 @@ "@babel/types" "^7.3.0" esutils "^2.0.0" -"@babel/helper-call-delegate@^7.1.0": - version "7.1.0" - resolved "https://registry.yarnpkg.com/@babel/helper-call-delegate/-/helper-call-delegate-7.1.0.tgz#6a957f105f37755e8645343d3038a22e1449cc4a" - integrity sha512-YEtYZrw3GUK6emQHKthltKNZwszBcHK58Ygcis+gVUrF4/FmTVr5CCqQNSfmvg2y+YDEANyYoaLz/SHsnusCwQ== +"@babel/helper-call-delegate@^7.4.0": + version "7.4.0" + resolved "https://registry.yarnpkg.com/@babel/helper-call-delegate/-/helper-call-delegate-7.4.0.tgz#f308eabe0d44f451217853aedf4dea5f6fe3294f" + integrity sha512-SdqDfbVdNQCBp3WhK2mNdDvHd3BD6qbmIc43CAyjnsfCmgHMeqgDcM3BzY2lchi7HBJGJ2CVdynLWbezaE4mmQ== dependencies: - "@babel/helper-hoist-variables" "^7.0.0" - "@babel/traverse" "^7.1.0" - "@babel/types" "^7.0.0" + "@babel/helper-hoist-variables" "^7.4.0" + "@babel/traverse" "^7.4.0" + "@babel/types" "^7.4.0" "@babel/helper-create-class-features-plugin@^7.3.0": - version "7.3.4" - resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.3.4.tgz#092711a7a3ad8ea34de3e541644c2ce6af1f6f0c" - integrity sha512-uFpzw6L2omjibjxa8VGZsJUPL5wJH0zzGKpoz0ccBkzIa6C8kWNUbiBmQ0rgOKWlHJ6qzmfa6lTiGchiV8SC+g== + version "7.4.0" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.4.0.tgz#30fd090e059d021995c1762a5b76798fa0b51d82" + integrity sha512-2K8NohdOT7P6Vyp23QH4w2IleP8yG3UJsbRKwA4YP6H8fErcLkFuuEEqbF2/BYBKSNci/FWJiqm6R3VhM/QHgw== dependencies: "@babel/helper-function-name" "^7.1.0" "@babel/helper-member-expression-to-functions" "^7.0.0" "@babel/helper-optimise-call-expression" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0" - "@babel/helper-replace-supers" "^7.3.4" - "@babel/helper-split-export-declaration" "^7.0.0" + "@babel/helper-replace-supers" "^7.4.0" + "@babel/helper-split-export-declaration" "^7.4.0" -"@babel/helper-define-map@^7.1.0": - version "7.1.0" - resolved "https://registry.yarnpkg.com/@babel/helper-define-map/-/helper-define-map-7.1.0.tgz#3b74caec329b3c80c116290887c0dd9ae468c20c" - integrity sha512-yPPcW8dc3gZLN+U1mhYV91QU3n5uTbx7DUdf8NnPbjS0RMwBuHi9Xt2MUgppmNz7CJxTBWsGczTiEp1CSOTPRg== +"@babel/helper-define-map@^7.1.0", "@babel/helper-define-map@^7.4.0": + version "7.4.0" + resolved "https://registry.yarnpkg.com/@babel/helper-define-map/-/helper-define-map-7.4.0.tgz#cbfd8c1b2f12708e262c26f600cd16ed6a3bc6c9" + integrity sha512-wAhQ9HdnLIywERVcSvX40CEJwKdAa1ID4neI9NXQPDOHwwA+57DqwLiPEVy2AIyWzAk0CQ8qx4awO0VUURwLtA== dependencies: "@babel/helper-function-name" "^7.1.0" - "@babel/types" "^7.0.0" - lodash "^4.17.10" + "@babel/types" "^7.4.0" + lodash "^4.17.11" "@babel/helper-explode-assignable-expression@^7.1.0": version "7.1.0" @@ -137,12 +137,12 @@ dependencies: "@babel/types" "^7.0.0" -"@babel/helper-hoist-variables@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.0.0.tgz#46adc4c5e758645ae7a45deb92bab0918c23bb88" - integrity sha512-Ggv5sldXUeSKsuzLkddtyhyHe2YantsxWKNi7A+7LeD12ExRDWTRk29JCXpaHPAbMaIPZSil7n+lq78WY2VY7w== +"@babel/helper-hoist-variables@^7.4.0": + version "7.4.0" + resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.4.0.tgz#25b621399ae229869329730a62015bbeb0a6fbd6" + integrity sha512-/NErCuoe/et17IlAQFKWM24qtyYYie7sFIrW/tIQXpck6vAu2hhtYYsKLBWQV+BQZMbcIYPU/QMYuTufrY4aQw== dependencies: - "@babel/types" "^7.0.0" + "@babel/types" "^7.4.0" "@babel/helper-member-expression-to-functions@^7.0.0": version "7.0.0" @@ -200,15 +200,15 @@ "@babel/traverse" "^7.1.0" "@babel/types" "^7.0.0" -"@babel/helper-replace-supers@^7.1.0", "@babel/helper-replace-supers@^7.3.4": - version "7.3.4" - resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.3.4.tgz#a795208e9b911a6eeb08e5891faacf06e7013e13" - integrity sha512-pvObL9WVf2ADs+ePg0jrqlhHoxRXlOa+SHRHzAXIz2xkYuOHfGl+fKxPMaS4Fq+uje8JQPobnertBBvyrWnQ1A== +"@babel/helper-replace-supers@^7.1.0", "@babel/helper-replace-supers@^7.4.0": + version "7.4.0" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.4.0.tgz#4f56adb6aedcd449d2da9399c2dcf0545463b64c" + integrity sha512-PVwCVnWWAgnal+kJ+ZSAphzyl58XrFeSKSAJRiqg5QToTsjL+Xu1f9+RJ+d+Q0aPhPfBGaYfkox66k86thxNSg== dependencies: "@babel/helper-member-expression-to-functions" "^7.0.0" "@babel/helper-optimise-call-expression" "^7.0.0" - "@babel/traverse" "^7.3.4" - "@babel/types" "^7.3.4" + "@babel/traverse" "^7.4.0" + "@babel/types" "^7.4.0" "@babel/helper-simple-access@^7.1.0": version "7.1.0" @@ -218,12 +218,12 @@ "@babel/template" "^7.1.0" "@babel/types" "^7.0.0" -"@babel/helper-split-export-declaration@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.0.0.tgz#3aae285c0311c2ab095d997b8c9a94cad547d813" - integrity sha512-MXkOJqva62dfC0w85mEf/LucPPS/1+04nmmRMPEBUB++hiiThQ2zPtX/mEWQ3mtzCEjIJvPY8nuwxXtQeQwUag== +"@babel/helper-split-export-declaration@^7.0.0", "@babel/helper-split-export-declaration@^7.4.0": + version "7.4.0" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.4.0.tgz#571bfd52701f492920d63b7f735030e9a3e10b55" + integrity sha512-7Cuc6JZiYShaZnybDmfwhY4UYHzI6rlqhWjaIqbsJGsIqPimEYy5uh3akSRLMg65LSdSEnJ8a8/bWQN6u2oMGw== dependencies: - "@babel/types" "^7.0.0" + "@babel/types" "^7.4.0" "@babel/helper-wrap-function@^7.1.0": version "7.2.0" @@ -235,14 +235,14 @@ "@babel/traverse" "^7.1.0" "@babel/types" "^7.2.0" -"@babel/helpers@^7.2.0": - version "7.3.1" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.3.1.tgz#949eec9ea4b45d3210feb7dc1c22db664c9e44b9" - integrity sha512-Q82R3jKsVpUV99mgX50gOPCWwco9Ec5Iln/8Vyu4osNIOQgSrd9RFrQeUvmvddFNoLwMyOUWU+5ckioEKpDoGA== +"@babel/helpers@^7.2.0", "@babel/helpers@^7.4.0": + version "7.4.2" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.4.2.tgz#3bdfa46a552ca77ef5a0f8551be5f0845ae989be" + integrity sha512-gQR1eQeroDzFBikhrCccm5Gs2xBjZ57DNjGbqTaHo911IpmSxflOQWMAHPw/TXk8L3isv7s9lYzUkexOeTQUYg== dependencies: - "@babel/template" "^7.1.2" - "@babel/traverse" "^7.1.5" - "@babel/types" "^7.3.0" + "@babel/template" "^7.4.0" + "@babel/traverse" "^7.4.0" + "@babel/types" "^7.4.0" "@babel/highlight@^7.0.0": version "7.0.0" @@ -253,10 +253,10 @@ esutils "^2.0.2" js-tokens "^4.0.0" -"@babel/parser@^7.0.0", "@babel/parser@^7.2.2", "@babel/parser@^7.3.4": - version "7.3.4" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.3.4.tgz#a43357e4bbf4b92a437fb9e465c192848287f27c" - integrity sha512-tXZCqWtlOOP4wgCp6RjRvLmfuhnqTLy9VHwRochJBCP2nDm27JnnuFEnXFASVyQNHk36jD1tAammsCEEqgscIQ== +"@babel/parser@^7.0.0", "@babel/parser@^7.2.2", "@babel/parser@^7.4.0": + version "7.4.2" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.4.2.tgz#b4521a400cb5a871eab3890787b4bc1326d38d91" + integrity sha512-9fJTDipQFvlfSVdD/JBtkiY0br9BtfvW2R8wo6CX/Ej2eMuV0gWPk1M67Mt3eggQvBqYW1FCEk8BN7WvGm/g5g== "@babel/plugin-proposal-async-generator-functions@^7.2.0": version "7.2.0" @@ -300,10 +300,10 @@ "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-syntax-object-rest-spread" "^7.2.0" -"@babel/plugin-proposal-object-rest-spread@^7.3.1", "@babel/plugin-proposal-object-rest-spread@^7.3.4": - version "7.3.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.3.4.tgz#47f73cf7f2a721aad5c0261205405c642e424654" - integrity sha512-j7VQmbbkA+qrzNqbKHrBsW3ddFnOeva6wzSe/zB7T+xaxGc+RCpwo44wCmRixAIGRoIpmVgvzFzNJqQcO3/9RA== +"@babel/plugin-proposal-object-rest-spread@^7.3.1", "@babel/plugin-proposal-object-rest-spread@^7.4.0": + version "7.4.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.4.0.tgz#e4960575205eadf2a1ab4e0c79f9504d5b82a97f" + integrity sha512-uTNi8pPYyUH2eWHyYWWSYJKwKg34hhgl4/dbejEjL+64OhbHjTX7wEVWMQl82tEmdDsGeu77+s8HHLS627h6OQ== dependencies: "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-syntax-object-rest-spread" "^7.2.0" @@ -316,14 +316,14 @@ "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-syntax-optional-catch-binding" "^7.2.0" -"@babel/plugin-proposal-unicode-property-regex@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.2.0.tgz#abe7281fe46c95ddc143a65e5358647792039520" - integrity sha512-LvRVYb7kikuOtIoUeWTkOxQEV1kYvL5B6U3iWEGCzPNRus1MzJweFqORTj+0jkxozkTSYNJozPOddxmqdqsRpw== +"@babel/plugin-proposal-unicode-property-regex@^7.2.0", "@babel/plugin-proposal-unicode-property-regex@^7.4.0": + version "7.4.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.4.0.tgz#202d91ee977d760ef83f4f416b280d568be84623" + integrity sha512-h/KjEZ3nK9wv1P1FSNb9G079jXrNYR0Ko+7XkOx85+gM24iZbPn0rh4vCftk+5QKY7y1uByFataBTmX7irEF1w== dependencies: "@babel/helper-plugin-utils" "^7.0.0" "@babel/helper-regex" "^7.0.0" - regexpu-core "^4.2.0" + regexpu-core "^4.5.4" "@babel/plugin-syntax-async-generators@^7.2.0": version "7.2.0" @@ -395,10 +395,10 @@ dependencies: "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-async-to-generator@^7.2.0", "@babel/plugin-transform-async-to-generator@^7.3.4": - version "7.3.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.3.4.tgz#4e45408d3c3da231c0e7b823f407a53a7eb3048c" - integrity sha512-Y7nCzv2fw/jEZ9f678MuKdMo99MFDJMT/PvD9LisrR5JDFcJH6vYeH6RnjVt3p5tceyGRvTtEN0VOlU+rgHZjA== +"@babel/plugin-transform-async-to-generator@^7.2.0", "@babel/plugin-transform-async-to-generator@^7.4.0": + version "7.4.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.4.0.tgz#234fe3e458dce95865c0d152d256119b237834b0" + integrity sha512-EeaFdCeUULM+GPFEsf7pFcNSxM7hYjoj5fiYbyuiXobW4JhFnjAv9OWzNwHyHcKoPNpAfeRDuW6VyaXEDUBa7g== dependencies: "@babel/helper-module-imports" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0" @@ -411,10 +411,10 @@ dependencies: "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-block-scoping@^7.2.0", "@babel/plugin-transform-block-scoping@^7.3.4": - version "7.3.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.3.4.tgz#5c22c339de234076eee96c8783b2fed61202c5c4" - integrity sha512-blRr2O8IOZLAOJklXLV4WhcEzpYafYQKSGT3+R26lWG41u/FODJuBggehtOwilVAcFu393v3OFj+HmaE6tVjhA== +"@babel/plugin-transform-block-scoping@^7.2.0", "@babel/plugin-transform-block-scoping@^7.4.0": + version "7.4.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.4.0.tgz#164df3bb41e3deb954c4ca32ffa9fcaa56d30bcb" + integrity sha512-AWyt3k+fBXQqt2qb9r97tn3iBwFpiv9xdAiG+Gr2HpAZpuayvbL55yWrsV3MyHvXk/4vmSiedhDRl1YI2Iy5nQ== dependencies: "@babel/helper-plugin-utils" "^7.0.0" lodash "^4.17.11" @@ -433,18 +433,18 @@ "@babel/helper-split-export-declaration" "^7.0.0" globals "^11.1.0" -"@babel/plugin-transform-classes@^7.2.0", "@babel/plugin-transform-classes@^7.3.4": - version "7.3.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.3.4.tgz#dc173cb999c6c5297e0b5f2277fdaaec3739d0cc" - integrity sha512-J9fAvCFBkXEvBimgYxCjvaVDzL6thk0j0dBvCeZmIUDBwyt+nv6HfbImsSrWsYXfDNDivyANgJlFXDUWRTZBuA== +"@babel/plugin-transform-classes@^7.2.0", "@babel/plugin-transform-classes@^7.4.0": + version "7.4.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.4.0.tgz#e3428d3c8a3d01f33b10c529b998ba1707043d4d" + integrity sha512-XGg1Mhbw4LDmrO9rSTNe+uI79tQPdGs0YASlxgweYRLZqo/EQktjaOV4tchL/UZbM0F+/94uOipmdNGoaGOEYg== dependencies: "@babel/helper-annotate-as-pure" "^7.0.0" - "@babel/helper-define-map" "^7.1.0" + "@babel/helper-define-map" "^7.4.0" "@babel/helper-function-name" "^7.1.0" "@babel/helper-optimise-call-expression" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0" - "@babel/helper-replace-supers" "^7.3.4" - "@babel/helper-split-export-declaration" "^7.0.0" + "@babel/helper-replace-supers" "^7.4.0" + "@babel/helper-split-export-declaration" "^7.4.0" globals "^11.1.0" "@babel/plugin-transform-computed-properties@^7.2.0": @@ -454,13 +454,20 @@ dependencies: "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-destructuring@7.3.2", "@babel/plugin-transform-destructuring@^7.2.0": +"@babel/plugin-transform-destructuring@7.3.2": version "7.3.2" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.3.2.tgz#f2f5520be055ba1c38c41c0e094d8a461dd78f2d" integrity sha512-Lrj/u53Ufqxl/sGxyjsJ2XNtNuEjDyjpqdhMNh5aZ+XFOdThL46KBj27Uem4ggoezSYBxKWAil6Hu8HtwqesYw== dependencies: "@babel/helper-plugin-utils" "^7.0.0" +"@babel/plugin-transform-destructuring@^7.2.0", "@babel/plugin-transform-destructuring@^7.4.0": + version "7.4.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.4.0.tgz#acbb9b2418d290107db333f4d6cd8aa6aea00343" + integrity sha512-HySkoatyYTY3ZwLI8GGvkRWCFrjAGXUHur5sMecmCIdIharnlcWWivOqDJI76vvmVZfzwb6G08NREsrY96RhGQ== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-transform-dotall-regex@^7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.2.0.tgz#f0aabb93d120a8ac61e925ea0ba440812dbe0e49" @@ -493,10 +500,10 @@ "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-syntax-flow" "^7.2.0" -"@babel/plugin-transform-for-of@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.2.0.tgz#ab7468befa80f764bb03d3cb5eef8cc998e1cad9" - integrity sha512-Kz7Mt0SsV2tQk6jG5bBv5phVbkd0gd27SgYD4hH1aLMJRchM0dzHaXvrWhVZ+WxAlDoAKZ7Uy3jVTW2mKXQ1WQ== +"@babel/plugin-transform-for-of@^7.2.0", "@babel/plugin-transform-for-of@^7.4.0": + version "7.4.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.4.0.tgz#56c8c36677f5d4a16b80b12f7b768de064aaeb5f" + integrity sha512-vWdfCEYLlYSxbsKj5lGtzA49K3KANtb8qCPQ1em07txJzsBwY+cKJzBHizj5fl3CCx7vt+WPdgDLTHmydkbQSQ== dependencies: "@babel/helper-plugin-utils" "^7.0.0" @@ -523,21 +530,21 @@ "@babel/helper-module-transforms" "^7.1.0" "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-modules-commonjs@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.2.0.tgz#c4f1933f5991d5145e9cfad1dfd848ea1727f404" - integrity sha512-V6y0uaUQrQPXUrmj+hgnks8va2L0zcZymeU7TtWEgdRLNkceafKXEduv7QzgQAE4lT+suwooG9dC7LFhdRAbVQ== +"@babel/plugin-transform-modules-commonjs@^7.2.0", "@babel/plugin-transform-modules-commonjs@^7.4.0": + version "7.4.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.4.0.tgz#3b8ec61714d3b75d20c5ccfa157f2c2e087fd4ca" + integrity sha512-iWKAooAkipG7g1IY0eah7SumzfnIT3WNhT4uYB2kIsvHnNSB6MDYVa5qyICSwaTBDBY2c4SnJ3JtEa6ltJd6Jw== dependencies: "@babel/helper-module-transforms" "^7.1.0" "@babel/helper-plugin-utils" "^7.0.0" "@babel/helper-simple-access" "^7.1.0" -"@babel/plugin-transform-modules-systemjs@^7.2.0", "@babel/plugin-transform-modules-systemjs@^7.3.4": - version "7.3.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.3.4.tgz#813b34cd9acb6ba70a84939f3680be0eb2e58861" - integrity sha512-VZ4+jlGOF36S7TjKs8g4ojp4MEI+ebCQZdswWb/T9I4X84j8OtFAyjXjt/M16iIm5RIZn0UMQgg/VgIwo/87vw== +"@babel/plugin-transform-modules-systemjs@^7.2.0", "@babel/plugin-transform-modules-systemjs@^7.4.0": + version "7.4.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.4.0.tgz#c2495e55528135797bc816f5d50f851698c586a1" + integrity sha512-gjPdHmqiNhVoBqus5qK60mWPp1CmYWp/tkh11mvb0rrys01HycEGD7NvvSoKXlWEfSM9TcL36CpsK8ElsADptQ== dependencies: - "@babel/helper-hoist-variables" "^7.0.0" + "@babel/helper-hoist-variables" "^7.4.0" "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-transform-modules-umd@^7.2.0": @@ -548,17 +555,17 @@ "@babel/helper-module-transforms" "^7.1.0" "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-named-capturing-groups-regex@^7.3.0": - version "7.3.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.3.0.tgz#140b52985b2d6ef0cb092ef3b29502b990f9cd50" - integrity sha512-NxIoNVhk9ZxS+9lSoAQ/LM0V2UEvARLttEHUrRDGKFaAxOYQcrkN/nLRE+BbbicCAvZPl7wMP0X60HsHE5DtQw== +"@babel/plugin-transform-named-capturing-groups-regex@^7.3.0", "@babel/plugin-transform-named-capturing-groups-regex@^7.4.2": + version "7.4.2" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.4.2.tgz#800391136d6cbcc80728dbdba3c1c6e46f86c12e" + integrity sha512-NsAuliSwkL3WO2dzWTOL1oZJHm0TM8ZY8ZSxk2ANyKkt5SQlToGA4pzctmq1BEjoacurdwZ3xp2dCQWJkME0gQ== dependencies: regexp-tree "^0.1.0" -"@babel/plugin-transform-new-target@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.0.0.tgz#ae8fbd89517fa7892d20e6564e641e8770c3aa4a" - integrity sha512-yin069FYjah+LbqfGeTfzIBODex/e++Yfa0rH0fpfam9uTbuEeEOx5GLGr210ggOV77mVRNoeqSYqeuaqSzVSw== +"@babel/plugin-transform-new-target@^7.0.0", "@babel/plugin-transform-new-target@^7.4.0": + version "7.4.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.4.0.tgz#67658a1d944edb53c8d4fa3004473a0dd7838150" + integrity sha512-6ZKNgMQmQmrEX/ncuCwnnw1yVGoaOW5KpxNhoWI7pCQdA0uZ0HqHGqenCUIENAnxRjy2WwNQ30gfGdIgqJXXqw== dependencies: "@babel/helper-plugin-utils" "^7.0.0" @@ -570,12 +577,12 @@ "@babel/helper-plugin-utils" "^7.0.0" "@babel/helper-replace-supers" "^7.1.0" -"@babel/plugin-transform-parameters@^7.2.0": - version "7.3.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.3.3.tgz#3a873e07114e1a5bee17d04815662c8317f10e30" - integrity sha512-IrIP25VvXWu/VlBWTpsjGptpomtIkYrN/3aDp4UKm7xK6UxZY88kcJ1UwETbzHAlwN21MnNfwlar0u8y3KpiXw== +"@babel/plugin-transform-parameters@^7.2.0", "@babel/plugin-transform-parameters@^7.4.0": + version "7.4.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.4.0.tgz#a1309426fac4eecd2a9439a4c8c35124a11a48a9" + integrity sha512-Xqv6d1X+doyiuCGDoVJFtlZx0onAX0tnc3dY8w71pv/O0dODAbusVv2Ale3cGOwfiyi895ivOBhYa9DhAM8dUA== dependencies: - "@babel/helper-call-delegate" "^7.1.0" + "@babel/helper-call-delegate" "^7.4.0" "@babel/helper-get-function-arity" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0" @@ -619,10 +626,10 @@ "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-syntax-jsx" "^7.2.0" -"@babel/plugin-transform-regenerator@^7.0.0", "@babel/plugin-transform-regenerator@^7.3.4": - version "7.3.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.3.4.tgz#1601655c362f5b38eead6a52631f5106b29fa46a" - integrity sha512-hvJg8EReQvXT6G9H2MvNPXkv9zK36Vxa1+csAVTpE1J3j0zlHplw76uudEbJxgvqZzAq9Yh45FLD4pk5mKRFQA== +"@babel/plugin-transform-regenerator@^7.0.0", "@babel/plugin-transform-regenerator@^7.4.0": + version "7.4.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.4.0.tgz#0780e27ee458cc3fdbad18294d703e972ae1f6d1" + integrity sha512-SZ+CgL4F0wm4npojPU6swo/cK4FcbLgxLd4cWpHaNXY/NJ2dpahODCqBbAwb2rDmVszVb3SSjnk9/vik3AYdBw== dependencies: regenerator-transform "^0.13.4" @@ -674,9 +681,9 @@ "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-transform-typescript@^7.1.0": - version "7.3.2" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.3.2.tgz#59a7227163e55738842f043d9e5bd7c040447d96" - integrity sha512-Pvco0x0ZSCnexJnshMfaibQ5hnK8aUHSvjCQhC1JR8eeg+iBwt0AtCO7gWxJ358zZevuf9wPSO5rv+WJcbHPXQ== + version "7.4.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.4.0.tgz#0389ec53a34e80f99f708c4ca311181449a68eb1" + integrity sha512-U7/+zKnRZg04ggM/Bm+xmu2B/PrwyDQTT/V89FXWYWNMxBDwSx56u6jtk9SEbfLFbZaEI72L+5LPvQjeZgFCrQ== dependencies: "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-syntax-typescript" "^7.2.0" @@ -740,50 +747,52 @@ semver "^5.3.0" "@babel/preset-env@^7.1.6": - version "7.3.4" - resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.3.4.tgz#887cf38b6d23c82f19b5135298bdb160062e33e1" - integrity sha512-2mwqfYMK8weA0g0uBKOt4FE3iEodiHy9/CW0b+nWXcbL+pGzLx8ESYc+j9IIxr6LTDHWKgPm71i9smo02bw+gA== + version "7.4.2" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.4.2.tgz#2f5ba1de2daefa9dcca653848f96c7ce2e406676" + integrity sha512-OEz6VOZaI9LW08CWVS3d9g/0jZA6YCn1gsKIy/fut7yZCJti5Lm1/Hi+uo/U+ODm7g4I6gULrCP+/+laT8xAsA== dependencies: "@babel/helper-module-imports" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-proposal-async-generator-functions" "^7.2.0" "@babel/plugin-proposal-json-strings" "^7.2.0" - "@babel/plugin-proposal-object-rest-spread" "^7.3.4" + "@babel/plugin-proposal-object-rest-spread" "^7.4.0" "@babel/plugin-proposal-optional-catch-binding" "^7.2.0" - "@babel/plugin-proposal-unicode-property-regex" "^7.2.0" + "@babel/plugin-proposal-unicode-property-regex" "^7.4.0" "@babel/plugin-syntax-async-generators" "^7.2.0" "@babel/plugin-syntax-json-strings" "^7.2.0" "@babel/plugin-syntax-object-rest-spread" "^7.2.0" "@babel/plugin-syntax-optional-catch-binding" "^7.2.0" "@babel/plugin-transform-arrow-functions" "^7.2.0" - "@babel/plugin-transform-async-to-generator" "^7.3.4" + "@babel/plugin-transform-async-to-generator" "^7.4.0" "@babel/plugin-transform-block-scoped-functions" "^7.2.0" - "@babel/plugin-transform-block-scoping" "^7.3.4" - "@babel/plugin-transform-classes" "^7.3.4" + "@babel/plugin-transform-block-scoping" "^7.4.0" + "@babel/plugin-transform-classes" "^7.4.0" "@babel/plugin-transform-computed-properties" "^7.2.0" - "@babel/plugin-transform-destructuring" "^7.2.0" + "@babel/plugin-transform-destructuring" "^7.4.0" "@babel/plugin-transform-dotall-regex" "^7.2.0" "@babel/plugin-transform-duplicate-keys" "^7.2.0" "@babel/plugin-transform-exponentiation-operator" "^7.2.0" - "@babel/plugin-transform-for-of" "^7.2.0" + "@babel/plugin-transform-for-of" "^7.4.0" "@babel/plugin-transform-function-name" "^7.2.0" "@babel/plugin-transform-literals" "^7.2.0" "@babel/plugin-transform-modules-amd" "^7.2.0" - "@babel/plugin-transform-modules-commonjs" "^7.2.0" - "@babel/plugin-transform-modules-systemjs" "^7.3.4" + "@babel/plugin-transform-modules-commonjs" "^7.4.0" + "@babel/plugin-transform-modules-systemjs" "^7.4.0" "@babel/plugin-transform-modules-umd" "^7.2.0" - "@babel/plugin-transform-named-capturing-groups-regex" "^7.3.0" - "@babel/plugin-transform-new-target" "^7.0.0" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.4.2" + "@babel/plugin-transform-new-target" "^7.4.0" "@babel/plugin-transform-object-super" "^7.2.0" - "@babel/plugin-transform-parameters" "^7.2.0" - "@babel/plugin-transform-regenerator" "^7.3.4" + "@babel/plugin-transform-parameters" "^7.4.0" + "@babel/plugin-transform-regenerator" "^7.4.0" "@babel/plugin-transform-shorthand-properties" "^7.2.0" "@babel/plugin-transform-spread" "^7.2.0" "@babel/plugin-transform-sticky-regex" "^7.2.0" "@babel/plugin-transform-template-literals" "^7.2.0" "@babel/plugin-transform-typeof-symbol" "^7.2.0" "@babel/plugin-transform-unicode-regex" "^7.2.0" - browserslist "^4.3.4" + "@babel/types" "^7.4.0" + browserslist "^4.4.2" + core-js-compat "^3.0.0" invariant "^2.2.2" js-levenshtein "^1.1.3" semver "^5.3.0" @@ -815,40 +824,40 @@ regenerator-runtime "^0.12.0" "@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.2.0": - version "7.3.4" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.3.4.tgz#73d12ba819e365fcf7fd152aed56d6df97d21c83" - integrity sha512-IvfvnMdSaLBateu0jfsYIpZTxAc2cKEXEMiezGGN75QcBcecDUKd3PgLAncT0oOgxKy8dd8hrJKj9MfzgfZd6g== + version "7.4.2" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.4.2.tgz#f5ab6897320f16decd855eed70b705908a313fe8" + integrity sha512-7Bl2rALb7HpvXFL7TETNzKSAeBVCPHELzc0C//9FCxN8nsiueWSJBqaF+2oIJScyILStASR/Cx5WMkXGYTiJFA== dependencies: - regenerator-runtime "^0.12.0" + regenerator-runtime "^0.13.2" -"@babel/template@^7.1.0", "@babel/template@^7.1.2", "@babel/template@^7.2.2": - version "7.2.2" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.2.2.tgz#005b3fdf0ed96e88041330379e0da9a708eb2907" - integrity sha512-zRL0IMM02AUDwghf5LMSSDEz7sBCO2YnNmpg3uWTZj/v1rcG2BmQUvaGU8GhU8BvfMh1k2KIAYZ7Ji9KXPUg7g== +"@babel/template@^7.1.0", "@babel/template@^7.2.2", "@babel/template@^7.4.0": + version "7.4.0" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.4.0.tgz#12474e9c077bae585c5d835a95c0b0b790c25c8b" + integrity sha512-SOWwxxClTTh5NdbbYZ0BmaBVzxzTh2tO/TeLTbF6MO6EzVhHTnff8CdBXx3mEtazFBoysmEM6GU/wF+SuSx4Fw== dependencies: "@babel/code-frame" "^7.0.0" - "@babel/parser" "^7.2.2" - "@babel/types" "^7.2.2" + "@babel/parser" "^7.4.0" + "@babel/types" "^7.4.0" -"@babel/traverse@^7.0.0", "@babel/traverse@^7.1.0", "@babel/traverse@^7.1.5", "@babel/traverse@^7.2.2", "@babel/traverse@^7.3.4": - version "7.3.4" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.3.4.tgz#1330aab72234f8dea091b08c4f8b9d05c7119e06" - integrity sha512-TvTHKp6471OYEcE/91uWmhR6PrrYywQntCHSaZ8CM8Vmp+pjAusal4nGB2WCCQd0rvI7nOMKn9GnbcvTUz3/ZQ== +"@babel/traverse@^7.0.0", "@babel/traverse@^7.1.0", "@babel/traverse@^7.2.2", "@babel/traverse@^7.4.0": + version "7.4.0" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.4.0.tgz#14006967dd1d2b3494cdd650c686db9daf0ddada" + integrity sha512-/DtIHKfyg2bBKnIN+BItaIlEg5pjAnzHOIQe5w+rHAw/rg9g0V7T4rqPX8BJPfW11kt3koyjAnTNwCzb28Y1PA== dependencies: "@babel/code-frame" "^7.0.0" - "@babel/generator" "^7.3.4" + "@babel/generator" "^7.4.0" "@babel/helper-function-name" "^7.1.0" - "@babel/helper-split-export-declaration" "^7.0.0" - "@babel/parser" "^7.3.4" - "@babel/types" "^7.3.4" + "@babel/helper-split-export-declaration" "^7.4.0" + "@babel/parser" "^7.4.0" + "@babel/types" "^7.4.0" debug "^4.1.0" globals "^11.1.0" lodash "^4.17.11" -"@babel/types@^7.0.0", "@babel/types@^7.1.6", "@babel/types@^7.2.0", "@babel/types@^7.2.2", "@babel/types@^7.3.0", "@babel/types@^7.3.4": - version "7.3.4" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.3.4.tgz#bf482eaeaffb367a28abbf9357a94963235d90ed" - integrity sha512-WEkp8MsLftM7O/ty580wAmZzN1nDmCACc5+jFzUt+GUFNNIi3LdRlueYz0YIlmJhlZx1QYDMZL5vdWCL0fNjFQ== +"@babel/types@^7.0.0", "@babel/types@^7.1.6", "@babel/types@^7.2.0", "@babel/types@^7.2.2", "@babel/types@^7.3.0", "@babel/types@^7.4.0": + version "7.4.0" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.4.0.tgz#670724f77d24cce6cc7d8cf64599d511d164894c" + integrity sha512-aPvkXyU2SPOnztlgo8n9cEiXW755mgyvueUPcpStqdzoSPm0fjO0vQBjLkt3JKJW7ufikfcnMTTPsN1xaTsBPA== dependencies: esutils "^2.0.2" lodash "^4.17.11" @@ -1046,9 +1055,9 @@ indefinite-observable "^1.0.1" "@types/node@*": - version "11.11.3" - resolved "https://registry.yarnpkg.com/@types/node/-/node-11.11.3.tgz#7c6b0f8eaf16ae530795de2ad1b85d34bf2f5c58" - integrity sha512-wp6IOGu1lxsfnrD+5mX6qwSwWuqsdkKKxTN4aQc4wByHAKZJf9/D4KXPQ1POUjEbnCP5LMggB0OEFNY9OTsMqg== + version "11.12.0" + resolved "https://registry.yarnpkg.com/@types/node/-/node-11.12.0.tgz#ec5594728811dc2797e42396cfcdf786f2052c12" + integrity sha512-Lg00egj78gM+4aE0Erw05cuDbvX9sLJbaaPwwRtdCdAMnIudqrQZ0oZX98Ek0yiSK/A2nubHgJfvII/rTT2Dwg== "@types/prop-types@*": version "15.7.0" @@ -2148,12 +2157,12 @@ browserslist@4.4.1: electron-to-chromium "^1.3.103" node-releases "^1.1.3" -browserslist@^4.0.0, browserslist@^4.3.4, browserslist@^4.3.5, browserslist@^4.4.2: - version "4.5.1" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.5.1.tgz#2226cada1947b33f4cfcf7b608dcb519b6128106" - integrity sha512-/pPw5IAUyqaQXGuD5vS8tcbudyPZ241jk1W5pQBsGDfcjNQt7p8qxZhgMNuygDShte1PibLFexecWUPgmVLfrg== +browserslist@^4.0.0, browserslist@^4.3.4, browserslist@^4.3.5, browserslist@^4.4.2, browserslist@^4.5.1: + version "4.5.2" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.5.2.tgz#36ad281f040af684555a23c780f5c2081c752df0" + integrity sha512-zmJVLiKLrzko0iszd/V4SsjTaomFeoVzQGYYOYgRgsbh7WNh95RgDB0CmBdFWYs/3MyFSt69NypjL/h3iaddKQ== dependencies: - caniuse-lite "^1.0.30000949" + caniuse-lite "^1.0.30000951" electron-to-chromium "^1.3.116" node-releases "^1.1.11" @@ -2308,10 +2317,10 @@ caniuse-api@^3.0.0: lodash.memoize "^4.1.2" lodash.uniq "^4.5.0" -caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000918, caniuse-lite@^1.0.30000929, caniuse-lite@^1.0.30000947, caniuse-lite@^1.0.30000949: - version "1.0.30000950" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000950.tgz#8c559d66e332b34e919d1086cc6d29c1948856ae" - integrity sha512-Cs+4U9T0okW2ftBsCIHuEYXXkki7mjXmjCh4c6PzYShk04qDEr76/iC7KwhLoWoY65wcra1XOsRD+S7BptEb5A== +caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000918, caniuse-lite@^1.0.30000929, caniuse-lite@^1.0.30000947, caniuse-lite@^1.0.30000951: + version "1.0.30000953" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000953.tgz#8054c4e5c4aa69dc3269353a4a5e102909759dbb" + integrity sha512-2stdF/q5MZTDhQ6uC65HWbSgI9UMKbc7+HKvlwH5JBIslKoD/J9dvabP4J4Uiifu3NljbHj3iMpfYflLSNt09A== capture-exit@^1.2.0: version "1.2.0" @@ -2383,9 +2392,9 @@ cheerio@^1.0.0-rc.2: parse5 "^3.0.1" chokidar@^2.0.0, chokidar@^2.0.2, chokidar@^2.0.4: - version "2.1.2" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.2.tgz#9c23ea40b01638439e0513864d362aeacc5ad058" - integrity sha512-IwXUx0FXc5ibYmPC2XeEj5mpXoV66sR+t3jqu2NS2GYwCktt3KF1/Qqjws/NkegajBA4RbZ5+DDwlOiJsxDHEg== + version "2.1.5" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.5.tgz#0ae8434d962281a5f56c72869e79cb6d9d86ad4d" + integrity sha512-i0TprVWp+Kj4WRPtInjexJ8Q+BqTE909VpH8xVhXrJkoc5QC8VO9TryGOqTr+2hljzc1sC62t22h5tZePodM/A== dependencies: anymatch "^2.0.0" async-each "^1.0.1" @@ -2397,7 +2406,7 @@ chokidar@^2.0.0, chokidar@^2.0.2, chokidar@^2.0.4: normalize-path "^3.0.0" path-is-absolute "^1.0.0" readdirp "^2.2.1" - upath "^1.1.0" + upath "^1.1.1" optionalDependencies: fsevents "^1.2.7" @@ -2714,11 +2723,31 @@ copy-descriptor@^0.1.0: resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= +core-js-compat@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.0.0.tgz#cd9810b8000742535a4a43773866185e310bd4f7" + integrity sha512-W/Ppz34uUme3LmXWjMgFlYyGnbo1hd9JvA0LNQ4EmieqVjg2GPYbj3H6tcdP2QGPGWdRKUqZVbVKLNIFVs/HiA== + dependencies: + browserslist "^4.5.1" + core-js "3.0.0" + core-js-pure "3.0.0" + semver "^5.6.0" + +core-js-pure@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.0.0.tgz#a5679adb4875427c8c0488afc93e6f5b7125859b" + integrity sha512-yPiS3fQd842RZDgo/TAKGgS0f3p2nxssF1H65DIZvZv0Od5CygP8puHXn3IQiM/39VAvgCbdaMQpresrbGgt9g== + core-js@2.6.4: version "2.6.4" resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.4.tgz#b8897c062c4d769dd30a0ac5c73976c47f92ea0d" integrity sha512-05qQ5hXShcqGkPZpXEFLIpxayZscVD2kuMBZewxiIPPEagukO4mqgPA9CWhUvFBJfy3ODdK2p9xyHh7FTU9/7A== +core-js@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.0.0.tgz#a8dbfa978d29bfc263bfb66c556d0ca924c28957" + integrity sha512-WBmxlgH2122EzEJ6GH8o9L/FeoUKxxxZ6q6VUxoTlsE4EvbTWKJb447eyVxTEuq0LpXjlq/kCB2qgBvsYRkLvQ== + core-js@^1.0.0: version "1.2.7" resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636" @@ -2745,14 +2774,13 @@ cosmiconfig@^4.0.0: require-from-string "^2.0.1" cosmiconfig@^5.0.0, cosmiconfig@^5.0.5, cosmiconfig@^5.0.7: - version "5.1.0" - resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-5.1.0.tgz#6c5c35e97f37f985061cdf653f114784231185cf" - integrity sha512-kCNPvthka8gvLtzAxQXvWo4FxqRB+ftRZyPZNuab5ngvM9Y7yw7hbEysglptLgpkGX9nAOKTBVkHUAe8xtYR6Q== + version "5.2.0" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-5.2.0.tgz#45038e4d28a7fe787203aede9c25bca4a08b12c8" + integrity sha512-nxt+Nfc3JAqf4WIWd0jXLjTJZmsPLrA9DDc4nRw2KFJQJK7DNooqSXrNI7tzLG50CF8axczly5UV929tBmh/7g== dependencies: import-fresh "^2.0.0" is-directory "^0.3.1" - js-yaml "^3.9.0" - lodash.get "^4.4.2" + js-yaml "^3.13.0" parse-json "^4.0.0" create-ecdh@^4.0.0: @@ -3448,9 +3476,9 @@ ee-first@1.1.1: integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= electron-to-chromium@^1.3.103, electron-to-chromium@^1.3.116: - version "1.3.116" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.116.tgz#1dbfee6a592a0c14ade77dbdfe54fef86387d702" - integrity sha512-NKwKAXzur5vFCZYBHpdWjTMO8QptNLNP80nItkSIgUOapPAo9Uia+RvkCaZJtO7fhQaVElSvBPWEc2ku6cKsPA== + version "1.3.119" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.119.tgz#9a7770da667252aeb81f667853f67c2b26e00197" + integrity sha512-3mtqcAWa4HgG+Djh/oNXlPH0cOH6MmtwxN1nHSaReb9P0Vn51qYPqYwLeoSuAX9loU1wrOBhFbiX3CkeIxPfgg== elliptic@^6.0.0: version "6.4.1" @@ -5013,9 +5041,9 @@ identity-obj-proxy@3.0.0: harmony-reflect "^1.4.6" ieee754@^1.1.4: - version "1.1.12" - resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.12.tgz#50bf24e5b9c8bb98af4964c941cdb0918da7b60b" - integrity sha512-GguP+DRY+pJ3soyIiGPTvdiVXjZ+DbXOxGpXn3eMvNW4x4irjqXm4wHKscC+TfxSJ0yw/S1F24tqdMNsMZTiLA== + version "1.1.13" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84" + integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg== iferr@^0.1.5: version "0.1.5" @@ -6057,10 +6085,10 @@ js-tokens@^3.0.2: resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" integrity sha1-mGbfOVECEw449/mWvOtlRDIJwls= -js-yaml@^3.12.0, js-yaml@^3.7.0, js-yaml@^3.9.0: - version "3.12.2" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.12.2.tgz#ef1d067c5a9d9cb65bd72f285b5d8105c77f14fc" - integrity sha512-QHn/Lh/7HhZ/Twc7vJYQTkjuCa0kaCcDcjK5Zlk2rvnUpy7DxMJ23+Jc2dcyvltwQVg1nygAVlB2oRDFHoRS5Q== +js-yaml@^3.12.0, js-yaml@^3.13.0, js-yaml@^3.7.0, js-yaml@^3.9.0: + version "3.13.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.0.tgz#38ee7178ac0eea2c97ff6d96fff4b18c7d8cf98e" + integrity sha512-pZZoSxcCYco+DIKBTimr67J6Hy+EYGZDY/HCWC+iAEA9h1ByhMXAIVUXMcMFpOCxQ/xjXmPI2MkDL5HRm5eFrQ== dependencies: argparse "^1.0.7" esprima "^4.0.0" @@ -6442,11 +6470,6 @@ lodash.flow@^3.3.0: resolved "https://registry.yarnpkg.com/lodash.flow/-/lodash.flow-3.5.0.tgz#87bf40292b8cf83e4e8ce1a3ae4209e20071675a" integrity sha1-h79AKSuM+D5OjOGjrkIJ4gBxZ1o= -lodash.get@^4.4.2: - version "4.4.2" - resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" - integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk= - lodash.isequal@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" @@ -6905,9 +6928,9 @@ mute-stream@0.0.7: integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s= nan@^2.10.0, nan@^2.9.2: - version "2.13.1" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.13.1.tgz#a15bee3790bde247e8f38f1d446edcdaeb05f2dd" - integrity sha512-I6YB/YEuDeUZMmhscXKxGgZlFnhsn5y0hgOZBadkzfTRrZBtJDZeg6eQf7PYMIEclwmorTKK8GztsyOUSVBREA== + version "2.13.2" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.13.2.tgz#f51dc7ae66ba7d5d55e1e6d4d8092e802c9aefe7" + integrity sha512-TghvYc72wlMGMVMluVo9WRJc0mB8KxxF/gZ4YYFy7V2ZQX9l7rgbPg7vjS9mt6U5HXODVFVI2bOduCzwOMv/lw== nanomatch@^1.2.9: version "1.2.13" @@ -8612,9 +8635,9 @@ querystring@0.2.0: integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA= querystringify@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.1.0.tgz#7ded8dfbf7879dcc60d0a644ac6754b283ad17ef" - integrity sha512-sluvZZ1YiTLD5jsqZcDmFyV2EwToyXZBfpoVOmktMmW+VEnhgakFHnasVph65fOjGPTWN0Nw3+XQaSeMayr0kg== + version "2.1.1" + resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.1.1.tgz#60e5a5fd64a7f8bfa4d2ab2ed6fdf4c85bad154e" + integrity sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA== raf@3.4.1, raf@^3.4.0: version "3.4.1" @@ -8738,14 +8761,14 @@ react-dev-utils@^8.0.0: text-table "0.2.0" react-dom@^16.5.2: - version "16.8.4" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.8.4.tgz#1061a8e01a2b3b0c8160037441c3bf00a0e3bc48" - integrity sha512-Ob2wK7XG2tUDt7ps7LtLzGYYB6DXMCLj0G5fO6WeEICtT4/HdpOi7W/xLzZnR6RCG1tYza60nMdqtxzA8FaPJQ== + version "16.8.5" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.8.5.tgz#b3e37d152b49e07faaa8de41fdf562be3463335e" + integrity sha512-VIEIvZLpFafsfu4kgmftP5L8j7P1f0YThfVTrANMhZUFMDOsA6e0kfR6wxw/8xxKs4NB59TZYbxNdPCDW34x4w== dependencies: loose-envify "^1.1.0" object-assign "^4.1.1" prop-types "^15.6.2" - scheduler "^0.13.4" + scheduler "^0.13.5" react-error-overlay@^5.1.4: version "5.1.4" @@ -8761,10 +8784,10 @@ react-event-listener@^0.6.2: prop-types "^15.6.0" warning "^4.0.1" -react-is@^16.6.0, react-is@^16.6.3, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.4: - version "16.8.4" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.4.tgz#90f336a68c3a29a096a3d648ab80e87ec61482a2" - integrity sha512-PVadd+WaUDOAciICm/J1waJaSvgq+4rHE/K70j0PFqKhkTBsPv/82UGQJNXAngz1fOQLLxI6z1sEDmJDQhCTAA== +react-is@^16.6.0, react-is@^16.6.3, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.4, react-is@^16.8.5: + version "16.8.5" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.5.tgz#c54ac229dd66b5afe0de5acbe47647c3da692ff8" + integrity sha512-sudt2uq5P/2TznPV4Wtdi+Lnq3yaYW8LfvPKLM9BKD8jJNBkxMVyB0C9/GmVhLw7Jbdmndk/73n7XQGeN9A3QQ== react-json-view@^1.19.1: version "1.19.1" @@ -8873,20 +8896,19 @@ react-scripts@2.1.8: optionalDependencies: fsevents "1.2.4" -react-splitter-layout@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/react-splitter-layout/-/react-splitter-layout-3.0.1.tgz#c2e00e69b35d240ab7a44f395d41803c5f4b70ef" - integrity sha512-XfFQvrvnow2XKil07FKIT9NuapNgeuQwcAJMZDGHLd/EZ8Ag6fb5U9sSF+g5qIeIux63bDY8RScl63kYaEFu7w== +react-splitter-layout@davemasselink/react-splitter-layout: + version "4.0.0" + resolved "https://codeload.github.com/davemasselink/react-splitter-layout/tar.gz/c47302f517cd2a8103e0ef22c61c347b41953f12" react-test-renderer@^16.0.0-0, react-test-renderer@^16.6.3: - version "16.8.4" - resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.8.4.tgz#abee4c2c3bf967a8892a7b37f77370c5570d5329" - integrity sha512-jQ9Tf/ilIGSr55Cz23AZ/7H3ABEdo9oy2zF9nDHZyhLHDSLKuoILxw2ifpBfuuwQvj4LCoqdru9iZf7gwFH28A== + version "16.8.5" + resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.8.5.tgz#4cba7a8aad73f7e8a0bc4379a0fe21632886a563" + integrity sha512-/pFpHYQH4f35OqOae/DgOCXJDxBqD3K3akVfDhLgR0qYHoHjnICI/XS9QDwIhbrOFHWL7okVW9kKMaHuKvt2ng== dependencies: object-assign "^4.1.1" prop-types "^15.6.2" - react-is "^16.8.4" - scheduler "^0.13.4" + react-is "^16.8.5" + scheduler "^0.13.5" react-textarea-autosize@^6.1.0: version "6.1.0" @@ -8896,9 +8918,9 @@ react-textarea-autosize@^6.1.0: prop-types "^15.6.0" react-transition-group@^2.2.1: - version "2.6.1" - resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.6.1.tgz#abf4a95e2f13fb9ba83a970a896fedbc5c4856a2" - integrity sha512-9DHwCy0aOYEe35frlEN68N9ut/THDQBLnVoQuKTvzF4/s3tk7lqkefCqxK2Nv96fOh6JXk6tQtliygk6tl3bQA== + version "2.7.1" + resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.7.1.tgz#1fe6d54e811e8f9dfd329aa836b39d9cd16587cb" + integrity sha512-b0VJTzNRnXxRpCuxng6QJbAzmmrhBn1BZJfPPnHbH2PIo8msdkajqwtfdyGm/OypPXZNfAHKEqeN15wjMXrRJQ== dependencies: dom-helpers "^3.3.1" loose-envify "^1.4.0" @@ -8906,14 +8928,14 @@ react-transition-group@^2.2.1: react-lifecycles-compat "^3.0.4" react@^16.5.2: - version "16.8.4" - resolved "https://registry.yarnpkg.com/react/-/react-16.8.4.tgz#fdf7bd9ae53f03a9c4cd1a371432c206be1c4768" - integrity sha512-0GQ6gFXfUH7aZcjGVymlPOASTuSjlQL4ZtVC5YKH+3JL6bBLCVO21DknzmaPlI90LN253ojj02nsapy+j7wIjg== + version "16.8.5" + resolved "https://registry.yarnpkg.com/react/-/react-16.8.5.tgz#49be3b655489d74504ad994016407e8a0445de66" + integrity sha512-daCb9TD6FZGvJ3sg8da1tRAtIuw29PbKZW++NN4wqkbEvxL+bZpaaYb4xuftW/SpXmgacf1skXl/ddX6CdOlDw== dependencies: loose-envify "^1.1.0" object-assign "^4.1.1" prop-types "^15.6.2" - scheduler "^0.13.4" + scheduler "^0.13.5" read-pkg-up@^1.0.1: version "1.0.1" @@ -9061,6 +9083,11 @@ regenerator-runtime@^0.12.0: resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz#fa1a71544764c036f8c49b13a08b2594c9f8a0de" integrity sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg== +regenerator-runtime@^0.13.2: + version "0.13.2" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.2.tgz#32e59c9a6fb9b1a4aff09b4930ca2d4477343447" + integrity sha512-S/TQAZJO+D3m9xeN1WTI8dLKBBiRgXBlTJvbWjCThHWZj9EvHK70Ff50/tYj2J/fvBY6JtFVwRuazHN2E7M9BA== + regenerator-transform@^0.13.4: version "0.13.4" resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.13.4.tgz#18f6763cf1382c69c36df76c6ce122cc694284fb" @@ -9102,7 +9129,7 @@ regexpu-core@^1.0.0: regjsgen "^0.2.0" regjsparser "^0.1.4" -regexpu-core@^4.1.3, regexpu-core@^4.2.0: +regexpu-core@^4.1.3, regexpu-core@^4.5.4: version "4.5.4" resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.5.4.tgz#080d9d02289aa87fe1667a4f5136bc98a6aebaae" integrity sha512-BtizvGtFQKGPUcTy56o3nk1bGRp4SZOTYrDtGNlqCQufptV5IkkLN6Emw+yunAJjzf+C9FQFtvq7IoA3+oMYHQ== @@ -9423,10 +9450,10 @@ sax@^1.2.4, sax@~1.2.4: resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== -scheduler@^0.13.4: - version "0.13.4" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.13.4.tgz#8fef05e7a3580c76c0364d2df5e550e4c9140298" - integrity sha512-cvSOlRPxOHs5dAhP9yiS/6IDmVAVxmk33f0CtTJRkmUWcb1Us+t7b1wqdzoC0REw2muC9V5f1L/w5R5uKGaepA== +scheduler@^0.13.5: + version "0.13.5" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.13.5.tgz#b7226625167041298af3b98088a9dbbf6d7733a8" + integrity sha512-K98vjkQX9OIt/riLhp6F+XtDPtMQhqNcf045vsh+pcuvHq+PHy1xCrH3pq1P40m6yR46lpVvVhKdEOtnimuUJw== dependencies: loose-envify "^1.1.0" object-assign "^4.1.1" @@ -10420,7 +10447,7 @@ ua-parser-js@^0.7.18: resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.19.tgz#94151be4c0a7fb1d001af7022fdaca4642659e4b" integrity sha512-T3PVJ6uz8i0HzPxOF9SWzWAlfN/DavlpQqepn22xgve/5QecC+XMCAtmUNnY7C9StehaV6exjUCI801lOI7QlQ== -uglify-js@3.4.x, uglify-js@^3.1.4: +uglify-js@3.4.x: version "3.4.10" resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.4.10.tgz#9ad9563d8eb3acdfb8d38597d2af1d815f6a755f" integrity sha512-Y2VsbPVs0FIshJztycsO2SfPk7/KAF/T72qzv9u5EpQ4kB2hQoHlhNQTsNyy6ul7lQtqJN/AoWeS23OzEiEFxw== @@ -10428,6 +10455,14 @@ uglify-js@3.4.x, uglify-js@^3.1.4: commander "~2.19.0" source-map "~0.6.1" +uglify-js@^3.1.4: + version "3.5.2" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.5.2.tgz#dc0c7ac2da0a4b7d15e84266818ff30e82529474" + integrity sha512-imog1WIsi9Yb56yRt5TfYVxGmnWs3WSGU73ieSOlMVFwhJCA9W8fqFFMMj4kgDqiS/80LGdsYnWL7O9UcjEBlg== + dependencies: + commander "~2.19.0" + source-map "~0.6.1" + unicode-canonical-property-names-ecmascript@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818" @@ -10527,7 +10562,7 @@ unset-value@^1.0.0: has-value "^0.3.1" isobject "^3.0.0" -upath@^1.1.0: +upath@^1.1.1: version "1.1.2" resolved "https://registry.yarnpkg.com/upath/-/upath-1.1.2.tgz#3db658600edaeeccbe6db5e684d67ee8c2acd068" integrity sha512-kXpym8nmDmlCBr7nKdIx8P2jNBa+pBpIUFRnKJ4dr8htyYGJFokkr2ZvERRtUN+9SY+JqXouNgUPtv6JQva/2Q== From 971760830dcbc028d4f07420ca1a77e5df2d07c9 Mon Sep 17 00:00:00 2001 From: David Masselink Date: Mon, 1 Apr 2019 17:31:28 -0700 Subject: [PATCH 15/15] #156 - rather than using custom react-splitter-layout fork, upgrade to v4.0.0 where a custom css file can be provided more simply --- app/package.json | 2 +- app/src/components/graph/GraphContainer.js | 2 +- app/src/components/results/Results.js | 2 +- app/src/shared/grabIcon.png | Bin 0 -> 743 bytes .../reactSplitterLayoutWithOverrides.css | 55 ++++++++++++++++++ app/yarn.lock | 5 +- 6 files changed, 61 insertions(+), 5 deletions(-) create mode 100644 app/src/shared/grabIcon.png create mode 100644 app/src/shared/reactSplitterLayoutWithOverrides.css diff --git a/app/package.json b/app/package.json index 7243769..c5e9121 100644 --- a/app/package.json +++ b/app/package.json @@ -21,7 +21,7 @@ "react-redux": "^5.1.1", "react-router-dom": "^4.3.1", "react-scripts": "2.1.8", - "react-splitter-layout": "davemasselink/react-splitter-layout", + "react-splitter-layout": "^4.0.0", "react-test-renderer": "^16.6.3", "redux": "^4.0.1", "redux-mock-store": "^1.5.3", diff --git a/app/src/components/graph/GraphContainer.js b/app/src/components/graph/GraphContainer.js index 88d5e42..0bc03ac 100644 --- a/app/src/components/graph/GraphContainer.js +++ b/app/src/components/graph/GraphContainer.js @@ -6,7 +6,7 @@ import { withRouter } from 'react-router-dom'; import { withStyles } from '@material-ui/core/styles'; import CircularProgress from '@material-ui/core/CircularProgress'; import SplitterLayout from 'react-splitter-layout'; -import 'react-splitter-layout/lib/index.css'; +import '../../shared/reactSplitterLayoutWithOverrides.css'; import "./Graph.css"; import Graph from './Graph'; diff --git a/app/src/components/results/Results.js b/app/src/components/results/Results.js index ba445a9..84098ef 100644 --- a/app/src/components/results/Results.js +++ b/app/src/components/results/Results.js @@ -9,7 +9,7 @@ import SearchIcon from '@material-ui/icons/Search'; import IconButton from '@material-ui/core/IconButton'; import InputAdornment from '@material-ui/core/InputAdornment'; import SplitterLayout from 'react-splitter-layout'; -import 'react-splitter-layout/lib/index.css'; +import '../../shared/reactSplitterLayoutWithOverrides.css'; import { ENTER_KEYCODE } from '../../config/appConfig'; import ResultList from './ResultList'; diff --git a/app/src/shared/grabIcon.png b/app/src/shared/grabIcon.png new file mode 100644 index 0000000000000000000000000000000000000000..a4b6e6ad28d06b465cccf83ca2745f7d5c5c3367 GIT binary patch literal 743 zcmeAS@N?(olHy`uVBq!ia0vp^AT}Qd8;}%R+`Ae`F%}28J29*~C-V}>;VkfoEM{Qf z76xHPhFNnYfC?frLn2Bde0{8v^KP? zWt5Z@Sn2DRmzV368|&p4rRy77T3YHG80i}s=>k>g7FXt#Bv$C=6)QswftllyTAW;z zSx}OhpQivaH!&%{w8U0P31kr*K-^i9nTD__uNdkrpa=CqGWv#k2KsQbfm&@qqE`Mz znW;dVLFU^T+JIG}h(YbK(Fa+MxY z&Uzho5SVvUWw*v`hyL0&nco~;><5=`40x?+{_%lk{=_+76*?*c{P#W+yyVR4sI>IR z8;i+hulYq~Zk(yPf2PJ-QtWv8b#85w)o{4d zQxZ8X;JT^$)HKn@yH$>F4Er^^*6u)%??;*1jU!%_R8t9+>v| zV84_9yHEf1pDg^h=iTm28vlMVHpWlsUov6(-pC(0$0k@Q*qOFB{jw .layout-splitter { + flex: 0 0 auto; + width: 12px; + height: 100%; + cursor: col-resize; + background-color: #ccc; + background-image: url("grabIcon.png"); + background-position: center; + background-repeat: no-repeat; +} + +.splitter-layout .layout-splitter:hover { + background-color: #bbb; +} + +.splitter-layout.layout-changing { + cursor: col-resize; +} + +.splitter-layout.layout-changing > .layout-splitter { + background-color: #aaa; +} + +.splitter-layout.splitter-layout-vertical { + flex-direction: column; +} + +.splitter-layout.splitter-layout-vertical.layout-changing { + cursor: row-resize; +} + +.splitter-layout.splitter-layout-vertical > .layout-splitter { + width: 100%; + height: 4px; + cursor: row-resize; +} diff --git a/app/yarn.lock b/app/yarn.lock index eb7aebe..e5d571c 100644 --- a/app/yarn.lock +++ b/app/yarn.lock @@ -8896,9 +8896,10 @@ react-scripts@2.1.8: optionalDependencies: fsevents "1.2.4" -react-splitter-layout@davemasselink/react-splitter-layout: +react-splitter-layout@^4.0.0: version "4.0.0" - resolved "https://codeload.github.com/davemasselink/react-splitter-layout/tar.gz/c47302f517cd2a8103e0ef22c61c347b41953f12" + resolved "https://registry.yarnpkg.com/react-splitter-layout/-/react-splitter-layout-4.0.0.tgz#70b43ca6a78c056f5e5fbf29c67b597f040fbf2e" + integrity sha512-SLqOjBOxRuizWUa83w6q5/u9cDWa9/yj9Iko9V9JFN8x+cqIXiDlUFWSx+icz3IIgvsN/oRIw3za5/32RjIwrA== react-test-renderer@^16.0.0-0, react-test-renderer@^16.6.3: version "16.8.5"