import { css } from '@emotion/css'; import React, { PureComponent } from 'react'; import { Unsubscribable } from 'rxjs'; import { DataQuery, DataSourceApi, DataSourceInstanceSettings, getDefaultTimeRange, LoadingState, PanelData, } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { DataSourcePicker, getDataSourceSrv } from '@grafana/runtime'; import { Button, CustomScrollbar, HorizontalGroup, InlineFormLabel, Modal, stylesFactory } from '@grafana/ui'; import { PluginHelp } from 'app/core/components/PluginHelp/PluginHelp'; import config from 'app/core/config'; import { backendSrv } from 'app/core/services/backend_srv'; import { addQuery } from 'app/core/utils/query'; import { dataSource as expressionDatasource } from 'app/features/expressions/ExpressionDatasource'; import { DashboardQueryEditor, isSharedDashboardQuery } from 'app/plugins/datasource/dashboard'; import { QueryGroupOptions } from 'app/types'; import { PanelQueryRunner } from '../state/PanelQueryRunner'; import { updateQueries } from '../state/updateQueries'; import { GroupActionComponents } from './QueryActionComponent'; import { QueryEditorRows } from './QueryEditorRows'; import { QueryGroupOptionsEditor } from './QueryGroupOptions'; interface Props { queryRunner: PanelQueryRunner; options: QueryGroupOptions; onOpenQueryInspector?: () => void; onRunQueries: () => void; onOptionsChange: (options: QueryGroupOptions) => void; } interface State { dataSource?: DataSourceApi; dsSettings?: DataSourceInstanceSettings; queries: DataQuery[]; helpContent: React.ReactNode; isLoadingHelp: boolean; isPickerOpen: boolean; isAddingMixed: boolean; data: PanelData; isHelpOpen: boolean; defaultDataSource?: DataSourceApi; scrollElement?: HTMLDivElement; } export class QueryGroup extends PureComponent { backendSrv = backendSrv; dataSourceSrv = getDataSourceSrv(); querySubscription: Unsubscribable | null = null; state: State = { isLoadingHelp: false, helpContent: null, isPickerOpen: false, isAddingMixed: false, isHelpOpen: false, queries: [], data: { state: LoadingState.NotStarted, series: [], timeRange: getDefaultTimeRange(), }, }; async componentDidMount() { const { queryRunner, options } = this.props; this.querySubscription = queryRunner.getData({ withTransforms: false, withFieldConfig: false }).subscribe({ next: (data: PanelData) => this.onPanelDataUpdate(data), }); try { const ds = await this.dataSourceSrv.get(options.dataSource); const dsSettings = this.dataSourceSrv.getInstanceSettings(options.dataSource); const defaultDataSource = await this.dataSourceSrv.get(); const datasource = ds.getRef(); const queries = options.queries.map((q) => (q.datasource ? q : { ...q, datasource })); this.setState({ queries, dataSource: ds, dsSettings, defaultDataSource }); } catch (error) { console.log('failed to load data source', error); } } componentWillUnmount() { if (this.querySubscription) { this.querySubscription.unsubscribe(); this.querySubscription = null; } } onPanelDataUpdate(data: PanelData) { this.setState({ data }); } onChangeDataSource = async (newSettings: DataSourceInstanceSettings) => { const { dsSettings } = this.state; const currentDS = dsSettings ? await getDataSourceSrv().get(dsSettings.uid) : undefined; const nextDS = await getDataSourceSrv().get(newSettings.uid); // 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 const queries = await updateQueries(nextDS, newSettings.uid, this.state.queries, currentDS); const dataSource = await this.dataSourceSrv.get(newSettings.name); this.onChange({ queries, dataSource: { name: newSettings.name, uid: newSettings.uid, type: newSettings.meta.id, default: newSettings.isDefault, }, }); this.setState({ queries, dataSource: dataSource, dsSettings: newSettings, }); }; onAddQueryClick = () => { const { queries } = this.state; this.onQueriesChange(addQuery(queries, this.newQuery())); this.onScrollBottom(); }; newQuery(): Partial { const { dsSettings, defaultDataSource } = this.state; const ds = !dsSettings?.meta.mixed ? dsSettings : defaultDataSource; return { datasource: { uid: ds?.uid, type: ds?.type }, }; } onChange(changedProps: Partial) { this.props.onOptionsChange({ ...this.props.options, ...changedProps, }); } onAddExpressionClick = () => { this.onQueriesChange(addQuery(this.state.queries, expressionDatasource.newQuery())); this.onScrollBottom(); }; onScrollBottom = () => { setTimeout(() => { if (this.state.scrollElement) { this.state.scrollElement.scrollTo({ top: 10000 }); } }, 20); }; onUpdateAndRun = (options: QueryGroupOptions) => { this.props.onOptionsChange(options); this.props.onRunQueries(); }; renderTopSection(styles: QueriesTabStyles) { const { onOpenQueryInspector, options } = this.props; const { dataSource, data } = this.state; return (
Data source
{dataSource && ( <>
{onOpenQueryInspector && (
)} )}
); } onOpenHelp = () => { this.setState({ isHelpOpen: true }); }; onCloseHelp = () => { this.setState({ isHelpOpen: false }); }; renderMixedPicker = () => { return ( ); }; onAddMixedQuery = (datasource: any) => { this.onAddQuery({ datasource: datasource.name }); this.setState({ isAddingMixed: false }); }; onMixedPickerBlur = () => { this.setState({ isAddingMixed: false }); }; onAddQuery = (query: Partial) => { const { dsSettings, queries } = this.state; this.onQueriesChange(addQuery(queries, query, { type: dsSettings?.type, uid: dsSettings?.uid })); this.onScrollBottom(); }; onQueriesChange = (queries: DataQuery[]) => { this.onChange({ queries }); this.setState({ queries }); }; renderQueries(dsSettings: DataSourceInstanceSettings) { const { onRunQueries } = this.props; const { data, queries } = this.state; if (isSharedDashboardQuery(dsSettings.name)) { return ( ); } return (
); } isExpressionsSupported(dsSettings: DataSourceInstanceSettings): boolean { return (dsSettings.meta.alerting || dsSettings.meta.mixed) === true; } renderExtraActions() { return GroupActionComponents.getAllExtraRenderAction() .map((action, index) => action({ onAddQuery: this.onAddQuery, onChangeDataSource: this.onChangeDataSource, key: index, }) ) .filter(Boolean); } renderAddQueryRow(dsSettings: DataSourceInstanceSettings, styles: QueriesTabStyles) { const { isAddingMixed } = this.state; const showAddButton = !(isAddingMixed || isSharedDashboardQuery(dsSettings.name)); return ( {showAddButton && ( )} {config.expressionsEnabled && this.isExpressionsSupported(dsSettings) && ( )} {this.renderExtraActions()} ); } setScrollRef = (scrollElement: HTMLDivElement): void => { this.setState({ scrollElement }); }; render() { const { isHelpOpen, dsSettings } = this.state; const styles = getStyles(); return (
{this.renderTopSection(styles)} {dsSettings && ( <>
{this.renderQueries(dsSettings)}
{this.renderAddQueryRow(dsSettings, styles)} {isHelpOpen && ( )} )}
); } } const getStyles = stylesFactory(() => { const { theme } = config; return { innerWrapper: css` display: flex; flex-direction: column; padding: ${theme.spacing.md}; `, dataSourceRow: css` display: flex; margin-bottom: ${theme.spacing.md}; `, dataSourceRowItem: css` margin-right: ${theme.spacing.inlineFormMargin}; `, dataSourceRowItemOptions: css` flex-grow: 1; margin-right: ${theme.spacing.inlineFormMargin}; `, queriesWrapper: css` padding-bottom: 16px; `, expressionWrapper: css``, expressionButton: css` margin-right: ${theme.spacing.sm}; `, }; }); type QueriesTabStyles = ReturnType;