diff --git a/client/app/scripts/charts/nodes-chart.js b/client/app/scripts/charts/nodes-chart.js index 8a60a924a..e200efe1f 100644 --- a/client/app/scripts/charts/nodes-chart.js +++ b/client/app/scripts/charts/nodes-chart.js @@ -1,15 +1,15 @@ import React from 'react'; import { connect } from 'react-redux'; -import Logo from '../components/logo'; import NodesChartElements from './nodes-chart-elements'; -import ZoomWrapper from '../components/zoom-wrapper'; +import ZoomableCanvas from '../components/zoomable-canvas'; import { clickBackground } from '../actions/app-actions'; import { graphZoomLimitsSelector, graphZoomStateSelector, } from '../selectors/graph-view/zoom'; + const EdgeMarkerDefinition = ({ selectedNodeId }) => { const markerOffset = selectedNodeId ? '35' : '40'; const markerSize = selectedNodeId ? '10' : '30'; @@ -47,16 +47,14 @@ class NodesChart extends React.Component { const { selectedNodeId } = this.props; return (
- - + - - - - + +
); } diff --git a/client/app/scripts/components/nodes-resources.js b/client/app/scripts/components/nodes-resources.js index 663834134..27f86c837 100644 --- a/client/app/scripts/components/nodes-resources.js +++ b/client/app/scripts/components/nodes-resources.js @@ -1,8 +1,7 @@ import React from 'react'; import { connect } from 'react-redux'; -import Logo from './logo'; -import ZoomWrapper from './zoom-wrapper'; +import ZoomableCanvas from './zoomable-canvas'; import NodesResourcesLayer from './nodes-resources/node-resources-layer'; import { layersTopologyIdsSelector } from '../selectors/resource-view/layout'; import { @@ -26,15 +25,12 @@ class NodesResources extends React.Component { render() { return (
- - - - {transform => this.renderLayers(transform)} - - + + {transform => this.renderLayers(transform)} +
); } diff --git a/client/app/scripts/components/zoom-control.js b/client/app/scripts/components/zoom-control.js new file mode 100644 index 000000000..75ca5e751 --- /dev/null +++ b/client/app/scripts/components/zoom-control.js @@ -0,0 +1,65 @@ +import React from 'react'; +import Slider from 'rc-slider'; +import { scaleLog } from 'd3-scale'; + + +const SLIDER_STEP = 0.001; +const CLICK_STEP = 0.05; + +// Returns a log-scale that maps zoom factors to slider values. +const getSliderScale = ({ minScale, maxScale }) => ( + scaleLog() + // Zoom limits may vary between different views. + .domain([minScale, maxScale]) + // Taking the unit range for the slider ensures consistency + // of the zoom button steps across different zoom domains. + .range([0, 1]) + // This makes sure the input values are always clamped into the valid domain/range. + .clamp(true) +); + +export default class ZoomControl extends React.Component { + constructor(props, context) { + super(props, context); + + this.handleChange = this.handleChange.bind(this); + this.handleZoomOut = this.handleZoomOut.bind(this); + this.handleZoomIn = this.handleZoomIn.bind(this); + this.getSliderValue = this.getSliderValue.bind(this); + this.toZoomScale = this.toZoomScale.bind(this); + } + + handleChange(sliderValue) { + this.props.zoomAction(this.toZoomScale(sliderValue)); + } + + handleZoomOut() { + this.props.zoomAction(this.toZoomScale(this.getSliderValue() - CLICK_STEP)); + } + + handleZoomIn() { + this.props.zoomAction(this.toZoomScale(this.getSliderValue() + CLICK_STEP)); + } + + getSliderValue() { + const toSliderValue = getSliderScale(this.props); + return toSliderValue(this.props.scale); + } + + toZoomScale(sliderValue) { + const toSliderValue = getSliderScale(this.props); + return toSliderValue.invert(sliderValue); + } + + render() { + const value = this.getSliderValue(); + + return ( +
+ + + +
+ ); + } +} diff --git a/client/app/scripts/components/zoom-wrapper.js b/client/app/scripts/components/zoomable-canvas.js similarity index 79% rename from client/app/scripts/components/zoom-wrapper.js rename to client/app/scripts/components/zoomable-canvas.js index 8e848de86..7eff294af 100644 --- a/client/app/scripts/components/zoom-wrapper.js +++ b/client/app/scripts/components/zoomable-canvas.js @@ -6,6 +6,8 @@ import { fromJS } from 'immutable'; import { event as d3Event, select } from 'd3-selection'; import { zoom, zoomIdentity } from 'd3-zoom'; +import Logo from '../components/logo'; +import ZoomControl from '../components/zoom-control'; import { cacheZoomState } from '../actions/app-actions'; import { transformToString } from '../utils/transform-utils'; import { activeTopologyZoomCacheKeyPathSelector } from '../selectors/zooming'; @@ -18,7 +20,7 @@ import { import { ZOOM_CACHE_DEBOUNCE_INTERVAL } from '../constants/timer'; -class ZoomWrapper extends React.Component { +class ZoomableCanvas extends React.Component { constructor(props, context) { super(props, context); @@ -36,13 +38,15 @@ class ZoomWrapper extends React.Component { }; this.debouncedCacheZoom = debounce(this.cacheZoom.bind(this), ZOOM_CACHE_DEBOUNCE_INTERVAL); + this.handleZoomControlAction = this.handleZoomControlAction.bind(this); + this.canChangeZoom = this.canChangeZoom.bind(this); this.zoomed = this.zoomed.bind(this); } componentDidMount() { this.zoomRestored = false; this.zoom = zoom().on('zoom', this.zoomed); - this.svg = select(`svg#${this.props.svg}`); + this.svg = select('svg#canvas'); this.setZoomTriggers(!this.props.disabled); this.updateZoomLimits(this.props); @@ -77,6 +81,18 @@ class ZoomWrapper extends React.Component { } } + handleZoomControlAction(scale) { + // Update the canvas scale (not touching the translation). + this.svg.call(this.zoom.scaleTo, scale); + + // Update the scale state and propagate to the global cache. + this.setState(this.cachableState({ + scaleX: scale, + scaleY: scale, + })); + this.debouncedCacheZoom(); + } + render() { // `forwardTransform` says whether the zoom transform is forwarded to the child // component. The advantage of that is more control rendering control in the @@ -86,8 +102,19 @@ class ZoomWrapper extends React.Component { const transform = forwardTransform ? '' : transformToString(this.state); return ( - - {forwardTransform ? children(this.state) : children} + + + + + {forwardTransform ? children(this.state) : children} + + + {this.canChangeZoom() && } ); } @@ -157,8 +184,14 @@ class ZoomWrapper extends React.Component { } } + canChangeZoom() { + const { disabled, layoutZoomLimits } = this.props; + const canvasHasContent = !layoutZoomLimits.isEmpty(); + return !disabled && canvasHasContent; + } + zoomed() { - if (!this.props.disabled) { + if (this.canChangeZoom()) { const updatedState = this.cachableState({ scaleX: d3Event.transform.k, scaleY: d3Event.transform.k, @@ -189,4 +222,4 @@ function mapStateToProps(state, props) { export default connect( mapStateToProps, { cacheZoomState } -)(ZoomWrapper); +)(ZoomableCanvas); diff --git a/client/app/scripts/constants/styles.js b/client/app/scripts/constants/styles.js index 8508fb999..4b189ab7f 100644 --- a/client/app/scripts/constants/styles.js +++ b/client/app/scripts/constants/styles.js @@ -40,7 +40,7 @@ export const EDGE_WAYPOINTS_CAP = 10; export const CANVAS_MARGINS = { [GRAPH_VIEW_MODE]: { top: 160, left: 40, right: 40, bottom: 150 }, [TABLE_VIEW_MODE]: { top: 160, left: 40, right: 40, bottom: 30 }, - [RESOURCE_VIEW_MODE]: { top: 160, left: 210, right: 40, bottom: 50 }, + [RESOURCE_VIEW_MODE]: { top: 140, left: 210, right: 40, bottom: 150 }, }; // Node details table constants diff --git a/client/app/styles/_base.scss b/client/app/styles/_base.scss index a9c86c9b4..7588841df 100644 --- a/client/app/styles/_base.scss +++ b/client/app/styles/_base.scss @@ -1,5 +1,6 @@ @import '~xterm/dist/xterm.css'; @import '~font-awesome/scss/font-awesome.scss'; +@import '~rc-slider/dist/rc-slider.css'; @font-face { font-family: "Roboto"; @@ -46,6 +47,16 @@ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.19), 0 6px 10px rgba(0, 0, 0, 0.23); } +.overlay-wrapper { + background-color: fade-out($background-average-color, 0.1); + border-radius: 4px; + color: $text-tertiary-color; + display: flex; + font-size: 0.7rem; + padding: 5px; + position: absolute; +} + .btn-opacity { @extend .palable; opacity: $btn-opacity-default; @@ -127,18 +138,13 @@ } .footer { - padding: 5px; - position: absolute; + @extend .overlay-wrapper; bottom: 11px; right: 43px; - color: $text-tertiary-color; - background-color: fade-out($background-average-color, .1); - font-size: 0.7rem; - display: flex; a { - color: $text-secondary-color; @extend .btn-opacity; + color: $text-secondary-color; cursor: pointer; } @@ -1748,6 +1754,36 @@ } } +// +// Zoom control +// + +.zoom-control { + @extend .overlay-wrapper; + align-items: center; + flex-direction: column; + padding: 10px 10px 5px; + bottom: 50px; + right: 40px; + + .zoom-in, .zoom-out { + @extend .btn-opacity; + color: $text-secondary-color; + cursor: pointer; + font-size: 150%; + } + + .rc-slider { + margin: 10px 0; + height: 60px; + + .rc-slider-step { cursor: pointer; } + .rc-slider-track { background-color: $text-tertiary-color; } + .rc-slider-rail { background-color: $border-light-color; } + .rc-slider-handle { border-color: $text-tertiary-color; } + } +} + // // Debug panel! // diff --git a/client/package.json b/client/package.json index f52b6c3b7..b8652323a 100644 --- a/client/package.json +++ b/client/package.json @@ -29,6 +29,7 @@ "moment": "2.18.1", "page": "1.7.1", "prop-types": "^15.5.8", + "rc-slider": "^7.0.2", "react": "15.5.4", "react-addons-perf": "15.4.2", "react-dom": "15.5.4", diff --git a/client/webpack.local.config.js b/client/webpack.local.config.js index 4b5f239d0..61ed98e2d 100644 --- a/client/webpack.local.config.js +++ b/client/webpack.local.config.js @@ -127,6 +127,7 @@ module.exports = { includePaths: [ path.resolve(__dirname, './node_modules/xterm'), path.resolve(__dirname, './node_modules/font-awesome'), + path.resolve(__dirname, './node_modules/rc-slider'), ] } }], diff --git a/client/webpack.production.config.js b/client/webpack.production.config.js index 0ba1f8611..df489f73e 100644 --- a/client/webpack.production.config.js +++ b/client/webpack.production.config.js @@ -135,7 +135,8 @@ module.exports = { minimize: true, includePaths: [ path.resolve(__dirname, './node_modules/xterm'), - path.resolve(__dirname, './node_modules/font-awesome') + path.resolve(__dirname, './node_modules/font-awesome'), + path.resolve(__dirname, './node_modules/rc-slider'), ] } }] diff --git a/client/yarn.lock b/client/yarn.lock index 77b762a0c..2368e09d7 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -54,6 +54,12 @@ acorn@^5.0.0: version "5.0.3" resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.0.3.tgz#c460df08491463f028ccb82eab3730bf01087b3d" +add-dom-event-listener@1.x: + version "1.0.2" + resolved "https://registry.yarnpkg.com/add-dom-event-listener/-/add-dom-event-listener-1.0.2.tgz#8faed2c41008721cf111da1d30d995b85be42bed" + dependencies: + object-assign "4.x" + ajv-keywords@^1.0.0, ajv-keywords@^1.1.1: version "1.5.1" resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-1.5.1.tgz#314dd0a4b3368fad3dfcdc54ede6171b886daf3c" @@ -858,7 +864,7 @@ babel-register@^6.24.1: mkdirp "^0.5.1" source-map-support "^0.4.2" -babel-runtime@^6.0.0, babel-runtime@^6.18.0, babel-runtime@^6.2.0, babel-runtime@^6.22.0, babel-runtime@^6.6.1: +babel-runtime@6.x, babel-runtime@^6.0.0, babel-runtime@^6.18.0, babel-runtime@^6.2.0, babel-runtime@^6.22.0, babel-runtime@^6.6.1: version "6.23.0" resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.23.0.tgz#0a9489f144de70efb3ce4300accdb329e2fc543b" dependencies: @@ -1370,6 +1376,16 @@ commondir@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" +component-classes@^1.2.5: + version "1.2.6" + resolved "https://registry.yarnpkg.com/component-classes/-/component-classes-1.2.6.tgz#c642394c3618a4d8b0b8919efccbbd930e5cd691" + dependencies: + component-indexof "0.0.3" + +component-indexof@0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/component-indexof/-/component-indexof-0.0.3.tgz#11d091312239eb8f32c8f25ae9cb002ffe8d3c24" + compress-commons@~0.2.0: version "0.2.9" resolved "https://registry.yarnpkg.com/compress-commons/-/compress-commons-0.2.9.tgz#422d927430c01abd06cd455b6dfc04cb4cf8003c" @@ -1486,7 +1502,7 @@ create-hmac@^1.1.0, create-hmac@^1.1.2: create-hash "^1.1.0" inherits "^2.0.1" -create-react-class@^15.5.1: +create-react-class@^15.5.1, create-react-class@^15.5.2: version "15.5.2" resolved "https://registry.yarnpkg.com/create-react-class/-/create-react-class-15.5.2.tgz#6a8758348df660b88326a0e764d569f274aad681" dependencies: @@ -1521,6 +1537,12 @@ crypto-browserify@^3.11.0: public-encrypt "^4.0.0" randombytes "^2.0.0" +css-animation@^1.3.0: + version "1.3.2" + resolved "https://registry.yarnpkg.com/css-animation/-/css-animation-1.3.2.tgz#df515820ef5903733ad2db0999403b3037b8b880" + dependencies: + component-classes "^1.2.5" + css-color-names@0.0.4: version "0.0.4" resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-0.0.4.tgz#808adc2e79cf84738069b646cb20ec27beb629e0" @@ -1893,6 +1915,10 @@ doctrine@^2.0.0: esutils "^2.0.2" isarray "^1.0.0" +dom-align@1.x: + version "1.5.3" + resolved "https://registry.yarnpkg.com/dom-align/-/dom-align-1.5.3.tgz#b906b616822a5e599f579ec8505e367c51da7588" + dom-converter@~0.1: version "0.1.4" resolved "https://registry.yarnpkg.com/dom-converter/-/dom-converter-0.1.4.tgz#a45ef5727b890c9bffe6d7c876e7b19cb0e17f3b" @@ -3806,6 +3832,22 @@ lodash.flow@^3.3.0: version "3.5.0" resolved "https://registry.yarnpkg.com/lodash.flow/-/lodash.flow-3.5.0.tgz#87bf40292b8cf83e4e8ce1a3ae4209e20071675a" +lodash.isarguments@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a" + +lodash.isarray@^3.0.0: + version "3.0.4" + resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-3.0.4.tgz#79e4eb88c36a8122af86f844aa9bcd851b5fbb55" + +lodash.keys@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/lodash.keys/-/lodash.keys-3.1.2.tgz#4dbc0472b156be50a0b286855d1bd0b0c656098a" + dependencies: + lodash._getnative "^3.0.0" + lodash.isarguments "^3.0.0" + lodash.isarray "^3.0.0" + lodash.memoize@^4.1.0: version "4.1.2" resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" @@ -4295,7 +4337,7 @@ oauth-sign@~0.8.1: version "0.8.2" resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43" -object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: +object-assign@4.x, object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" @@ -4885,7 +4927,7 @@ promise@^7.1.1: dependencies: asap "~2.0.3" -prop-types@^15.0.0, prop-types@^15.5.7, prop-types@^15.5.8, prop-types@~15.5.7: +prop-types@^15.0.0, prop-types@^15.5.4, prop-types@^15.5.6, prop-types@^15.5.7, prop-types@^15.5.8, prop-types@~15.5.7: version "15.5.8" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.5.8.tgz#6b7b2e141083be38c8595aa51fc55775c7199394" dependencies: @@ -4976,6 +5018,57 @@ range-parser@^1.0.3, range-parser@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e" +rc-align@2.x: + version "2.3.4" + resolved "https://registry.yarnpkg.com/rc-align/-/rc-align-2.3.4.tgz#d83bdab7560f0142e72a3de1d495dab6ba225249" + dependencies: + dom-align "1.x" + prop-types "^15.5.8" + rc-util "4.x" + +rc-animate@2.x: + version "2.3.6" + resolved "https://registry.yarnpkg.com/rc-animate/-/rc-animate-2.3.6.tgz#4177a2822c67adf4fd5b908ceba7cee8d13c1fac" + dependencies: + css-animation "^1.3.0" + prop-types "^15.5.6" + +rc-slider@^7.0.2: + version "7.0.3" + resolved "https://registry.yarnpkg.com/rc-slider/-/rc-slider-7.0.3.tgz#22eb144e54bd5643308bfec2cb396444c747db79" + dependencies: + babel-runtime "6.x" + classnames "^2.2.5" + prop-types "^15.5.4" + rc-tooltip "^3.4.2" + rc-util "^4.0.0" + warning "^3.0.0" + +rc-tooltip@^3.4.2: + version "3.4.3" + resolved "https://registry.yarnpkg.com/rc-tooltip/-/rc-tooltip-3.4.3.tgz#6ae6ef0eccea39ca2add125bccdb34802c612eec" + dependencies: + prop-types "^15.5.8" + rc-trigger "1.x" + +rc-trigger@1.x: + version "1.10.3" + resolved "https://registry.yarnpkg.com/rc-trigger/-/rc-trigger-1.10.3.tgz#6bf367ac69705662224003d8b0277b9dfbf5cef4" + dependencies: + babel-runtime "6.x" + create-react-class "^15.5.2" + prop-types "^15.5.8" + rc-align "2.x" + rc-animate "2.x" + rc-util "4.x" + +rc-util@4.x, rc-util@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/rc-util/-/rc-util-4.0.2.tgz#5804f8b141a13c8c14d7c265a7d21d298195af46" + dependencies: + add-dom-event-listener "1.x" + shallowequal "^0.2.2" + rc@^1.1.7: version "1.1.7" resolved "https://registry.yarnpkg.com/rc/-/rc-1.1.7.tgz#c5ea564bb07aff9fd3a5b32e906c1d3a65940fea" @@ -5532,6 +5625,12 @@ shallow-clone@^0.1.2: lazy-cache "^0.2.3" mixin-object "^2.0.1" +shallowequal@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-0.2.2.tgz#1e32fd5bcab6ad688a4812cb0cc04efc75c7014e" + dependencies: + lodash.keys "^3.1.2" + shelljs@^0.7.5: version "0.7.7" resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.7.7.tgz#b2f5c77ef97148f4b4f6e22682e10bba8667cff1" @@ -6064,6 +6163,12 @@ walker@~1.0.5: dependencies: makeerror "1.0.x" +warning@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/warning/-/warning-3.0.0.tgz#32e5377cb572de4ab04753bdf8821c01ed605b7c" + dependencies: + loose-envify "^1.0.0" + watch@~0.10.0: version "0.10.0" resolved "https://registry.yarnpkg.com/watch/-/watch-0.10.0.tgz#77798b2da0f9910d595f1ace5b0c2258521f21dc"