diff --git a/src/datasources/data-frame/DataFrameDataSource.ts b/src/datasources/data-frame/DataFrameDataSource.ts index 1ce6068b3..29390663b 100644 --- a/src/datasources/data-frame/DataFrameDataSource.ts +++ b/src/datasources/data-frame/DataFrameDataSource.ts @@ -1,7 +1,7 @@ import TTLCache from '@isaacs/ttlcache'; import deepEqual from 'fast-deep-equal'; import { DataQueryRequest, DataSourceInstanceSettings, FieldType, TimeRange, FieldDTO, dateTime, DataFrameDTO, MetricFindValue, TestDataSourceResponse, DataSourceJsonData } from '@grafana/data'; -import { BackendSrv, TemplateSrv, getBackendSrv, getTemplateSrv } from '@grafana/runtime'; +import { BackendSrv, TemplateSrv, getBackendSrv, getTemplateSrv, locationService } from '@grafana/runtime'; import { ColumnDataType, DataFrameQuery, @@ -35,12 +35,21 @@ export class DataFrameDataSource extends DataSourceBase { + async runQuery( + query: DataFrameQuery, + request: DataQueryRequest + ): Promise { + const { range, scopedVars, maxDataPoints, targets } = request; const processedQuery = this.processQuery(query); processedQuery.tableId = this.templateSrv.replace(processedQuery.tableId, scopedVars); processedQuery.columns = replaceVariables(processedQuery.columns, this.templateSrv); const properties = await this.getTableProperties(processedQuery.tableId); + this.initializeFetchHighResolutionData( + targets.some(t => t.fetchHighResolutionData), + request.panelId?.toString(), + ); + if (processedQuery.type === DataFrameQueryType.Properties) { return { refId: processedQuery.refId, @@ -49,7 +58,22 @@ export class DataFrameDataSource extends DataSourceBase { + // return [Number(Math.random() * 100).toString(), Number(Math.random() * 100).toString()]; + // }), + // columns: columns.map(c => c.name), + // } + // } return { refId: processedQuery.refId, name: properties.name, @@ -74,11 +98,45 @@ export class DataFrameDataSource extends DataSourceBase { + async getDecimatedTableData( + query: DataFrameQuery, + columns: Column[], + timeRange: TimeRange, + intervals = 1000, + // panelId = '' + ): Promise { const filters: ColumnFilter[] = []; - if (query.applyTimeFilters) { - filters.push(...this.constructTimeFilters(columns, timeRange)); + // let enabledPanelIds: string[] = this.getEnabledPanelIds(); + // const isHighResolutionEnabled = enabledPanelIds.includes(panelId) && columns.length > 0; + const isHighResolutionEnabled = query.fetchHighResolutionData; + if (isHighResolutionEnabled) { + /** + * If `x-axis` selection is of type "TIMESTAMP", the below filters should be applied. + * Only after data source migration, we'll get those details here as a part of `columns`. Hence just handled numerical values for now. + * filters.push(...this.constructTimeFilters(columns, timeRange)); + */ + + if (columns[0].dataType === 'TIMESTAMP') { + filters.push(...this.constructTimeFilters(columns, timeRange)); + } else { + const queryParams = locationService.getSearchObject(); + // Once we have a seprate control to select x-axis, we can use that value directly instead of assuming the first column as the x-axis. + const xField = columns[0].name; + const xMin = queryParams[`${xField}-min`]; + const xMax = queryParams[`${xField}-max`]; + if ( + xMin !== '' + && xMax !== '' + && xMin !== undefined + && xMax !== undefined + // For session storage + // && xMin !== null + // && xMax !== null + ) { + filters.push(...this.constructXAxisNumberFilters(xField, Math.floor(Number(xMin)), Math.floor(Number(xMax)))); + } + } } if (query.filterNulls) { @@ -124,6 +182,45 @@ export class DataFrameDataSource extends DataSourceBase { const tableProperties = await this.getTableProperties(tableQuery.tableId); return tableProperties.columns.map(col => ({ text: col.name, value: col.name })); @@ -179,6 +276,13 @@ export class DataFrameDataSource extends DataSourceBase { const filters: ColumnFilter[] = []; diff --git a/src/datasources/data-frame/components/DataFrameQueryEditor.tsx b/src/datasources/data-frame/components/DataFrameQueryEditor.tsx index c4cbe6a93..fecb6f2f0 100644 --- a/src/datasources/data-frame/components/DataFrameQueryEditor.tsx +++ b/src/datasources/data-frame/components/DataFrameQueryEditor.tsx @@ -1,10 +1,10 @@ -import React, { useState } from 'react'; -import { useAsync } from 'react-use'; +import React, { useEffect, useMemo, useState } from 'react'; +import { useAsync, useLocation } from 'react-use'; import { SelectableValue, toOption } from '@grafana/data'; import { InlineField, InlineSwitch, MultiSelect, Select, AsyncSelect, RadioButtonGroup } from '@grafana/ui'; import { decimationMethods } from '../constants'; import _ from 'lodash'; -import { getTemplateSrv } from '@grafana/runtime'; +import { getTemplateSrv, locationService } from '@grafana/runtime'; import { isValidId } from '../utils'; import { FloatingError, parseErrorMessage } from '../../../core/errors'; import { DataFrameQueryEditorCommon, Props } from './DataFrameQueryEditorCommon'; @@ -14,7 +14,10 @@ import { DataFrameQueryType } from '../types'; export const DataFrameQueryEditor = (props: Props) => { const [errorMsg, setErrorMsg] = useState(''); const handleError = (error: Error) => setErrorMsg(parseErrorMessage(error)); - const common = new DataFrameQueryEditorCommon(props, handleError); + const common = useMemo( + () => new DataFrameQueryEditorCommon(props, handleError) + , [props] + ); const tableProperties = useAsync(() => common.datasource.getTableProperties(common.query.tableId).catch(handleError), [common.query.tableId]); const handleColumnChange = (items: Array>) => { @@ -27,6 +30,68 @@ export const DataFrameQueryEditor = (props: Props) => { return columnOptions; } + // When the user toggles the "Fetch high resolution data on zoom" switch, we update the URL query parameters + const handleFetchHighResolutionDataStateUpdate = (fetchHighResolutionData: boolean) => { + const editPanelId = getPanelId(); + let fetchHighResolutionDataEnabledPanelIds: string[] = getEnabledPanelIds(); + + if (fetchHighResolutionData && !fetchHighResolutionDataEnabledPanelIds.includes(editPanelId)) { + fetchHighResolutionDataEnabledPanelIds.push(editPanelId); + } + + if (!fetchHighResolutionData && fetchHighResolutionDataEnabledPanelIds.includes(editPanelId)) { + fetchHighResolutionDataEnabledPanelIds = fetchHighResolutionDataEnabledPanelIds.filter((panelId) => panelId !== editPanelId); + } + + locationService.partial({ + [`fetchHighResolutionData`]: fetchHighResolutionDataEnabledPanelIds.join(','), + }, true); + } + + // When URL query parameters change, we check if the current panel is enabled for fetching high resolution data and update the state in all the queries available in that panel - To sync the state across queries. + const location = useLocation(); + useEffect(() => { + const editPanelId = getPanelId(); + const fetchHighResolutionDataEnabledPanelIds: string[] = getEnabledPanelIds(); + + const updatedFetchHighResolutionDataState = fetchHighResolutionDataEnabledPanelIds.includes(editPanelId ?? ''); + if (updatedFetchHighResolutionDataState !== common.query.fetchHighResolutionData) { + common.handleQueryChange({ ...common.query, fetchHighResolutionData: updatedFetchHighResolutionDataState }, true); + } + }, [location.search, common]); + + // Utility functions + const getEnabledPanelIds = (): string[] => { + const queryParams = locationService.getSearchObject(); + const fetchHighResolutionDataOnZoom = queryParams['fetchHighResolutionData']; + let enabledPanelIds: string[] = []; + + if ( + fetchHighResolutionDataOnZoom !== undefined + && typeof fetchHighResolutionDataOnZoom === 'string' + && fetchHighResolutionDataOnZoom !== '' + ) { + enabledPanelIds = fetchHighResolutionDataOnZoom.split(','); + } + + return enabledPanelIds; + } + + const getPanelId = (): string => { + const queryParams = locationService.getSearchObject(); + const editPanelId = queryParams['editPanel']; + + if ( + editPanelId !== undefined + && (typeof editPanelId === 'string' || typeof editPanelId === 'number') + && editPanelId !== '' + ) { + return editPanelId.toString(); + } + return ''; + } + + return (
@@ -74,10 +139,10 @@ export const DataFrameQueryEditor = (props: Props) => { onChange={event => common.handleQueryChange({ ...common.query, filterNulls: event.currentTarget.checked }, true)} > - + common.handleQueryChange({ ...common.query, applyTimeFilters: event.currentTarget.checked }, true)} + value={common.query.fetchHighResolutionData} + onChange={event => handleFetchHighResolutionDataStateUpdate(event.currentTarget.checked)} > diff --git a/src/datasources/data-frame/types.ts b/src/datasources/data-frame/types.ts index abc62902b..39e1959e3 100644 --- a/src/datasources/data-frame/types.ts +++ b/src/datasources/data-frame/types.ts @@ -12,7 +12,7 @@ export interface DataFrameQuery extends DataQuery { columns?: string[]; decimationMethod?: string; filterNulls?: boolean; - applyTimeFilters?: boolean; + fetchHighResolutionData?: boolean; } export const defaultQuery: Omit = { @@ -21,7 +21,7 @@ export const defaultQuery: Omit = { columns: [], decimationMethod: 'LOSSY', filterNulls: false, - applyTimeFilters: false + fetchHighResolutionData: false }; export type ValidDataFrameQuery = DataFrameQuery & Required>; diff --git a/src/panels/plotly/PlotlyPanel.tsx b/src/panels/plotly/PlotlyPanel.tsx index c652a0d1d..4ca288ce6 100644 --- a/src/panels/plotly/PlotlyPanel.tsx +++ b/src/panels/plotly/PlotlyPanel.tsx @@ -12,7 +12,7 @@ import { } from '@grafana/data'; import { AxisLabels, PanelOptions } from './types'; import { useTheme2, ContextMenu, MenuItemsGroup, linkModelToContextMenuItems } from '@grafana/ui'; -import { getTemplateSrv, PanelDataErrorView } from '@grafana/runtime'; +import { getTemplateSrv, PanelDataErrorView, locationService } from '@grafana/runtime'; import { getFieldsByName, notEmpty, Plot, renderMenuItems, useTraceColors } from './utils'; import { AxisType, Legend, PlotData, PlotType, toImage, Icons, PlotlyHTMLElement } from 'plotly.js-basic-dist-min'; import { saveAs } from 'file-saver'; @@ -28,7 +28,7 @@ interface MenuState { interface Props extends PanelProps {} export const PlotlyPanel: React.FC = (props) => { - const { data, width, height, options } = props; + const { data, width, height, options, id, timeRange } = props; const [menu, setMenu] = useState({ x: 0, y: 0, show: false, items: [] }); const theme = useTheme2(); @@ -153,7 +153,25 @@ export const PlotlyPanel: React.FC = (props) => { props.onOptionsChange({...options, xAxis: { ...options.xAxis, min: from.valueOf(), max: to.valueOf() } }); } } else { - props.onOptionsChange({...options, xAxis: { ...options.xAxis, min: xAxisMin, max: xAxisMax } }); + const queryParams = locationService.getSearchObject(); + const fetchHighResolutionDataOnZoom = queryParams['fetchHighResolutionData']; + + if ( + fetchHighResolutionDataOnZoom !== undefined + && typeof fetchHighResolutionDataOnZoom === 'string' + && fetchHighResolutionDataOnZoom !== '' + && fetchHighResolutionDataOnZoom.split(',').includes(id.toString()) + ) { + locationService.partial({ + [`${options.xAxis.field}-min`]: Math.floor(xAxisMin), + [`${options.xAxis.field}-max`]: Math.ceil(xAxisMax) + }, true); + // (document.querySelector('[aria-label="Refresh dashboard"]') as HTMLButtonElement).click(); + props.onChangeTimeRange({ from: timeRange.from.valueOf() + 1, to: timeRange.to.valueOf() + 1 }); + props.onChangeTimeRange({ from: timeRange.from.valueOf(), to: timeRange.to.valueOf() }); + } else { + props.onOptionsChange({...options, xAxis: { ...options.xAxis, min: xAxisMin, max: xAxisMax } }); + } } };