Explore.tsx 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503
  1. import { css, cx } from '@emotion/css';
  2. import memoizeOne from 'memoize-one';
  3. import React, { createRef } from 'react';
  4. import { connect, ConnectedProps } from 'react-redux';
  5. import AutoSizer from 'react-virtualized-auto-sizer';
  6. import { compose } from 'redux';
  7. import { Unsubscribable } from 'rxjs';
  8. import { AbsoluteTimeRange, DataQuery, GrafanaTheme2, LoadingState, RawTimeRange } from '@grafana/data';
  9. import { selectors } from '@grafana/e2e-selectors';
  10. import { Collapse, CustomScrollbar, ErrorBoundaryAlert, Themeable2, withTheme2, PanelContainer } from '@grafana/ui';
  11. import { FILTER_FOR_OPERATOR, FILTER_OUT_OPERATOR, FilterItem } from '@grafana/ui/src/components/Table/types';
  12. import appEvents from 'app/core/app_events';
  13. import { supportedFeatures } from 'app/core/history/richHistoryStorageProvider';
  14. import { getNodeGraphDataFrames } from 'app/plugins/panel/nodeGraph/utils';
  15. import { StoreState } from 'app/types';
  16. import { AbsoluteTimeEvent } from 'app/types/events';
  17. import { ExploreGraphStyle, ExploreId, ExploreItemState } from 'app/types/explore';
  18. import { getTimeZone } from '../profile/state/selectors';
  19. import { ExploreGraph } from './ExploreGraph';
  20. import { ExploreGraphLabel } from './ExploreGraphLabel';
  21. import ExploreQueryInspector from './ExploreQueryInspector';
  22. import { ExploreToolbar } from './ExploreToolbar';
  23. import LogsContainer from './LogsContainer';
  24. import { LogsVolumePanel } from './LogsVolumePanel';
  25. import { NoData } from './NoData';
  26. import { NoDataSourceCallToAction } from './NoDataSourceCallToAction';
  27. import { NodeGraphContainer } from './NodeGraphContainer';
  28. import { QueryRows } from './QueryRows';
  29. import { ResponseErrorContainer } from './ResponseErrorContainer';
  30. import RichHistoryContainer from './RichHistory/RichHistoryContainer';
  31. import { SecondaryActions } from './SecondaryActions';
  32. import TableContainer from './TableContainer';
  33. import { TraceViewContainer } from './TraceView/TraceViewContainer';
  34. import { changeSize, changeGraphStyle } from './state/explorePane';
  35. import { splitOpen } from './state/main';
  36. import { addQueryRow, loadLogsVolumeData, modifyQueries, scanStart, scanStopAction, setQueries } from './state/query';
  37. import { makeAbsoluteTime, updateTimeRange } from './state/time';
  38. const getStyles = (theme: GrafanaTheme2) => {
  39. return {
  40. exploreMain: css`
  41. label: exploreMain;
  42. // Is needed for some transition animations to work.
  43. position: relative;
  44. margin-top: 21px;
  45. `,
  46. button: css`
  47. label: button;
  48. margin: 1em 4px 0 0;
  49. `,
  50. queryContainer: css`
  51. label: queryContainer;
  52. // Need to override normal css class and don't want to count on ordering of the classes in html.
  53. height: auto !important;
  54. flex: unset !important;
  55. display: unset !important;
  56. padding: ${theme.spacing(1)};
  57. `,
  58. exploreContainer: css`
  59. display: flex;
  60. flex: 1 1 auto;
  61. flex-direction: column;
  62. padding: ${theme.spacing(2)};
  63. padding-top: 0;
  64. `,
  65. };
  66. };
  67. export interface ExploreProps extends Themeable2 {
  68. exploreId: ExploreId;
  69. theme: GrafanaTheme2;
  70. }
  71. enum ExploreDrawer {
  72. RichHistory,
  73. QueryInspector,
  74. }
  75. interface ExploreState {
  76. openDrawer?: ExploreDrawer;
  77. }
  78. export type Props = ExploreProps & ConnectedProps<typeof connector>;
  79. /**
  80. * Explore provides an area for quick query iteration for a given datasource.
  81. * Once a datasource is selected it populates the query section at the top.
  82. * When queries are run, their results are being displayed in the main section.
  83. * The datasource determines what kind of query editor it brings, and what kind
  84. * of results viewers it supports. The state is managed entirely in Redux.
  85. *
  86. * SPLIT VIEW
  87. *
  88. * Explore can have two Explore areas side-by-side. This is handled in `Wrapper.tsx`.
  89. * Since there can be multiple Explores (e.g., left and right) each action needs
  90. * the `exploreId` as first parameter so that the reducer knows which Explore state
  91. * is affected.
  92. *
  93. * DATASOURCE REQUESTS
  94. *
  95. * A click on Run Query creates transactions for all DataQueries for all expanded
  96. * result viewers. New runs are discarding previous runs. Upon completion a transaction
  97. * saves the result. The result viewers construct their data from the currently existing
  98. * transactions.
  99. *
  100. * The result viewers determine some of the query options sent to the datasource, e.g.,
  101. * `format`, to indicate eventual transformations by the datasources' result transformers.
  102. */
  103. export class Explore extends React.PureComponent<Props, ExploreState> {
  104. scrollElement: HTMLDivElement | undefined;
  105. absoluteTimeUnsubsciber: Unsubscribable | undefined;
  106. topOfViewRef = createRef<HTMLDivElement>();
  107. constructor(props: Props) {
  108. super(props);
  109. this.state = {
  110. openDrawer: undefined,
  111. };
  112. }
  113. componentDidMount() {
  114. this.absoluteTimeUnsubsciber = appEvents.subscribe(AbsoluteTimeEvent, this.onMakeAbsoluteTime);
  115. }
  116. componentWillUnmount() {
  117. this.absoluteTimeUnsubsciber?.unsubscribe();
  118. }
  119. onChangeTime = (rawRange: RawTimeRange) => {
  120. const { updateTimeRange, exploreId } = this.props;
  121. updateTimeRange({ exploreId, rawRange });
  122. };
  123. // Use this in help pages to set page to a single query
  124. onClickExample = (query: DataQuery) => {
  125. this.props.setQueries(this.props.exploreId, [query]);
  126. };
  127. onCellFilterAdded = (filter: FilterItem) => {
  128. const { value, key, operator } = filter;
  129. if (operator === FILTER_FOR_OPERATOR) {
  130. this.onClickFilterLabel(key, value);
  131. }
  132. if (operator === FILTER_OUT_OPERATOR) {
  133. this.onClickFilterOutLabel(key, value);
  134. }
  135. };
  136. onClickFilterLabel = (key: string, value: string) => {
  137. this.onModifyQueries({ type: 'ADD_FILTER', key, value });
  138. };
  139. onClickFilterOutLabel = (key: string, value: string) => {
  140. this.onModifyQueries({ type: 'ADD_FILTER_OUT', key, value });
  141. };
  142. onClickAddQueryRowButton = () => {
  143. const { exploreId, queryKeys } = this.props;
  144. this.props.addQueryRow(exploreId, queryKeys.length);
  145. };
  146. onMakeAbsoluteTime = () => {
  147. const { makeAbsoluteTime } = this.props;
  148. makeAbsoluteTime();
  149. };
  150. onModifyQueries = (action: any, index?: number) => {
  151. const { datasourceInstance } = this.props;
  152. if (datasourceInstance?.modifyQuery) {
  153. const modifier = (queries: DataQuery, modification: any) =>
  154. datasourceInstance.modifyQuery!(queries, modification);
  155. this.props.modifyQueries(this.props.exploreId, action, modifier, index);
  156. }
  157. };
  158. onResize = (size: { height: number; width: number }) => {
  159. this.props.changeSize(this.props.exploreId, size);
  160. };
  161. onStartScanning = () => {
  162. // Scanner will trigger a query
  163. this.props.scanStart(this.props.exploreId);
  164. };
  165. onStopScanning = () => {
  166. this.props.scanStopAction({ exploreId: this.props.exploreId });
  167. };
  168. onUpdateTimeRange = (absoluteRange: AbsoluteTimeRange) => {
  169. const { exploreId, updateTimeRange } = this.props;
  170. updateTimeRange({ exploreId, absoluteRange });
  171. };
  172. onChangeGraphStyle = (graphStyle: ExploreGraphStyle) => {
  173. const { exploreId, changeGraphStyle } = this.props;
  174. changeGraphStyle(exploreId, graphStyle);
  175. };
  176. toggleShowRichHistory = () => {
  177. this.setState((state) => {
  178. return {
  179. openDrawer: state.openDrawer === ExploreDrawer.RichHistory ? undefined : ExploreDrawer.RichHistory,
  180. };
  181. });
  182. };
  183. toggleShowQueryInspector = () => {
  184. this.setState((state) => {
  185. return {
  186. openDrawer: state.openDrawer === ExploreDrawer.QueryInspector ? undefined : ExploreDrawer.QueryInspector,
  187. };
  188. });
  189. };
  190. renderEmptyState(exploreContainerStyles: string) {
  191. return (
  192. <div className={cx(exploreContainerStyles)}>
  193. <NoDataSourceCallToAction />
  194. </div>
  195. );
  196. }
  197. renderNoData() {
  198. return <NoData />;
  199. }
  200. renderGraphPanel(width: number) {
  201. const { graphResult, absoluteRange, timeZone, splitOpen, queryResponse, loading, theme, graphStyle } = this.props;
  202. const spacing = parseInt(theme.spacing(2).slice(0, -2), 10);
  203. const label = <ExploreGraphLabel graphStyle={graphStyle} onChangeGraphStyle={this.onChangeGraphStyle} />;
  204. return (
  205. <Collapse label={label} loading={loading} isOpen>
  206. <ExploreGraph
  207. graphStyle={graphStyle}
  208. data={graphResult!}
  209. height={400}
  210. width={width - spacing}
  211. absoluteRange={absoluteRange}
  212. onChangeTime={this.onUpdateTimeRange}
  213. timeZone={timeZone}
  214. annotations={queryResponse.annotations}
  215. splitOpenFn={splitOpen}
  216. loadingState={queryResponse.state}
  217. />
  218. </Collapse>
  219. );
  220. }
  221. renderLogsVolume(width: number) {
  222. const { logsVolumeData, exploreId, loadLogsVolumeData, absoluteRange, timeZone, splitOpen } = this.props;
  223. return (
  224. <LogsVolumePanel
  225. absoluteRange={absoluteRange}
  226. width={width}
  227. logsVolumeData={logsVolumeData}
  228. onUpdateTimeRange={this.onUpdateTimeRange}
  229. timeZone={timeZone}
  230. splitOpen={splitOpen}
  231. onLoadLogsVolume={() => loadLogsVolumeData(exploreId)}
  232. />
  233. );
  234. }
  235. renderTablePanel(width: number) {
  236. const { exploreId, datasourceInstance, timeZone } = this.props;
  237. return (
  238. <TableContainer
  239. ariaLabel={selectors.pages.Explore.General.table}
  240. width={width}
  241. exploreId={exploreId}
  242. onCellFilterAdded={datasourceInstance?.modifyQuery ? this.onCellFilterAdded : undefined}
  243. timeZone={timeZone}
  244. />
  245. );
  246. }
  247. renderLogsPanel(width: number) {
  248. const { exploreId, syncedTimes, theme, queryResponse } = this.props;
  249. const spacing = parseInt(theme.spacing(2).slice(0, -2), 10);
  250. return (
  251. <LogsContainer
  252. exploreId={exploreId}
  253. loadingState={queryResponse.state}
  254. syncedTimes={syncedTimes}
  255. width={width - spacing}
  256. onClickFilterLabel={this.onClickFilterLabel}
  257. onClickFilterOutLabel={this.onClickFilterOutLabel}
  258. onStartScanning={this.onStartScanning}
  259. onStopScanning={this.onStopScanning}
  260. />
  261. );
  262. }
  263. renderNodeGraphPanel() {
  264. const { exploreId, showTrace, queryResponse, datasourceInstance } = this.props;
  265. const datasourceType = datasourceInstance ? datasourceInstance?.type : 'unknown';
  266. return (
  267. <NodeGraphContainer
  268. dataFrames={this.memoizedGetNodeGraphDataFrames(queryResponse.series)}
  269. exploreId={exploreId}
  270. withTraceView={showTrace}
  271. datasourceType={datasourceType}
  272. />
  273. );
  274. }
  275. memoizedGetNodeGraphDataFrames = memoizeOne(getNodeGraphDataFrames);
  276. renderTraceViewPanel() {
  277. const { queryResponse, splitOpen, exploreId } = this.props;
  278. const dataFrames = queryResponse.series.filter((series) => series.meta?.preferredVisualisationType === 'trace');
  279. return (
  280. // If there is no data (like 404) we show a separate error so no need to show anything here
  281. dataFrames.length && (
  282. <TraceViewContainer
  283. exploreId={exploreId}
  284. dataFrames={dataFrames}
  285. splitOpenFn={splitOpen}
  286. scrollElement={this.scrollElement}
  287. queryResponse={queryResponse}
  288. topOfViewRef={this.topOfViewRef}
  289. />
  290. )
  291. );
  292. }
  293. render() {
  294. const {
  295. datasourceInstance,
  296. datasourceMissing,
  297. exploreId,
  298. graphResult,
  299. queryResponse,
  300. isLive,
  301. theme,
  302. showMetrics,
  303. showTable,
  304. showLogs,
  305. showTrace,
  306. showNodeGraph,
  307. timeZone,
  308. } = this.props;
  309. const { openDrawer } = this.state;
  310. const styles = getStyles(theme);
  311. const showPanels = queryResponse && queryResponse.state !== LoadingState.NotStarted;
  312. const showRichHistory = openDrawer === ExploreDrawer.RichHistory;
  313. const richHistoryRowButtonHidden = !supportedFeatures().queryHistoryAvailable;
  314. const showQueryInspector = openDrawer === ExploreDrawer.QueryInspector;
  315. const showNoData =
  316. queryResponse.state === LoadingState.Done &&
  317. [
  318. queryResponse.logsFrames,
  319. queryResponse.graphFrames,
  320. queryResponse.nodeGraphFrames,
  321. queryResponse.tableFrames,
  322. queryResponse.traceFrames,
  323. ].every((e) => e.length === 0);
  324. return (
  325. <CustomScrollbar
  326. testId={selectors.pages.Explore.General.scrollView}
  327. autoHeightMin={'100%'}
  328. scrollRefCallback={(scrollElement) => (this.scrollElement = scrollElement || undefined)}
  329. >
  330. <ExploreToolbar exploreId={exploreId} onChangeTime={this.onChangeTime} topOfViewRef={this.topOfViewRef} />
  331. {datasourceMissing ? this.renderEmptyState(styles.exploreContainer) : null}
  332. {datasourceInstance && (
  333. <div className={cx(styles.exploreContainer)}>
  334. <PanelContainer className={styles.queryContainer}>
  335. <QueryRows exploreId={exploreId} />
  336. <SecondaryActions
  337. addQueryRowButtonDisabled={isLive}
  338. // We cannot show multiple traces at the same time right now so we do not show add query button.
  339. //TODO:unification
  340. addQueryRowButtonHidden={false}
  341. richHistoryRowButtonHidden={richHistoryRowButtonHidden}
  342. richHistoryButtonActive={showRichHistory}
  343. queryInspectorButtonActive={showQueryInspector}
  344. onClickAddQueryRowButton={this.onClickAddQueryRowButton}
  345. onClickRichHistoryButton={this.toggleShowRichHistory}
  346. onClickQueryInspectorButton={this.toggleShowQueryInspector}
  347. />
  348. <ResponseErrorContainer exploreId={exploreId} />
  349. </PanelContainer>
  350. <AutoSizer onResize={this.onResize} disableHeight>
  351. {({ width }) => {
  352. if (width === 0) {
  353. return null;
  354. }
  355. return (
  356. <main className={cx(styles.exploreMain)} style={{ width }}>
  357. <ErrorBoundaryAlert>
  358. {showPanels && (
  359. <>
  360. {showMetrics && graphResult && (
  361. <ErrorBoundaryAlert>{this.renderGraphPanel(width)}</ErrorBoundaryAlert>
  362. )}
  363. {<ErrorBoundaryAlert>{this.renderLogsVolume(width)}</ErrorBoundaryAlert>}
  364. {showTable && <ErrorBoundaryAlert>{this.renderTablePanel(width)}</ErrorBoundaryAlert>}
  365. {showLogs && <ErrorBoundaryAlert>{this.renderLogsPanel(width)}</ErrorBoundaryAlert>}
  366. {showNodeGraph && <ErrorBoundaryAlert>{this.renderNodeGraphPanel()}</ErrorBoundaryAlert>}
  367. {showTrace && <ErrorBoundaryAlert>{this.renderTraceViewPanel()}</ErrorBoundaryAlert>}
  368. {showNoData && <ErrorBoundaryAlert>{this.renderNoData()}</ErrorBoundaryAlert>}
  369. </>
  370. )}
  371. {showRichHistory && (
  372. <RichHistoryContainer
  373. width={width}
  374. exploreId={exploreId}
  375. onClose={this.toggleShowRichHistory}
  376. />
  377. )}
  378. {showQueryInspector && (
  379. <ExploreQueryInspector
  380. exploreId={exploreId}
  381. width={width}
  382. onClose={this.toggleShowQueryInspector}
  383. timeZone={timeZone}
  384. />
  385. )}
  386. </ErrorBoundaryAlert>
  387. </main>
  388. );
  389. }}
  390. </AutoSizer>
  391. </div>
  392. )}
  393. </CustomScrollbar>
  394. );
  395. }
  396. }
  397. function mapStateToProps(state: StoreState, { exploreId }: ExploreProps) {
  398. const explore = state.explore;
  399. const { syncedTimes } = explore;
  400. const item: ExploreItemState = explore[exploreId]!;
  401. const timeZone = getTimeZone(state.user);
  402. const {
  403. datasourceInstance,
  404. datasourceMissing,
  405. queryKeys,
  406. isLive,
  407. graphResult,
  408. logsVolumeData,
  409. logsResult,
  410. showLogs,
  411. showMetrics,
  412. showTable,
  413. showTrace,
  414. absoluteRange,
  415. queryResponse,
  416. showNodeGraph,
  417. loading,
  418. graphStyle,
  419. } = item;
  420. return {
  421. datasourceInstance,
  422. datasourceMissing,
  423. queryKeys,
  424. isLive,
  425. graphResult,
  426. logsVolumeData,
  427. logsResult: logsResult ?? undefined,
  428. absoluteRange,
  429. queryResponse,
  430. syncedTimes,
  431. timeZone,
  432. showLogs,
  433. showMetrics,
  434. showTable,
  435. showTrace,
  436. showNodeGraph,
  437. loading,
  438. graphStyle,
  439. };
  440. }
  441. const mapDispatchToProps = {
  442. changeSize,
  443. changeGraphStyle,
  444. modifyQueries,
  445. scanStart,
  446. scanStopAction,
  447. setQueries,
  448. updateTimeRange,
  449. makeAbsoluteTime,
  450. loadLogsVolumeData,
  451. addQueryRow,
  452. splitOpen,
  453. };
  454. const connector = connect(mapStateToProps, mapDispatchToProps);
  455. export default compose(connector, withTheme2)(Explore) as React.ComponentType<{ exploreId: ExploreId }>;