diff --git a/client/app/scripts/actions/app-actions.js b/client/app/scripts/actions/app-actions.js index 300c4bbfe..d6212f200 100644 --- a/client/app/scripts/actions/app-actions.js +++ b/client/app/scripts/actions/app-actions.js @@ -643,6 +643,17 @@ export function receiveNotFound(nodeId) { }; } +export function toggleContrastMode(enabled) { + return (dispatch, getState) => { + dispatch({ + type: ActionTypes.TOGGLE_CONTRAST_MODE, + enabled, + }); + + updateRoute(getState); + }; +} + export function route(urlState) { return (dispatch, getState) => { dispatch({ @@ -664,6 +675,10 @@ export function route(urlState) { state.get('nodeDetails'), dispatch ); + + if (urlState.contrastMode) { + dispatch(toggleContrastMode(true)); + } }; } diff --git a/client/app/scripts/charts/edge.js b/client/app/scripts/charts/edge.js index 1deab2f04..f443a1953 100644 --- a/client/app/scripts/charts/edge.js +++ b/client/app/scripts/charts/edge.js @@ -3,7 +3,6 @@ import { connect } from 'react-redux'; import classNames from 'classnames'; import { enterEdge, leaveEdge } from '../actions/app-actions'; -import { isContrastMode } from '../utils/contrast-utils'; import { NODE_BASE_SIZE } from '../constants/styles'; class Edge extends React.Component { @@ -15,9 +14,9 @@ class Edge extends React.Component { } render() { - const { id, path, highlighted, blurred, focused, scale } = this.props; + const { id, path, highlighted, blurred, focused, scale, contrastMode } = this.props; const className = classNames('edge', { highlighted, blurred, focused }); - const thickness = scale * (isContrastMode() ? 0.02 : 0.01) * NODE_BASE_SIZE; + const thickness = scale * (contrastMode ? 0.02 : 0.01) * NODE_BASE_SIZE; // Draws the edge so that its thickness reflects the zoom scale. // Edge shadow is always made 10x thicker than the edge itself. @@ -41,7 +40,13 @@ class Edge extends React.Component { } } +function mapStateToProps(state) { + return { + contrastMode: state.get('contrastMode') + }; +} + export default connect( - null, + mapStateToProps, { enterEdge, leaveEdge } )(Edge); diff --git a/client/app/scripts/charts/node-networks-overlay.js b/client/app/scripts/charts/node-networks-overlay.js index ae1b45710..0522a83e0 100644 --- a/client/app/scripts/charts/node-networks-overlay.js +++ b/client/app/scripts/charts/node-networks-overlay.js @@ -1,8 +1,9 @@ import React from 'react'; import { scaleBand } from 'd3-scale'; import { List as makeList } from 'immutable'; +import { connect } from 'react-redux'; + import { getNetworkColor } from '../utils/color-utils'; -import { isContrastMode } from '../utils/contrast-utils'; import { NODE_BASE_SIZE } from '../constants/styles'; // Min size is about a quarter of the width, feels about right. @@ -13,7 +14,7 @@ const borderRadius = 0.01; const offset = 0.67; const x = scaleBand(); -function NodeNetworksOverlay({ stack, networks = makeList() }) { +function NodeNetworksOverlay({ stack, networks = makeList(), contrastMode }) { const barWidth = Math.max(1, minBarWidth * networks.size); const yPosition = offset - (barHeight * 0.5); @@ -37,7 +38,7 @@ function NodeNetworksOverlay({ stack, networks = makeList() }) { /> )); - const translateY = stack && isContrastMode() ? 0.15 : 0; + const translateY = stack && contrastMode ? 0.15 : 0; return ( {bars.toJS()} @@ -45,4 +46,10 @@ function NodeNetworksOverlay({ stack, networks = makeList() }) { ); } -export default NodeNetworksOverlay; +function mapStateToProps(state) { + return { + contrastMode: state.get('contrastMode') + }; +} + +export default connect(mapStateToProps)(NodeNetworksOverlay); diff --git a/client/app/scripts/charts/node-shape-stack.js b/client/app/scripts/charts/node-shape-stack.js index 268ab1c04..330a948cb 100644 --- a/client/app/scripts/charts/node-shape-stack.js +++ b/client/app/scripts/charts/node-shape-stack.js @@ -1,10 +1,10 @@ import React from 'react'; +import { connect } from 'react-redux'; import { NODE_BASE_SIZE } from '../constants/styles'; -import { isContrastMode } from '../utils/contrast-utils'; -export default function NodeShapeStack(props) { - const shift = isContrastMode() ? 0.15 : 0.1; +function NodeShapeStack(props) { + const shift = props.contrastMode ? 0.15 : 0.1; const highlightScale = [1, 1 + shift]; const dy = NODE_BASE_SIZE * shift; @@ -26,3 +26,11 @@ export default function NodeShapeStack(props) { ); } + +function mapStateToProps(state) { + return { + contrastMode: state.get('contrastMode') + }; +} + +export default connect(mapStateToProps)(NodeShapeStack); diff --git a/client/app/scripts/components/app.js b/client/app/scripts/components/app.js index 05ac9bf59..73f3dce47 100644 --- a/client/app/scripts/components/app.js +++ b/client/app/scripts/components/app.js @@ -13,7 +13,7 @@ import Topologies from './topologies'; import TopologyOptions from './topology-options'; import { getApiDetails, getTopologies } from '../utils/web-api-utils'; import { focusSearch, pinNextMetric, hitBackspace, hitEnter, hitEsc, unpinMetric, - selectMetric, toggleHelp, toggleGridMode } from '../actions/app-actions'; + selectMetric, toggleHelp, toggleGridMode, toggleContrastMode } from '../actions/app-actions'; import Details from './details'; import Nodes from './nodes'; import GridModeSelector from './grid-mode-selector'; @@ -153,11 +153,12 @@ function mapStateToProps(state) { showingMetricsSelector: state.get('availableCanvasMetrics').count() > 0, showingNetworkSelector: state.get('availableNetworks').count() > 0, showingTerminal: state.get('controlPipes').size > 0, - urlState: getUrlState(state) + urlState: getUrlState(state), + contrastMode: state.get('contrastMode') }; } - export default connect( - mapStateToProps + mapStateToProps, + dispatch => ({ dispatch, toggleContrastMode }) )(App); diff --git a/client/app/scripts/components/footer.js b/client/app/scripts/components/footer.js index 3633ece00..a9c8bdf05 100644 --- a/client/app/scripts/components/footer.js +++ b/client/app/scripts/components/footer.js @@ -4,19 +4,22 @@ import moment from 'moment'; import Plugins from './plugins'; import { getUpdateBufferSize } from '../utils/update-buffer-utils'; -import { contrastModeUrl, isContrastMode } from '../utils/contrast-utils'; import { clickDownloadGraph, clickForceRelayout, clickPauseUpdate, - clickResumeUpdate, toggleHelp, toggleTroubleshootingMenu } from '../actions/app-actions'; -import { basePathSlash } from '../utils/web-api-utils'; + clickResumeUpdate, toggleHelp, toggleTroubleshootingMenu, toggleContrastMode } from '../actions/app-actions'; class Footer extends React.Component { - render() { - const { hostname, updatePausedAt, version, versionUpdate } = this.props; - const contrastMode = isContrastMode(); + constructor(props, context) { + super(props, context); + + this.handleContrastClick = this.handleContrastClick.bind(this); + } + handleContrastClick(e) { + e.preventDefault(); + this.props.toggleContrastMode(!this.props.contrastMode); + } + render() { + const { hostname, updatePausedAt, version, versionUpdate, contrastMode } = this.props; - // link url to switch contrast with current UI state - const otherContrastModeUrl = contrastMode - ? basePathSlash(window.location.pathname) : contrastModeUrl; const otherContrastModeTitle = contrastMode ? 'Switch to normal contrast' : 'Switch to high contrast'; const forceRelayoutTitle = 'Force re-layout (might reduce edge crossings, ' @@ -76,7 +79,7 @@ class Footer extends React.Component { title={forceRelayoutTitle}> - + - - - ), document.getElementById('app')); -} - -renderApp(); -if (module.hot) { - module.hot.accept('./components/app', renderApp); -} diff --git a/client/app/scripts/reducers/root.js b/client/app/scripts/reducers/root.js index 81d686b8b..5913dfcca 100644 --- a/client/app/scripts/reducers/root.js +++ b/client/app/scripts/reducers/root.js @@ -1,3 +1,4 @@ +/* eslint-disable import/no-webpack-loader-syntax, import/no-unresolved */ import debug from 'debug'; import { size, each, includes } from 'lodash'; import { fromJS, is as isDeepEqual, List as makeList, Map as makeMap, @@ -30,6 +31,7 @@ const topologySorter = topology => topology.get('rank'); export const initialState = makeMap({ availableCanvasMetrics: makeList(), availableNetworks: makeList(), + contrastMode: false, controlPipes: makeOrderedMap(), // pipeId -> controlPipe controlStatus: makeMap(), currentTopology: null, @@ -721,6 +723,33 @@ export function rootReducer(state = initialState, action) { return state.set('showingTroubleshootingMenu', !state.get('showingTroubleshootingMenu')); } + case ActionTypes.TOGGLE_CONTRAST_MODE: { + const modules = [ + require.resolve('../../styles/main.scss'), + require.resolve('../../styles/contrast.scss') + ]; + // Bust the webpack require cache to for a re-download of the stylesheets + modules.forEach((i) => { + const children = require.cache[i] ? require.cache[i].children : []; + children.forEach((c) => { + delete require.cache[c]; + }); + delete require.cache[i]; + }); + + if (action.enabled) { + require.ensure([], () => { + require('../../styles/contrast.scss'); + }); + } else { + require.ensure([], () => { + require('../../styles/main.scss'); + }); + } + + return state.set('contrastMode', action.enabled); + } + default: { return state; } diff --git a/client/app/scripts/utils/contrast-utils.js b/client/app/scripts/utils/contrast-utils.js deleted file mode 100644 index e7f4c7a46..000000000 --- a/client/app/scripts/utils/contrast-utils.js +++ /dev/null @@ -1,7 +0,0 @@ -export const contrastModeUrl = 'contrast.html'; - -const contrastMode = window.location.pathname.indexOf(contrastModeUrl) > -1; - -export function isContrastMode() { - return contrastMode; -} diff --git a/client/app/scripts/utils/router-utils.js b/client/app/scripts/utils/router-utils.js index ffc4d85f1..6bc7d330c 100644 --- a/client/app/scripts/utils/router-utils.js +++ b/client/app/scripts/utils/router-utils.js @@ -19,11 +19,18 @@ function encodeURL(url) { .replace(new RegExp(SLASH, 'g'), SLASH_REPLACEMENT); } -function decodeURL(url) { +export function decodeURL(url) { return decodeURIComponent(url.replace(new RegExp(SLASH_REPLACEMENT, 'g'), SLASH)) .replace(new RegExp(PERCENT_REPLACEMENT, 'g'), PERCENT); } +export function parseHashState(hash = window.location.hash) { + const urlStateString = hash + .replace('#!/state/', '') + .replace('#!/', '') || '{}'; + return JSON.parse(decodeURL(urlStateString)); +} + function shouldReplaceState(prevState, nextState) { // Opening a new terminal while an existing one is open. const terminalToTerminal = (prevState.controlPipe && nextState.controlPipe); @@ -50,7 +57,8 @@ export function getUrlState(state) { gridSortedBy: state.get('gridSortedBy'), gridSortedDesc: state.get('gridSortedDesc'), topologyId: state.get('currentTopologyId'), - topologyOptions: state.get('topologyOptions').toJS() // all options + topologyOptions: state.get('topologyOptions').toJS(), // all options, + contrastMode: state.get('contrastMode') }; if (state.get('showingNetworks')) { @@ -67,10 +75,7 @@ export function updateRoute(getState) { const state = getUrlState(getState()); const stateUrl = encodeURL(JSON.stringify(state)); const dispatch = false; - const urlStateString = window.location.hash - .replace('#!/state/', '') - .replace('#!/', '') || '{}'; - const prevState = JSON.parse(decodeURL(urlStateString)); + const prevState = parseHashState(); // back up state in storage as well storageSet(STORAGE_STATE_KEY, stateUrl); diff --git a/client/build/favicon.ico b/client/build/favicon.ico deleted file mode 100644 index 2d15c7808..000000000 Binary files a/client/build/favicon.ico and /dev/null differ diff --git a/client/webpack.local.config.js b/client/webpack.local.config.js index 1994c44aa..512982651 100644 --- a/client/webpack.local.config.js +++ b/client/webpack.local.config.js @@ -2,7 +2,6 @@ const webpack = require('webpack'); const autoprefixer = require('autoprefixer'); const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); - /** * This is the Webpack configuration file for local development. * It contains local-specific configuration which includes: @@ -28,10 +27,6 @@ module.exports = { './app/scripts/main.dev', 'webpack-hot-middleware/client' ], - 'contrast-app': [ - './app/scripts/contrast-main', - 'webpack-hot-middleware/client' - ], 'terminal-app': [ './app/scripts/terminal-main', 'webpack-hot-middleware/client' @@ -45,7 +40,7 @@ module.exports = { // Used by Webpack Dev Middleware output: { publicPath: '', - path: '/', + path: path.join(__dirname, 'build'), filename: '[name].js' }, @@ -56,11 +51,6 @@ module.exports = { new webpack.HotModuleReplacementPlugin(), new webpack.NoErrorsPlugin(), new webpack.IgnorePlugin(/^\.\/locale$/, [/moment$/]), - new HtmlWebpackPlugin({ - chunks: ['vendors', 'contrast-app'], - template: 'app/html/index.html', - filename: 'contrast.html' - }), new HtmlWebpackPlugin({ chunks: ['vendors', 'terminal-app'], template: 'app/html/index.html', diff --git a/client/webpack.production.config.js b/client/webpack.production.config.js index 5dd6c2181..a605688a6 100644 --- a/client/webpack.production.config.js +++ b/client/webpack.production.config.js @@ -31,7 +31,6 @@ module.exports = { entry: { app: './app/scripts/main', - 'contrast-app': './app/scripts/contrast-main', 'terminal-app': './app/scripts/terminal-main', // keep only some in here, to make vendors and app bundles roughly same size vendors: ['babel-polyfill', 'classnames', 'immutable', @@ -112,12 +111,6 @@ module.exports = { } }), new ExtractTextPlugin('style-[name]-[chunkhash].css'), - new HtmlWebpackPlugin({ - hash: true, - chunks: ['vendors', 'contrast-app'], - template: 'app/html/index.html', - filename: 'contrast.html' - }), new HtmlWebpackPlugin({ hash: true, chunks: ['vendors', 'terminal-app'],