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:
Filip Barl
2017-04-27 14:26:07 +02:00
committed by GitHub
parent 1849f0f561
commit 65b9b48b53
16 changed files with 291 additions and 96 deletions
+35 -38
View File
@@ -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();
+9 -1
View File
@@ -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'),
};
}
+8 -1
View File
@@ -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());
}
+34 -10
View File
@@ -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)
};
}
+27 -6
View File
@@ -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,
+29 -4
View File
@@ -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);
+10 -3
View File
@@ -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'),
+8
View File
@@ -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);
}
}