diff --git a/client/app/scripts/actions/app-actions.js b/client/app/scripts/actions/app-actions.js index 4085b8e7ff..3e18f40cea 100644 --- a/client/app/scripts/actions/app-actions.js +++ b/client/app/scripts/actions/app-actions.js @@ -2,9 +2,7 @@ import debug from 'debug'; import ActionTypes from '../constants/action-types'; import { saveGraph } from '../utils/file-utils'; -import { modulo } from '../utils/math-utils'; import { updateRoute } from '../utils/router-utils'; -import { parseQuery } from '../utils/search-utils'; import { bufferDeltaUpdate, resumeUpdate, @@ -26,12 +24,14 @@ import { storageSet } from '../utils/storage-utils'; import { loadTheme } from '../utils/contrast-utils'; import { availableMetricTypesSelector, - selectedMetricTypeSelector, + nextPinnedMetricTypeSelector, + previousPinnedMetricTypeSelector, pinnedMetricSelector, } from '../selectors/node-metric'; import { activeTopologyOptionsSelector, isResourceViewModeSelector, + resourceViewAvailableSelector, } from '../selectors/topology'; import { GRAPH_VIEW_MODE, @@ -161,15 +161,27 @@ export function unpinMetric() { }; } -export function pinNextMetric(delta) { +export function pinNextMetric() { return (dispatch, getState) => { - const state = getState(); - const metricTypes = availableMetricTypesSelector(state); - const currentIndex = metricTypes.indexOf(selectedMetricTypeSelector(state)); - const nextIndex = modulo(currentIndex + delta, metricTypes.count()); - const nextMetricType = metricTypes.get(nextIndex); + const nextPinnedMetricType = nextPinnedMetricTypeSelector(getState()); + dispatch(pinMetric(nextPinnedMetricType)); + }; +} - dispatch(pinMetric(nextMetricType)); +export function pinPreviousMetric() { + return (dispatch, getState) => { + const previousPinnedMetricType = previousPinnedMetricTypeSelector(getState()); + dispatch(pinMetric(previousPinnedMetricType)); + }; +} + +export function pinSearch() { + return (dispatch, getState) => { + dispatch({ + type: ActionTypes.PIN_SEARCH, + query: getState().get('searchQuery'), + }); + updateRoute(getState); }; } @@ -310,18 +322,20 @@ export function setTableView() { export function setResourceView() { return (dispatch, getState) => { - dispatch({ - type: ActionTypes.SET_VIEW_MODE, - viewMode: RESOURCE_VIEW_MODE, - }); - // Pin the first metric if none of the visible ones is pinned. - const state = getState(); - if (!pinnedMetricSelector(state)) { - const firstAvailableMetricType = availableMetricTypesSelector(state).first(); - dispatch(pinMetric(firstAvailableMetricType)); + if (resourceViewAvailableSelector(getState())) { + dispatch({ + type: ActionTypes.SET_VIEW_MODE, + viewMode: RESOURCE_VIEW_MODE, + }); + // Pin the first metric if none of the visible ones is pinned. + const state = getState(); + if (!pinnedMetricSelector(state)) { + const firstAvailableMetricType = availableMetricTypesSelector(state).first(); + dispatch(pinMetric(firstAvailableMetricType)); + } + getResourceViewNodesSnapshot(getState, dispatch); + updateRoute(getState); } - getResourceViewNodesSnapshot(getState, dispatch); - updateRoute(getState); }; } @@ -504,23 +518,6 @@ export function hitBackspace() { }; } -export function hitEnter() { - return (dispatch, getState) => { - const state = getState(); - // pin query based on current search field - if (state.get('searchFocused')) { - const query = state.get('searchQuery'); - if (query && parseQuery(query)) { - dispatch({ - type: ActionTypes.PIN_SEARCH, - query - }); - updateRoute(getState); - } - } - }; -} - export function hitEsc() { return (dispatch, getState) => { const state = getState(); diff --git a/client/app/scripts/charts/node.js b/client/app/scripts/charts/node.js index 41c4cc36dd..f5882cb5ef 100644 --- a/client/app/scripts/charts/node.js +++ b/client/app/scripts/charts/node.js @@ -7,6 +7,8 @@ import { clickNode, enterNode, leaveNode } from '../actions/app-actions'; import { getNodeColor } from '../utils/color-utils'; import MatchedText from '../components/matched-text'; import MatchedResults from '../components/matched-results'; +import { trackMixpanelEvent } from '../utils/tracking-utils'; +import { GRAPH_VIEW_MODE } from '../constants/naming'; import { NODE_BASE_SIZE } from '../constants/styles'; import NodeShapeStack from './node-shape-stack'; @@ -141,6 +143,11 @@ class Node extends React.Component { handleMouseClick(ev) { ev.stopPropagation(); + trackMixpanelEvent('scope.node.click', { + layout: GRAPH_VIEW_MODE, + topologyId: this.props.currentTopology.get('id'), + parentTopologyId: this.props.currentTopology.get('parentId'), + }); this.props.clickNode(this.props.id, this.props.label, this.shapeRef.getBoundingClientRect()); } @@ -159,7 +166,8 @@ function mapStateToProps(state) { return { exportingGraph: state.get('exportingGraph'), showingNetworks: state.get('showingNetworks'), - contrastMode: state.get('contrastMode') + currentTopology: state.get('currentTopology'), + contrastMode: state.get('contrastMode'), }; } diff --git a/client/app/scripts/charts/nodes-grid.js b/client/app/scripts/charts/nodes-grid.js index 2d1058c9a1..7826c54ad2 100644 --- a/client/app/scripts/charts/nodes-grid.js +++ b/client/app/scripts/charts/nodes-grid.js @@ -1,11 +1,13 @@ /* eslint react/jsx-no-bind: "off", no-multi-comp: "off" */ - import React from 'react'; import { connect } from 'react-redux'; import { List as makeList, Map as makeMap } from 'immutable'; + import NodeDetailsTable from '../components/node-details/node-details-table'; import { clickNode, sortOrderChanged } from '../actions/app-actions'; import { shownNodesSelector } from '../selectors/node-filters'; +import { trackMixpanelEvent } from '../utils/tracking-utils'; +import { TABLE_VIEW_MODE } from '../constants/naming'; import { canvasMarginsSelector, canvasHeightSelector } from '../selectors/canvas'; import { searchNodeMatchesSelector } from '../selectors/search'; @@ -89,6 +91,11 @@ class NodesGrid extends React.Component { if (ev.target.className === 'node-details-table-node-link') { return; } + trackMixpanelEvent('scope.node.click', { + layout: TABLE_VIEW_MODE, + topologyId: this.props.currentTopology.get('id'), + parentTopologyId: this.props.currentTopology.get('parentId'), + }); this.props.clickNode(node.id, node.label, ev.target.getBoundingClientRect()); } diff --git a/client/app/scripts/components/app.js b/client/app/scripts/components/app.js index a98bbbdcb6..6311912989 100644 --- a/client/app/scripts/components/app.js +++ b/client/app/scripts/components/app.js @@ -15,8 +15,8 @@ import { getApiDetails, getTopologies } from '../utils/web-api-utils'; import { focusSearch, pinNextMetric, + pinPreviousMetric, hitBackspace, - hitEnter, hitEsc, unpinMetric, toggleHelp, @@ -31,6 +31,7 @@ import ViewModeSelector from './view-mode-selector'; import NetworkSelector from './networks-selector'; import DebugToolbar, { showingDebugToolbar, toggleDebugToolbar } from './debug-toolbar'; import { getRouter, getUrlState } from '../utils/router-utils'; +import { trackMixpanelEvent } from '../utils/tracking-utils'; import { availableNetworksSelector } from '../selectors/node-networks'; import { activeTopologyOptionsSelector, @@ -38,17 +39,18 @@ import { isTableViewModeSelector, isGraphViewModeSelector, } from '../selectors/topology'; +import { + BACKSPACE_KEY_CODE, + ESC_KEY_CODE, +} from '../constants/key-codes'; - -const BACKSPACE_KEY_CODE = 8; -const ENTER_KEY_CODE = 13; -const ESC_KEY_CODE = 27; const keyPressLog = debug('scope:app-key-press'); -class App extends React.Component { +class App extends React.Component { constructor(props, context) { super(props, context); + this.onKeyPress = this.onKeyPress.bind(this); this.onKeyUp = this.onKeyUp.bind(this); } @@ -79,8 +81,6 @@ class App extends React.Component { // don't get esc in onKeyPress if (ev.keyCode === ESC_KEY_CODE) { this.props.dispatch(hitEsc()); - } else if (ev.keyCode === ENTER_KEY_CODE) { - this.props.dispatch(hitEnter()); } else if (ev.keyCode === BACKSPACE_KEY_CODE) { this.props.dispatch(hitBackspace()); } else if (ev.code === 'KeyD' && ev.ctrlKey && !showingTerminal) { @@ -100,16 +100,28 @@ class App extends React.Component { keyPressLog('onKeyPress', 'keyCode', ev.keyCode, ev); const char = String.fromCharCode(ev.charCode); if (char === '<') { - dispatch(pinNextMetric(-1)); + dispatch(pinPreviousMetric()); + this.trackEvent('scope.metric.selector.pin.previous.keypress', { + metricType: this.props.pinnedMetricType + }); } else if (char === '>') { - dispatch(pinNextMetric(1)); + dispatch(pinNextMetric()); + this.trackEvent('scope.metric.selector.pin.next.keypress', { + metricType: this.props.pinnedMetricType + }); } else if (char === 'g') { dispatch(setGraphView()); + this.trackEvent('scope.layout.selector.keypress'); } else if (char === 't') { dispatch(setTableView()); + this.trackEvent('scope.layout.selector.keypress'); } else if (char === 'r') { dispatch(setResourceView()); + this.trackEvent('scope.layout.selector.keypress'); } else if (char === 'q') { + this.trackEvent('scope.metric.selector.unpin.keypress', { + metricType: this.props.pinnedMetricType + }); dispatch(unpinMetric()); } else if (char === '/') { ev.preventDefault(); @@ -120,6 +132,15 @@ class App extends React.Component { } } + trackEvent(eventName, additionalProps = {}) { + trackMixpanelEvent(eventName, { + layout: this.props.topologyViewMode, + topologyId: this.props.currentTopology.get('id'), + parentTopologyId: this.props.currentTopology.get('parentId'), + ...additionalProps, + }); + } + render() { const { isTableViewMode, isGraphViewMode, isResourceViewMode, showingDetails, showingHelp, showingNetworkSelector, showingTroubleshootingMenu } = this.props; @@ -164,9 +185,11 @@ class App extends React.Component { function mapStateToProps(state) { return { activeTopologyOptions: activeTopologyOptionsSelector(state), + currentTopology: state.get('currentTopology'), isResourceViewMode: isResourceViewModeSelector(state), isTableViewMode: isTableViewModeSelector(state), isGraphViewMode: isGraphViewModeSelector(state), + pinnedMetricType: state.get('pinnedMetricType'), routeSet: state.get('routeSet'), searchFocused: state.get('searchFocused'), searchQuery: state.get('searchQuery'), @@ -175,6 +198,7 @@ function mapStateToProps(state) { showingTroubleshootingMenu: state.get('showingTroubleshootingMenu'), showingNetworkSelector: availableNetworksSelector(state).count() > 0, showingTerminal: state.get('controlPipes').size > 0, + topologyViewMode: state.get('topologyViewMode'), urlState: getUrlState(state) }; } diff --git a/client/app/scripts/components/footer.js b/client/app/scripts/components/footer.js index 238c1597f2..5da5c3c33e 100644 --- a/client/app/scripts/components/footer.js +++ b/client/app/scripts/components/footer.js @@ -4,19 +4,39 @@ import moment from 'moment'; import Plugins from './plugins'; import { getUpdateBufferSize } from '../utils/update-buffer-utils'; -import { clickDownloadGraph, clickForceRelayout, clickPauseUpdate, - clickResumeUpdate, toggleHelp, toggleTroubleshootingMenu, setContrastMode } from '../actions/app-actions'; +import { trackMixpanelEvent } from '../utils/tracking-utils'; +import { + clickDownloadGraph, + clickForceRelayout, + clickPauseUpdate, + clickResumeUpdate, + toggleHelp, + toggleTroubleshootingMenu, + setContrastMode +} from '../actions/app-actions'; + class Footer extends React.Component { constructor(props, context) { super(props, context); this.handleContrastClick = this.handleContrastClick.bind(this); + this.handleRelayoutClick = this.handleRelayoutClick.bind(this); } - handleContrastClick(e) { - e.preventDefault(); + + handleContrastClick(ev) { + ev.preventDefault(); this.props.setContrastMode(!this.props.contrastMode); } + + handleRelayoutClick(ev) { + ev.preventDefault(); + trackMixpanelEvent('scope.layout.refresh.click', { + layout: this.props.topologyViewMode, + }); + this.props.clickForceRelayout(); + } + render() { const { hostname, updatePausedAt, version, versionUpdate, contrastMode } = this.props; @@ -75,7 +95,7 @@ class Footer extends React.Component { @@ -103,9 +123,10 @@ function mapStateToProps(state) { return { hostname: state.get('hostname'), updatePausedAt: state.get('updatePausedAt'), + topologyViewMode: state.get('topologyViewMode'), version: state.get('version'), versionUpdate: state.get('versionUpdate'), - contrastMode: state.get('contrastMode') + contrastMode: state.get('contrastMode'), }; } diff --git a/client/app/scripts/components/metric-selector-item.js b/client/app/scripts/components/metric-selector-item.js index 11f2cb2279..fda5bbce24 100644 --- a/client/app/scripts/components/metric-selector-item.js +++ b/client/app/scripts/components/metric-selector-item.js @@ -4,6 +4,7 @@ import { connect } from 'react-redux'; import { hoverMetric, pinMetric, unpinMetric } from '../actions/app-actions'; import { selectedMetricTypeSelector } from '../selectors/node-metric'; +import { trackMixpanelEvent } from '../utils/tracking-utils'; class MetricSelectorItem extends React.Component { @@ -14,6 +15,15 @@ class MetricSelectorItem extends React.Component { this.onMouseClick = this.onMouseClick.bind(this); } + trackEvent(eventName) { + trackMixpanelEvent(eventName, { + metricType: this.props.metric.get('label'), + layout: this.props.topologyViewMode, + topologyId: this.props.currentTopology.get('id'), + parentTopologyId: this.props.currentTopology.get('parentId'), + }); + } + onMouseOver() { const metricType = this.props.metric.get('label'); this.props.hoverMetric(metricType); @@ -24,8 +34,10 @@ class MetricSelectorItem extends React.Component { const pinnedMetricType = this.props.pinnedMetricType; if (metricType !== pinnedMetricType) { + this.trackEvent('scope.metric.selector.pin.click'); this.props.pinMetric(metricType); } else { + this.trackEvent('scope.metric.selector.unpin.click'); this.props.unpinMetric(); } } @@ -54,8 +66,10 @@ class MetricSelectorItem extends React.Component { function mapStateToProps(state) { return { - selectedMetricType: selectedMetricTypeSelector(state), + topologyViewMode: state.get('topologyViewMode'), + currentTopology: state.get('currentTopology'), pinnedMetricType: state.get('pinnedMetricType'), + selectedMetricType: selectedMetricTypeSelector(state), }; } diff --git a/client/app/scripts/components/node-details/node-details-relatives-link.js b/client/app/scripts/components/node-details/node-details-relatives-link.js index beace639d7..f7c0daa916 100644 --- a/client/app/scripts/components/node-details/node-details-relatives-link.js +++ b/client/app/scripts/components/node-details/node-details-relatives-link.js @@ -2,17 +2,23 @@ import React from 'react'; import { connect } from 'react-redux'; import { clickRelative } from '../../actions/app-actions'; +import { trackMixpanelEvent } from '../../utils/tracking-utils'; import MatchedText from '../matched-text'; + class NodeDetailsRelativesLink extends React.Component { constructor(props, context) { super(props, context); + this.handleClick = this.handleClick.bind(this); this.saveNodeRef = this.saveNodeRef.bind(this); } handleClick(ev) { ev.preventDefault(); + trackMixpanelEvent('scope.node.relative.click', { + topologyId: this.props.topologyId, + }); this.props.dispatch(clickRelative( this.props.id, this.props.topologyId, diff --git a/client/app/scripts/components/node-details/node-details-table-node-link.js b/client/app/scripts/components/node-details/node-details-table-node-link.js index e052896a3a..b5c8404dc0 100644 --- a/client/app/scripts/components/node-details/node-details-table-node-link.js +++ b/client/app/scripts/components/node-details/node-details-table-node-link.js @@ -2,16 +2,22 @@ import React from 'react'; import { connect } from 'react-redux'; import { clickRelative } from '../../actions/app-actions'; +import { trackMixpanelEvent } from '../../utils/tracking-utils'; + class NodeDetailsTableNodeLink extends React.Component { constructor(props, context) { super(props, context); + this.handleClick = this.handleClick.bind(this); this.saveNodeRef = this.saveNodeRef.bind(this); } handleClick(ev) { ev.preventDefault(); + trackMixpanelEvent('scope.node.relative.click', { + topologyId: this.props.topologyId, + }); this.props.dispatch(clickRelative( this.props.nodeId, this.props.topologyId, diff --git a/client/app/scripts/components/search.js b/client/app/scripts/components/search.js index 6bcf73e008..103a9687ff 100644 --- a/client/app/scripts/components/search.js +++ b/client/app/scripts/components/search.js @@ -3,12 +3,15 @@ import { connect } from 'react-redux'; import classnames from 'classnames'; import { debounce } from 'lodash'; -import { blurSearch, doSearch, focusSearch, toggleHelp } from '../actions/app-actions'; +import { blurSearch, doSearch, focusSearch, pinSearch, toggleHelp } from '../actions/app-actions'; import { searchMatchCountByTopologySelector } from '../selectors/search'; import { isResourceViewModeSelector } from '../selectors/topology'; import { slugify } from '../utils/string-utils'; +import { parseQuery } from '../utils/search-utils'; import { isTopologyEmpty } from '../utils/topology-utils'; +import { trackMixpanelEvent } from '../utils/tracking-utils'; import SearchItem from './search-item'; +import { ENTER_KEY_CODE } from '../constants/key-codes'; function shortenHintLabel(text) { @@ -48,6 +51,7 @@ class Search extends React.Component { super(props, context); this.handleBlur = this.handleBlur.bind(this); this.handleChange = this.handleChange.bind(this); + this.handleKeyUp = this.handleKeyUp.bind(this); this.handleFocus = this.handleFocus.bind(this); this.saveQueryInputRef = this.saveQueryInputRef.bind(this); this.doSearch = debounce(this.doSearch.bind(this), 200); @@ -71,15 +75,34 @@ class Search extends React.Component { if (this.state.value && value === '') { value = null; } - this.setState({value}); + this.setState({ value }); this.doSearch(inputValue); } + handleKeyUp(ev) { + // If the search query is parsable, pin it when ENTER key is hit. + if (ev.keyCode === ENTER_KEY_CODE && parseQuery(this.props.searchQuery)) { + trackMixpanelEvent('scope.search.query.pin', { + layout: this.props.topologyViewMode, + topologyId: this.props.currentTopology.get('id'), + parentTopologyId: this.props.currentTopology.get('parentId'), + }); + this.props.pinSearch(); + } + } + handleFocus() { this.props.focusSearch(); } doSearch(value) { + if (value !== '') { + trackMixpanelEvent('scope.search.query.change', { + layout: this.props.topologyViewMode, + topologyId: this.props.currentTopology.get('id'), + parentTopologyId: this.props.currentTopology.get('parentId'), + }); + } this.props.doSearch(value); } @@ -130,7 +153,7 @@ class Search extends React.Component { .map(query => )} @@ -155,13 +178,15 @@ class Search extends React.Component { export default connect( state => ({ nodes: state.get('nodes'), + topologyViewMode: state.get('topologyViewMode'), isResourceViewMode: isResourceViewModeSelector(state), isTopologyEmpty: isTopologyEmpty(state), + currentTopology: state.get('currentTopology'), topologiesLoaded: state.get('topologiesLoaded'), pinnedSearches: state.get('pinnedSearches'), searchFocused: state.get('searchFocused'), searchQuery: state.get('searchQuery'), searchMatchCountByTopology: searchMatchCountByTopologySelector(state), }), - { blurSearch, doSearch, focusSearch, toggleHelp } + { blurSearch, doSearch, focusSearch, pinSearch, toggleHelp } )(Search); diff --git a/client/app/scripts/components/topologies.js b/client/app/scripts/components/topologies.js index a5060ac990..880141b68b 100644 --- a/client/app/scripts/components/topologies.js +++ b/client/app/scripts/components/topologies.js @@ -2,6 +2,7 @@ import React from 'react'; import { connect } from 'react-redux'; import classnames from 'classnames'; +import { trackMixpanelEvent } from '../utils/tracking-utils'; import { searchMatchCountByTopologySelector } from '../selectors/search'; import { isResourceViewModeSelector } from '../selectors/topology'; import { clickTopology } from '../actions/app-actions'; @@ -25,8 +26,12 @@ class Topologies extends React.Component { this.onTopologyClick = this.onTopologyClick.bind(this); } - onTopologyClick(ev) { + onTopologyClick(ev, topology) { ev.preventDefault(); + trackMixpanelEvent('scope.topology.selector.click', { + topologyId: topology.get('id'), + parentTopologyId: topology.get('parentId'), + }); this.props.clickTopology(ev.currentTarget.getAttribute('rel')); } @@ -44,7 +49,7 @@ class Topologies extends React.Component { return (
+ onClick={ev => this.onTopologyClick(ev, subTopology)}>
{subTopology.get('name')}
@@ -65,7 +70,9 @@ class Topologies extends React.Component { return (
-
+
this.onTopologyClick(ev, topology)}>
{topology.get('name')}
diff --git a/client/app/scripts/components/topology-options.js b/client/app/scripts/components/topology-options.js index f16c8f9237..7375b9aa75 100644 --- a/client/app/scripts/components/topology-options.js +++ b/client/app/scripts/components/topology-options.js @@ -3,6 +3,7 @@ import { connect } from 'react-redux'; import { Map as makeMap } from 'immutable'; import includes from 'lodash/includes'; +import { trackMixpanelEvent } from '../utils/tracking-utils'; import { getCurrentTopologyOptions } from '../utils/topology-utils'; import { activeTopologyOptionsSelector } from '../selectors/topology'; import TopologyOptionAction from './topology-option-action'; @@ -12,10 +13,21 @@ class TopologyOptions extends React.Component { constructor(props, context) { super(props, context); + this.trackOptionClick = this.trackOptionClick.bind(this); this.handleOptionClick = this.handleOptionClick.bind(this); this.handleNoneClick = this.handleNoneClick.bind(this); } + trackOptionClick(optionId, nextOptions) { + trackMixpanelEvent('scope.topology.option.click', { + optionId, + value: nextOptions, + layout: this.props.topologyViewMode, + topologyId: this.props.currentTopology.get('id'), + parentTopologyId: this.props.currentTopology.get('parentId'), + }); + } + handleOptionClick(optionId, value, topologyId) { let nextOptions = [value]; const { activeOptions, options } = this.props; @@ -44,15 +56,18 @@ class TopologyOptions extends React.Component { nextOptions = nextOptions.filter(o => o !== 'none'); } } + this.trackOptionClick(optionId, nextOptions); this.props.changeTopologyOption(optionId, nextOptions, topologyId); } handleNoneClick(optionId, value, topologyId) { - this.props.changeTopologyOption(optionId, ['none'], topologyId); + const nextOptions = ['none']; + this.trackOptionClick(optionId, nextOptions); + this.props.changeTopologyOption(optionId, nextOptions, topologyId); } renderOption(option) { - const { activeOptions, topologyId } = this.props; + const { activeOptions, currentTopologyId } = this.props; const optionId = option.get('id'); const activeValue = activeOptions && activeOptions.has(optionId) ? activeOptions.get(optionId) @@ -68,7 +83,7 @@ class TopologyOptions extends React.Component { } @@ -102,7 +117,9 @@ class TopologyOptions extends React.Component { function mapStateToProps(state) { return { options: getCurrentTopologyOptions(state), - topologyId: state.get('currentTopologyId'), + topologyViewMode: state.get('topologyViewMode'), + currentTopology: state.get('currentTopology'), + currentTopologyId: state.get('currentTopologyId'), activeOptions: activeTopologyOptionsSelector(state) }; } diff --git a/client/app/scripts/components/view-mode-selector.js b/client/app/scripts/components/view-mode-selector.js index 4e8cfdaa98..ba447b3c74 100644 --- a/client/app/scripts/components/view-mode-selector.js +++ b/client/app/scripts/components/view-mode-selector.js @@ -3,32 +3,20 @@ import { connect } from 'react-redux'; import classNames from 'classnames'; import MetricSelector from './metric-selector'; +import { trackMixpanelEvent } from '../utils/tracking-utils'; import { setGraphView, setTableView, setResourceView } from '../actions/app-actions'; -import { layersTopologyIdsSelector } from '../selectors/resource-view/layout'; import { availableMetricsSelector } from '../selectors/node-metric'; import { - isGraphViewModeSelector, - isTableViewModeSelector, isResourceViewModeSelector, + resourceViewAvailableSelector, } from '../selectors/topology'; +import { + GRAPH_VIEW_MODE, + TABLE_VIEW_MODE, + RESOURCE_VIEW_MODE, +} from '../constants/naming'; -const Item = (icons, label, isSelected, onClick, isEnabled = true) => { - const className = classNames('view-mode-selector-action', { - 'view-mode-selector-action-selected': isSelected, - }); - return ( -
- - {label} -
- ); -}; - class ViewModeSelector extends React.Component { componentWillReceiveProps(nextProps) { if (nextProps.isResourceViewMode && !nextProps.hasResourceView) { @@ -36,16 +24,42 @@ class ViewModeSelector extends React.Component { } } + renderItem(icons, label, viewMode, setViewModeAction, isEnabled = true) { + const isSelected = (this.props.topologyViewMode === viewMode); + const className = classNames('view-mode-selector-action', { + 'view-mode-selector-action-selected': isSelected, + }); + const onClick = () => { + trackMixpanelEvent('scope.layout.selector.click', { + layout: viewMode, + topologyId: this.props.currentTopology.get('id'), + parentTopologyId: this.props.currentTopology.get('parentId'), + }); + setViewModeAction(); + }; + + return ( +
+ + {label} +
+ ); + } + render() { - const { isGraphViewMode, isTableViewMode, isResourceViewMode, hasResourceView } = this.props; + const { hasResourceView } = this.props; return (
- {Item('fa fa-share-alt', 'Graph', isGraphViewMode, this.props.setGraphView)} - {Item('fa fa-table', 'Table', isTableViewMode, this.props.setTableView)} - {Item('fa fa-bar-chart', 'Resources', isResourceViewMode, this.props.setResourceView, - hasResourceView)} + {this.renderItem('fa fa-share-alt', 'Graph', GRAPH_VIEW_MODE, this.props.setGraphView)} + {this.renderItem('fa fa-table', 'Table', TABLE_VIEW_MODE, this.props.setTableView)} + {this.renderItem('fa fa-bar-chart', 'Resources', RESOURCE_VIEW_MODE, + this.props.setResourceView, hasResourceView)}
@@ -55,11 +69,11 @@ class ViewModeSelector extends React.Component { function mapStateToProps(state) { return { - isGraphViewMode: isGraphViewModeSelector(state), - isTableViewMode: isTableViewModeSelector(state), isResourceViewMode: isResourceViewModeSelector(state), - hasResourceView: !layersTopologyIdsSelector(state).isEmpty(), + hasResourceView: resourceViewAvailableSelector(state), showingMetricsSelector: availableMetricsSelector(state).count() > 0, + topologyViewMode: state.get('topologyViewMode'), + currentTopology: state.get('currentTopology'), }; } diff --git a/client/app/scripts/constants/key-codes.js b/client/app/scripts/constants/key-codes.js new file mode 100644 index 0000000000..7aa6b15e69 --- /dev/null +++ b/client/app/scripts/constants/key-codes.js @@ -0,0 +1,4 @@ + +export const BACKSPACE_KEY_CODE = 8; +export const ENTER_KEY_CODE = 13; +export const ESC_KEY_CODE = 27; diff --git a/client/app/scripts/selectors/node-metric.js b/client/app/scripts/selectors/node-metric.js index 2a81ecd05f..6ac3f5d448 100644 --- a/client/app/scripts/selectors/node-metric.js +++ b/client/app/scripts/selectors/node-metric.js @@ -2,6 +2,7 @@ import { createSelector } from 'reselect'; import { createMapSelector, createListSelector } from 'reselect-map'; import { fromJS, Map as makeMap, List as makeList } from 'immutable'; +import { modulo } from '../utils/math-utils'; import { isGraphViewModeSelector, isResourceViewModeSelector } from '../selectors/topology'; import { RESOURCE_VIEW_METRICS } from '../constants/resources'; @@ -54,6 +55,30 @@ export const pinnedMetricSelector = createSelector( (availableMetrics, metricType) => availableMetrics.find(m => m.get('label') === metricType) ); +export const nextPinnedMetricTypeSelector = createSelector( + [ + availableMetricTypesSelector, + state => state.get('pinnedMetricType'), + ], + (metricTypes, pinnedMetricType) => { + const currentIndex = metricTypes.indexOf(pinnedMetricType); + const nextIndex = modulo(currentIndex + 1, metricTypes.count()); + return metricTypes.get(pinnedMetricType ? nextIndex : 0); + } +); + +export const previousPinnedMetricTypeSelector = createSelector( + [ + availableMetricTypesSelector, + state => state.get('pinnedMetricType'), + ], + (metricTypes, pinnedMetricType) => { + const currentIndex = metricTypes.indexOf(pinnedMetricType); + const previousIndex = modulo(currentIndex - 1, metricTypes.count()); + return metricTypes.get(pinnedMetricType ? previousIndex : 0); + } +); + export const selectedMetricTypeSelector = createSelector( [ state => state.get('pinnedMetricType'), diff --git a/client/app/scripts/selectors/topology.js b/client/app/scripts/selectors/topology.js index 30b39fea7b..90672a1d10 100644 --- a/client/app/scripts/selectors/topology.js +++ b/client/app/scripts/selectors/topology.js @@ -1,5 +1,6 @@ import { createSelector } from 'reselect'; +import { layersTopologyIdsSelector } from './resource-view/layout'; import { RESOURCE_VIEW_MODE, GRAPH_VIEW_MODE, @@ -30,6 +31,13 @@ export const isResourceViewModeSelector = createSelector( viewMode => viewMode === RESOURCE_VIEW_MODE ); +export const resourceViewAvailableSelector = createSelector( + [ + layersTopologyIdsSelector + ], + layersTopologyIds => !layersTopologyIds.isEmpty() +); + // Checks if graph complexity is high. Used to trigger // table view on page load and decide on animations. export const graphExceedsComplexityThreshSelector = createSelector( diff --git a/client/app/scripts/utils/tracking-utils.js b/client/app/scripts/utils/tracking-utils.js new file mode 100644 index 0000000000..94f5b73253 --- /dev/null +++ b/client/app/scripts/utils/tracking-utils.js @@ -0,0 +1,12 @@ +import debug from 'debug'; + +const log = debug('service:tracking'); + +// Track mixpanel events only if Scope is running inside of Weave Cloud. +export function trackMixpanelEvent(name, props) { + if (window.mixpanel && process.env.WEAVE_CLOUD) { + window.mixpanel.track(name, props); + } else { + log('trackMixpanelEvent', name, props); + } +}