2019-11-04 13:53:44 -06:00
|
|
|
import React, { PureComponent } from 'react';
|
2020-04-15 09:51:51 -05:00
|
|
|
import { Unsubscribable } from 'rxjs';
|
|
|
|
import { connect, MapStateToProps } from 'react-redux';
|
2020-04-20 08:27:33 -05:00
|
|
|
import { InspectSubtitle } from './InspectSubtitle';
|
2020-04-15 09:51:51 -05:00
|
|
|
import { InspectJSONTab } from './InspectJSONTab';
|
|
|
|
import { QueryInspector } from './QueryInspector';
|
2020-02-14 07:14:38 -06:00
|
|
|
|
2019-11-04 13:53:44 -06:00
|
|
|
import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
|
2020-04-24 01:48:04 -05:00
|
|
|
import { CustomScrollbar, Drawer, JSONFormatter, TabContent } from '@grafana/ui';
|
|
|
|
import { getDataSourceSrv, getLocationSrv } from '@grafana/runtime';
|
2020-01-29 08:41:25 -06:00
|
|
|
import {
|
|
|
|
DataFrame,
|
|
|
|
DataQueryError,
|
2020-04-24 01:48:04 -05:00
|
|
|
DataSourceApi,
|
2020-04-15 09:51:51 -05:00
|
|
|
FieldType,
|
2020-03-18 07:00:14 -05:00
|
|
|
formattedValueToString,
|
2020-04-24 01:48:04 -05:00
|
|
|
getDisplayProcessor,
|
2020-04-15 09:51:51 -05:00
|
|
|
LoadingState,
|
2020-04-24 01:48:04 -05:00
|
|
|
PanelData,
|
2020-04-15 09:51:51 -05:00
|
|
|
PanelPlugin,
|
2020-04-24 01:48:04 -05:00
|
|
|
QueryResultMetaStat,
|
|
|
|
SelectableValue,
|
2020-01-29 08:41:25 -06:00
|
|
|
} from '@grafana/data';
|
2020-01-10 00:59:23 -06:00
|
|
|
import { config } from 'app/core/config';
|
2020-04-15 09:51:51 -05:00
|
|
|
import { getPanelInspectorStyles } from './styles';
|
|
|
|
import { StoreState } from 'app/types';
|
|
|
|
import { InspectDataTab } from './InspectDataTab';
|
2020-04-24 01:48:04 -05:00
|
|
|
import { e2e } from '@grafana/e2e';
|
2019-11-04 13:53:44 -06:00
|
|
|
|
2020-04-15 09:51:51 -05:00
|
|
|
interface OwnProps {
|
2019-11-04 13:53:44 -06:00
|
|
|
dashboard: DashboardModel;
|
|
|
|
panel: PanelModel;
|
2020-04-15 09:51:51 -05:00
|
|
|
defaultTab: InspectTab;
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface ConnectedProps {
|
|
|
|
plugin?: PanelPlugin | null;
|
2019-11-04 13:53:44 -06:00
|
|
|
}
|
|
|
|
|
2020-04-15 09:51:51 -05:00
|
|
|
export type Props = OwnProps & ConnectedProps;
|
|
|
|
|
2020-01-29 08:41:25 -06:00
|
|
|
export enum InspectTab {
|
2020-01-10 00:59:23 -06:00
|
|
|
Data = 'data',
|
|
|
|
Meta = 'meta', // When result metadata exists
|
2020-01-29 08:41:25 -06:00
|
|
|
Error = 'error',
|
2020-03-11 07:19:06 -05:00
|
|
|
Stats = 'stats',
|
2020-04-15 09:51:51 -05:00
|
|
|
JSON = 'json',
|
|
|
|
Query = 'query',
|
2020-01-10 00:59:23 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
interface State {
|
2020-04-15 09:51:51 -05:00
|
|
|
isLoading: boolean;
|
2020-01-10 00:59:23 -06:00
|
|
|
// The last raw response
|
2020-02-14 07:14:38 -06:00
|
|
|
last: PanelData;
|
2020-03-11 07:19:06 -05:00
|
|
|
// Data from the last response
|
2020-01-10 00:59:23 -06:00
|
|
|
data: DataFrame[];
|
|
|
|
// The Selected Tab
|
2020-04-15 09:51:51 -05:00
|
|
|
currentTab: InspectTab;
|
2020-01-10 00:59:23 -06:00
|
|
|
// If the datasource supports custom metadata
|
|
|
|
metaDS?: DataSourceApi;
|
2020-04-15 09:51:51 -05:00
|
|
|
// drawer width
|
2020-02-14 07:14:38 -06:00
|
|
|
drawerWidth: string;
|
2020-01-10 00:59:23 -06:00
|
|
|
}
|
|
|
|
|
2020-04-15 09:51:51 -05:00
|
|
|
export class PanelInspectorUnconnected extends PureComponent<Props, State> {
|
|
|
|
querySubscription?: Unsubscribable;
|
|
|
|
|
2020-01-10 00:59:23 -06:00
|
|
|
constructor(props: Props) {
|
|
|
|
super(props);
|
2020-04-15 09:51:51 -05:00
|
|
|
|
2020-01-10 00:59:23 -06:00
|
|
|
this.state = {
|
2020-04-15 09:51:51 -05:00
|
|
|
isLoading: true,
|
2020-02-14 07:14:38 -06:00
|
|
|
last: {} as PanelData,
|
2020-01-10 00:59:23 -06:00
|
|
|
data: [],
|
2020-04-15 09:51:51 -05:00
|
|
|
currentTab: props.defaultTab ?? InspectTab.Data,
|
2020-03-11 07:19:06 -05:00
|
|
|
drawerWidth: '50%',
|
2020-01-10 00:59:23 -06:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2020-04-15 09:51:51 -05:00
|
|
|
componentDidMount() {
|
|
|
|
const { plugin } = this.props;
|
2020-02-19 07:39:44 -06:00
|
|
|
|
2020-04-15 09:51:51 -05:00
|
|
|
if (plugin) {
|
|
|
|
this.init();
|
2020-01-10 00:59:23 -06:00
|
|
|
}
|
2020-04-15 09:51:51 -05:00
|
|
|
}
|
2020-01-10 00:59:23 -06:00
|
|
|
|
2020-04-15 09:51:51 -05:00
|
|
|
componentDidUpdate(prevProps: Props) {
|
|
|
|
if (prevProps.plugin !== this.props.plugin) {
|
|
|
|
this.init();
|
|
|
|
}
|
|
|
|
}
|
2020-02-19 07:39:44 -06:00
|
|
|
|
2020-04-15 09:51:51 -05:00
|
|
|
/**
|
|
|
|
* 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),
|
|
|
|
});
|
2020-01-10 00:59:23 -06:00
|
|
|
}
|
2020-04-15 09:51:51 -05:00
|
|
|
}
|
2020-01-10 00:59:23 -06:00
|
|
|
|
2020-04-15 09:51:51 -05:00
|
|
|
componentWillUnmount() {
|
|
|
|
if (this.querySubscription) {
|
|
|
|
this.querySubscription.unsubscribe();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async onUpdateData(lastResult: PanelData) {
|
2020-01-10 00:59:23 -06:00
|
|
|
let metaDS: DataSourceApi;
|
2020-02-14 07:14:38 -06:00
|
|
|
const data = lastResult.series;
|
|
|
|
const error = lastResult.error;
|
|
|
|
|
2020-02-16 07:12:40 -06:00
|
|
|
const targets = lastResult.request?.targets || [];
|
2020-01-29 08:41:25 -06:00
|
|
|
|
2020-02-16 07:12:40 -06:00
|
|
|
// Find the first DataSource wanting to show custom metadata
|
|
|
|
if (data && targets.length) {
|
2020-01-10 00:59:23 -06:00
|
|
|
for (const frame of data) {
|
2020-03-18 07:00:14 -05:00
|
|
|
if (frame.meta && frame.meta.custom) {
|
|
|
|
// get data source from first query
|
|
|
|
const dataSource = await getDataSourceSrv().get(targets[0].datasource);
|
2020-02-19 07:39:44 -06:00
|
|
|
|
2020-01-29 08:41:25 -06:00
|
|
|
if (dataSource && dataSource.components?.MetadataInspector) {
|
|
|
|
metaDS = dataSource;
|
2020-01-10 00:59:23 -06:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Set last result, but no metadata inspector
|
2020-01-29 08:41:25 -06:00
|
|
|
this.setState(prevState => ({
|
2020-04-15 09:51:51 -05:00
|
|
|
isLoading: lastResult.state === LoadingState.Loading,
|
2020-01-10 00:59:23 -06:00
|
|
|
last: lastResult,
|
|
|
|
data,
|
|
|
|
metaDS,
|
2020-04-15 09:51:51 -05:00
|
|
|
currentTab: error ? InspectTab.Error : prevState.currentTab,
|
2020-01-29 08:41:25 -06:00
|
|
|
}));
|
2020-01-10 00:59:23 -06:00
|
|
|
}
|
|
|
|
|
2020-04-15 09:51:51 -05:00
|
|
|
onClose = () => {
|
2019-11-04 13:53:44 -06:00
|
|
|
getLocationSrv().update({
|
2020-04-15 09:51:51 -05:00
|
|
|
query: { inspect: null, inspectTab: null },
|
2019-11-04 13:53:44 -06:00
|
|
|
partial: true,
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2020-02-14 07:14:38 -06:00
|
|
|
onToggleExpand = () => {
|
|
|
|
this.setState(prevState => ({
|
|
|
|
drawerWidth: prevState.drawerWidth === '100%' ? '40%' : '100%',
|
|
|
|
}));
|
|
|
|
};
|
|
|
|
|
2020-01-10 00:59:23 -06:00
|
|
|
onSelectTab = (item: SelectableValue<InspectTab>) => {
|
2020-04-15 09:51:51 -05:00
|
|
|
this.setState({ currentTab: item.value || InspectTab.Data });
|
2020-01-10 00:59:23 -06:00
|
|
|
};
|
|
|
|
|
|
|
|
renderMetadataInspector() {
|
|
|
|
const { metaDS, data } = this.state;
|
|
|
|
if (!metaDS || !metaDS.components?.MetadataInspector) {
|
|
|
|
return <div>No Metadata Inspector</div>;
|
|
|
|
}
|
2020-03-11 07:19:06 -05:00
|
|
|
return <metaDS.components.MetadataInspector datasource={metaDS} data={data} />;
|
2020-01-10 00:59:23 -06:00
|
|
|
}
|
|
|
|
|
2020-01-29 08:41:25 -06:00
|
|
|
renderDataTab() {
|
2020-04-17 07:03:21 -05:00
|
|
|
const { last, isLoading } = this.state;
|
|
|
|
return <InspectDataTab data={last.series} isLoading={isLoading} />;
|
2020-01-10 00:59:23 -06:00
|
|
|
}
|
|
|
|
|
2020-01-29 08:41:25 -06:00
|
|
|
renderErrorTab(error?: DataQueryError) {
|
|
|
|
if (!error) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
if (error.data) {
|
|
|
|
return (
|
2020-03-11 07:19:06 -05:00
|
|
|
<>
|
2020-01-29 08:41:25 -06:00
|
|
|
<h3>{error.data.message}</h3>
|
2020-03-11 07:19:06 -05:00
|
|
|
<JSONFormatter json={error} open={2} />
|
|
|
|
</>
|
2020-01-29 08:41:25 -06:00
|
|
|
);
|
|
|
|
}
|
|
|
|
return <div>{error.message}</div>;
|
|
|
|
}
|
|
|
|
|
2020-03-11 07:19:06 -05:00
|
|
|
renderStatsTab() {
|
2020-03-18 07:00:14 -05:00
|
|
|
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 (
|
2020-04-24 01:48:04 -05:00
|
|
|
<div aria-label={e2e.components.PanelInspector.Stats.selectors.content}>
|
2020-03-18 07:00:14 -05:00
|
|
|
{this.renderStatsTable('Stats', stats)}
|
2020-04-15 09:51:51 -05:00
|
|
|
{this.renderStatsTable('Data source stats', dataStats)}
|
2020-04-24 01:48:04 -05:00
|
|
|
</div>
|
2020-03-18 07:00:14 -05:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
renderStatsTable(name: string, stats: QueryResultMetaStat[]) {
|
2020-04-15 09:51:51 -05:00
|
|
|
if (!stats || !stats.length) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2020-01-29 08:41:25 -06:00
|
|
|
return (
|
2020-03-18 07:00:14 -05:00
|
|
|
<div style={{ paddingBottom: '16px' }}>
|
|
|
|
<div className="section-heading">{name}</div>
|
|
|
|
<table className="filter-table width-30">
|
|
|
|
<tbody>
|
2020-04-02 09:12:53 -05:00
|
|
|
{stats.map((stat, index) => {
|
2020-03-18 07:00:14 -05:00
|
|
|
return (
|
2020-04-02 09:12:53 -05:00
|
|
|
<tr key={`${stat.title}-${index}`}>
|
2020-03-18 07:00:14 -05:00
|
|
|
<td>{stat.title}</td>
|
2020-04-15 09:51:51 -05:00
|
|
|
<td style={{ textAlign: 'right' }}>{formatStat(stat)}</td>
|
2020-03-18 07:00:14 -05:00
|
|
|
</tr>
|
|
|
|
);
|
|
|
|
})}
|
|
|
|
</tbody>
|
|
|
|
</table>
|
|
|
|
</div>
|
2020-01-29 08:41:25 -06:00
|
|
|
);
|
2020-01-10 00:59:23 -06:00
|
|
|
}
|
|
|
|
|
2020-04-20 08:27:33 -05:00
|
|
|
drawerSubtitle(tabs: Array<{ label: string; value: InspectTab }>, activeTab: InspectTab) {
|
2020-04-15 09:51:51 -05:00
|
|
|
const { last } = this.state;
|
|
|
|
|
2020-04-20 08:27:33 -05:00
|
|
|
return <InspectSubtitle tabs={tabs} tab={activeTab} panelData={last} onSelectTab={this.onSelectTab} />;
|
2020-04-15 09:51:51 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
getTabs() {
|
|
|
|
const { dashboard, plugin } = this.props;
|
|
|
|
const { last } = this.state;
|
2020-01-29 08:41:25 -06:00
|
|
|
const error = last?.error;
|
|
|
|
const tabs = [];
|
2020-02-19 07:39:44 -06:00
|
|
|
|
2020-04-15 09:51:51 -05:00
|
|
|
if (plugin && !plugin.meta.skipDataQuery) {
|
2020-01-29 08:41:25 -06:00
|
|
|
tabs.push({ label: 'Data', value: InspectTab.Data });
|
2020-04-15 09:51:51 -05:00
|
|
|
tabs.push({ label: 'Stats', value: InspectTab.Stats });
|
2020-01-29 08:41:25 -06:00
|
|
|
}
|
2020-02-19 07:39:44 -06:00
|
|
|
|
2020-01-10 00:59:23 -06:00
|
|
|
if (this.state.metaDS) {
|
|
|
|
tabs.push({ label: 'Meta Data', value: InspectTab.Meta });
|
|
|
|
}
|
2020-02-19 07:39:44 -06:00
|
|
|
|
2020-04-15 09:51:51 -05:00
|
|
|
tabs.push({ label: 'JSON', value: InspectTab.JSON });
|
|
|
|
|
2020-01-29 08:41:25 -06:00
|
|
|
if (error && error.message) {
|
|
|
|
tabs.push({ label: 'Error', value: InspectTab.Error });
|
|
|
|
}
|
2020-02-19 07:39:44 -06:00
|
|
|
|
2020-04-15 09:51:51 -05:00
|
|
|
if (dashboard.meta.canEdit) {
|
|
|
|
tabs.push({ label: 'Query', value: InspectTab.Query });
|
|
|
|
}
|
|
|
|
return tabs;
|
|
|
|
}
|
2020-02-14 07:14:38 -06:00
|
|
|
|
|
|
|
render() {
|
2020-04-15 09:51:51 -05:00
|
|
|
const { panel, dashboard, plugin } = this.props;
|
|
|
|
const { currentTab } = this.state;
|
|
|
|
|
|
|
|
if (!plugin) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
const { last, drawerWidth } = this.state;
|
|
|
|
const styles = getPanelInspectorStyles();
|
2020-02-14 07:14:38 -06:00
|
|
|
const error = last?.error;
|
2020-04-15 09:51:51 -05:00
|
|
|
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;
|
|
|
|
}
|
2020-02-14 07:14:38 -06:00
|
|
|
|
|
|
|
return (
|
2020-04-20 08:27:33 -05:00
|
|
|
<Drawer
|
|
|
|
title={panel.title || 'Panel inspect'}
|
|
|
|
subtitle={this.drawerSubtitle(tabs, activeTab)}
|
|
|
|
width={drawerWidth}
|
|
|
|
onClose={this.onClose}
|
|
|
|
expandable
|
|
|
|
>
|
2020-04-15 09:51:51 -05:00
|
|
|
{activeTab === InspectTab.Data && this.renderDataTab()}
|
|
|
|
<CustomScrollbar autoHeightMin="100%">
|
|
|
|
<TabContent className={styles.tabContent}>
|
|
|
|
{activeTab === InspectTab.Meta && this.renderMetadataInspector()}
|
|
|
|
{activeTab === InspectTab.JSON && (
|
|
|
|
<InspectJSONTab panel={panel} dashboard={dashboard} data={last} onClose={this.onClose} />
|
|
|
|
)}
|
|
|
|
{activeTab === InspectTab.Error && this.renderErrorTab(error)}
|
|
|
|
{activeTab === InspectTab.Stats && this.renderStatsTab()}
|
|
|
|
{activeTab === InspectTab.Query && <QueryInspector panel={panel} />}
|
|
|
|
</TabContent>
|
|
|
|
</CustomScrollbar>
|
2019-12-18 06:57:07 -06:00
|
|
|
</Drawer>
|
2019-11-04 13:53:44 -06:00
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
2020-02-19 07:39:44 -06:00
|
|
|
|
2020-04-15 09:51:51 -05:00
|
|
|
function formatStat(stat: QueryResultMetaStat): string {
|
|
|
|
const display = getDisplayProcessor({
|
|
|
|
field: {
|
|
|
|
type: FieldType.number,
|
|
|
|
config: stat,
|
|
|
|
},
|
|
|
|
theme: config.theme,
|
|
|
|
});
|
|
|
|
return formattedValueToString(display(stat.value));
|
2020-03-18 07:00:14 -05:00
|
|
|
}
|
|
|
|
|
2020-04-15 09:51:51 -05:00
|
|
|
const mapStateToProps: MapStateToProps<ConnectedProps, OwnProps, StoreState> = (state, props) => {
|
|
|
|
const panelState = state.dashboard.panels[props.panel.id];
|
|
|
|
if (!panelState) {
|
|
|
|
return { plugin: null };
|
|
|
|
}
|
|
|
|
|
2020-02-19 07:39:44 -06:00
|
|
|
return {
|
2020-04-15 09:51:51 -05:00
|
|
|
plugin: panelState.plugin,
|
2020-02-19 07:39:44 -06:00
|
|
|
};
|
2020-04-15 09:51:51 -05:00
|
|
|
};
|
|
|
|
|
|
|
|
export const PanelInspector = connect(mapStateToProps)(PanelInspectorUnconnected);
|