D7net
Home
Console
Upload
information
Create File
Create Folder
About
Tools
:
/
proc
/
self
/
root
/
usr
/
share
/
grafana
/
public
/
app
/
features
/
query
/
components
/
Filename :
QueryGroup.tsx
back
Copy
import { css } from '@emotion/css'; import React, { PureComponent } from 'react'; import { Unsubscribable } from 'rxjs'; import { CoreApp, DataSourceApi, DataSourceInstanceSettings, getDefaultTimeRange, LoadingState, PanelData, PluginType, } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { getDataSourceSrv, locationService } from '@grafana/runtime'; import { DataQuery } from '@grafana/schema'; 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, 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 { AngularDeprecationPluginNotice } from 'app/features/plugins/angularDeprecation/AngularDeprecationPluginNotice'; import { DashboardQueryEditor, isSharedDashboardQuery } from 'app/plugins/datasource/dashboard'; import { GrafanaQuery } from 'app/plugins/datasource/grafana/types'; import { QueryGroupOptions } from 'app/types'; import { isAngularDatasourcePlugin } from '../../plugins/angularDeprecation/utils'; 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<Props, State> { 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); // Clean up the first panel flag since the modal is now open if (!!locationService.getSearchObject().firstPanel) { locationService.partial({ firstPanel: null }, true); } } 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.log('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, type: newSettings.meta.id, default: newSettings.isDefault, }, }); 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<DataQuery> { const { dsSettings, defaultDataSource } = this.state; const ds = !dsSettings?.meta.mixed ? dsSettings : defaultDataSource; return { ...this.state.dataSource?.getDefaultQuery?.(CoreApp.PanelEditor), datasource: { uid: ds?.uid, type: ds?.type }, }; } onChange(changedProps: Partial<QueryGroupOptions>) { 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 ( <div> <div className={styles.dataSourceRow}> <InlineFormLabel htmlFor="data-source-picker" width={'auto'}> Data source </InlineFormLabel> <div className={styles.dataSourceRowItem}>{this.renderDataSourcePickerWithPrompt()}</div> {dataSource && ( <> <div className={styles.dataSourceRowItem}> <Button variant="secondary" icon="question-circle" title="Open data source help" onClick={this.onOpenHelp} data-testid="query-tab-help-button" /> </div> <div className={styles.dataSourceRowItemOptions}> <QueryGroupOptionsEditor options={options} dataSource={dataSource} data={data} onChange={this.onUpdateAndRun} /> </div> {onOpenQueryInspector && ( <div className={styles.dataSourceRowItem}> <Button variant="secondary" onClick={onOpenQueryInspector} aria-label={selectors.components.QueryTab.queryInspectorButton} > Query inspector </Button> </div> )} </> )} </div> {dataSource && isAngularDatasourcePlugin(dataSource.uid) && ( <AngularDeprecationPluginNotice pluginId={dataSource.type} pluginType={PluginType.datasource} angularSupportEnabled={config?.angularSupportEnabled} showPluginDetailsLink={true} /> )} </div> ); } onOpenHelp = () => { this.setState({ isHelpOpen: true }); }; onCloseHelp = () => { this.setState({ isHelpOpen: false }); }; onCloseDataSourceModal = () => { this.setState({ isDataSourceModalOpen: false }); }; renderDataSourcePickerWithPrompt = () => { const { isDataSourceModalOpen } = this.state; const commonProps = { metrics: true, mixed: true, dashboard: true, variables: true, current: this.props.options.dataSource, uploadFile: true, onChange: async (ds: DataSourceInstanceSettings, defaultQueries?: DataQuery[] | GrafanaQuery[]) => { await this.onChangeDataSource(ds, defaultQueries); this.onCloseDataSourceModal(); }, }; return ( <> {isDataSourceModalOpen && config.featureToggles.advancedDataSourcePicker && ( <DataSourceModal {...commonProps} onDismiss={this.onCloseDataSourceModal}></DataSourceModal> )} <DataSourcePicker {...commonProps} /> </> ); }; onAddQuery = (query: Partial<DataQuery>) => { const { dsSettings, queries } = this.state; this.onQueriesChange(addQuery(queries, query, { type: dsSettings?.type, uid: dsSettings?.uid })); this.onScrollBottom(); }; onQueriesChange = (queries: DataQuery[] | GrafanaQuery[]) => { this.onChange({ queries }); this.setState({ queries }); }; renderQueries(dsSettings: DataSourceInstanceSettings) { const { onRunQueries } = this.props; const { data, queries } = this.state; if (isSharedDashboardQuery(dsSettings.name)) { return ( <DashboardQueryEditor queries={queries} panelData={data} onChange={this.onQueriesChange} onRunQueries={onRunQueries} /> ); } return ( <div aria-label={selectors.components.QueryTab.content}> <QueryEditorRows queries={queries} dsSettings={dsSettings} onQueriesChange={this.onQueriesChange} onAddQuery={this.onAddQuery} onRunQueries={onRunQueries} data={data} /> </div> ); } 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 showAddButton = !isSharedDashboardQuery(dsSettings.name); return ( <HorizontalGroup spacing="md" align="flex-start"> {showAddButton && ( <Button icon="plus" onClick={this.onAddQueryClick} variant="secondary" aria-label={selectors.components.QueryTab.addQuery} data-testid="query-tab-add-query" > Query </Button> )} {config.expressionsEnabled && this.isExpressionsSupported(dsSettings) && ( <Button icon="plus" onClick={this.onAddExpressionClick} variant="secondary" className={styles.expressionButton} data-testid="query-tab-add-expression" > <span>Expression </span> </Button> )} {this.renderExtraActions()} </HorizontalGroup> ); } setScrollRef = (scrollElement: HTMLDivElement): void => { this.setState({ scrollElement }); }; render() { const { isHelpOpen, dsSettings } = this.state; const styles = getStyles(); return ( <CustomScrollbar autoHeightMin="100%" scrollRefCallback={this.setScrollRef}> <div className={styles.innerWrapper}> {this.renderTopSection(styles)} {dsSettings && ( <> <div className={styles.queriesWrapper}>{this.renderQueries(dsSettings)}</div> {this.renderAddQueryRow(dsSettings, styles)} {isHelpOpen && ( <Modal title="Data source help" isOpen={true} onDismiss={this.onCloseHelp}> <PluginHelp pluginId={dsSettings.meta.id} /> </Modal> )} </> )} </div> </CustomScrollbar> ); } } 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<typeof getStyles>;