diff --git a/packages/grafana-data/src/types/data.ts b/packages/grafana-data/src/types/data.ts index 5e7b6da5c07..70f044b0c5e 100644 --- a/packages/grafana-data/src/types/data.ts +++ b/packages/grafana-data/src/types/data.ts @@ -19,6 +19,13 @@ export interface QueryResultMeta { // Used in Explore to show limit applied to search result limit?: number; + + // HACK: save the datassource name in the meta so we can load it from the response + // we should be able to find the datasource from the refId + datasource?: string; + + // DatasSource Specific Values + custom?: Record; } export interface QueryResultBase { diff --git a/packages/grafana-data/src/types/datasource.ts b/packages/grafana-data/src/types/datasource.ts index 71b77da0ba3..e7d4c6a5e80 100644 --- a/packages/grafana-data/src/types/datasource.ts +++ b/packages/grafana-data/src/types/datasource.ts @@ -91,6 +91,11 @@ export class DataSourcePlugin< return this; } + setMetadataInspector(MetadataInspector: ComponentType>) { + this.components.MetadataInspector = MetadataInspector; + return this; + } + setComponentsFromLegacyExports(pluginExports: any) { this.angularConfigCtrl = pluginExports.ConfigCtrl; @@ -137,6 +142,7 @@ export interface DataSourcePluginComponents< ExploreLogsQueryField?: ComponentType>; ExploreStartPage?: ComponentType; ConfigEditor?: ComponentType>; + MetadataInspector?: ComponentType>; } // Only exported for tests @@ -331,6 +337,17 @@ export function updateDatasourcePluginResetKeyOption(props: DataSourcePluginOpti props.onOptionsChange(config); } +export interface MetadataInspectorProps< + DSType extends DataSourceApi, + TQuery extends DataQuery = DataQuery, + TOptions extends DataSourceJsonData = DataSourceJsonData +> { + datasource: DSType; + + // All Data from this DataSource + data: DataFrame[]; +} + export interface QueryEditorProps< DSType extends DataSourceApi, TQuery extends DataQuery = DataQuery, diff --git a/public/app/features/dashboard/components/Inspector/PanelInspector.tsx b/public/app/features/dashboard/components/Inspector/PanelInspector.tsx index 58c2c851c5c..e74673228dc 100644 --- a/public/app/features/dashboard/components/Inspector/PanelInspector.tsx +++ b/public/app/features/dashboard/components/Inspector/PanelInspector.tsx @@ -1,17 +1,88 @@ -// Libraries import React, { PureComponent } from 'react'; import { DashboardModel, PanelModel } from 'app/features/dashboard/state'; -import { Drawer, JSONFormatter } from '@grafana/ui'; -import { css } from 'emotion'; -import { getLocationSrv } from '@grafana/runtime'; +import { JSONFormatter, Drawer, Select, Table } from '@grafana/ui'; +import { getLocationSrv, getDataSourceSrv } from '@grafana/runtime'; +import { DataFrame, DataSourceApi, SelectableValue, applyFieldOverrides } from '@grafana/data'; +import { config } from 'app/core/config'; interface Props { dashboard: DashboardModel; panel: PanelModel; } -export class PanelInspector extends PureComponent { +enum InspectTab { + Data = 'data', + Raw = 'raw', + Issue = 'issue', + Meta = 'meta', // When result metadata exists +} + +interface State { + // The last raw response + last?: any; + + // 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; +} + +export class PanelInspector extends PureComponent { + constructor(props: Props) { + super(props); + this.state = { + data: [], + selected: 0, + tab: InspectTab.Data, + }; + } + + async componentDidMount() { + const { panel } = this.props; + if (!panel) { + this.onDismiss(); // Try to close the component + return; + } + + // TODO? should we get the result with an observable once? + const lastResult = (panel.getQueryRunner() as any).lastResult; + if (!lastResult) { + this.onDismiss(); // Usually opened from refresh? + return; + } + + // Find the first DataSource wanting to show custom metadata + let metaDS: DataSourceApi; + const data = lastResult?.series as DataFrame[]; + if (data) { + for (const frame of data) { + const key = frame.meta?.datasource; + if (key) { + const ds = await getDataSourceSrv().get(key); + if (ds && ds.components.MetadataInspector) { + metaDS = ds; + break; + } + } + } + } + + // Set last result, but no metadata inspector + this.setState({ + last: lastResult, + data, + metaDS, + }); + } + onDismiss = () => { getLocationSrv().update({ query: { inspect: null }, @@ -19,24 +90,98 @@ export class PanelInspector extends PureComponent { }); }; + onSelectTab = (item: SelectableValue) => { + this.setState({ tab: item.value || InspectTab.Data }); + }; + + onSelectedFrameChanged = (item: SelectableValue) => { + this.setState({ selected: item.value || 0 }); + }; + + renderMetadataInspector() { + const { metaDS, data } = this.state; + if (!metaDS || !metaDS.components?.MetadataInspector) { + return
No Metadata Inspector
; + } + return ; + } + + renderDataTab() { + const { data, selected } = this.state; + if (!data || !data.length) { + return
No Data
; + } + 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 ( +
+ {choices.length > 1 && ( +
+ t.value === tab)} onChange={this.onSelectTab} /> + + {tab === InspectTab.Data && this.renderDataTab()} + + {tab === InspectTab.Meta && this.renderMetadataInspector()} + + {tab === InspectTab.Issue && this.renderIssueTab()} + + {tab === InspectTab.Raw && ( +
+ +
+ )} ); } diff --git a/public/app/plugins/datasource/graphite/MetricTankMetaInspector.tsx b/public/app/plugins/datasource/graphite/MetricTankMetaInspector.tsx new file mode 100644 index 00000000000..0842f2cb73e --- /dev/null +++ b/public/app/plugins/datasource/graphite/MetricTankMetaInspector.tsx @@ -0,0 +1,58 @@ +import React, { PureComponent } from 'react'; +import { MetadataInspectorProps, DataFrame } from '@grafana/data'; +import { GraphiteDatasource } from './datasource'; +import { GraphiteQuery, GraphiteOptions, MetricTankMeta, MetricTankResultMeta } from './types'; +import { parseSchemaRetentions } from './meta'; + +export type Props = MetadataInspectorProps; + +export interface State { + index: number; +} + +export class MetricTankMetaInspector extends PureComponent { + state = { index: 0 }; + + renderInfo = (info: MetricTankResultMeta, frame: DataFrame) => { + const buckets = parseSchemaRetentions(info['schema-retentions']); + return ( +
+

Info

+ + + {buckets.map(row => ( + + + + + + + + ))} + +
{row.interval}  {row.retention}  {row.chunkspan}  {row.numchunks}  {row.ready}  
+
{JSON.stringify(info, null, 2)}
+
+ ); + }; + + render() { + const { data } = this.props; + if (!data || !data.length) { + return
No Metadata
; + } + + const frame = data[this.state.index]; + const meta = frame.meta?.custom as MetricTankMeta; + if (!meta || !meta.info) { + return <>No Metadatata on DataFrame; + } + return ( +
+

MetricTank Request

+
{JSON.stringify(meta.request, null, 2)}
+ {meta.info.map(info => this.renderInfo(info, frame))} +
+ ); + } +} diff --git a/public/app/plugins/datasource/graphite/datasource.ts b/public/app/plugins/datasource/graphite/datasource.ts index 455651ec12c..1a79bf8749d 100644 --- a/public/app/plugins/datasource/graphite/datasource.ts +++ b/public/app/plugins/datasource/graphite/datasource.ts @@ -103,7 +103,7 @@ export class GraphiteDatasource extends DataSourceApi { const data: DataFrame[] = []; if (!result || !result.data) { return { data }; @@ -124,14 +124,17 @@ export class GraphiteDatasource extends DataSourceApi { + it('should parse schema retentions', () => { + const retentions = '1s:35d:20min:5:1542274085,1min:38d:2h:1:true,10min:120d:6h:1:true,2h:2y:6h:2'; + const info = parseSchemaRetentions(retentions); + expect(info).toMatchInlineSnapshot(` + Array [ + Object { + "chunkspan": "20min", + "interval": "1s", + "numchunks": 5, + "ready": 1542274085, + "retention": "35d", + }, + Object { + "chunkspan": "2h", + "interval": "1min", + "numchunks": 1, + "ready": true, + "retention": "38d", + }, + Object { + "chunkspan": "6h", + "interval": "10min", + "numchunks": 1, + "ready": true, + "retention": "120d", + }, + Object { + "chunkspan": "6h", + "interval": "2h", + "numchunks": 2, + "ready": undefined, + "retention": "2y", + }, + ] + `); + }); +}); diff --git a/public/app/plugins/datasource/graphite/meta.ts b/public/app/plugins/datasource/graphite/meta.ts new file mode 100644 index 00000000000..b92b6d89544 --- /dev/null +++ b/public/app/plugins/datasource/graphite/meta.ts @@ -0,0 +1,49 @@ +export interface MetricTankResultMeta { + 'schema-name': string; + 'schema-retentions': string; //"1s:35d:20min:5:1542274085,1min:38d:2h:1:true,10min:120d:6h:1:true,2h:2y:6h:2", +} + +// https://github.com/grafana/metrictank/blob/master/scripts/config/storage-schemas.conf#L15-L46 + +export interface RetentionInfo { + interval: string; + retention?: string; + chunkspan?: string; + numchunks?: number; + ready?: boolean | number; // whether, or as of what data timestamp, the archive is ready for querying. +} + +function toInteger(val?: string): number | undefined { + if (val) { + return parseInt(val, 10); + } + return undefined; +} +function toBooleanOrTimestamp(val?: string): number | boolean | undefined { + if (val) { + if (val === 'true') { + return true; + } + if (val === 'false') { + return false; + } + return parseInt(val, 10); + } + return undefined; +} + +export function parseSchemaRetentions(spec: string): RetentionInfo[] { + if (!spec) { + return []; + } + return spec.split(',').map(str => { + const vals = str.split(':'); + return { + interval: vals[0], + retention: vals[1], + chunkspan: vals[2], + numchunks: toInteger(vals[3]), + ready: toBooleanOrTimestamp(vals[4]), + }; + }); +} diff --git a/public/app/plugins/datasource/graphite/module.ts b/public/app/plugins/datasource/graphite/module.ts index 7b004a3a570..174061621b0 100644 --- a/public/app/plugins/datasource/graphite/module.ts +++ b/public/app/plugins/datasource/graphite/module.ts @@ -2,6 +2,7 @@ import { GraphiteDatasource } from './datasource'; import { GraphiteQueryCtrl } from './query_ctrl'; import { DataSourcePlugin } from '@grafana/data'; import { ConfigEditor } from './configuration/ConfigEditor'; +import { MetricTankMetaInspector } from './MetricTankMetaInspector'; class AnnotationsQueryCtrl { static templateUrl = 'partials/annotations.editor.html'; @@ -10,4 +11,5 @@ class AnnotationsQueryCtrl { export const plugin = new DataSourcePlugin(GraphiteDatasource) .setQueryCtrl(GraphiteQueryCtrl) .setConfigEditor(ConfigEditor) + .setMetadataInspector(MetricTankMetaInspector) .setAnnotationQueryCtrl(AnnotationsQueryCtrl); diff --git a/public/app/plugins/datasource/graphite/types.ts b/public/app/plugins/datasource/graphite/types.ts index 58eab51675d..9c9a1ec6531 100644 --- a/public/app/plugins/datasource/graphite/types.ts +++ b/public/app/plugins/datasource/graphite/types.ts @@ -13,3 +13,24 @@ export enum GraphiteType { Default = 'default', Metrictank = 'metrictank', } + +export interface MetricTankRequestMeta { + [key: string]: any; // TODO -- fill this with real values from metrictank +} + +export interface MetricTankResultMeta { + 'schema-name': string; + 'schema-retentions': string; //"1s:35d:20min:5:1542274085,1min:38d:2h:1:true,10min:120d:6h:1:true,2h:2y:6h:2", + 'archive-read': number; + 'archive-interval': number; + 'aggnum-norm': number; + 'consolidate-normfetch': string; //"MaximumConsolidator", + 'aggnum-rc': number; + 'consolidate-rc': string; //"MaximumConsolidator", + count: number; +} + +export interface MetricTankMeta { + request: MetricTankRequestMeta; + info: MetricTankResultMeta[]; +}