import classNames from 'classnames'; import { cloneDeep, filter, uniqBy, uniqueId } from 'lodash'; import pluralize from 'pluralize'; import { PureComponent, ReactNode } from 'react'; import { CoreApp, DataSourceApi, DataSourceInstanceSettings, DataSourcePluginContextProvider, PluginExtensionQueryEditorRowAdaptiveTelemetryV1Context, EventBusExtended, HistoryItem, LoadingState, PanelData, QueryResultMetaNotice, TimeRange, getDataSourceRef, PluginExtensionPoints, } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { Trans, t } from '@grafana/i18n'; import { getDataSourceSrv, renderLimitedComponents, reportInteraction, usePluginComponents } from '@grafana/runtime'; import { DataQuery } from '@grafana/schema'; import { Badge, ErrorBoundaryAlert, List } from '@grafana/ui'; import { OperationRowHelp } from 'app/core/components/QueryOperationRow/OperationRowHelp'; import { QueryOperationAction, QueryOperationToggleAction, } from 'app/core/components/QueryOperationRow/QueryOperationAction'; import { QueryOperationRow, QueryOperationRowRenderProps, } from 'app/core/components/QueryOperationRow/QueryOperationRow'; import { useQueryLibraryContext } from '../../explore/QueryLibrary/QueryLibraryContext'; import { QueryActionComponent, RowActionComponents } from './QueryActionComponent'; import { QueryEditorRowHeader } from './QueryEditorRowHeader'; import { QueryErrorAlert } from './QueryErrorAlert'; export interface Props { data: PanelData; query: TQuery; queries: TQuery[]; id: string; index: number; dataSource: DataSourceInstanceSettings; onChangeDataSource?: (dsSettings: DataSourceInstanceSettings) => void; onDataSourceLoaded?: (instance: DataSourceApi) => void; renderHeaderExtras?: () => ReactNode; onAddQuery: (query: TQuery) => void; onRemoveQuery: (query: TQuery) => void; onChange: (query: TQuery) => void; onReplace?: (query: DataQuery) => void; onRunQuery: () => void; visualization?: ReactNode; hideHideQueryButton?: boolean; app?: CoreApp; range: TimeRange; history?: Array>; eventBus?: EventBusExtended; hideActionButtons?: boolean; onQueryCopied?: () => void; onQueryRemoved?: () => void; onQueryToggled?: (queryStatus?: boolean | undefined) => void; onQueryReplacedFromLibrary?: () => void; collapsable?: boolean; hideRefId?: boolean; } interface State { /** DatasourceUid or ds variable expression used to resolve current datasource */ queriedDataSourceIdentifier?: string | null; datasource: DataSourceApi | null; datasourceUid?: string | null; data?: PanelData; isOpen?: boolean; showingHelp: boolean; } export class QueryEditorRow extends PureComponent, State> { dataSourceSrv = getDataSourceSrv(); id = ''; state: State = { datasource: null, data: undefined, isOpen: true, showingHelp: false, }; componentDidMount() { const { data, query, id } = this.props; const dataFilteredByRefId = filterPanelDataToQuery(data, query.refId); this.id = uniqueId(id + '_'); this.setState({ data: dataFilteredByRefId }); this.loadDatasource(); } /** * When datasource variables are used the query.datasource.uid property is a string variable expression * DataSourceSettings.uid can also be this variable expression. * This function always returns the current interpolated datasource uid. */ getInterpolatedDataSourceUID(): string | undefined { if (this.props.query.datasource) { const instanceSettings = this.dataSourceSrv.getInstanceSettings(this.props.query.datasource); return instanceSettings?.rawRef?.uid ?? instanceSettings?.uid; } return this.props.dataSource.rawRef?.uid ?? this.props.dataSource.uid; } async loadDatasource() { let datasource: DataSourceApi; const interpolatedUID = this.getInterpolatedDataSourceUID(); try { datasource = await this.dataSourceSrv.get(interpolatedUID); } catch (error) { // If the DS doesn't exist, it fails. Getting with no args returns the default DS. datasource = await this.dataSourceSrv.get(); } if (typeof this.props.onDataSourceLoaded === 'function') { this.props.onDataSourceLoaded(datasource); } this.setState({ datasource: datasource as unknown as DataSourceApi, queriedDataSourceIdentifier: interpolatedUID, }); } componentDidUpdate(prevProps: Props) { const { datasource, queriedDataSourceIdentifier } = this.state; const { data, query } = this.props; if (prevProps.id !== this.props.id) { this.id = uniqueId(this.props.id + '_'); } if (data !== prevProps.data) { const dataFilteredByRefId = filterPanelDataToQuery(data, query.refId); this.setState({ data: dataFilteredByRefId }); } // check if we need to load another datasource if (datasource && queriedDataSourceIdentifier !== this.getInterpolatedDataSourceUID()) { this.loadDatasource(); return; } } getQueryEditor(ds: DataSourceApi) { if (!ds) { return; } switch (this.props.app) { case CoreApp.Explore: return ( ds.components?.ExploreMetricsQueryField || ds.components?.ExploreLogsQueryField || ds.components?.ExploreQueryField || ds.components?.QueryEditor ); case CoreApp.PanelEditor: case CoreApp.Dashboard: default: return ds.components?.QueryEditor; } } isWaitingForDatasourceToLoad(): boolean { // if we not yet have loaded the datasource in state the // ds in props and the ds in state will have different values. return this.getInterpolatedDataSourceUID() !== this.state.queriedDataSourceIdentifier; } renderPluginEditor = () => { const { query, onChange, queries, onRunQuery, onAddQuery, range, app = CoreApp.PanelEditor, history } = this.props; const { datasource, data } = this.state; if (this.isWaitingForDatasourceToLoad()) { return null; } if (datasource) { let QueryEditor = this.getQueryEditor(datasource); if (QueryEditor) { return ( ); } } return (
Data source plugin does not export any Query Editor component
); }; onRemoveQuery = () => { const { onRemoveQuery, query, onQueryRemoved } = this.props; onRemoveQuery(query); if (onQueryRemoved) { onQueryRemoved(); } }; onCopyQuery = () => { const { query, onAddQuery, onQueryCopied } = this.props; const copy = cloneDeep(query); onAddQuery(copy); if (onQueryCopied) { onQueryCopied(); } }; onHideQuery = () => { const { query, onChange, onRunQuery, onQueryToggled } = this.props; onChange({ ...query, hide: !query.hide }); onRunQuery(); if (onQueryToggled) { onQueryToggled(query.hide); } reportInteraction('query_editor_row_hide_query_clicked', { hide: !query.hide, }); }; onToggleHelp = () => { this.setState((state) => ({ showingHelp: !state.showingHelp, })); }; onClickExample = (query: TQuery) => { if (query.datasource === undefined) { query.datasource = getDataSourceRef(this.props.dataSource); } this.props.onChange({ ...query, refId: this.props.query.refId, }); this.onToggleHelp(); }; renderCollapsedText(): string | null { const { datasource } = this.state; if (datasource?.getQueryDisplayText) { return datasource.getQueryDisplayText(this.props.query); } return null; } renderWarnings = (type: string): JSX.Element | null => { const { data, query } = this.props; const dataFilteredByRefId = filterPanelDataToQuery(data, query.refId)?.series ?? []; const allWarnings = dataFilteredByRefId.reduce((acc: QueryResultMetaNotice[], serie) => { if (!serie.meta?.notices) { return acc; } const warnings = filter(serie.meta.notices, (item: QueryResultMetaNotice) => item.severity === type) ?? []; return acc.concat(warnings); }, []); const uniqueWarnings = uniqBy(allWarnings, 'text'); const hasWarnings = uniqueWarnings.length > 0; if (!hasWarnings) { return null; } const key = 'query-' + type + 's'; const colour = type === 'warning' ? 'orange' : 'blue'; const iconName = type === 'warning' ? 'exclamation-triangle' : 'file-landscape-alt'; const listItems = uniqueWarnings.map((warning) => warning.text); const serializedWarnings = <>{item}} />; return ( {uniqueWarnings.length} {pluralize(type, uniqueWarnings.length)} } tooltip={serializedWarnings} /> ); }; renderExtraActions = () => { const { query, queries, data, onAddQuery, dataSource, app } = this.props; const unscopedActions = RowActionComponents.getAllExtraRenderAction(); let scopedActions: QueryActionComponent[] = []; if (app !== undefined) { scopedActions = RowActionComponents.getScopedExtraRenderAction(app); } const extraActions = [...unscopedActions, ...scopedActions] .map((action, index) => action({ query, queries, timeRange: data.timeRange, onAddQuery: onAddQuery as (query: DataQuery) => void, dataSource, key: index, }) ) .filter(Boolean); extraActions.push(this.renderWarnings('info')); extraActions.push(this.renderWarnings('warning')); extraActions.push(); return extraActions; }; renderActions = (props: QueryOperationRowRenderProps) => { const { query, hideHideQueryButton: hideHideQueryButton = false, onReplace, onQueryReplacedFromLibrary, } = this.props; const { datasource, showingHelp } = this.state; const isHidden = !!query.hide; const hasEditorHelp = datasource?.components?.QueryEditorHelp; return ( <> {hasEditorHelp && ( )} {this.renderExtraActions()} { onQueryReplacedFromLibrary?.(); onReplace?.(query); }} /> {!hideHideQueryButton ? ( ) : null} ); }; renderHeader = (props: QueryOperationRowRenderProps) => { const { app, query, dataSource, onChangeDataSource, onChange, queries, renderHeaderExtras, hideRefId } = this.props; return (