QueryGroup.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402
  1. import { css } from '@emotion/css';
  2. import React, { PureComponent } from 'react';
  3. import { Unsubscribable } from 'rxjs';
  4. import {
  5. DataQuery,
  6. DataSourceApi,
  7. DataSourceInstanceSettings,
  8. getDefaultTimeRange,
  9. LoadingState,
  10. PanelData,
  11. } from '@grafana/data';
  12. import { selectors } from '@grafana/e2e-selectors';
  13. import { DataSourcePicker, getDataSourceSrv } from '@grafana/runtime';
  14. import { Button, CustomScrollbar, HorizontalGroup, InlineFormLabel, Modal, stylesFactory } from '@grafana/ui';
  15. import { PluginHelp } from 'app/core/components/PluginHelp/PluginHelp';
  16. import config from 'app/core/config';
  17. import { backendSrv } from 'app/core/services/backend_srv';
  18. import { addQuery } from 'app/core/utils/query';
  19. import { dataSource as expressionDatasource } from 'app/features/expressions/ExpressionDatasource';
  20. import { DashboardQueryEditor, isSharedDashboardQuery } from 'app/plugins/datasource/dashboard';
  21. import { QueryGroupOptions } from 'app/types';
  22. import { PanelQueryRunner } from '../state/PanelQueryRunner';
  23. import { updateQueries } from '../state/updateQueries';
  24. import { GroupActionComponents } from './QueryActionComponent';
  25. import { QueryEditorRows } from './QueryEditorRows';
  26. import { QueryGroupOptionsEditor } from './QueryGroupOptions';
  27. interface Props {
  28. queryRunner: PanelQueryRunner;
  29. options: QueryGroupOptions;
  30. onOpenQueryInspector?: () => void;
  31. onRunQueries: () => void;
  32. onOptionsChange: (options: QueryGroupOptions) => void;
  33. }
  34. interface State {
  35. dataSource?: DataSourceApi;
  36. dsSettings?: DataSourceInstanceSettings;
  37. queries: DataQuery[];
  38. helpContent: React.ReactNode;
  39. isLoadingHelp: boolean;
  40. isPickerOpen: boolean;
  41. isAddingMixed: boolean;
  42. data: PanelData;
  43. isHelpOpen: boolean;
  44. defaultDataSource?: DataSourceApi;
  45. scrollElement?: HTMLDivElement;
  46. }
  47. export class QueryGroup extends PureComponent<Props, State> {
  48. backendSrv = backendSrv;
  49. dataSourceSrv = getDataSourceSrv();
  50. querySubscription: Unsubscribable | null = null;
  51. state: State = {
  52. isLoadingHelp: false,
  53. helpContent: null,
  54. isPickerOpen: false,
  55. isAddingMixed: false,
  56. isHelpOpen: false,
  57. queries: [],
  58. data: {
  59. state: LoadingState.NotStarted,
  60. series: [],
  61. timeRange: getDefaultTimeRange(),
  62. },
  63. };
  64. async componentDidMount() {
  65. const { queryRunner, options } = this.props;
  66. this.querySubscription = queryRunner.getData({ withTransforms: false, withFieldConfig: false }).subscribe({
  67. next: (data: PanelData) => this.onPanelDataUpdate(data),
  68. });
  69. try {
  70. const ds = await this.dataSourceSrv.get(options.dataSource);
  71. const dsSettings = this.dataSourceSrv.getInstanceSettings(options.dataSource);
  72. const defaultDataSource = await this.dataSourceSrv.get();
  73. const datasource = ds.getRef();
  74. const queries = options.queries.map((q) => (q.datasource ? q : { ...q, datasource }));
  75. this.setState({ queries, dataSource: ds, dsSettings, defaultDataSource });
  76. } catch (error) {
  77. console.log('failed to load data source', error);
  78. }
  79. }
  80. componentWillUnmount() {
  81. if (this.querySubscription) {
  82. this.querySubscription.unsubscribe();
  83. this.querySubscription = null;
  84. }
  85. }
  86. onPanelDataUpdate(data: PanelData) {
  87. this.setState({ data });
  88. }
  89. onChangeDataSource = async (newSettings: DataSourceInstanceSettings) => {
  90. const { dsSettings } = this.state;
  91. const currentDS = dsSettings ? await getDataSourceSrv().get(dsSettings.uid) : undefined;
  92. const nextDS = await getDataSourceSrv().get(newSettings.uid);
  93. // We need to pass in newSettings.uid as well here as that can be a variable expression and we want to store that in the query model not the current ds variable value
  94. const queries = await updateQueries(nextDS, newSettings.uid, this.state.queries, currentDS);
  95. const dataSource = await this.dataSourceSrv.get(newSettings.name);
  96. this.onChange({
  97. queries,
  98. dataSource: {
  99. name: newSettings.name,
  100. uid: newSettings.uid,
  101. type: newSettings.meta.id,
  102. default: newSettings.isDefault,
  103. },
  104. });
  105. this.setState({
  106. queries,
  107. dataSource: dataSource,
  108. dsSettings: newSettings,
  109. });
  110. };
  111. onAddQueryClick = () => {
  112. const { queries } = this.state;
  113. this.onQueriesChange(addQuery(queries, this.newQuery()));
  114. this.onScrollBottom();
  115. };
  116. newQuery(): Partial<DataQuery> {
  117. const { dsSettings, defaultDataSource } = this.state;
  118. const ds = !dsSettings?.meta.mixed ? dsSettings : defaultDataSource;
  119. return {
  120. datasource: { uid: ds?.uid, type: ds?.type },
  121. };
  122. }
  123. onChange(changedProps: Partial<QueryGroupOptions>) {
  124. this.props.onOptionsChange({
  125. ...this.props.options,
  126. ...changedProps,
  127. });
  128. }
  129. onAddExpressionClick = () => {
  130. this.onQueriesChange(addQuery(this.state.queries, expressionDatasource.newQuery()));
  131. this.onScrollBottom();
  132. };
  133. onScrollBottom = () => {
  134. setTimeout(() => {
  135. if (this.state.scrollElement) {
  136. this.state.scrollElement.scrollTo({ top: 10000 });
  137. }
  138. }, 20);
  139. };
  140. onUpdateAndRun = (options: QueryGroupOptions) => {
  141. this.props.onOptionsChange(options);
  142. this.props.onRunQueries();
  143. };
  144. renderTopSection(styles: QueriesTabStyles) {
  145. const { onOpenQueryInspector, options } = this.props;
  146. const { dataSource, data } = this.state;
  147. return (
  148. <div>
  149. <div className={styles.dataSourceRow}>
  150. <InlineFormLabel htmlFor="data-source-picker" width={'auto'}>
  151. Data source
  152. </InlineFormLabel>
  153. <div className={styles.dataSourceRowItem}>
  154. <DataSourcePicker
  155. onChange={this.onChangeDataSource}
  156. current={options.dataSource}
  157. metrics={true}
  158. mixed={true}
  159. dashboard={true}
  160. variables={true}
  161. />
  162. </div>
  163. {dataSource && (
  164. <>
  165. <div className={styles.dataSourceRowItem}>
  166. <Button
  167. variant="secondary"
  168. icon="question-circle"
  169. title="Open data source help"
  170. onClick={this.onOpenHelp}
  171. />
  172. </div>
  173. <div className={styles.dataSourceRowItemOptions}>
  174. <QueryGroupOptionsEditor
  175. options={options}
  176. dataSource={dataSource}
  177. data={data}
  178. onChange={this.onUpdateAndRun}
  179. />
  180. </div>
  181. {onOpenQueryInspector && (
  182. <div className={styles.dataSourceRowItem}>
  183. <Button
  184. variant="secondary"
  185. onClick={onOpenQueryInspector}
  186. aria-label={selectors.components.QueryTab.queryInspectorButton}
  187. >
  188. Query inspector
  189. </Button>
  190. </div>
  191. )}
  192. </>
  193. )}
  194. </div>
  195. </div>
  196. );
  197. }
  198. onOpenHelp = () => {
  199. this.setState({ isHelpOpen: true });
  200. };
  201. onCloseHelp = () => {
  202. this.setState({ isHelpOpen: false });
  203. };
  204. renderMixedPicker = () => {
  205. return (
  206. <DataSourcePicker
  207. mixed={false}
  208. onChange={this.onAddMixedQuery}
  209. current={null}
  210. autoFocus={true}
  211. variables={true}
  212. onBlur={this.onMixedPickerBlur}
  213. openMenuOnFocus={true}
  214. />
  215. );
  216. };
  217. onAddMixedQuery = (datasource: any) => {
  218. this.onAddQuery({ datasource: datasource.name });
  219. this.setState({ isAddingMixed: false });
  220. };
  221. onMixedPickerBlur = () => {
  222. this.setState({ isAddingMixed: false });
  223. };
  224. onAddQuery = (query: Partial<DataQuery>) => {
  225. const { dsSettings, queries } = this.state;
  226. this.onQueriesChange(addQuery(queries, query, { type: dsSettings?.type, uid: dsSettings?.uid }));
  227. this.onScrollBottom();
  228. };
  229. onQueriesChange = (queries: DataQuery[]) => {
  230. this.onChange({ queries });
  231. this.setState({ queries });
  232. };
  233. renderQueries(dsSettings: DataSourceInstanceSettings) {
  234. const { onRunQueries } = this.props;
  235. const { data, queries } = this.state;
  236. if (isSharedDashboardQuery(dsSettings.name)) {
  237. return (
  238. <DashboardQueryEditor
  239. queries={queries}
  240. panelData={data}
  241. onChange={this.onQueriesChange}
  242. onRunQueries={onRunQueries}
  243. />
  244. );
  245. }
  246. return (
  247. <div aria-label={selectors.components.QueryTab.content}>
  248. <QueryEditorRows
  249. queries={queries}
  250. dsSettings={dsSettings}
  251. onQueriesChange={this.onQueriesChange}
  252. onAddQuery={this.onAddQuery}
  253. onRunQueries={onRunQueries}
  254. data={data}
  255. />
  256. </div>
  257. );
  258. }
  259. isExpressionsSupported(dsSettings: DataSourceInstanceSettings): boolean {
  260. return (dsSettings.meta.alerting || dsSettings.meta.mixed) === true;
  261. }
  262. renderExtraActions() {
  263. return GroupActionComponents.getAllExtraRenderAction()
  264. .map((action, index) =>
  265. action({
  266. onAddQuery: this.onAddQuery,
  267. onChangeDataSource: this.onChangeDataSource,
  268. key: index,
  269. })
  270. )
  271. .filter(Boolean);
  272. }
  273. renderAddQueryRow(dsSettings: DataSourceInstanceSettings, styles: QueriesTabStyles) {
  274. const { isAddingMixed } = this.state;
  275. const showAddButton = !(isAddingMixed || isSharedDashboardQuery(dsSettings.name));
  276. return (
  277. <HorizontalGroup spacing="md" align="flex-start">
  278. {showAddButton && (
  279. <Button
  280. icon="plus"
  281. onClick={this.onAddQueryClick}
  282. variant="secondary"
  283. aria-label={selectors.components.QueryTab.addQuery}
  284. >
  285. Query
  286. </Button>
  287. )}
  288. {config.expressionsEnabled && this.isExpressionsSupported(dsSettings) && (
  289. <Button
  290. icon="plus"
  291. onClick={this.onAddExpressionClick}
  292. variant="secondary"
  293. className={styles.expressionButton}
  294. >
  295. <span>Expression&nbsp;</span>
  296. </Button>
  297. )}
  298. {this.renderExtraActions()}
  299. </HorizontalGroup>
  300. );
  301. }
  302. setScrollRef = (scrollElement: HTMLDivElement): void => {
  303. this.setState({ scrollElement });
  304. };
  305. render() {
  306. const { isHelpOpen, dsSettings } = this.state;
  307. const styles = getStyles();
  308. return (
  309. <CustomScrollbar autoHeightMin="100%" scrollRefCallback={this.setScrollRef}>
  310. <div className={styles.innerWrapper}>
  311. {this.renderTopSection(styles)}
  312. {dsSettings && (
  313. <>
  314. <div className={styles.queriesWrapper}>{this.renderQueries(dsSettings)}</div>
  315. {this.renderAddQueryRow(dsSettings, styles)}
  316. {isHelpOpen && (
  317. <Modal title="Data source help" isOpen={true} onDismiss={this.onCloseHelp}>
  318. <PluginHelp plugin={dsSettings.meta} type="query_help" />
  319. </Modal>
  320. )}
  321. </>
  322. )}
  323. </div>
  324. </CustomScrollbar>
  325. );
  326. }
  327. }
  328. const getStyles = stylesFactory(() => {
  329. const { theme } = config;
  330. return {
  331. innerWrapper: css`
  332. display: flex;
  333. flex-direction: column;
  334. padding: ${theme.spacing.md};
  335. `,
  336. dataSourceRow: css`
  337. display: flex;
  338. margin-bottom: ${theme.spacing.md};
  339. `,
  340. dataSourceRowItem: css`
  341. margin-right: ${theme.spacing.inlineFormMargin};
  342. `,
  343. dataSourceRowItemOptions: css`
  344. flex-grow: 1;
  345. margin-right: ${theme.spacing.inlineFormMargin};
  346. `,
  347. queriesWrapper: css`
  348. padding-bottom: 16px;
  349. `,
  350. expressionWrapper: css``,
  351. expressionButton: css`
  352. margin-right: ${theme.spacing.sm};
  353. `,
  354. };
  355. });
  356. type QueriesTabStyles = ReturnType<typeof getStyles>;