import React, { PureComponent } from 'react'; import { Unsubscribable } from 'rxjs'; import { connect, MapStateToProps } from 'react-redux'; import { InspectHeader } from './InspectHeader'; import { InspectJSONTab } from './InspectJSONTab'; import { QueryInspector } from './QueryInspector'; import { DashboardModel, PanelModel } from 'app/features/dashboard/state'; import { JSONFormatter, Drawer, TabContent, CustomScrollbar } from '@grafana/ui'; import { getLocationSrv, getDataSourceSrv } from '@grafana/runtime'; import { DataFrame, DataSourceApi, SelectableValue, getDisplayProcessor, DataQueryError, PanelData, FieldType, formattedValueToString, QueryResultMetaStat, LoadingState, PanelPlugin, } from '@grafana/data'; import { config } from 'app/core/config'; import { getPanelInspectorStyles } from './styles'; import { StoreState } from 'app/types'; import { InspectDataTab } from './InspectDataTab'; interface OwnProps { dashboard: DashboardModel; panel: PanelModel; defaultTab: InspectTab; } export interface ConnectedProps { plugin?: PanelPlugin | null; } 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; } 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%', }; } componentDidMount() { const { plugin } = this.props; if (plugin) { this.init(); } } componentDidUpdate(prevProps: Props) { if (prevProps.plugin !== this.props.plugin) { 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; if (plugin && !plugin.meta.skipDataQuery) { this.querySubscription = panel .getQueryRunner() .getData() .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 }); }; renderMetadataInspector() { const { metaDS, data } = this.state; if (!metaDS || !metaDS.components?.MetadataInspector) { return
No Metadata Inspector
; } return ; } renderDataTab() { const { last, isLoading } = 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({ title: 'Total request time', value: requestTime, unit: 'ms' }); stats.push({ title: 'Data processing time', value: processingTime, unit: 'ms' }); stats.push({ title: 'Number of queries', value: request.targets.length }); stats.push({ title: '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; } return (
{name}
{stats.map((stat, index) => { return ( ); })}
{stat.title} {formatStat(stat)}
); } drawerHeader(tabs: Array<{ label: string; value: InspectTab }>, activeTab: InspectTab) { const { panel } = this.props; const { last } = this.state; return ( ); } getTabs() { const { dashboard, plugin } = this.props; const { last } = this.state; const error = last?.error; const tabs = []; if (plugin && !plugin.meta.skipDataQuery) { 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) { 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): string { const display = getDisplayProcessor({ field: { type: FieldType.number, config: stat, }, theme: config.theme, }); return formattedValueToString(display(stat.value)); } const mapStateToProps: MapStateToProps = (state, props) => { const panelState = state.dashboard.panels[props.panel.id]; if (!panelState) { return { plugin: null }; } return { plugin: panelState.plugin, }; }; export const PanelInspector = connect(mapStateToProps)(PanelInspectorUnconnected);