app.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308
  1. import debug from 'debug';
  2. import React from 'react';
  3. import PropTypes from 'prop-types';
  4. import classNames from 'classnames';
  5. import { connect } from 'react-redux';
  6. import { debounce, isEqual } from 'lodash';
  7. import { ThemeProvider } from 'styled-components';
  8. import commonTheme from 'weaveworks-ui-components/lib/theme';
  9. import GlobalStyle from './global-style';
  10. // import Logo from './logo';
  11. import Footer from './footer';
  12. import Sidebar from './sidebar';
  13. import HelpPanel from './help-panel';
  14. import TroubleshootingMenu from './troubleshooting-menu';
  15. import Search from './search';
  16. import Status from './status';
  17. import Topologies from './topologies';
  18. import TopologyOptions from './topology-options';
  19. import Overlay from './overlay';
  20. import {
  21. pinNextMetric,
  22. pinPreviousMetric,
  23. hitEsc,
  24. unpinMetric,
  25. toggleHelp,
  26. setGraphView,
  27. setMonitorState,
  28. setTableView,
  29. setStoreViewState,
  30. setViewportDimensions,
  31. } from '../actions/app-actions';
  32. import {
  33. focusSearch,
  34. getApiDetails,
  35. setResourceView,
  36. getTopologiesWithInitialPoll,
  37. shutdown,
  38. } from '../actions/request-actions';
  39. import Details from './details';
  40. import Nodes from './nodes';
  41. import TimeControl from './time-control';
  42. import TimeTravelWrapper from './time-travel-wrapper';
  43. import ViewModeSelector from './view-mode-selector';
  44. import NetworkSelector from './networks-selector';
  45. import DebugToolbar, { showingDebugToolbar, toggleDebugToolbar } from './debug-toolbar';
  46. import { getUrlState } from '../utils/router-utils';
  47. import { getRouter } from '../router';
  48. import { trackAnalyticsEvent } from '../utils/tracking-utils';
  49. import { availableNetworksSelector } from '../selectors/node-networks';
  50. import { timeTravelSupportedSelector } from '../selectors/time-travel';
  51. import {
  52. isResourceViewModeSelector,
  53. isTableViewModeSelector,
  54. isGraphViewModeSelector,
  55. } from '../selectors/topology';
  56. import defaultTheme from '../themes/default';
  57. import contrastTheme from '../themes/contrast';
  58. import { VIEWPORT_RESIZE_DEBOUNCE_INTERVAL } from '../constants/timer';
  59. import {
  60. ESC_KEY_CODE,
  61. } from '../constants/key-codes';
  62. const keyPressLog = debug('scope:app-key-press');
  63. class App extends React.Component {
  64. constructor(props, context) {
  65. super(props, context);
  66. this.props.dispatch(setMonitorState(this.props.monitor));
  67. this.props.dispatch(setStoreViewState(!this.props.disableStoreViewState));
  68. this.setViewportDimensions = this.setViewportDimensions.bind(this);
  69. this.handleResize = debounce(this.setViewportDimensions, VIEWPORT_RESIZE_DEBOUNCE_INTERVAL);
  70. this.handleRouteChange = debounce(props.onRouteChange, 50);
  71. this.saveAppRef = this.saveAppRef.bind(this);
  72. this.onKeyPress = this.onKeyPress.bind(this);
  73. this.onKeyUp = this.onKeyUp.bind(this);
  74. }
  75. componentDidMount() {
  76. this.setViewportDimensions();
  77. window.addEventListener('resize', this.handleResize);
  78. window.addEventListener('keypress', this.onKeyPress);
  79. window.addEventListener('keyup', this.onKeyUp);
  80. this.router = this.props.dispatch(getRouter(this.props.urlState));
  81. this.router.start({ hashbang: true });
  82. if (!this.props.routeSet || process.env.WEAVE_CLOUD) {
  83. // dont request topologies when already done via router.
  84. // If running as a component, always request topologies when the app mounts.
  85. this.props.dispatch(getTopologiesWithInitialPoll());
  86. }
  87. getApiDetails(this.props.dispatch);
  88. }
  89. componentWillUnmount() {
  90. window.removeEventListener('resize', this.handleResize);
  91. window.removeEventListener('keypress', this.onKeyPress);
  92. window.removeEventListener('keyup', this.onKeyUp);
  93. this.props.dispatch(shutdown());
  94. this.router.stop();
  95. }
  96. componentWillReceiveProps(nextProps) {
  97. if (nextProps.monitor !== this.props.monitor) {
  98. this.props.dispatch(setMonitorState(nextProps.monitor));
  99. }
  100. if (nextProps.disableStoreViewState !== this.props.disableStoreViewState) {
  101. this.props.dispatch(setStoreViewState(!nextProps.disableStoreViewState));
  102. }
  103. // Debounce-notify about the route change if the URL state changes its content.
  104. if (!isEqual(nextProps.urlState, this.props.urlState)) {
  105. this.handleRouteChange(nextProps.urlState);
  106. }
  107. }
  108. onKeyUp(ev) {
  109. const { showingTerminal } = this.props;
  110. keyPressLog('onKeyUp', 'keyCode', ev.keyCode, ev);
  111. // don't get esc in onKeyPress
  112. if (ev.keyCode === ESC_KEY_CODE) {
  113. this.props.dispatch(hitEsc());
  114. } else if (ev.code === 'KeyD' && ev.ctrlKey && !showingTerminal) {
  115. toggleDebugToolbar();
  116. this.forceUpdate();
  117. }
  118. }
  119. onKeyPress(ev) {
  120. const { dispatch, searchFocused, showingTerminal } = this.props;
  121. //
  122. // keyup gives 'key'
  123. // keypress gives 'char'
  124. // Distinction is important for international keyboard layouts where there
  125. // is often a different {key: char} mapping.
  126. if (!searchFocused && !showingTerminal) {
  127. keyPressLog('onKeyPress', 'keyCode', ev.keyCode, ev);
  128. const char = String.fromCharCode(ev.charCode);
  129. if (char === '<') {
  130. dispatch(pinPreviousMetric());
  131. this.trackEvent('scope.metric.selector.pin.previous.keypress', {
  132. metricType: this.props.pinnedMetricType
  133. });
  134. } else if (char === '>') {
  135. dispatch(pinNextMetric());
  136. this.trackEvent('scope.metric.selector.pin.next.keypress', {
  137. metricType: this.props.pinnedMetricType
  138. });
  139. } else if (char === 'g') {
  140. dispatch(setGraphView());
  141. this.trackEvent('scope.layout.selector.keypress');
  142. } else if (char === 't') {
  143. dispatch(setTableView());
  144. this.trackEvent('scope.layout.selector.keypress');
  145. } else if (char === 'r') {
  146. dispatch(setResourceView());
  147. this.trackEvent('scope.layout.selector.keypress');
  148. } else if (char === 'q') {
  149. this.trackEvent('scope.metric.selector.unpin.keypress', {
  150. metricType: this.props.pinnedMetricType
  151. });
  152. dispatch(unpinMetric());
  153. } else if (char === '/') {
  154. ev.preventDefault();
  155. ev.stopPropagation();
  156. dispatch(focusSearch());
  157. } else if (char === '?') {
  158. dispatch(toggleHelp());
  159. }
  160. }
  161. }
  162. trackEvent(eventName, additionalProps = {}) {
  163. trackAnalyticsEvent(eventName, {
  164. layout: this.props.topologyViewMode,
  165. parentTopologyId: this.props.currentTopology.get('parentId'),
  166. topologyId: this.props.currentTopology.get('id'),
  167. ...additionalProps,
  168. });
  169. }
  170. setViewportDimensions() {
  171. if (this.appRef) {
  172. const { width, height } = this.appRef.getBoundingClientRect();
  173. this.props.dispatch(setViewportDimensions(width, height));
  174. }
  175. }
  176. saveAppRef(ref) {
  177. this.appRef = ref;
  178. }
  179. render() {
  180. const {
  181. isTableViewMode, isGraphViewMode, isResourceViewMode, showingDetails,
  182. showingHelp, showingNetworkSelector, showingTroubleshootingMenu,
  183. timeTravelTransitioning, timeTravelSupported, contrastMode,
  184. } = this.props;
  185. const className = classNames('scope-app', {
  186. 'contrast-mode': contrastMode,
  187. 'time-travel-open': timeTravelSupported,
  188. });
  189. const isIframe = window !== window.top;
  190. return (
  191. <ThemeProvider theme={{...commonTheme, scope: contrastMode ? contrastTheme : defaultTheme }}>
  192. <>
  193. <GlobalStyle />
  194. <div className={className} ref={this.saveAppRef}>
  195. {showingDebugToolbar() && <DebugToolbar />}
  196. {showingHelp && <HelpPanel />}
  197. {showingTroubleshootingMenu && <TroubleshootingMenu />}
  198. {showingDetails && (
  199. <Details
  200. renderNodeDetailsExtras={this.props.renderNodeDetailsExtras}
  201. />
  202. )}
  203. <div className="header">
  204. {timeTravelSupported && this.props.renderTimeTravel()}
  205. <div className="selectors">
  206. {/* <div className="logo">
  207. {!isIframe
  208. && (
  209. <svg width="100%" height="100%" viewBox="0 0 1089 217">
  210. <Logo />
  211. </svg>
  212. )
  213. }
  214. </div> */}
  215. <Search />
  216. <Topologies />
  217. <ViewModeSelector />
  218. <TimeControl />
  219. </div>
  220. </div>
  221. <Nodes />
  222. <Sidebar classNames={isTableViewMode ? 'sidebar-gridmode' : ''}>
  223. {/* {showingNetworkSelector && isGraphViewMode && <NetworkSelector />} */}
  224. {/* {!isResourceViewMode && <Status />} */}
  225. {!isResourceViewMode && <TopologyOptions />}
  226. </Sidebar>
  227. <Footer />
  228. <Overlay faded={timeTravelTransitioning} />
  229. </div>
  230. </>
  231. </ThemeProvider>
  232. );
  233. }
  234. }
  235. function mapStateToProps(state) {
  236. return {
  237. contrastMode: state.get('contrastMode'),
  238. currentTopology: state.get('currentTopology'),
  239. isGraphViewMode: isGraphViewModeSelector(state),
  240. isResourceViewMode: isResourceViewModeSelector(state),
  241. isTableViewMode: isTableViewModeSelector(state),
  242. pinnedMetricType: state.get('pinnedMetricType'),
  243. routeSet: state.get('routeSet'),
  244. searchFocused: state.get('searchFocused'),
  245. searchQuery: state.get('searchQuery'),
  246. showingDetails: state.get('nodeDetails').size > 0,
  247. showingHelp: state.get('showingHelp'),
  248. showingNetworkSelector: availableNetworksSelector(state).count() > 0,
  249. showingTerminal: state.get('controlPipes').size > 0,
  250. showingTroubleshootingMenu: state.get('showingTroubleshootingMenu'),
  251. timeTravelSupported: timeTravelSupportedSelector(state),
  252. timeTravelTransitioning: state.get('timeTravelTransitioning'),
  253. topologyViewMode: state.get('topologyViewMode'),
  254. urlState: getUrlState(state)
  255. };
  256. }
  257. App.propTypes = {
  258. disableStoreViewState: PropTypes.bool,
  259. monitor: PropTypes.bool,
  260. onRouteChange: PropTypes.func,
  261. renderNodeDetailsExtras: PropTypes.func,
  262. renderTimeTravel: PropTypes.func,
  263. };
  264. App.defaultProps = {
  265. disableStoreViewState: false,
  266. monitor: false,
  267. onRouteChange: () => null,
  268. renderNodeDetailsExtras: () => null,
  269. renderTimeTravel: () => <TimeTravelWrapper />,
  270. };
  271. export default connect(mapStateToProps)(App);