Explore.tsx 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532
  1. import React from 'react';
  2. import { hot } from 'react-hot-loader';
  3. import { connect } from 'react-redux';
  4. import _ from 'lodash';
  5. import { withSize } from 'react-sizeme';
  6. import { RawTimeRange, TimeRange } from '@grafana/ui';
  7. import { DataSourceSelectItem } from 'app/types/datasources';
  8. import { ExploreUrlState, HistoryItem, QueryTransaction, RangeScanner } from 'app/types/explore';
  9. import { DataQuery } from 'app/types/series';
  10. import store from 'app/core/store';
  11. import { LAST_USED_DATASOURCE_KEY, ensureQueries } from 'app/core/utils/explore';
  12. import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker';
  13. import { Emitter } from 'app/core/utils/emitter';
  14. import {
  15. addQueryRow,
  16. changeDatasource,
  17. changeQuery,
  18. changeSize,
  19. changeTime,
  20. clickClear,
  21. clickExample,
  22. clickGraphButton,
  23. clickLogsButton,
  24. clickTableButton,
  25. highlightLogsExpression,
  26. initializeExplore,
  27. modifyQueries,
  28. removeQueryRow,
  29. runQueries,
  30. scanStart,
  31. scanStop,
  32. } from './state/actions';
  33. import { ExploreState } from './state/reducers';
  34. import Panel from './Panel';
  35. import QueryRows from './QueryRows';
  36. import Graph from './Graph';
  37. import Logs from './Logs';
  38. import Table from './Table';
  39. import ErrorBoundary from './ErrorBoundary';
  40. import { Alert } from './Error';
  41. import TimePicker, { parseTime } from './TimePicker';
  42. import { LogsModel } from 'app/core/logs_model';
  43. import TableModel from 'app/core/table_model';
  44. interface ExploreProps {
  45. StartPage?: any;
  46. addQueryRow: typeof addQueryRow;
  47. changeDatasource: typeof changeDatasource;
  48. changeQuery: typeof changeQuery;
  49. changeTime: typeof changeTime;
  50. clickClear: typeof clickClear;
  51. clickExample: typeof clickExample;
  52. clickGraphButton: typeof clickGraphButton;
  53. clickLogsButton: typeof clickLogsButton;
  54. clickTableButton: typeof clickTableButton;
  55. datasourceError: string;
  56. datasourceInstance: any;
  57. datasourceLoading: boolean | null;
  58. datasourceMissing: boolean;
  59. exploreDatasources: DataSourceSelectItem[];
  60. graphResult?: any[];
  61. highlightLogsExpression: typeof highlightLogsExpression;
  62. history: HistoryItem[];
  63. initialDatasource?: string;
  64. initialQueries: DataQuery[];
  65. initializeExplore: typeof initializeExplore;
  66. logsHighlighterExpressions?: string[];
  67. logsResult?: LogsModel;
  68. modifyQueries: typeof modifyQueries;
  69. onChangeSplit: (split: boolean, state?: ExploreState) => void;
  70. onSaveState: (key: string, state: ExploreState) => void;
  71. position: string;
  72. queryTransactions: QueryTransaction[];
  73. removeQueryRow: typeof removeQueryRow;
  74. range: RawTimeRange;
  75. runQueries: typeof runQueries;
  76. scanner?: RangeScanner;
  77. scanning?: boolean;
  78. scanRange?: RawTimeRange;
  79. scanStart: typeof scanStart;
  80. scanStop: typeof scanStop;
  81. split: boolean;
  82. splitState?: ExploreState;
  83. stateKey: string;
  84. showingGraph: boolean;
  85. showingLogs: boolean;
  86. showingStartPage?: boolean;
  87. showingTable: boolean;
  88. supportsGraph: boolean | null;
  89. supportsLogs: boolean | null;
  90. supportsTable: boolean | null;
  91. tableResult?: TableModel;
  92. urlState: ExploreUrlState;
  93. }
  94. /**
  95. * Explore provides an area for quick query iteration for a given datasource.
  96. * Once a datasource is selected it populates the query section at the top.
  97. * When queries are run, their results are being displayed in the main section.
  98. * The datasource determines what kind of query editor it brings, and what kind
  99. * of results viewers it supports.
  100. *
  101. * QUERY HANDLING
  102. *
  103. * TLDR: to not re-render Explore during edits, query editing is not "controlled"
  104. * in a React sense: values need to be pushed down via `initialQueries`, while
  105. * edits travel up via `this.modifiedQueries`.
  106. *
  107. * By default the query rows start without prior state: `initialQueries` will
  108. * contain one empty DataQuery. While the user modifies the DataQuery, the
  109. * modifications are being tracked in `this.modifiedQueries`, which need to be
  110. * used whenever a query is sent to the datasource to reflect what the user sees
  111. * on the screen. Query"react-popper": "^0.7.5", rows can be initialized or reset using `initialQueries`,
  112. * by giving the respec"react-popper": "^0.7.5",tive row a new key. This wipes the old row and its state.
  113. * This property is als"react-popper": "^0.7.5",o used to govern how many query rows there are (minimum 1).
  114. *
  115. * This flow makes sure that a query row can be arbitrarily complex without the
  116. * fear of being wiped or re-initialized via props. The query row is free to keep
  117. * its own state while the user edits or builds a query. Valid queries can be sent
  118. * up to Explore via the `onChangeQuery` prop.
  119. *
  120. * DATASOURCE REQUESTS
  121. *
  122. * A click on Run Query creates transactions for all DataQueries for all expanded
  123. * result viewers. New runs are discarding previous runs. Upon completion a transaction
  124. * saves the result. The result viewers construct their data from the currently existing
  125. * transactions.
  126. *
  127. * The result viewers determine some of the query options sent to the datasource, e.g.,
  128. * `format`, to indicate eventual transformations by the datasources' result transformers.
  129. */
  130. export class Explore extends React.PureComponent<ExploreProps, any> {
  131. el: any;
  132. exploreEvents: Emitter;
  133. /**
  134. * Timepicker to control scanning
  135. */
  136. timepickerRef: React.RefObject<TimePicker>;
  137. constructor(props) {
  138. super(props);
  139. this.exploreEvents = new Emitter();
  140. this.timepickerRef = React.createRef();
  141. }
  142. async componentDidMount() {
  143. // Load URL state and parse range
  144. const { datasource, queries, range } = this.props.urlState as ExploreUrlState;
  145. const initialDatasource = datasource || store.get(LAST_USED_DATASOURCE_KEY);
  146. const initialQueries: DataQuery[] = ensureQueries(queries);
  147. const initialRange = { from: parseTime(range.from), to: parseTime(range.to) };
  148. const width = this.el ? this.el.offsetWidth : 0;
  149. this.props.initializeExplore(initialDatasource, initialQueries, initialRange, width, this.exploreEvents);
  150. }
  151. componentWillUnmount() {
  152. this.exploreEvents.removeAllListeners();
  153. }
  154. getRef = el => {
  155. this.el = el;
  156. };
  157. onAddQueryRow = index => {
  158. this.props.addQueryRow(index);
  159. };
  160. onChangeDatasource = async option => {
  161. this.props.changeDatasource(option.value);
  162. };
  163. onChangeQuery = (query: DataQuery, index: number, override?: boolean) => {
  164. const { changeQuery, datasourceInstance } = this.props;
  165. changeQuery(query, index, override);
  166. if (query && !override && datasourceInstance.getHighlighterExpression && index === 0) {
  167. // Live preview of log search matches. Only use on first row for now
  168. this.updateLogsHighlights(query);
  169. }
  170. };
  171. onChangeTime = (range: TimeRange, changedByScanner?: boolean) => {
  172. if (this.props.scanning && !changedByScanner) {
  173. this.onStopScanning();
  174. }
  175. this.props.changeTime(range);
  176. };
  177. onClickClear = () => {
  178. this.props.clickClear();
  179. };
  180. onClickCloseSplit = () => {
  181. const { onChangeSplit } = this.props;
  182. if (onChangeSplit) {
  183. onChangeSplit(false);
  184. }
  185. };
  186. onClickGraphButton = () => {
  187. this.props.clickGraphButton();
  188. };
  189. onClickLogsButton = () => {
  190. this.props.clickLogsButton();
  191. };
  192. // Use this in help pages to set page to a single query
  193. onClickExample = (query: DataQuery) => {
  194. this.props.clickExample(query);
  195. };
  196. onClickSplit = () => {
  197. const { onChangeSplit } = this.props;
  198. if (onChangeSplit) {
  199. // const state = this.cloneState();
  200. // onChangeSplit(true, state);
  201. }
  202. };
  203. onClickTableButton = () => {
  204. this.props.clickTableButton();
  205. };
  206. onClickLabel = (key: string, value: string) => {
  207. this.onModifyQueries({ type: 'ADD_FILTER', key, value });
  208. };
  209. onModifyQueries = (action, index?: number) => {
  210. const { datasourceInstance } = this.props;
  211. if (datasourceInstance && datasourceInstance.modifyQuery) {
  212. const modifier = (queries: DataQuery, action: any) => datasourceInstance.modifyQuery(queries, action);
  213. this.props.modifyQueries(action, index, modifier);
  214. }
  215. };
  216. onRemoveQueryRow = index => {
  217. this.props.removeQueryRow(index);
  218. };
  219. onStartScanning = () => {
  220. // Scanner will trigger a query
  221. const scanner = this.scanPreviousRange;
  222. this.props.scanStart(scanner);
  223. };
  224. scanPreviousRange = (): RawTimeRange => {
  225. // Calling move() on the timepicker will trigger this.onChangeTime()
  226. return this.timepickerRef.current.move(-1, true);
  227. };
  228. onStopScanning = () => {
  229. this.props.scanStop();
  230. };
  231. onSubmit = () => {
  232. this.props.runQueries();
  233. };
  234. updateLogsHighlights = _.debounce((value: DataQuery) => {
  235. const { datasourceInstance } = this.props;
  236. if (datasourceInstance.getHighlighterExpression) {
  237. const expressions = [datasourceInstance.getHighlighterExpression(value)];
  238. this.props.highlightLogsExpression(expressions);
  239. }
  240. }, 500);
  241. // cloneState(): ExploreState {
  242. // // Copy state, but copy queries including modifications
  243. // return {
  244. // ...this.state,
  245. // queryTransactions: [],
  246. // initialQueries: [...this.modifiedQueries],
  247. // };
  248. // }
  249. // saveState = () => {
  250. // const { stateKey, onSaveState } = this.props;
  251. // onSaveState(stateKey, this.cloneState());
  252. // };
  253. render() {
  254. const {
  255. StartPage,
  256. datasourceInstance,
  257. datasourceError,
  258. datasourceLoading,
  259. datasourceMissing,
  260. exploreDatasources,
  261. graphResult,
  262. history,
  263. initialQueries,
  264. logsHighlighterExpressions,
  265. logsResult,
  266. queryTransactions,
  267. position,
  268. range,
  269. scanning,
  270. scanRange,
  271. showingGraph,
  272. showingLogs,
  273. showingStartPage,
  274. showingTable,
  275. split,
  276. supportsGraph,
  277. supportsLogs,
  278. supportsTable,
  279. tableResult,
  280. } = this.props;
  281. const graphHeight = showingGraph && showingTable ? '200px' : '400px';
  282. const exploreClass = split ? 'explore explore-split' : 'explore';
  283. const selectedDatasource = datasourceInstance
  284. ? exploreDatasources.find(d => d.name === datasourceInstance.name)
  285. : undefined;
  286. const graphLoading = queryTransactions.some(qt => qt.resultType === 'Graph' && !qt.done);
  287. const tableLoading = queryTransactions.some(qt => qt.resultType === 'Table' && !qt.done);
  288. const logsLoading = queryTransactions.some(qt => qt.resultType === 'Logs' && !qt.done);
  289. const loading = queryTransactions.some(qt => !qt.done);
  290. return (
  291. <div className={exploreClass} ref={this.getRef}>
  292. <div className="navbar">
  293. {position === 'left' ? (
  294. <div>
  295. <a className="navbar-page-btn">
  296. <i className="fa fa-rocket" />
  297. Explore
  298. </a>
  299. </div>
  300. ) : (
  301. <div className="navbar-buttons explore-first-button">
  302. <button className="btn navbar-button" onClick={this.onClickCloseSplit}>
  303. Close Split
  304. </button>
  305. </div>
  306. )}
  307. {!datasourceMissing ? (
  308. <div className="navbar-buttons">
  309. <DataSourcePicker
  310. onChange={this.onChangeDatasource}
  311. datasources={exploreDatasources}
  312. current={selectedDatasource}
  313. />
  314. </div>
  315. ) : null}
  316. <div className="navbar__spacer" />
  317. {position === 'left' && !split ? (
  318. <div className="navbar-buttons">
  319. <button className="btn navbar-button" onClick={this.onClickSplit}>
  320. Split
  321. </button>
  322. </div>
  323. ) : null}
  324. <TimePicker ref={this.timepickerRef} range={range} onChangeTime={this.onChangeTime} />
  325. <div className="navbar-buttons">
  326. <button className="btn navbar-button navbar-button--no-icon" onClick={this.onClickClear}>
  327. Clear All
  328. </button>
  329. </div>
  330. <div className="navbar-buttons relative">
  331. <button className="btn navbar-button navbar-button--primary" onClick={this.onSubmit}>
  332. Run Query{' '}
  333. {loading ? <i className="fa fa-spinner fa-fw fa-spin run-icon" /> : <i className="fa fa-level-down fa-fw run-icon" />}
  334. </button>
  335. </div>
  336. </div>
  337. {datasourceLoading ? <div className="explore-container">Loading datasource...</div> : null}
  338. {datasourceMissing ? (
  339. <div className="explore-container">Please add a datasource that supports Explore (e.g., Prometheus).</div>
  340. ) : null}
  341. {datasourceError && (
  342. <div className="explore-container">
  343. <Alert message={`Error connecting to datasource: ${datasourceError}`} />
  344. </div>
  345. )}
  346. {datasourceInstance && !datasourceError ? (
  347. <div className="explore-container">
  348. <QueryRows
  349. datasource={datasourceInstance}
  350. history={history}
  351. initialQueries={initialQueries}
  352. onAddQueryRow={this.onAddQueryRow}
  353. onChangeQuery={this.onChangeQuery}
  354. onClickHintFix={this.onModifyQueries}
  355. onExecuteQuery={this.onSubmit}
  356. onRemoveQueryRow={this.onRemoveQueryRow}
  357. transactions={queryTransactions}
  358. exploreEvents={this.exploreEvents}
  359. range={range}
  360. />
  361. <main className="m-t-2">
  362. <ErrorBoundary>
  363. {showingStartPage && <StartPage onClickExample={this.onClickExample} />}
  364. {!showingStartPage && (
  365. <>
  366. {supportsGraph && (
  367. <Panel
  368. label="Graph"
  369. isOpen={showingGraph}
  370. loading={graphLoading}
  371. onToggle={this.onClickGraphButton}
  372. >
  373. <Graph
  374. data={graphResult}
  375. height={graphHeight}
  376. id={`explore-graph-${position}`}
  377. onChangeTime={this.onChangeTime}
  378. range={range}
  379. split={split}
  380. />
  381. </Panel>
  382. )}
  383. {supportsTable && (
  384. <Panel
  385. label="Table"
  386. loading={tableLoading}
  387. isOpen={showingTable}
  388. onToggle={this.onClickTableButton}
  389. >
  390. <Table data={tableResult} loading={tableLoading} onClickCell={this.onClickLabel} />
  391. </Panel>
  392. )}
  393. {supportsLogs && (
  394. <Panel label="Logs" loading={logsLoading} isOpen={showingLogs} onToggle={this.onClickLogsButton}>
  395. <Logs
  396. data={logsResult}
  397. key={logsResult.id}
  398. highlighterExpressions={logsHighlighterExpressions}
  399. loading={logsLoading}
  400. position={position}
  401. onChangeTime={this.onChangeTime}
  402. onClickLabel={this.onClickLabel}
  403. onStartScanning={this.onStartScanning}
  404. onStopScanning={this.onStopScanning}
  405. range={range}
  406. scanning={scanning}
  407. scanRange={scanRange}
  408. />
  409. </Panel>
  410. )}
  411. </>
  412. )}
  413. </ErrorBoundary>
  414. </main>
  415. </div>
  416. ) : null}
  417. </div>
  418. );
  419. }
  420. }
  421. function mapStateToProps({ explore }) {
  422. const {
  423. StartPage,
  424. datasourceError,
  425. datasourceInstance,
  426. datasourceLoading,
  427. datasourceMissing,
  428. exploreDatasources,
  429. graphResult,
  430. initialDatasource,
  431. initialQueries,
  432. history,
  433. logsHighlighterExpressions,
  434. logsResult,
  435. queryTransactions,
  436. range,
  437. scanning,
  438. scanRange,
  439. showingGraph,
  440. showingLogs,
  441. showingStartPage,
  442. showingTable,
  443. supportsGraph,
  444. supportsLogs,
  445. supportsTable,
  446. tableResult,
  447. } = explore as ExploreState;
  448. return {
  449. StartPage,
  450. datasourceError,
  451. datasourceInstance,
  452. datasourceLoading,
  453. datasourceMissing,
  454. exploreDatasources,
  455. graphResult,
  456. initialDatasource,
  457. initialQueries,
  458. history,
  459. logsHighlighterExpressions,
  460. logsResult,
  461. queryTransactions,
  462. range,
  463. scanning,
  464. scanRange,
  465. showingGraph,
  466. showingLogs,
  467. showingStartPage,
  468. showingTable,
  469. supportsGraph,
  470. supportsLogs,
  471. supportsTable,
  472. tableResult,
  473. };
  474. }
  475. const mapDispatchToProps = {
  476. addQueryRow,
  477. changeDatasource,
  478. changeQuery,
  479. changeTime,
  480. clickClear,
  481. clickExample,
  482. clickGraphButton,
  483. clickLogsButton,
  484. clickTableButton,
  485. highlightLogsExpression,
  486. initializeExplore,
  487. modifyQueries,
  488. onSize: changeSize, // used by withSize HOC
  489. removeQueryRow,
  490. runQueries,
  491. scanStart,
  492. scanStop,
  493. };
  494. export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(withSize()(Explore)));