diff --git a/packages/grafana-runtime/src/services/dataSourceSrv.ts b/packages/grafana-runtime/src/services/dataSourceSrv.ts index 225ca76cd31..db4eb57b453 100644 --- a/packages/grafana-runtime/src/services/dataSourceSrv.ts +++ b/packages/grafana-runtime/src/services/dataSourceSrv.ts @@ -13,7 +13,7 @@ export interface DataSourceSrv { * @param name - name of the datasource plugin you want to use. * @param scopedVars - variables used to interpolate a templated passed as name. */ - get(name?: string, scopedVars?: ScopedVars): Promise; + get(name?: string | null, scopedVars?: ScopedVars): Promise; /** * Returns metadata based on UID. diff --git a/public/app/features/dashboard/components/Inspector/InspectContent.tsx b/public/app/features/dashboard/components/Inspector/InspectContent.tsx new file mode 100644 index 00000000000..b93a1ea2437 --- /dev/null +++ b/public/app/features/dashboard/components/Inspector/InspectContent.tsx @@ -0,0 +1,100 @@ +import React, { useState } from 'react'; +import { getPanelInspectorStyles } from './styles'; +import { CustomScrollbar, Drawer, TabContent } from '@grafana/ui'; +import { InspectSubtitle } from './InspectSubtitle'; +import { InspectDataTab } from './InspectDataTab'; +import { InspectMetadataTab } from './InspectMetadataTab'; +import { InspectJSONTab } from './InspectJSONTab'; +import { InspectErrorTab } from './InspectErrorTab'; +import { InspectStatsTab } from './InspectStatsTab'; +import { QueryInspector } from './QueryInspector'; +import { InspectTab } from './types'; +import { DashboardModel, PanelModel } from '../../state'; +import { DataSourceApi, PanelData, PanelPlugin } from '@grafana/data'; +import { GetDataOptions } from '../../state/PanelQueryRunner'; + +interface Props { + dashboard: DashboardModel; + panel: PanelModel; + plugin?: PanelPlugin | null; + defaultTab: InspectTab; + tabs: Array<{ label: string; value: InspectTab }>; + // The last raw response + data?: PanelData; + isDataLoading: boolean; + dataOptions: GetDataOptions; + // If the datasource supports custom metadata + metadataDatasource?: DataSourceApi; + onDataOptionsChange: (options: GetDataOptions) => void; + onClose: () => void; +} + +export const InspectContent: React.FC = ({ + panel, + plugin, + dashboard, + tabs, + data, + isDataLoading, + dataOptions, + metadataDatasource, + defaultTab, + onDataOptionsChange, + onClose, +}) => { + const [currentTab, setCurrentTab] = useState(defaultTab ?? InspectTab.Data); + + if (!plugin) { + return null; + } + + const styles = getPanelInspectorStyles(); + const error = data?.error; + + // Validate that the active tab is actually valid and allowed + let activeTab = currentTab; + if (!tabs.find(item => item.value === currentTab)) { + activeTab = InspectTab.JSON; + } + + return ( + setCurrentTab(item.value || InspectTab.Data)} + /> + } + width="50%" + onClose={onClose} + expandable + > + {activeTab === InspectTab.Data && ( + + )} + + + {data && activeTab === InspectTab.Meta && ( + + )} + + {activeTab === InspectTab.JSON && ( + + )} + {activeTab === InspectTab.Error && } + {data && activeTab === InspectTab.Stats && } + {data && activeTab === InspectTab.Query && } + + + + ); +}; diff --git a/public/app/features/dashboard/components/Inspector/InspectDataTab.tsx b/public/app/features/dashboard/components/Inspector/InspectDataTab.tsx index 6622dcaa2e2..c0e097e1cb6 100644 --- a/public/app/features/dashboard/components/Inspector/InspectDataTab.tsx +++ b/public/app/features/dashboard/components/Inspector/InspectDataTab.tsx @@ -14,7 +14,17 @@ import { DisplayProcessor, getDisplayProcessor, } from '@grafana/data'; -import { Button, Field, Icon, LegacyForms, Select, Table } from '@grafana/ui'; +import { + Button, + Container, + Field, + HorizontalGroup, + Icon, + LegacyForms, + Select, + Table, + VerticalGroup, +} from '@grafana/ui'; import { selectors } from '@grafana/e2e-selectors'; import AutoSizer from 'react-virtualized-auto-sizer'; @@ -23,15 +33,13 @@ import { config } from 'app/core/config'; import { saveAs } from 'file-saver'; import { css, cx } from 'emotion'; import { GetDataOptions } from '../../state/PanelQueryRunner'; -import { QueryOperationRow } from 'app/core/components/QueryOperationRow/QueryOperationRow'; -import { DashboardModel, PanelModel } from 'app/features/dashboard/state'; - +import { QueryOperationRow } from '../../../../core/components/QueryOperationRow/QueryOperationRow'; +import { PanelModel } from '../../state'; const { Switch } = LegacyForms; interface Props { - dashboard: DashboardModel; panel: PanelModel; - data: DataFrame[]; + data?: DataFrame[]; isLoading: boolean; options: GetDataOptions; onOptionsChange: (options: GetDataOptions) => void; @@ -187,7 +195,7 @@ export class InspectDataTab extends PureComponent { }; render() { - const { isLoading, data } = this.props; + const { isLoading, data, options, onOptionsChange } = this.props; const { dataFrameIndex, transformId, transformationOptions } = this.state; const styles = getPanelInspectorStyles(); @@ -212,48 +220,77 @@ export class InspectDataTab extends PureComponent { }; }); + const panelTransformations = this.props.panel.getTransformations(); + return (
-
-
-
- {data.length > 1 && ( - - + + + )} + {choices.length > 1 && ( + + + - - )} -
- {this.renderDataOptions()} -
- -
- -
-
+
+ + + + -
+ {({ width, height }) => { if (width === 0) { @@ -267,7 +304,7 @@ export class InspectDataTab extends PureComponent { ); }} -
+ ); } diff --git a/public/app/features/dashboard/components/Inspector/InspectErrorTab.tsx b/public/app/features/dashboard/components/Inspector/InspectErrorTab.tsx new file mode 100644 index 00000000000..dcb00122cf5 --- /dev/null +++ b/public/app/features/dashboard/components/Inspector/InspectErrorTab.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import { DataQueryError } from '@grafana/data'; +import { JSONFormatter } from '@grafana/ui'; + +interface InspectErrorTabProps { + error?: DataQueryError; +} + +export const InspectErrorTab: React.FC = ({ error }) => { + if (!error) { + return null; + } + if (error.data) { + return ( + <> +

{error.data.message}

+ + + ); + } + return
{error.message}
; +}; diff --git a/public/app/features/dashboard/components/Inspector/InspectMetadataTab.tsx b/public/app/features/dashboard/components/Inspector/InspectMetadataTab.tsx new file mode 100644 index 00000000000..a3ab5bda303 --- /dev/null +++ b/public/app/features/dashboard/components/Inspector/InspectMetadataTab.tsx @@ -0,0 +1,13 @@ +import React from 'react'; +import { DataSourceApi, PanelData } from '@grafana/data'; + +interface InspectMetadataTabProps { + data: PanelData; + metadataDatasource?: DataSourceApi; +} +export const InspectMetadataTab: React.FC = ({ data, metadataDatasource }) => { + if (!metadataDatasource || !metadataDatasource.components?.MetadataInspector) { + return
No Metadata Inspector
; + } + return ; +}; diff --git a/public/app/features/dashboard/components/Inspector/InspectStatsTab.tsx b/public/app/features/dashboard/components/Inspector/InspectStatsTab.tsx new file mode 100644 index 00000000000..c487181cf42 --- /dev/null +++ b/public/app/features/dashboard/components/Inspector/InspectStatsTab.tsx @@ -0,0 +1,45 @@ +import { PanelData, QueryResultMetaStat } from '@grafana/data'; +import { selectors } from '@grafana/e2e-selectors'; +import { InspectStatsTable } from './InspectStatsTable'; +import React from 'react'; +import { DashboardModel } from 'app/features/dashboard/state'; + +interface InspectStatsTabProps { + data: PanelData; + dashboard: DashboardModel; +} +export const InspectStatsTab: React.FC = ({ data, dashboard }) => { + if (!data.request) { + return null; + } + + let stats: QueryResultMetaStat[] = []; + + const requestTime = data.request.endTime ? data.request.endTime - data.request.startTime : -1; + const processingTime = data.timings?.dataProcessingTime || -1; + let dataRows = 0; + + for (const frame of data.series) { + dataRows += frame.length; + } + + stats.push({ displayName: 'Total request time', value: requestTime, unit: 'ms' }); + stats.push({ displayName: 'Data processing time', value: processingTime, unit: 'ms' }); + stats.push({ displayName: 'Number of queries', value: data.request.targets.length }); + stats.push({ displayName: 'Total number rows', value: dataRows }); + + let dataStats: QueryResultMetaStat[] = []; + + for (const series of data.series) { + if (series.meta && series.meta.stats) { + dataStats = dataStats.concat(series.meta.stats); + } + } + + return ( +
+ + +
+ ); +}; diff --git a/public/app/features/dashboard/components/Inspector/InspectStatsTable.tsx b/public/app/features/dashboard/components/Inspector/InspectStatsTable.tsx new file mode 100644 index 00000000000..9778f30ca4e --- /dev/null +++ b/public/app/features/dashboard/components/Inspector/InspectStatsTable.tsx @@ -0,0 +1,68 @@ +import React from 'react'; +import { + FieldType, + formattedValueToString, + getDisplayProcessor, + GrafanaTheme, + QueryResultMetaStat, + TimeZone, +} from '@grafana/data'; +import { DashboardModel } from 'app/features/dashboard/state'; +import { config } from 'app/core/config'; +import { stylesFactory, useTheme } from '@grafana/ui'; +import { css } from 'emotion'; + +interface InspectStatsTableProps { + dashboard: DashboardModel; + name: string; + stats: QueryResultMetaStat[]; +} +export const InspectStatsTable: React.FC = ({ dashboard, name, stats }) => { + const theme = useTheme(); + const styles = getStyles(theme); + + if (!stats || !stats.length) { + return null; + } + + return ( +
+
{name}
+ + + {stats.map((stat, index) => { + return ( + + + + + ); + })} + +
{stat.displayName}{formatStat(stat, dashboard.getTimezone())}
+
+ ); +}; + +function formatStat(stat: QueryResultMetaStat, timeZone?: TimeZone): string { + const display = getDisplayProcessor({ + field: { + type: FieldType.number, + config: stat, + }, + theme: config.theme, + timeZone, + }); + return formattedValueToString(display(stat.value)); +} + +const getStyles = stylesFactory((theme: GrafanaTheme) => { + return { + wrapper: css` + padding-bottom: ${theme.spacing.md}; + `, + cell: css` + text-align: right; + `, + }; +}); diff --git a/public/app/features/dashboard/components/Inspector/InspectSubtitle.tsx b/public/app/features/dashboard/components/Inspector/InspectSubtitle.tsx index 4c7822e1ab7..98b555ac14b 100644 --- a/public/app/features/dashboard/components/Inspector/InspectSubtitle.tsx +++ b/public/app/features/dashboard/components/Inspector/InspectSubtitle.tsx @@ -2,22 +2,22 @@ import React, { FC } from 'react'; import { css } from 'emotion'; import { stylesFactory, Tab, TabsBar, useTheme } from '@grafana/ui'; import { GrafanaTheme, SelectableValue, PanelData, getValueFormat, formattedValueToString } from '@grafana/data'; -import { InspectTab } from './PanelInspector'; +import { InspectTab } from './types'; interface Props { tab: InspectTab; tabs: Array<{ label: string; value: InspectTab }>; - panelData: PanelData; + data?: PanelData; onSelectTab: (tab: SelectableValue) => void; } -export const InspectSubtitle: FC = ({ tab, tabs, onSelectTab, panelData }) => { +export const InspectSubtitle: FC = ({ tab, tabs, onSelectTab, data }) => { const theme = useTheme(); const styles = getStyles(theme); return ( <> -
{formatStats(panelData)}
+ {data &&
{formatStats(data)}
} {tabs.map((t, index) => { return ( @@ -43,8 +43,8 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => { }; }); -function formatStats(panelData: PanelData) { - const { request } = panelData; +function formatStats(data: PanelData) { + const { request } = data; if (!request) { return ''; } diff --git a/public/app/features/dashboard/components/Inspector/PanelInspector.tsx b/public/app/features/dashboard/components/Inspector/PanelInspector.tsx index 974eb0b9f49..c3f24862bce 100644 --- a/public/app/features/dashboard/components/Inspector/PanelInspector.tsx +++ b/public/app/features/dashboard/components/Inspector/PanelInspector.tsx @@ -1,34 +1,16 @@ -import React, { PureComponent } from 'react'; -import { Unsubscribable } from 'rxjs'; -import { connect, MapStateToProps } from 'react-redux'; -import { InspectSubtitle } from './InspectSubtitle'; -import { InspectJSONTab } from './InspectJSONTab'; -import { QueryInspector } from './QueryInspector'; +import React, { useCallback, useState } from 'react'; +import { connect, MapStateToProps, useDispatch } from 'react-redux'; import { DashboardModel, PanelModel } from 'app/features/dashboard/state'; -import { CustomScrollbar, Drawer, JSONFormatter, TabContent } from '@grafana/ui'; -import { selectors } from '@grafana/e2e-selectors'; -import { getDataSourceSrv, getLocationSrv } from '@grafana/runtime'; -import { - DataFrame, - DataQueryError, - DataSourceApi, - FieldType, - formattedValueToString, - getDisplayProcessor, - LoadingState, - PanelData, - PanelPlugin, - QueryResultMetaStat, - SelectableValue, - TimeZone, -} from '@grafana/data'; -import { config } from 'app/core/config'; -import { getPanelInspectorStyles } from './styles'; + +import { PanelPlugin } from '@grafana/data'; import { StoreState } from 'app/types'; -import { InspectDataTab } from './InspectDataTab'; -import { supportsDataQuery } from '../PanelEditor/utils'; import { GetDataOptions } from '../../state/PanelQueryRunner'; +import { usePanelLatestData } from '../PanelEditor/usePanelLatestData'; +import { InspectContent } from './InspectContent'; +import { useDatasourceMetadata, useInspectTabs } from './hooks'; +import { InspectTab } from './types'; +import { updateLocation } from 'app/core/actions'; interface OwnProps { dashboard: DashboardModel; @@ -42,337 +24,44 @@ export interface ConnectedProps { export type Props = OwnProps & ConnectedProps; -export enum InspectTab { - Data = 'data', - Meta = 'meta', // When result metadata exists - Error = 'error', - Stats = 'stats', - JSON = 'json', - Query = 'query', -} - -interface State { - isLoading: boolean; - // The last raw response - last: PanelData; - // Data from the last response - data: DataFrame[]; - // The Selected Tab - currentTab: InspectTab; - // If the datasource supports custom metadata - metaDS?: DataSourceApi; - // drawer width - drawerWidth: string; - withTransforms: boolean; - withFieldConfig: boolean; -} - -export class PanelInspectorUnconnected extends PureComponent { - querySubscription?: Unsubscribable; - - constructor(props: Props) { - super(props); - - this.state = { - isLoading: true, - last: {} as PanelData, - data: [], - currentTab: props.defaultTab ?? InspectTab.Data, - drawerWidth: '50%', - withTransforms: true, - withFieldConfig: false, - }; - } - - componentDidMount() { - const { plugin } = this.props; - - if (plugin) { - this.init(); - } - } - - componentDidUpdate(prevProps: Props, prevState: State) { - if ( - prevProps.plugin !== this.props.plugin || - this.state.withTransforms !== prevState.withTransforms || - this.state.withFieldConfig !== prevState.withFieldConfig - ) { - this.init(); - } - } - - /** - * This init process where we do not have a plugin to start with is to handle full page reloads with inspect url parameter - * When this inspect drawer loads the plugin is not yet loaded. - */ - init() { - const { plugin, panel } = this.props; - const { withTransforms, withFieldConfig } = this.state; - - if (plugin && !plugin.meta.skipDataQuery) { - if (this.querySubscription) { - this.querySubscription.unsubscribe(); - } - this.querySubscription = panel - .getQueryRunner() - .getData({ withTransforms, withFieldConfig }) - .subscribe({ - next: data => this.onUpdateData(data), - }); - } - } - - componentWillUnmount() { - if (this.querySubscription) { - this.querySubscription.unsubscribe(); - } - } - - async onUpdateData(lastResult: PanelData) { - let metaDS: DataSourceApi; - const data = lastResult.series; - const error = lastResult.error; - - const targets = lastResult.request?.targets || []; - - // Find the first DataSource wanting to show custom metadata - if (data && targets.length) { - for (const frame of data) { - if (frame.meta && frame.meta.custom) { - // get data source from first query - const dataSource = await getDataSourceSrv().get(targets[0].datasource); - - if (dataSource && dataSource.components?.MetadataInspector) { - metaDS = dataSource; - break; - } - } - } - } - - // Set last result, but no metadata inspector - this.setState(prevState => ({ - isLoading: lastResult.state === LoadingState.Loading, - last: lastResult, - data, - metaDS, - currentTab: error ? InspectTab.Error : prevState.currentTab, - })); - } - - onClose = () => { - getLocationSrv().update({ - query: { inspect: null, inspectTab: null }, - partial: true, - }); - }; - - onToggleExpand = () => { - this.setState(prevState => ({ - drawerWidth: prevState.drawerWidth === '100%' ? '40%' : '100%', - })); - }; - - onSelectTab = (item: SelectableValue) => { - this.setState({ currentTab: item.value || InspectTab.Data }); - }; - onDataTabOptionsChange = (options: GetDataOptions) => { - this.setState({ withTransforms: !!options.withTransforms, withFieldConfig: !!options.withFieldConfig }); - }; - - renderMetadataInspector() { - const { metaDS, data } = this.state; - if (!metaDS || !metaDS.components?.MetadataInspector) { - return
No Metadata Inspector
; - } - return ; - } - - renderDataTab() { - const { last, isLoading, withFieldConfig, withTransforms } = this.state; - return ( - - ); - } - - renderErrorTab(error?: DataQueryError) { - if (!error) { - return null; - } - if (error.data) { - return ( - <> -

{error.data.message}

- - - ); - } - return
{error.message}
; - } - - renderStatsTab() { - const { last } = this.state; - const { request } = last; - - if (!request) { - return null; - } - - let stats: QueryResultMetaStat[] = []; - - const requestTime = request.endTime ? request.endTime - request.startTime : -1; - const processingTime = last.timings?.dataProcessingTime || -1; - let dataRows = 0; - - for (const frame of last.series) { - dataRows += frame.length; - } - - stats.push({ displayName: 'Total request time', value: requestTime, unit: 'ms' }); - stats.push({ displayName: 'Data processing time', value: processingTime, unit: 'ms' }); - stats.push({ displayName: 'Number of queries', value: request.targets.length }); - stats.push({ displayName: 'Total number rows', value: dataRows }); - - let dataStats: QueryResultMetaStat[] = []; - - for (const series of last.series) { - if (series.meta && series.meta.stats) { - dataStats = dataStats.concat(series.meta.stats); - } - } - - return ( -
- {this.renderStatsTable('Stats', stats)} - {this.renderStatsTable('Data source stats', dataStats)} -
- ); - } - - renderStatsTable(name: string, stats: QueryResultMetaStat[]) { - if (!stats || !stats.length) { - return null; - } - - const { dashboard } = this.props; - - return ( -
- - - {stats.map((stat, index) => { - return ( - - - - - ); - })} - -
{stat.displayName}{formatStat(stat, dashboard.getTimezone())}
-
+const PanelInspectorUnconnected: React.FC = ({ panel, dashboard, defaultTab, plugin }) => { + const dispatch = useDispatch(); + const [dataOptions, setDataOptions] = useState({ + withTransforms: false, + withFieldConfig: false, + }); + const { data, isLoading, error } = usePanelLatestData(panel, dataOptions); + const metaDs = useDatasourceMetadata(data); + const tabs = useInspectTabs(plugin, dashboard, error, metaDs); + const onClose = useCallback(() => { + dispatch( + updateLocation({ + query: { inspect: null, inspectTab: null }, + partial: true, + }) ); - } - - drawerSubtitle(tabs: Array<{ label: string; value: InspectTab }>, activeTab: InspectTab) { - const { last } = this.state; + }, [updateLocation]); - return ; + if (!plugin) { + return null; } - getTabs() { - const { dashboard, plugin } = this.props; - const { last } = this.state; - const error = last?.error; - const tabs = []; - - if (supportsDataQuery(plugin)) { - tabs.push({ label: 'Data', value: InspectTab.Data }); - tabs.push({ label: 'Stats', value: InspectTab.Stats }); - } - - if (this.state.metaDS) { - tabs.push({ label: 'Meta Data', value: InspectTab.Meta }); - } - - tabs.push({ label: 'JSON', value: InspectTab.JSON }); - - if (error && error.message) { - tabs.push({ label: 'Error', value: InspectTab.Error }); - } - - if (dashboard.meta.canEdit && supportsDataQuery(plugin)) { - tabs.push({ label: 'Query', value: InspectTab.Query }); - } - return tabs; - } - - render() { - const { panel, dashboard, plugin } = this.props; - const { currentTab } = this.state; - - if (!plugin) { - return null; - } - - const { last, drawerWidth } = this.state; - const styles = getPanelInspectorStyles(); - const error = last?.error; - const tabs = this.getTabs(); - - // Validate that the active tab is actually valid and allowed - let activeTab = currentTab; - if (!tabs.find(item => item.value === currentTab)) { - activeTab = InspectTab.JSON; - } - - return ( - - {activeTab === InspectTab.Data && this.renderDataTab()} - - - {activeTab === InspectTab.Meta && this.renderMetadataInspector()} - {activeTab === InspectTab.JSON && ( - - )} - {activeTab === InspectTab.Error && this.renderErrorTab(error)} - {activeTab === InspectTab.Stats && this.renderStatsTab()} - {activeTab === InspectTab.Query && } - - - - ); - } -} - -function formatStat(stat: QueryResultMetaStat, timeZone?: TimeZone): string { - const display = getDisplayProcessor({ - field: { - type: FieldType.number, - config: stat, - }, - theme: config.theme, - timeZone, - }); - return formattedValueToString(display(stat.value)); -} + return ( + + ); +}; const mapStateToProps: MapStateToProps = (state, props) => { const panelState = state.dashboard.panels[props.panel.id]; diff --git a/public/app/features/dashboard/components/Inspector/hooks.ts b/public/app/features/dashboard/components/Inspector/hooks.ts new file mode 100644 index 00000000000..72d06588fa9 --- /dev/null +++ b/public/app/features/dashboard/components/Inspector/hooks.ts @@ -0,0 +1,64 @@ +import { DataQueryError, DataSourceApi, PanelData, PanelPlugin } from '@grafana/data'; +import useAsync from 'react-use/lib/useAsync'; +import { getDataSourceSrv } from '@grafana/runtime'; +import { DashboardModel } from 'app/features/dashboard/state'; +import { useMemo } from 'react'; +import { supportsDataQuery } from '../PanelEditor/utils'; +import { InspectTab } from './types'; + +/** + * Given PanelData return first data source supporting metadata inspector + */ +export const useDatasourceMetadata = (data?: PanelData) => { + const state = useAsync(async () => { + const targets = data?.request?.targets || []; + + if (data && data.series && targets.length) { + for (const frame of data.series) { + if (frame.meta && frame.meta.custom) { + // get data source from first query + const dataSource = await getDataSourceSrv().get(targets[0].datasource); + if (dataSource && dataSource.components?.MetadataInspector) { + return dataSource; + } + } + } + } + + return undefined; + }, [data]); + return state.value; +}; + +/** + * Configures tabs for PanelInspector + */ +export const useInspectTabs = ( + plugin: PanelPlugin, + dashboard: DashboardModel, + error?: DataQueryError, + metaDs?: DataSourceApi +) => { + return useMemo(() => { + const tabs = []; + if (supportsDataQuery(plugin)) { + tabs.push({ label: 'Data', value: InspectTab.Data }); + tabs.push({ label: 'Stats', value: InspectTab.Stats }); + } + + if (metaDs) { + tabs.push({ label: 'Meta Data', value: InspectTab.Meta }); + } + + tabs.push({ label: 'JSON', value: InspectTab.JSON }); + + if (error && error.message) { + tabs.push({ label: 'Error', value: InspectTab.Error }); + } + + if (dashboard.meta.canEdit && supportsDataQuery(plugin)) { + tabs.push({ label: 'Query', value: InspectTab.Query }); + } + return tabs; + }, [plugin, metaDs, dashboard, error]); +}; diff --git a/public/app/features/dashboard/components/Inspector/types.ts b/public/app/features/dashboard/components/Inspector/types.ts new file mode 100644 index 00000000000..757278ce5ef --- /dev/null +++ b/public/app/features/dashboard/components/Inspector/types.ts @@ -0,0 +1,8 @@ +export enum InspectTab { + Data = 'data', + Meta = 'meta', // When result metadata exists + Error = 'error', + Stats = 'stats', + JSON = 'json', + Query = 'query', +} diff --git a/public/app/features/dashboard/components/PanelEditor/OptionsPaneContent.tsx b/public/app/features/dashboard/components/PanelEditor/OptionsPaneContent.tsx index c555c3f9292..a4a9f56c99f 100644 --- a/public/app/features/dashboard/components/PanelEditor/OptionsPaneContent.tsx +++ b/public/app/features/dashboard/components/PanelEditor/OptionsPaneContent.tsx @@ -35,7 +35,7 @@ export const OptionsPaneContent: React.FC = ({ const styles = getStyles(theme); const [activeTab, setActiveTab] = useState('options'); const [isSearching, setSearchMode] = useState(false); - const [currentData, hasSeries] = usePanelLatestData(panel, { withTransforms: true, withFieldConfig: false }); + const { data, hasSeries } = usePanelLatestData(panel, { withTransforms: true, withFieldConfig: false }); const renderFieldOptions = useCallback( (plugin: PanelPlugin) => { @@ -51,11 +51,11 @@ export const OptionsPaneContent: React.FC = ({ plugin={plugin} onChange={onFieldConfigsChange} /* hasSeries makes sure current data is there */ - data={currentData!.series} + data={data!.series} /> ); }, - [currentData, plugin, panel, onFieldConfigsChange] + [data, plugin, panel, onFieldConfigsChange] ); const renderFieldOverrideOptions = useCallback( @@ -72,11 +72,11 @@ export const OptionsPaneContent: React.FC = ({ plugin={plugin} onChange={onFieldConfigsChange} /* hasSeries makes sure current data is there */ - data={currentData!.series} + data={data!.series} /> ); }, - [currentData, plugin, panel, onFieldConfigsChange] + [data, plugin, panel, onFieldConfigsChange] ); // When the panel has no query only show the main tab @@ -106,7 +106,7 @@ export const OptionsPaneContent: React.FC = ({ panel={panel} plugin={plugin} dashboard={dashboard} - data={currentData} + data={data} onPanelConfigChange={onPanelConfigChange} onPanelOptionsChanged={onPanelOptionsChanged} /> diff --git a/public/app/features/dashboard/components/PanelEditor/usePanelLatestData.ts b/public/app/features/dashboard/components/PanelEditor/usePanelLatestData.ts index 4e1def053dd..a4aeeecbb1c 100644 --- a/public/app/features/dashboard/components/PanelEditor/usePanelLatestData.ts +++ b/public/app/features/dashboard/components/PanelEditor/usePanelLatestData.ts @@ -1,12 +1,22 @@ -import { PanelData } from '@grafana/data'; +import { DataQueryError, LoadingState, PanelData } from '@grafana/data'; import { useEffect, useRef, useState } from 'react'; import { PanelModel } from '../../state'; import { Unsubscribable } from 'rxjs'; import { GetDataOptions } from '../../state/PanelQueryRunner'; -export const usePanelLatestData = (panel: PanelModel, options: GetDataOptions): [PanelData | null, boolean] => { +interface UsePanelLatestData { + data?: PanelData; + error?: DataQueryError; + isLoading: boolean; + hasSeries: boolean; +} + +/** + * Subscribes and returns latest panel data from PanelQueryRunner + */ +export const usePanelLatestData = (panel: PanelModel, options: GetDataOptions): UsePanelLatestData => { const querySubscription = useRef(null); - const [latestData, setLatestData] = useState(null); + const [latestData, setLatestData] = useState(); useEffect(() => { querySubscription.current = panel @@ -18,15 +28,19 @@ export const usePanelLatestData = (panel: PanelModel, options: GetDataOptions): return () => { if (querySubscription.current) { - console.log('unsubscribing'); querySubscription.current.unsubscribe(); } }; - }, [panel]); + /** + * Adding separate options to dependencies array to avoid additional hook for comparing previous options with current. + * Otherwise, passing different references to the same object may cause troubles. + */ + }, [panel, options.withFieldConfig, options.withTransforms]); - return [ - latestData, - // TODO: make this more clever, use PanelData.state - !!(latestData && latestData.series), - ]; + return { + data: latestData, + error: latestData && latestData.error, + isLoading: latestData ? latestData.state === LoadingState.Loading : true, + hasSeries: latestData ? !!latestData.series : false, + }; }; diff --git a/public/app/features/dashboard/containers/DashboardPage.tsx b/public/app/features/dashboard/containers/DashboardPage.tsx index 64e13a839c8..1dcd3e9b805 100644 --- a/public/app/features/dashboard/containers/DashboardPage.tsx +++ b/public/app/features/dashboard/containers/DashboardPage.tsx @@ -27,7 +27,8 @@ import { } from 'app/types'; import { DashboardModel, PanelModel } from 'app/features/dashboard/state'; -import { InspectTab, PanelInspector } from '../components/Inspector/PanelInspector'; +import { InspectTab } from '../components/Inspector/types'; +import { PanelInspector } from '../components/Inspector/PanelInspector'; import { SubMenu } from '../components/SubMenu/SubMenu'; import { cleanUpDashboardAndVariables } from '../state/actions'; import { cancelVariables } from '../../variables/state/actions'; diff --git a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderCorner.tsx b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderCorner.tsx index 30a74b13b77..c4086e8fbb1 100644 --- a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderCorner.tsx +++ b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderCorner.tsx @@ -7,7 +7,7 @@ import { getLocationSrv } from '@grafana/runtime'; import { PanelModel } from 'app/features/dashboard/state/PanelModel'; import templateSrv from 'app/features/templating/template_srv'; import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv'; -import { InspectTab } from '../../components/Inspector/PanelInspector'; +import { InspectTab } from '../../components/Inspector/types'; enum InfoMode { Error = 'Error', diff --git a/public/app/features/dashboard/state/PanelQueryRunner.test.ts b/public/app/features/dashboard/state/PanelQueryRunner.test.ts index 3ab0f3f979a..b89782b8332 100644 --- a/public/app/features/dashboard/state/PanelQueryRunner.test.ts +++ b/public/app/features/dashboard/state/PanelQueryRunner.test.ts @@ -298,4 +298,48 @@ describe('PanelQueryRunner', () => { getTransformations: () => [({} as unknown) as DataTransformerConfig], } ); + + describeQueryRunnerScenario( + 'getData', + ctx => { + it('should not apply transformations when transform option is false', async () => { + const spy = jest.spyOn(grafanaData, 'transformDataFrame'); + spy.mockClear(); + ctx.runner.getData({ withTransforms: false, withFieldConfig: true }).subscribe({ + next: (data: PanelData) => { + return data; + }, + }); + + expect(spy).not.toBeCalled(); + }); + + it('should not apply field config when applyFieldConfig option is false', async () => { + const spy = jest.spyOn(grafanaData, 'applyFieldOverrides'); + spy.mockClear(); + ctx.runner.getData({ withFieldConfig: false, withTransforms: true }).subscribe({ + next: (data: PanelData) => { + return data; + }, + }); + + expect(spy).not.toBeCalled(); + }); + }, + { + getFieldOverrideOptions: () => ({ + fieldConfig: { + defaults: { + unit: 'm/s', + }, + // @ts-ignore + overrides: [], + }, + replaceVariables: v => v, + theme: {} as GrafanaTheme, + }), + // @ts-ignore + getTransformations: () => [{}], + } + ); });