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 =>