mirror of
https://github.com/weaveworks/scope.git
synced 2026-08-18 03:46:45 +00:00
Added mixpanel tracking for some basic events (#2462)
* Added mixpanel tracking for bunch of events. * Changed hitEnter action to pinSearch. * Moved all the event tracking out of app-actions.js * Addressed @foot's comment. * Added more keypress events tracking. * Disable 'r' keyboard shortcut when Resource View is disabled
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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'),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
</a>
|
||||
<a
|
||||
className="footer-icon"
|
||||
onClick={this.props.clickForceRelayout}
|
||||
onClick={this.handleRelayoutClick}
|
||||
title={forceRelayoutTitle}>
|
||||
<span className="fa fa-refresh" />
|
||||
</a>
|
||||
@@ -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'),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 => <SearchItem query={query} key={query} />)}
|
||||
<input
|
||||
className="search-input-field" type="text" id={inputId}
|
||||
value={value} onChange={this.handleChange}
|
||||
value={value} onChange={this.handleChange} onKeyUp={this.handleKeyUp}
|
||||
onFocus={this.handleFocus} onBlur={this.handleBlur}
|
||||
disabled={disabled} ref={this.saveQueryInputRef} />
|
||||
</div>
|
||||
@@ -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);
|
||||
|
||||
@@ -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 (
|
||||
<div
|
||||
className={className} title={title} key={topologyId} rel={topologyId}
|
||||
onClick={this.onTopologyClick}>
|
||||
onClick={ev => this.onTopologyClick(ev, subTopology)}>
|
||||
<div className="topologies-sub-item-label">
|
||||
{subTopology.get('name')}
|
||||
</div>
|
||||
@@ -65,7 +70,9 @@ class Topologies extends React.Component {
|
||||
|
||||
return (
|
||||
<div className="topologies-item" key={topologyId}>
|
||||
<div className={className} title={title} rel={topologyId} onClick={this.onTopologyClick}>
|
||||
<div
|
||||
className={className} title={title} rel={topologyId}
|
||||
onClick={ev => this.onTopologyClick(ev, topology)}>
|
||||
<div className="topologies-item-label">
|
||||
{topology.get('name')}
|
||||
</div>
|
||||
|
||||
@@ -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 {
|
||||
<TopologyOptionAction
|
||||
onClick={this.handleOptionClick}
|
||||
optionId={optionId}
|
||||
topologyId={topologyId}
|
||||
topologyId={currentTopologyId}
|
||||
key={item.get('value')}
|
||||
activeValue={activeValue}
|
||||
item={item}
|
||||
@@ -79,7 +94,7 @@ class TopologyOptions extends React.Component {
|
||||
onClick={this.handleNoneClick}
|
||||
optionId={optionId}
|
||||
item={noneItem}
|
||||
topologyId={topologyId}
|
||||
topologyId={currentTopologyId}
|
||||
activeValue={activeValue}
|
||||
/>
|
||||
}
|
||||
@@ -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)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<div
|
||||
className={className}
|
||||
disabled={!isEnabled}
|
||||
onClick={isEnabled && onClick}
|
||||
title={`View ${label.toLowerCase()}`}>
|
||||
<span className={icons} style={{fontSize: 12}} />
|
||||
<span className="label">{label}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<div
|
||||
className={className}
|
||||
disabled={!isEnabled}
|
||||
onClick={isEnabled && onClick}
|
||||
title={`View ${label.toLowerCase()}`}>
|
||||
<span className={icons} style={{ fontSize: 12 }} />
|
||||
<span className="label">{label}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { isGraphViewMode, isTableViewMode, isResourceViewMode, hasResourceView } = this.props;
|
||||
const { hasResourceView } = this.props;
|
||||
|
||||
return (
|
||||
<div className="view-mode-selector">
|
||||
<div className="view-mode-selector-wrapper">
|
||||
{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)}
|
||||
</div>
|
||||
<MetricSelector />
|
||||
</div>
|
||||
@@ -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'),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
|
||||
export const BACKSPACE_KEY_CODE = 8;
|
||||
export const ENTER_KEY_CODE = 13;
|
||||
export const ESC_KEY_CODE = 27;
|
||||
@@ -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'),
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user