diff --git a/public/app/features/dashboard/components/PanelEditor/PanelEditorQueries.tsx b/public/app/features/dashboard/components/PanelEditor/PanelEditorQueries.tsx index 2e974c3ede6..dfe8f45e0d1 100644 --- a/public/app/features/dashboard/components/PanelEditor/PanelEditorQueries.tsx +++ b/public/app/features/dashboard/components/PanelEditor/PanelEditorQueries.tsx @@ -1,16 +1,96 @@ import React, { PureComponent } from 'react'; -import { QueriesTab } from 'app/features/query/components/QueriesTab'; -import { DashboardModel, PanelModel } from '../../state'; +import { QueryGroup } from 'app/features/query/components/QueryGroup'; +import { QueryGroupOptions } from 'app/features/query/components/QueryGroupOptions'; +import { PanelModel } from '../../state'; +import { DataQuery, DataSourceSelectItem } from '@grafana/data'; +import { getLocationSrv } from '@grafana/runtime'; interface Props { panel: PanelModel; - dashboard: DashboardModel; } -export class PanelEditorQueries extends PureComponent { - render() { - const { panel, dashboard } = this.props; +interface State { + options: QueryGroupOptions; +} - return ; +export class PanelEditorQueries extends PureComponent { + constructor(props: Props) { + super(props); + + this.state = { options: this.buildQueryOptions(props) }; + } + + buildQueryOptions({ panel }: Props): QueryGroupOptions { + return { + maxDataPoints: panel.maxDataPoints, + minInterval: panel.interval, + timeRange: { + from: panel.timeFrom, + shift: panel.timeShift, + hide: panel.hideTimeOverride, + }, + }; + } + + onDataSourceChange = (ds: DataSourceSelectItem, queries: DataQuery[]) => { + const { panel } = this.props; + + panel.datasource = ds.value; + panel.targets = queries; + panel.refresh(); + + this.forceUpdate(); + }; + + onRunQueries = () => { + this.props.panel.refresh(); + }; + + onQueriesChange = (queries: DataQuery[]) => { + const { panel } = this.props; + + panel.targets = queries; + panel.refresh(); + + this.forceUpdate(); + }; + + onOpenQueryInspector = () => { + getLocationSrv().update({ + query: { inspect: this.props.panel.id, inspectTab: 'query' }, + partial: true, + }); + }; + + onQueryOptionsChange = (options: QueryGroupOptions) => { + const { panel } = this.props; + + panel.timeFrom = options.timeRange?.from; + panel.timeShift = options.timeRange?.shift; + panel.hideTimeOverride = options.timeRange?.hide; + panel.interval = options.minInterval; + panel.maxDataPoints = options.maxDataPoints; + panel.refresh(); + + this.setState({ options: options }); + }; + + render() { + const { panel } = this.props; + const { options } = this.state; + + return ( + + ); } } diff --git a/public/app/features/dashboard/components/PanelEditor/PanelEditorTabs.tsx b/public/app/features/dashboard/components/PanelEditor/PanelEditorTabs.tsx index 944cffda809..996b6c11d98 100644 --- a/public/app/features/dashboard/components/PanelEditor/PanelEditorTabs.tsx +++ b/public/app/features/dashboard/components/PanelEditor/PanelEditorTabs.tsx @@ -76,7 +76,7 @@ export class PanelEditorTabs extends PureComponent { })} - {activeTab.id === PanelEditorTabId.Query && } + {activeTab.id === PanelEditorTabId.Query && } {activeTab.id === PanelEditorTabId.Alert && } {activeTab.id === PanelEditorTabId.Transform && } diff --git a/public/app/features/dashboard/state/PanelModel.ts b/public/app/features/dashboard/state/PanelModel.ts index 36e7525749b..da160c4ac97 100644 --- a/public/app/features/dashboard/state/PanelModel.ts +++ b/public/app/features/dashboard/state/PanelModel.ts @@ -132,8 +132,8 @@ export class PanelModel implements DataConfigSource { }; fieldConfig: FieldConfigSource; - maxDataPoints?: number; - interval?: string; + maxDataPoints?: number | null; + interval?: string | null; description?: string; links?: DataLink[]; transparent: boolean; diff --git a/public/app/features/query/components/QueryEditorRow.tsx b/public/app/features/query/components/QueryEditorRow.tsx index 37ce76f2dd4..ae3423136c4 100644 --- a/public/app/features/query/components/QueryEditorRow.tsx +++ b/public/app/features/query/components/QueryEditorRow.tsx @@ -6,9 +6,6 @@ import _ from 'lodash'; import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; import { AngularComponent, getAngularLoader } from '@grafana/runtime'; import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv'; -// Types -import { PanelModel } from '../../dashboard/state/PanelModel'; - import { ErrorBoundaryAlert, HorizontalGroup } from '@grafana/ui'; import { DataQuery, @@ -25,12 +22,11 @@ import { QueryOperationRow } from 'app/core/components/QueryOperationRow/QueryOp import { QueryOperationAction } from 'app/core/components/QueryOperationRow/QueryOperationAction'; import { DashboardModel } from '../../dashboard/state/DashboardModel'; import { selectors } from '@grafana/e2e-selectors'; +import { PanelModel } from 'app/features/dashboard/state'; interface Props { - panel: PanelModel; data: PanelData; query: DataQuery; - dashboard?: DashboardModel; dataSourceValue: string | null; inMixedMode?: boolean; id: string; @@ -38,6 +34,7 @@ interface Props { onAddQuery: (query?: DataQuery) => void; onRemoveQuery: (query: DataQuery) => void; onChange: (query: DataQuery) => void; + onRunQuery: () => void; } interface State { @@ -72,15 +69,20 @@ export class QueryEditorRow extends PureComponent { } getAngularQueryComponentScope(): AngularQueryComponentScope { - const { panel, query, dashboard } = this.props; + const { query, onChange } = this.props; const { datasource } = this.state; + const panel = new PanelModel({}); + const dashboard = {} as DashboardModel; return { datasource: datasource, target: query, panel: panel, - dashboard: dashboard!, - refresh: () => panel.refresh(), + dashboard: dashboard, + refresh: () => { + // Old angular editors modify the query model and just call refresh + onChange(query); + }, render: () => () => console.log('legacy render function called, it does nothing'), events: panel.events, range: getTimeSrv().timeRange(), @@ -88,12 +90,12 @@ export class QueryEditorRow extends PureComponent { } async loadDatasource() { - const { query, panel, dataSourceValue } = this.props; + const { query, dataSourceValue } = this.props; const dataSourceSrv = getDatasourceSrv(); let datasource; try { - const datasourceName = dataSourceValue || query.datasource || panel.datasource; + const datasourceName = dataSourceValue || query.datasource; datasource = await dataSourceSrv.get(datasourceName); } catch (error) { datasource = await dataSourceSrv.get(); @@ -108,7 +110,7 @@ export class QueryEditorRow extends PureComponent { componentDidUpdate(prevProps: Props) { const { loadedDataSourceValue } = this.state; - const { data, query, panel } = this.props; + const { data, query } = this.props; if (data !== prevProps.data) { this.setState({ data: filterPanelDataToQuery(data, query.refId) }); @@ -118,7 +120,7 @@ export class QueryEditorRow extends PureComponent { } if (this.angularQueryEditor) { - notifyAngularQueryEditorsOfData(panel, data, this.angularQueryEditor); + notifyAngularQueryEditorsOfData(this.angularScope?.panel!, data, this.angularQueryEditor); } } @@ -161,7 +163,7 @@ export class QueryEditorRow extends PureComponent { }; onRunQuery = () => { - this.props.panel.refresh(); + this.props.onRunQuery(); }; renderPluginEditor = () => { diff --git a/public/app/features/query/components/QueryEditorRows.tsx b/public/app/features/query/components/QueryEditorRows.tsx index 008a4e92374..09d43dcacef 100644 --- a/public/app/features/query/components/QueryEditorRows.tsx +++ b/public/app/features/query/components/QueryEditorRows.tsx @@ -2,11 +2,8 @@ import React, { PureComponent } from 'react'; // Types -import { PanelModel } from '../../dashboard/state/PanelModel'; import { DataQuery, PanelData, DataSourceSelectItem } from '@grafana/data'; -import { DashboardModel } from '../../dashboard/state/DashboardModel'; import { QueryEditorRow } from './QueryEditorRow'; -import { addQuery } from 'app/core/utils/query'; import { DragDropContext, Droppable, DropResult } from 'react-beautiful-dnd'; interface Props { @@ -15,35 +12,21 @@ interface Props { datasource: DataSourceSelectItem; // Query editing - onChangeQueries: (queries: DataQuery[]) => void; - onScrollBottom: () => void; - - // Dashboard Configs - panel: PanelModel; - dashboard?: DashboardModel; + onQueriesChange: (queries: DataQuery[]) => void; + onAddQuery: (query: DataQuery) => void; + onRunQueries: () => void; // Query Response Data data: PanelData; } export class QueryEditorRows extends PureComponent { - onAddQuery = (query?: Partial) => { - const { queries, onChangeQueries } = this.props; - onChangeQueries(addQuery(queries, query)); - this.props.onScrollBottom(); - }; - onRemoveQuery = (query: DataQuery) => { - const { queries, onChangeQueries, panel } = this.props; - const removed = queries.filter(q => { - return q !== query; - }); - onChangeQueries(removed); - panel.refresh(); + this.props.onQueriesChange(this.props.queries.filter(item => item !== query)); }; onChangeQuery(query: DataQuery, index: number) { - const { queries, onChangeQueries } = this.props; + const { queries, onQueriesChange } = this.props; const old = queries[index]; @@ -54,7 +37,7 @@ export class QueryEditorRows extends PureComponent { } // update query in array - onChangeQueries( + onQueriesChange( queries.map((item, itemIndex) => { if (itemIndex === index) { return query; @@ -65,7 +48,7 @@ export class QueryEditorRows extends PureComponent { } onDragEnd = (result: DropResult) => { - const { queries, onChangeQueries, panel } = this.props; + const { queries, onQueriesChange } = this.props; if (!result || !result.destination) { return; @@ -80,12 +63,12 @@ export class QueryEditorRows extends PureComponent { const update = Array.from(queries); const [removed] = update.splice(startIndex, 1); update.splice(endIndex, 0, removed); - onChangeQueries(update); - panel.refresh(); + onQueriesChange(update); }; render() { const { props } = this; + return ( @@ -98,13 +81,12 @@ export class QueryEditorRows extends PureComponent { id={query.refId} index={index} key={query.refId} - panel={props.panel} - dashboard={props.dashboard} data={props.data} query={query} onChange={query => this.onChangeQuery(query, index)} onRemoveQuery={this.onRemoveQuery} - onAddQuery={this.onAddQuery} + onAddQuery={this.props.onAddQuery} + onRunQuery={this.props.onRunQueries} inMixedMode={props.datasource.meta.mixed} /> ))} diff --git a/public/app/features/query/components/QueriesTab.tsx b/public/app/features/query/components/QueryGroup.tsx similarity index 72% rename from public/app/features/query/components/QueriesTab.tsx rename to public/app/features/query/components/QueryGroup.tsx index a292d6419b0..14086ec55a9 100644 --- a/public/app/features/query/components/QueriesTab.tsx +++ b/public/app/features/query/components/QueryGroup.tsx @@ -2,17 +2,14 @@ import React, { PureComponent } from 'react'; // Components import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker'; -import { QueryOptions } from './QueryOptions'; import { Button, CustomScrollbar, HorizontalGroup, Modal, stylesFactory, Field } from '@grafana/ui'; -import { getLocationSrv, getDataSourceSrv } from '@grafana/runtime'; +import { getDataSourceSrv } from '@grafana/runtime'; import { QueryEditorRows } from './QueryEditorRows'; // Services import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; import { backendSrv } from 'app/core/services/backend_srv'; import config from 'app/core/config'; // Types -import { PanelModel } from '../../dashboard/state/PanelModel'; -import { DashboardModel } from '../../dashboard/state/DashboardModel'; import { DataQuery, DataSourceSelectItem, @@ -24,14 +21,23 @@ import { import { PluginHelp } from 'app/core/components/PluginHelp/PluginHelp'; import { addQuery } from 'app/core/utils/query'; import { Unsubscribable } from 'rxjs'; -import { DashboardQueryEditor, isSharedDashboardQuery } from 'app/plugins/datasource/dashboard'; import { expressionDatasource, ExpressionDatasourceID } from 'app/features/expressions/ExpressionDatasource'; -import { css } from 'emotion'; import { selectors } from '@grafana/e2e-selectors'; +import { PanelQueryRunner } from '../state/PanelQueryRunner'; +import { QueryGroupOptions, QueryGroupOptionsEditor } from './QueryGroupOptions'; +import { DashboardQueryEditor, isSharedDashboardQuery } from 'app/plugins/datasource/dashboard'; +import { css } from 'emotion'; interface Props { - panel: PanelModel; - dashboard?: DashboardModel; + queryRunner: PanelQueryRunner; + queries: DataQuery[]; + dataSourceName: string | null; + options: QueryGroupOptions; + onOpenQueryInspector?: () => void; + onRunQueries: () => void; + onQueriesChange: (queries: DataQuery[]) => void; + onDataSourceChange: (ds: DataSourceSelectItem, queries: DataQuery[]) => void; + onOptionsChange: (options: QueryGroupOptions) => void; } interface State { @@ -47,14 +53,14 @@ interface State { isHelpOpen: boolean; } -export class QueriesTab extends PureComponent { +export class QueryGroup extends PureComponent { datasources: DataSourceSelectItem[] = getDatasourceSrv().getMetricSources(); backendSrv = backendSrv; querySubscription: Unsubscribable | null; state: State = { isLoadingHelp: false, - dataSourceItem: this.findCurrentDataSource(), + dataSourceItem: this.findCurrentDataSource(this.props.dataSourceName), helpContent: null, isPickerOpen: false, isAddingMixed: false, @@ -68,15 +74,14 @@ export class QueriesTab extends PureComponent { }; async componentDidMount() { - const { panel } = this.props; - const queryRunner = panel.getQueryRunner(); + const { queryRunner, dataSourceName: datasourceName } = this.props; this.querySubscription = queryRunner.getData({ withTransforms: false, withFieldConfig: false }).subscribe({ next: (data: PanelData) => this.onPanelDataUpdate(data), }); try { - const ds = await getDataSourceSrv().get(panel.datasource); + const ds = await getDataSourceSrv().get(datasourceName); this.setState({ dataSource: ds }); } catch (error) { const ds = await getDataSourceSrv().get(); @@ -96,85 +101,62 @@ export class QueriesTab extends PureComponent { this.setState({ data }); } - findCurrentDataSource(dataSourceName: string | null = this.props.panel.datasource): DataSourceSelectItem { + findCurrentDataSource(dataSourceName: string | null): DataSourceSelectItem { return this.datasources.find(datasource => datasource.value === dataSourceName) || this.datasources[0]; } onChangeDataSource = async (newDsItem: DataSourceSelectItem) => { - const { panel } = this.props; + let { queries } = this.props; const { dataSourceItem } = this.state; // switching to mixed if (newDsItem.meta.mixed) { - // Set the datasource on all targets - panel.targets.forEach(target => { - if (target.datasource !== ExpressionDatasourceID) { - target.datasource = panel.datasource; - if (!target.datasource) { - target.datasource = config.defaultDatasource; + for (const query of queries) { + if (query.datasource !== ExpressionDatasourceID) { + query.datasource = query.datasource; + if (!query.datasource) { + query.datasource = config.defaultDatasource; } } - }); + } } else if (dataSourceItem) { // if switching from mixed if (dataSourceItem.meta.mixed) { // Remove the explicit datasource - for (const target of panel.targets) { - if (target.datasource !== ExpressionDatasourceID) { - delete target.datasource; + for (const query of queries) { + if (query.datasource !== ExpressionDatasourceID) { + delete query.datasource; } } } else if (dataSourceItem.meta.id !== newDsItem.meta.id) { // we are changing data source type, clear queries - panel.targets = [{ refId: 'A' }]; + queries = [{ refId: 'A' }]; } } const dataSource = await getDataSourceSrv().get(newDsItem.value); - panel.datasource = newDsItem.value; + this.props.onDataSourceChange(newDsItem, queries); - this.setState( - { - dataSourceItem: newDsItem, - dataSource: dataSource, - dataSourceError: undefined, - }, - () => panel.refresh() - ); - }; - - openQueryInspector = () => { - const { panel } = this.props; - - getLocationSrv().update({ - query: { inspect: panel.id, inspectTab: 'query' }, - partial: true, + this.setState({ + dataSourceItem: newDsItem, + dataSource: dataSource, + dataSourceError: undefined, }); }; - /** - * Sets the queries for the panel - */ - onUpdateQueries = (queries: DataQuery[]) => { - this.props.panel.updateQueries(queries); - - // Need to force update to rerender query rows. - this.forceUpdate(); - }; - onAddQueryClick = () => { if (this.state.dataSourceItem.meta.mixed) { this.setState({ isAddingMixed: true }); return; } - this.onUpdateQueries(addQuery(this.props.panel.targets)); + this.props.onQueriesChange(addQuery(this.props.queries)); this.onScrollBottom(); }; onAddExpressionClick = () => { - this.onUpdateQueries(addQuery(this.props.panel.targets, expressionDatasource.newQuery())); + this.props.onQueriesChange(addQuery(this.props.queries, expressionDatasource.newQuery())); this.onScrollBottom(); }; @@ -183,8 +165,8 @@ export class QueriesTab extends PureComponent { }; renderTopSection(styles: QueriesTabStyls) { - const { panel } = this.props; - const { dataSourceItem, data, dataSource, dataSourceError } = this.state; + const { onOpenQueryInspector, options, onOptionsChange } = this.props; + const { dataSourceItem, dataSource, dataSourceError, data } = this.state; if (!dataSource) { return null; @@ -211,17 +193,19 @@ export class QueriesTab extends PureComponent { />
- -
-
- +
+ {onOpenQueryInspector && ( +
+ +
+ )} ); @@ -253,18 +237,18 @@ export class QueriesTab extends PureComponent { }; onAddMixedQuery = (datasource: any) => { - this.props.panel.targets = addQuery(this.props.panel.targets, { datasource: datasource.name }); + this.onAddQuery({ datasource: datasource.name }); this.setState({ isAddingMixed: false, scrollTop: this.state.scrollTop + 10000 }); - this.forceUpdate(); }; onMixedPickerBlur = () => { this.setState({ isAddingMixed: false }); }; - onQueryChange = (query: DataQuery, index: number) => { - this.props.panel.changeQuery(query, index); - this.forceUpdate(); + onAddQuery = (query: Partial) => { + const { queries, onQueriesChange } = this.props; + onQueriesChange(addQuery(queries, query)); + this.onScrollBottom(); }; setScrollTop = (event: React.MouseEvent) => { @@ -273,22 +257,21 @@ export class QueriesTab extends PureComponent { }; renderQueries() { - const { panel, dashboard } = this.props; + const { onQueriesChange, queries, onRunQueries } = this.props; const { dataSourceItem, data } = this.state; if (isSharedDashboardQuery(dataSourceItem.name)) { - return this.onUpdateQueries([query])} />; + return ; } return (
diff --git a/public/app/features/query/components/QueryOptions.tsx b/public/app/features/query/components/QueryGroupOptions.tsx similarity index 68% rename from public/app/features/query/components/QueryOptions.tsx rename to public/app/features/query/components/QueryGroupOptions.tsx index 0d820a682c4..dc0e27bbda1 100644 --- a/public/app/features/query/components/QueryOptions.tsx +++ b/public/app/features/query/components/QueryGroupOptions.tsx @@ -8,51 +8,47 @@ import { rangeUtil, PanelData, DataSourceApi } from '@grafana/data'; import { Switch, Input, InlineField, InlineFormLabel, stylesFactory } from '@grafana/ui'; // Types -import { PanelModel } from '../../dashboard/state'; import { QueryOperationRow } from 'app/core/components/QueryOperationRow/QueryOperationRow'; import { config } from 'app/core/config'; import { css } from 'emotion'; -const timeRangeValidation = (value: string) => { - if (!value) { - return true; - } - return rangeUtil.isValidTimeSpan(value); -}; - -const emptyToNull = (value: string) => { - return value === '' ? null : value; -}; +export interface QueryGroupOptions { + maxDataPoints?: number | null; + minInterval?: string | null; + cacheTimeout?: string | null; + timeRange?: { + from?: string | null; + shift?: string | null; + hide?: boolean; + }; +} interface Props { - panel: PanelModel; + options: QueryGroupOptions; dataSource: DataSourceApi; data: PanelData; + onChange: (options: QueryGroupOptions) => void; } interface State { - relativeTime: string; - timeShift: string; - cacheTimeout: string; - maxDataPoints: number | string; - interval: string; - hideTimeOverride: boolean; + timeRangeFrom: string; + timeRangeShift: string; + timeRangeHide: boolean; isOpen: boolean; relativeTimeIsValid: boolean; timeShiftIsValid: boolean; } -export class QueryOptions extends PureComponent { +export class QueryGroupOptionsEditor extends PureComponent { constructor(props: Props) { super(props); + const { options } = props; + this.state = { - relativeTime: props.panel.timeFrom || '', - timeShift: props.panel.timeShift || '', - cacheTimeout: props.panel.cacheTimeout || '', - maxDataPoints: props.panel.maxDataPoints ?? '', - interval: props.panel.interval || '', - hideTimeOverride: props.panel.hideTimeOverride || false, + timeRangeFrom: options.timeRange?.from || '', + timeRangeShift: options.timeRange?.shift || '', + timeRangeHide: options.timeRange?.hide ?? false, isOpen: false, relativeTimeIsValid: true, timeShiftIsValid: true, @@ -61,79 +57,103 @@ export class QueryOptions extends PureComponent { onRelativeTimeChange = (event: ChangeEvent) => { this.setState({ - relativeTime: event.target.value, + timeRangeFrom: event.target.value, }); }; onTimeShiftChange = (event: ChangeEvent) => { this.setState({ - timeShift: event.target.value, + timeRangeShift: event.target.value, }); }; onOverrideTime = (event: FocusEvent) => { - const { value } = event.target; - const { panel } = this.props; - const emptyToNullValue = emptyToNull(value); - const isValid = timeRangeValidation(value); + const { options, onChange } = this.props; - if (isValid && panel.timeFrom !== emptyToNullValue) { - panel.timeFrom = emptyToNullValue; - panel.refresh(); + const newValue = emptyToNull(event.target.value); + const isValid = timeRangeValidation(newValue); + + if (isValid && options.timeRange?.from !== newValue) { + onChange({ + ...options, + timeRange: { + ...(options.timeRange ?? {}), + from: newValue, + }, + }); } + this.setState({ relativeTimeIsValid: isValid }); }; onTimeShift = (event: FocusEvent) => { - const { value } = event.target; - const { panel } = this.props; - const emptyToNullValue = emptyToNull(value); - const isValid = timeRangeValidation(value); + const { options, onChange } = this.props; - if (isValid && panel.timeShift !== emptyToNullValue) { - panel.timeShift = emptyToNullValue; - panel.refresh(); + const newValue = emptyToNull(event.target.value); + const isValid = timeRangeValidation(newValue); + + if (isValid && options.timeRange?.shift !== newValue) { + onChange({ + ...options, + timeRange: { + ...(options.timeRange ?? {}), + shift: newValue, + }, + }); } + this.setState({ timeShiftIsValid: isValid }); }; onToggleTimeOverride = () => { - const { panel } = this.props; - this.setState({ hideTimeOverride: !this.state.hideTimeOverride }, () => { - panel.hideTimeOverride = this.state.hideTimeOverride; - panel.refresh(); + const { onChange, options } = this.props; + + this.setState({ timeRangeHide: !this.state.timeRangeHide }, () => { + onChange({ + ...options, + timeRange: { + ...(options.timeRange ?? {}), + hide: this.state.timeRangeHide, + }, + }); }); }; - onDataSourceOptionBlur = (panelKey: string) => () => { - const { panel } = this.props; - - // @ts-ignore - panel[panelKey] = this.state[panelKey]; - panel.refresh(); + onCacheTimeoutBlur = (event: ChangeEvent) => { + const { options, onChange } = this.props; + onChange({ + ...options, + cacheTimeout: emptyToNull(event.target.value), + }); }; - onDataSourceOptionChange = (panelKey: string) => (event: ChangeEvent) => { - this.setState({ ...this.state, [panelKey]: event.target.value }); - }; + onMaxDataPointsBlur = (event: ChangeEvent) => { + const { options, onChange } = this.props; - onMaxDataPointsBlur = () => { - const { panel } = this.props; + let maxDataPoints: number | null = parseInt(event.target.value as string, 10); - const maxDataPoints = parseInt(this.state.maxDataPoints as string, 10); - - if (isNaN(maxDataPoints)) { - delete panel.maxDataPoints; - } else { - panel.maxDataPoints = maxDataPoints; + if (isNaN(maxDataPoints) || maxDataPoints === 0) { + maxDataPoints = null; } - panel.refresh(); + onChange({ + ...options, + maxDataPoints: maxDataPoints, + }); + }; + + onMinIntervalBlur = (event: ChangeEvent) => { + const { options, onChange } = this.props; + + onChange({ + ...options, + minInterval: emptyToNull(event.target.value), + }); }; renderCacheTimeoutOption() { - const { dataSource } = this.props; - const { cacheTimeout } = this.state; + const { dataSource, options } = this.props; + const tooltip = `If your time series store has a query cache this option can override the default cache timeout. Specify a numeric value in seconds.`; @@ -153,9 +173,8 @@ export class QueryOptions extends PureComponent { placeholder="60" name={name} spellCheck={false} - onBlur={this.onDataSourceOptionBlur('cacheTimeout')} - onChange={this.onDataSourceOptionChange('cacheTimeout')} - value={cacheTimeout} + onBlur={this.onCacheTimeoutBlur} + defaultValue={options.cacheTimeout ?? ''} /> @@ -163,10 +182,10 @@ export class QueryOptions extends PureComponent { } renderMaxDataPointsOption() { - const { data } = this.props; - const { maxDataPoints } = this.state; + const { data, options } = this.props; const realMd = data.request?.maxDataPoints; - const isAuto = maxDataPoints === ''; + const value = options.maxDataPoints ?? ''; + const isAuto = value === ''; return (
@@ -189,8 +208,7 @@ export class QueryOptions extends PureComponent { name={name} spellCheck={false} onBlur={this.onMaxDataPointsBlur} - onChange={this.onDataSourceOptionChange('maxDataPoints')} - value={maxDataPoints} + defaultValue={value} /> {isAuto && ( <> @@ -204,8 +222,7 @@ export class QueryOptions extends PureComponent { } renderIntervalOption() { - const { data, dataSource } = this.props; - const { interval } = this.state; + const { data, dataSource, options } = this.props; const realInterval = data.request?.interval; const minIntervalOnDs = dataSource.interval ?? 'No limit'; @@ -231,9 +248,8 @@ export class QueryOptions extends PureComponent { placeholder={`${minIntervalOnDs}`} name={name} spellCheck={false} - onBlur={this.onDataSourceOptionBlur('interval')} - onChange={this.onDataSourceOptionChange('interval')} - value={interval} + onBlur={this.onMinIntervalBlur} + defaultValue={options.minInterval ?? ''} />
@@ -268,19 +284,19 @@ export class QueryOptions extends PureComponent { }; renderCollapsedText(styles: StylesType): React.ReactNode | undefined { - const { data } = this.props; - const { isOpen, maxDataPoints, interval } = this.state; + const { data, options } = this.props; + const { isOpen } = this.state; if (isOpen) { return undefined; } - let mdDesc = maxDataPoints; - if (maxDataPoints === '' && data.request) { + let mdDesc = options.maxDataPoints ?? ''; + if (mdDesc === '' && data.request) { mdDesc = `auto = ${data.request.maxDataPoints}`; } - let intervalDesc = interval; + let intervalDesc = options.minInterval; if (data.request) { intervalDesc = `${data.request.interval}`; } @@ -294,8 +310,8 @@ export class QueryOptions extends PureComponent { } render() { - const { hideTimeOverride, relativeTimeIsValid, timeShiftIsValid } = this.state; - const { relativeTime, timeShift, isOpen } = this.state; + const { timeRangeHide: hideTimeOverride, relativeTimeIsValid, timeShiftIsValid } = this.state; + const { timeRangeFrom: relativeTime, timeRangeShift: timeShift, isOpen } = this.state; const styles = getStyles(); return ( @@ -349,6 +365,18 @@ export class QueryOptions extends PureComponent { } } +const timeRangeValidation = (value: string | null) => { + if (!value) { + return true; + } + + return rangeUtil.isValidTimeSpan(value); +}; + +const emptyToNull = (value: string) => { + return value === '' ? null : value; +}; + const getStyles = stylesFactory(() => { const { theme } = config; diff --git a/public/app/features/query/state/PanelQueryRunner.ts b/public/app/features/query/state/PanelQueryRunner.ts index 7aa8daac174..ddc050d7f00 100644 --- a/public/app/features/query/state/PanelQueryRunner.ts +++ b/public/app/features/query/state/PanelQueryRunner.ts @@ -34,7 +34,7 @@ export interface QueryRunnerOptions< > { datasource: string | DataSourceApi | null; queries: TQuery[]; - panelId: number; + panelId?: number; dashboardId?: number; timezone: TimeZone; timeRange: TimeRange; diff --git a/public/app/features/query/state/types.ts b/public/app/features/query/state/types.ts new file mode 100644 index 00000000000..009688dc2c8 --- /dev/null +++ b/public/app/features/query/state/types.ts @@ -0,0 +1,5 @@ +import { EventBusExtended } from '@grafana/data'; + +export interface PanelModelForLegacyQueryEditors { + events: EventBusExtended; +} diff --git a/public/app/features/sandbox/TestStuffPage.tsx b/public/app/features/sandbox/TestStuffPage.tsx index 11d6bc4bdbb..cc7cff6cb73 100644 --- a/public/app/features/sandbox/TestStuffPage.tsx +++ b/public/app/features/sandbox/TestStuffPage.tsx @@ -1,32 +1,120 @@ -import React, { FC, useState } from 'react'; -import { PanelModel } from '../dashboard/state'; -import { QueriesTab } from '../query/components/QueriesTab'; +import { + ApplyFieldOverrideOptions, + DataQuery, + DataSourceSelectItem, + DataTransformerConfig, + dateMath, + FieldColorModeId, + PanelData, +} from '@grafana/data'; +import { GraphNG, Table } from '@grafana/ui'; +import { config } from 'app/core/config'; +import React, { FC, useMemo, useState } from 'react'; +import { useObservable } from 'react-use'; +import { QueryGroup } from '../query/components/QueryGroup'; +import { QueryGroupOptions } from '../query/components/QueryGroupOptions'; +import { PanelQueryRunner } from '../query/state/PanelQueryRunner'; interface State { - panel: PanelModel; + queries: DataQuery[]; + queryRunner: PanelQueryRunner; + dataSourceName: string | null; + queryOptions: QueryGroupOptions; + data?: PanelData; } export const TestStuffPage: FC = () => { - const [state] = useState(getDefaultState()); + const [state, setState] = useState(getDefaultState()); + const { queryOptions, queryRunner, queries, dataSourceName } = state; + + const onDataSourceChange = (ds: DataSourceSelectItem, queries: DataQuery[]) => { + setState({ + ...state, + dataSourceName: ds.value, + queries: queries, + }); + }; + + const onRunQueries = () => { + const timeRange = { from: 'now-1h', to: 'now' }; + + queryRunner.run({ + queries, + timezone: 'browser', + datasource: dataSourceName, + timeRange: { from: dateMath.parse(timeRange.from)!, to: dateMath.parse(timeRange.to)!, raw: timeRange }, + maxDataPoints: queryOptions.maxDataPoints ?? 100, + minInterval: queryOptions.minInterval, + }); + }; + + const onQueriesChange = (queries: DataQuery[]) => { + setState({ ...state, queries: queries }); + }; + + const onQueryOptionsChange = (queryOptions: QueryGroupOptions) => { + setState({ ...state, queryOptions }); + }; + + /** + * Subscribe to data + */ + const observable = useMemo(() => queryRunner.getData({ withFieldConfig: true, withTransforms: true }), []); + const data = useObservable(observable); return ( -
-

Hello

+
+

New page

+
+ +
- + {data && ( +
+ +
+ + + )} ); }; export function getDefaultState(): State { - const panel = new PanelModel({ - datasource: 'gdev-testdata', - id: 10, - targets: [], - }); + const options: ApplyFieldOverrideOptions = { + fieldConfig: { + defaults: { + color: { + mode: FieldColorModeId.PaletteClassic, + }, + }, + overrides: [], + }, + replaceVariables: (v: string) => v, + theme: config.theme, + }; + + const dataConfig = { + getTransformations: () => [] as DataTransformerConfig[], + getFieldOverrideOptions: () => options, + }; return { - panel, + queries: [], + dataSourceName: 'gdev-testdata', + queryRunner: new PanelQueryRunner(dataConfig), + queryOptions: { + maxDataPoints: 100, + }, }; } diff --git a/public/app/plugins/datasource/dashboard/DashboardQueryEditor.tsx b/public/app/plugins/datasource/dashboard/DashboardQueryEditor.tsx index a49afcb6133..bd23b8d9016 100644 --- a/public/app/plugins/datasource/dashboard/DashboardQueryEditor.tsx +++ b/public/app/plugins/datasource/dashboard/DashboardQueryEditor.tsx @@ -28,9 +28,9 @@ function getQueryDisplayText(query: DataQuery): string { } interface Props { - panel: PanelModel; + queries: DataQuery[]; panelData: PanelData; - onChange: (query: DashboardQuery) => void; + onChange: (queries: DataQuery[]) => void; } type State = { @@ -48,8 +48,7 @@ export class DashboardQueryEditor extends PureComponent { } getQuery(): DashboardQuery { - const { panel } = this.props; - return panel.targets[0] as DashboardQuery; + return this.props.queries[0] as DashboardQuery; } async componentDidMount() { @@ -57,10 +56,14 @@ export class DashboardQueryEditor extends PureComponent { } async componentDidUpdate(prevProps: Props) { - const { panelData } = this.props; + const { panelData, queries } = this.props; + + if (queries.length < 0) { + return; + } if (!prevProps || prevProps.panelData !== panelData) { - const query = this.props.panel.targets[0] as DashboardQuery; + const query = queries[0] as DashboardQuery; const defaultDS = await getDatasourceSrv().get(); const dashboard = getDashboardSrv().getCurrent(); const panel = dashboard.getPanelById(query.panelId ?? -124134); @@ -97,10 +100,7 @@ export class DashboardQueryEditor extends PureComponent { const { onChange } = this.props; const query = this.getQuery(); query.panelId = id; - onChange(query); - - // Update the - this.props.panel.refresh(); + onChange([query]); }; renderQueryData(editURL: string) {