import { css } from '@emotion/css'; import { PureComponent, useEffect, useState } from 'react'; import * as React from 'react'; import { Unsubscribable } from 'rxjs'; import { CoreApp, DataSourceApi, DataSourceInstanceSettings, getDataSourceRef, getDefaultTimeRange, LoadingState, PanelData, } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { Trans, t } from '@grafana/i18n'; import { getDataSourceSrv, locationService } from '@grafana/runtime'; import { DataQuery } from '@grafana/schema'; import { Button, HorizontalGroup, InlineFormLabel, Modal, ScrollContainer, 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, queryIsEmpty } from 'app/core/utils/query'; import { DataSourceModal } from 'app/features/datasources/components/picker/DataSourceModal'; import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker'; import { dataSource as expressionDatasource } from 'app/features/expressions/ExpressionDatasource'; import { isSharedDashboardQuery } from 'app/plugins/datasource/dashboard/runSharedRequest'; import { GrafanaQuery } from 'app/plugins/datasource/grafana/types'; import { QueryGroupOptions } from 'app/types/query'; import { PanelQueryRunner } from '../state/PanelQueryRunner'; import { updateQueries } from '../state/updateQueries'; import { GroupActionComponents } from './QueryActionComponent'; import { QueryEditorRows } from './QueryEditorRows'; import { QueryGroupOptionsEditor } from './QueryGroupOptions'; export 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; isDataSourceModalOpen: boolean; data: PanelData; isHelpOpen: boolean; defaultDataSource?: DataSourceApi; scrollElement?: HTMLDivElement; } export class QueryGroup extends PureComponent { backendSrv = backendSrv; dataSourceSrv = getDataSourceSrv(); querySubscription: Unsubscribable | null = null; state: State = { isDataSourceModalOpen: !!locationService.getSearchObject().firstPanel, isLoadingHelp: false, helpContent: null, isPickerOpen: false, isHelpOpen: false, queries: [], data: { state: LoadingState.NotStarted, series: [], timeRange: getDefaultTimeRange(), }, }; async componentDidMount() { const { options, queryRunner } = this.props; this.querySubscription = queryRunner.getData({ withTransforms: false, withFieldConfig: false }).subscribe({ next: (data: PanelData) => this.onPanelDataUpdate(data), }); this.setNewQueriesAndDatasource(options); } componentWillUnmount() { if (this.querySubscription) { this.querySubscription.unsubscribe(); this.querySubscription = null; } } async componentDidUpdate() { const { options } = this.props; const currentDS = await getDataSourceSrv().get(options.dataSource); if (this.state.dataSource && currentDS.uid !== this.state.dataSource?.uid) { this.setNewQueriesAndDatasource(options); } } async setNewQueriesAndDatasource(options: QueryGroupOptions) { 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) => ({ ...(queryIsEmpty(q) && ds?.getDefaultQuery?.(CoreApp.PanelEditor)), datasource, ...q, })); this.setState({ queries, dataSource: ds, dsSettings, defaultDataSource, }); } catch (error) { console.error('failed to load data source', error); } } onPanelDataUpdate(data: PanelData) { this.setState({ data }); } onChangeDataSource = async ( newSettings: DataSourceInstanceSettings, defaultQueries?: DataQuery[] | GrafanaQuery[] ) => { 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 = defaultQueries || (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, ...getDataSourceRef(newSettings), }, }); this.setState({ queries, dataSource: dataSource, dsSettings: newSettings, }); if (defaultQueries) { this.props.onRunQueries(); } }; onAddQueryClick = () => { const { queries } = this.state; this.onQueriesChange(addQuery(queries, this.newQuery())); this.onScrollBottom(); }; newQuery(): Partial { const { dsSettings, defaultDataSource } = this.state; const ds = dsSettings && !dsSettings.meta.mixed ? getDataSourceRef(dsSettings) : defaultDataSource ? defaultDataSource.getRef() : { type: undefined, uid: undefined }; return { ...this.state.dataSource?.getDefaultQuery?.(CoreApp.PanelEditor), datasource: ds, }; } 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, dsSettings } = this.state; if (!dsSettings || !dataSource) { return null; } return ( ); } onOpenHelp = () => { this.setState({ isHelpOpen: true }); }; onCloseHelp = () => { this.setState({ isHelpOpen: false }); }; onCloseDataSourceModal = () => { this.setState({ isDataSourceModalOpen: false }); }; onAddQuery = (query: Partial) => { const { dsSettings, queries } = this.state; this.onQueriesChange( addQuery(queries, query, dsSettings ? getDataSourceRef(dsSettings) : { type: undefined, uid: undefined }) ); this.onScrollBottom(); }; onQueriesChange = (queries: DataQuery[] | GrafanaQuery[]) => { this.onChange({ queries }); this.setState({ queries }); }; renderQueries(dsSettings: DataSourceInstanceSettings) { const { onRunQueries } = this.props; const { data, queries } = this.state; return (
); } isExpressionsSupported(dsSettings: DataSourceInstanceSettings): boolean { return (dsSettings.meta.backend || 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 showAddButton = !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', flexDirection: 'column', padding: theme.spacing.md, }), dataSourceRow: css({ display: 'flex', marginBottom: theme.spacing.md, }), dataSourceRowItem: css({ marginRight: theme.spacing.inlineFormMargin, }), dataSourceRowItemOptions: css({ flexGrow: 1, marginRight: theme.spacing.inlineFormMargin, }), queriesWrapper: css({ paddingBottom: '16px', }), expressionWrapper: css({}), expressionButton: css({ marginRight: theme.spacing.sm, }), }; }); type QueriesTabStyles = ReturnType; interface QueryGroupTopSectionProps { data: PanelData; dataSource: DataSourceApi; dsSettings: DataSourceInstanceSettings; options: QueryGroupOptions; onOpenQueryInspector?: () => void; onOptionsChange?: (options: QueryGroupOptions) => void; onDataSourceChange?: (ds: DataSourceInstanceSettings, defaultQueries?: DataQuery[] | GrafanaQuery[]) => Promise; } export function QueryGroupTopSection({ dataSource, options, data, dsSettings, onDataSourceChange, onOptionsChange, onOpenQueryInspector, }: QueryGroupTopSectionProps) { const styles = getStyles(); const [isHelpOpen, setIsHelpOpen] = useState(false); return ( <>
Data source
{ return await onDataSourceChange?.(ds, defaultQueries); }} isDataSourceModalOpen={Boolean(locationService.getSearchObject().firstPanel)} />
{dataSource && ( <>
{ onOptionsChange?.(opts); }} />
{onOpenQueryInspector && (
)} )}
{isHelpOpen && ( setIsHelpOpen(false)} > )} ); } interface DataSourcePickerWithPromptProps { isDataSourceModalOpen?: boolean; options: QueryGroupOptions; onChange: (ds: DataSourceInstanceSettings, defaultQueries?: DataQuery[] | GrafanaQuery[]) => Promise; } function DataSourcePickerWithPrompt({ options, onChange, ...otherProps }: DataSourcePickerWithPromptProps) { const [isDataSourceModalOpen, setIsDataSourceModalOpen] = useState(Boolean(otherProps.isDataSourceModalOpen)); useEffect(() => { // Clean up the first panel flag since the modal is now open if (!!locationService.getSearchObject().firstPanel) { locationService.partial({ firstPanel: null }, true); } }, []); const commonProps = { metrics: true, mixed: true, dashboard: true, variables: true, current: options.dataSource, uploadFile: true, onChange: async (ds: DataSourceInstanceSettings, defaultQueries?: DataQuery[] | GrafanaQuery[]) => { await onChange(ds, defaultQueries); setIsDataSourceModalOpen(false); }, }; return ( <> {isDataSourceModalOpen && ( setIsDataSourceModalOpen(false)}> )} ); }