2019-11-05 03:53:44 +08:00
|
|
|
import React, { PureComponent } from 'react';
|
2020-01-16 18:11:33 +08:00
|
|
|
import AutoSizer from 'react-virtualized-auto-sizer';
|
2020-01-17 19:55:21 +08:00
|
|
|
import { saveAs } from 'file-saver';
|
|
|
|
|
import { css } from 'emotion';
|
2019-11-05 03:53:44 +08:00
|
|
|
|
2020-02-14 21:14:38 +08:00
|
|
|
import { InspectHeader } from './InspectHeader';
|
|
|
|
|
|
2019-11-05 03:53:44 +08:00
|
|
|
import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
|
2020-02-14 21:14:38 +08:00
|
|
|
import { JSONFormatter, Drawer, Select, Table, TabContent, Forms, stylesFactory, CustomScrollbar } from '@grafana/ui';
|
2020-01-10 14:59:23 +08:00
|
|
|
import { getLocationSrv, getDataSourceSrv } from '@grafana/runtime';
|
2020-01-29 22:41:25 +08:00
|
|
|
import {
|
|
|
|
|
DataFrame,
|
|
|
|
|
DataSourceApi,
|
|
|
|
|
SelectableValue,
|
|
|
|
|
applyFieldOverrides,
|
|
|
|
|
toCSV,
|
|
|
|
|
DataQueryError,
|
|
|
|
|
PanelData,
|
2020-02-16 21:12:40 +08:00
|
|
|
DataQuery,
|
2020-01-29 22:41:25 +08:00
|
|
|
} from '@grafana/data';
|
2020-01-10 14:59:23 +08:00
|
|
|
import { config } from 'app/core/config';
|
2019-11-05 03:53:44 +08:00
|
|
|
|
|
|
|
|
interface Props {
|
|
|
|
|
dashboard: DashboardModel;
|
|
|
|
|
panel: PanelModel;
|
2020-01-29 22:41:25 +08:00
|
|
|
selectedTab: InspectTab;
|
2019-11-05 03:53:44 +08:00
|
|
|
}
|
|
|
|
|
|
2020-01-29 22:41:25 +08:00
|
|
|
export enum InspectTab {
|
2020-01-10 14:59:23 +08:00
|
|
|
Data = 'data',
|
|
|
|
|
Raw = 'raw',
|
|
|
|
|
Issue = 'issue',
|
|
|
|
|
Meta = 'meta', // When result metadata exists
|
2020-01-29 22:41:25 +08:00
|
|
|
Error = 'error',
|
2020-01-10 14:59:23 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface State {
|
|
|
|
|
// The last raw response
|
2020-02-14 21:14:38 +08:00
|
|
|
last: PanelData;
|
2020-01-10 14:59:23 +08:00
|
|
|
|
|
|
|
|
// Data frem the last response
|
|
|
|
|
data: DataFrame[];
|
|
|
|
|
|
|
|
|
|
// The selected data frame
|
|
|
|
|
selected: number;
|
|
|
|
|
|
|
|
|
|
// The Selected Tab
|
|
|
|
|
tab: InspectTab;
|
|
|
|
|
|
|
|
|
|
// If the datasource supports custom metadata
|
|
|
|
|
metaDS?: DataSourceApi;
|
2020-02-14 21:14:38 +08:00
|
|
|
|
|
|
|
|
stats: { requestTime: number; queries: number; dataSources: number };
|
|
|
|
|
|
|
|
|
|
drawerWidth: string;
|
2020-01-10 14:59:23 +08:00
|
|
|
}
|
|
|
|
|
|
2020-01-17 19:55:21 +08:00
|
|
|
const getStyles = stylesFactory(() => {
|
|
|
|
|
return {
|
|
|
|
|
toolbar: css`
|
|
|
|
|
display: flex;
|
|
|
|
|
margin: 8px 0;
|
|
|
|
|
justify-content: flex-end;
|
|
|
|
|
align-items: center;
|
|
|
|
|
`,
|
|
|
|
|
dataFrameSelect: css`
|
|
|
|
|
flex-grow: 2;
|
|
|
|
|
`,
|
|
|
|
|
downloadCsv: css`
|
|
|
|
|
margin-left: 16px;
|
|
|
|
|
`,
|
2020-01-29 22:41:25 +08:00
|
|
|
tabContent: css`
|
|
|
|
|
height: calc(100% - 32px);
|
|
|
|
|
`,
|
|
|
|
|
dataTabContent: css`
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
height: 100%;
|
|
|
|
|
width: 100%;
|
|
|
|
|
`,
|
2020-01-17 19:55:21 +08:00
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
|
2020-01-10 14:59:23 +08:00
|
|
|
export class PanelInspector extends PureComponent<Props, State> {
|
|
|
|
|
constructor(props: Props) {
|
|
|
|
|
super(props);
|
|
|
|
|
this.state = {
|
2020-02-14 21:14:38 +08:00
|
|
|
last: {} as PanelData,
|
2020-01-10 14:59:23 +08:00
|
|
|
data: [],
|
|
|
|
|
selected: 0,
|
2020-01-29 22:41:25 +08:00
|
|
|
tab: props.selectedTab || InspectTab.Data,
|
2020-02-14 21:14:38 +08:00
|
|
|
drawerWidth: '40%',
|
|
|
|
|
stats: { requestTime: 0, queries: 0, dataSources: 0 },
|
2020-01-10 14:59:23 +08:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async componentDidMount() {
|
|
|
|
|
const { panel } = this.props;
|
|
|
|
|
if (!panel) {
|
|
|
|
|
this.onDismiss(); // Try to close the component
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2020-01-29 22:41:25 +08:00
|
|
|
const lastResult = panel.getQueryRunner().getLastResult();
|
2020-01-10 14:59:23 +08:00
|
|
|
if (!lastResult) {
|
|
|
|
|
this.onDismiss(); // Usually opened from refresh?
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let metaDS: DataSourceApi;
|
2020-02-14 21:14:38 +08:00
|
|
|
const data = lastResult.series;
|
|
|
|
|
const error = lastResult.error;
|
|
|
|
|
|
2020-02-16 21:12:40 +08:00
|
|
|
const targets = lastResult.request?.targets || [];
|
2020-02-14 21:14:38 +08:00
|
|
|
const requestTime = lastResult.request?.endTime ? lastResult.request?.endTime - lastResult.request.startTime : -1;
|
|
|
|
|
const dataSources = new Set(targets.map(t => t.datasource)).size;
|
2020-01-29 22:41:25 +08:00
|
|
|
|
2020-02-16 21:12:40 +08:00
|
|
|
// Find the first DataSource wanting to show custom metadata
|
|
|
|
|
if (data && targets.length) {
|
|
|
|
|
const queries: Record<string, DataQuery> = {};
|
|
|
|
|
for (const target of targets) {
|
|
|
|
|
queries[target.refId] = target;
|
|
|
|
|
}
|
|
|
|
|
|
2020-01-10 14:59:23 +08:00
|
|
|
for (const frame of data) {
|
2020-02-16 21:12:40 +08:00
|
|
|
const q = queries[frame.refId];
|
|
|
|
|
if (q && frame.meta.custom) {
|
|
|
|
|
const dataSource = await getDataSourceSrv().get(q.datasource);
|
2020-01-29 22:41:25 +08:00
|
|
|
if (dataSource && dataSource.components?.MetadataInspector) {
|
|
|
|
|
metaDS = dataSource;
|
2020-01-10 14:59:23 +08:00
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Set last result, but no metadata inspector
|
2020-01-29 22:41:25 +08:00
|
|
|
this.setState(prevState => ({
|
2020-01-10 14:59:23 +08:00
|
|
|
last: lastResult,
|
|
|
|
|
data,
|
|
|
|
|
metaDS,
|
2020-01-29 22:41:25 +08:00
|
|
|
tab: error ? InspectTab.Error : prevState.tab,
|
2020-02-14 21:14:38 +08:00
|
|
|
stats: {
|
|
|
|
|
requestTime,
|
2020-02-16 21:12:40 +08:00
|
|
|
queries: targets.length,
|
2020-02-14 21:14:38 +08:00
|
|
|
dataSources,
|
|
|
|
|
},
|
2020-01-29 22:41:25 +08:00
|
|
|
}));
|
2020-01-10 14:59:23 +08:00
|
|
|
}
|
|
|
|
|
|
2019-11-05 03:53:44 +08:00
|
|
|
onDismiss = () => {
|
|
|
|
|
getLocationSrv().update({
|
2020-01-29 22:41:25 +08:00
|
|
|
query: { inspect: null, tab: null },
|
2019-11-05 03:53:44 +08:00
|
|
|
partial: true,
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
2020-02-14 21:14:38 +08:00
|
|
|
onToggleExpand = () => {
|
|
|
|
|
this.setState(prevState => ({
|
|
|
|
|
drawerWidth: prevState.drawerWidth === '100%' ? '40%' : '100%',
|
|
|
|
|
}));
|
|
|
|
|
};
|
|
|
|
|
|
2020-01-10 14:59:23 +08:00
|
|
|
onSelectTab = (item: SelectableValue<InspectTab>) => {
|
|
|
|
|
this.setState({ tab: item.value || InspectTab.Data });
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
onSelectedFrameChanged = (item: SelectableValue<number>) => {
|
|
|
|
|
this.setState({ selected: item.value || 0 });
|
|
|
|
|
};
|
|
|
|
|
|
2020-01-17 19:55:21 +08:00
|
|
|
exportCsv = (dataFrame: DataFrame) => {
|
|
|
|
|
const dataFrameCsv = toCSV([dataFrame]);
|
|
|
|
|
|
|
|
|
|
const blob = new Blob([dataFrameCsv], {
|
|
|
|
|
type: 'application/csv;charset=utf-8',
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
saveAs(blob, dataFrame.name + '-' + new Date().getUTCDate() + '.csv');
|
|
|
|
|
};
|
|
|
|
|
|
2020-01-10 14:59:23 +08:00
|
|
|
renderMetadataInspector() {
|
|
|
|
|
const { metaDS, data } = this.state;
|
|
|
|
|
if (!metaDS || !metaDS.components?.MetadataInspector) {
|
|
|
|
|
return <div>No Metadata Inspector</div>;
|
|
|
|
|
}
|
2020-01-29 22:41:25 +08:00
|
|
|
return (
|
|
|
|
|
<CustomScrollbar>
|
|
|
|
|
<metaDS.components.MetadataInspector datasource={metaDS} data={data} />
|
|
|
|
|
</CustomScrollbar>
|
|
|
|
|
);
|
2020-01-10 14:59:23 +08:00
|
|
|
}
|
|
|
|
|
|
2020-01-29 22:41:25 +08:00
|
|
|
renderDataTab() {
|
2020-01-10 14:59:23 +08:00
|
|
|
const { data, selected } = this.state;
|
2020-01-17 19:55:21 +08:00
|
|
|
const styles = getStyles();
|
2020-01-29 22:41:25 +08:00
|
|
|
|
2020-01-10 14:59:23 +08:00
|
|
|
if (!data || !data.length) {
|
|
|
|
|
return <div>No Data</div>;
|
|
|
|
|
}
|
|
|
|
|
const choices = data.map((frame, index) => {
|
|
|
|
|
return {
|
|
|
|
|
value: index,
|
|
|
|
|
label: `${frame.name} (${index})`,
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Apply dummy styles
|
|
|
|
|
const processed = applyFieldOverrides({
|
|
|
|
|
data,
|
|
|
|
|
theme: config.theme,
|
|
|
|
|
fieldOptions: { defaults: {}, overrides: [] },
|
|
|
|
|
replaceVariables: (value: string) => {
|
|
|
|
|
return value;
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return (
|
2020-01-29 22:41:25 +08:00
|
|
|
<div className={styles.dataTabContent}>
|
2020-01-17 19:55:21 +08:00
|
|
|
<div className={styles.toolbar}>
|
|
|
|
|
{choices.length > 1 && (
|
|
|
|
|
<div className={styles.dataFrameSelect}>
|
|
|
|
|
<Select
|
|
|
|
|
options={choices}
|
|
|
|
|
value={choices.find(t => t.value === selected)}
|
|
|
|
|
onChange={this.onSelectedFrameChanged}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
<div className={styles.downloadCsv}>
|
|
|
|
|
<Forms.Button variant="primary" onClick={() => this.exportCsv(processed[selected])}>
|
|
|
|
|
Download CSV
|
|
|
|
|
</Forms.Button>
|
2020-01-10 14:59:23 +08:00
|
|
|
</div>
|
2020-01-17 19:55:21 +08:00
|
|
|
</div>
|
2020-01-29 22:41:25 +08:00
|
|
|
<div style={{ flexGrow: 1 }}>
|
|
|
|
|
<AutoSizer>
|
|
|
|
|
{({ width, height }) => {
|
|
|
|
|
if (width === 0) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
return (
|
|
|
|
|
<div style={{ width, height }}>
|
|
|
|
|
<Table width={width} height={height} data={processed[selected]} />
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}}
|
|
|
|
|
</AutoSizer>
|
|
|
|
|
</div>
|
2020-01-10 14:59:23 +08:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
renderIssueTab() {
|
2020-01-29 22:41:25 +08:00
|
|
|
return <CustomScrollbar>TODO: show issue form</CustomScrollbar>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
renderErrorTab(error?: DataQueryError) {
|
|
|
|
|
if (!error) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
if (error.data) {
|
|
|
|
|
return (
|
|
|
|
|
<CustomScrollbar>
|
|
|
|
|
<h3>{error.data.message}</h3>
|
|
|
|
|
<pre>
|
|
|
|
|
<code>{error.data.error}</code>
|
|
|
|
|
</pre>
|
|
|
|
|
</CustomScrollbar>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
return <div>{error.message}</div>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
renderRawJsonTab(last: PanelData) {
|
|
|
|
|
return (
|
|
|
|
|
<CustomScrollbar>
|
|
|
|
|
<JSONFormatter json={last} open={2} />
|
|
|
|
|
</CustomScrollbar>
|
|
|
|
|
);
|
2020-01-10 14:59:23 +08:00
|
|
|
}
|
|
|
|
|
|
2020-02-14 21:14:38 +08:00
|
|
|
drawerHeader = () => {
|
|
|
|
|
const { tab, last, stats } = this.state;
|
2020-01-29 22:41:25 +08:00
|
|
|
const error = last?.error;
|
|
|
|
|
const tabs = [];
|
|
|
|
|
if (last && last?.series?.length > 0) {
|
|
|
|
|
tabs.push({ label: 'Data', value: InspectTab.Data });
|
|
|
|
|
}
|
2020-01-10 14:59:23 +08:00
|
|
|
if (this.state.metaDS) {
|
|
|
|
|
tabs.push({ label: 'Meta Data', value: InspectTab.Meta });
|
|
|
|
|
}
|
2020-01-29 22:41:25 +08:00
|
|
|
if (error && error.message) {
|
|
|
|
|
tabs.push({ label: 'Error', value: InspectTab.Error });
|
|
|
|
|
}
|
|
|
|
|
tabs.push({ label: 'Raw JSON', value: InspectTab.Raw });
|
2020-01-10 14:59:23 +08:00
|
|
|
|
2019-11-05 03:53:44 +08:00
|
|
|
return (
|
2020-02-14 21:14:38 +08:00
|
|
|
<InspectHeader
|
|
|
|
|
tabs={tabs}
|
|
|
|
|
tab={tab}
|
|
|
|
|
stats={stats}
|
|
|
|
|
onSelectTab={this.onSelectTab}
|
|
|
|
|
onClose={this.onDismiss}
|
|
|
|
|
panel={this.props.panel}
|
|
|
|
|
onToggleExpand={this.onToggleExpand}
|
|
|
|
|
isExpanded={this.state.drawerWidth === '100%'}
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
render() {
|
|
|
|
|
const { last, tab, drawerWidth } = this.state;
|
|
|
|
|
const styles = getStyles();
|
|
|
|
|
const error = last?.error;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Drawer title={this.drawerHeader} width={drawerWidth} onClose={this.onDismiss}>
|
2020-01-29 22:41:25 +08:00
|
|
|
<TabContent className={styles.tabContent}>
|
|
|
|
|
{tab === InspectTab.Data ? (
|
|
|
|
|
this.renderDataTab()
|
|
|
|
|
) : (
|
|
|
|
|
<AutoSizer>
|
|
|
|
|
{({ width, height }) => {
|
|
|
|
|
if (width === 0) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
return (
|
|
|
|
|
<div style={{ width, height }}>
|
|
|
|
|
{tab === InspectTab.Meta && this.renderMetadataInspector()}
|
|
|
|
|
{tab === InspectTab.Issue && this.renderIssueTab()}
|
|
|
|
|
{tab === InspectTab.Raw && this.renderRawJsonTab(last)}
|
|
|
|
|
{tab === InspectTab.Error && this.renderErrorTab(error)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}}
|
|
|
|
|
</AutoSizer>
|
|
|
|
|
)}
|
2020-01-14 16:03:14 +08:00
|
|
|
</TabContent>
|
2019-12-18 20:57:07 +08:00
|
|
|
</Drawer>
|
2019-11-05 03:53:44 +08:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|