mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Graphite: Rollup Indicator (#22738)
* WIP: Rollup indiator progress * Progress * Progress, can now open inspector with right tab * changed type and made inspect * Showing stats * Progress * Progress * Getting ready for v1 * Added option and fixed some strict nulls * Updated * Fixed test
This commit is contained in:
parent
579abad9cc
commit
044ec40112
@ -1,3 +1,4 @@
|
||||
import { FieldConfig } from './dataFrame';
|
||||
import { DataTransformerConfig } from './transformations';
|
||||
import { ApplyFieldOverrideOptions } from './fieldOverrides';
|
||||
|
||||
@ -15,19 +16,40 @@ export enum LoadingState {
|
||||
}
|
||||
|
||||
export interface QueryResultMeta {
|
||||
[key: string]: any;
|
||||
|
||||
// Match the result to the query
|
||||
requestId?: string;
|
||||
|
||||
// Used in Explore for highlighting
|
||||
searchWords?: string[];
|
||||
|
||||
// Used in Explore to show limit applied to search result
|
||||
limit?: number;
|
||||
|
||||
// DatasSource Specific Values
|
||||
/** DatasSource Specific Values */
|
||||
custom?: Record<string, any>;
|
||||
|
||||
/** Stats */
|
||||
stats?: QueryResultMetaStat[];
|
||||
|
||||
/** Meta Notices */
|
||||
notices?: QueryResultMetaNotice[];
|
||||
|
||||
/** Used to track transformation ids that where part of the processing */
|
||||
transformations?: string[];
|
||||
|
||||
/**
|
||||
* Legacy data source specific, should be moved to custom
|
||||
* */
|
||||
gmdMeta?: any[]; // used by cloudwatch
|
||||
rawQuery?: string; // used by stackdriver
|
||||
alignmentPeriod?: string; // used by stackdriver
|
||||
query?: string; // used by azure log
|
||||
searchWords?: string[]; // used by log models and loki
|
||||
limit?: number; // used by log models and loki
|
||||
json?: boolean; // used to keep track of old json doc values
|
||||
}
|
||||
|
||||
export interface QueryResultMetaStat extends FieldConfig {
|
||||
title: string;
|
||||
value: number;
|
||||
}
|
||||
|
||||
export interface QueryResultMetaNotice {
|
||||
severity: 'info' | 'warning' | 'error';
|
||||
text: string;
|
||||
url?: string;
|
||||
inspect?: 'meta' | 'error' | 'data' | 'stats';
|
||||
}
|
||||
|
||||
export interface QueryResultBase {
|
||||
|
@ -16,14 +16,31 @@ export interface PanelPluginMeta extends PluginMeta {
|
||||
}
|
||||
|
||||
export interface PanelData {
|
||||
/**
|
||||
* State of the data (loading, done, error, streaming)
|
||||
*/
|
||||
state: LoadingState;
|
||||
|
||||
/**
|
||||
* Contains data frames with field overrides applied
|
||||
*/
|
||||
series: DataFrame[];
|
||||
|
||||
/**
|
||||
* Request contains the queries and properties sent to the datasource
|
||||
*/
|
||||
request?: DataQueryRequest;
|
||||
|
||||
/**
|
||||
* Timing measurements
|
||||
*/
|
||||
timings?: DataQueryTimings;
|
||||
|
||||
/**
|
||||
* Any query errors
|
||||
*/
|
||||
error?: DataQueryError;
|
||||
|
||||
/**
|
||||
* Contains the range from the request or a shifted time range if a request uses relative time
|
||||
*/
|
||||
|
@ -29,10 +29,9 @@ export const onUpdateDatasourceJsonDataOptionSelect = <J, S, K extends keyof J>(
|
||||
|
||||
export const onUpdateDatasourceJsonDataOptionChecked = <J, S, K extends keyof J>(
|
||||
props: DataSourcePluginOptionsEditorProps<J, S>,
|
||||
key: K,
|
||||
val: boolean
|
||||
) => (event?: React.SyntheticEvent<HTMLInputElement>) => {
|
||||
updateDatasourcePluginJsonDataOption(props, key, val);
|
||||
key: K
|
||||
) => (event: React.SyntheticEvent<HTMLInputElement>) => {
|
||||
updateDatasourcePluginJsonDataOption(props, key, event.currentTarget.checked);
|
||||
};
|
||||
|
||||
export const onUpdateDatasourceSecureJsonDataOptionSelect = <J, S extends {} = KeyValue>(
|
||||
|
@ -37,7 +37,7 @@ export interface Props extends Themeable {
|
||||
height: number;
|
||||
width: number;
|
||||
field: FieldConfig;
|
||||
display: DisplayProcessor;
|
||||
display?: DisplayProcessor;
|
||||
value: DisplayValue;
|
||||
orientation: VizOrientation;
|
||||
itemSpacing?: number;
|
||||
|
@ -18,10 +18,11 @@ export interface Props {
|
||||
height: number;
|
||||
/** Minimal column width specified in pixels */
|
||||
columnMinWidth?: number;
|
||||
noHeader?: boolean;
|
||||
onCellClick?: TableFilterActionCallback;
|
||||
}
|
||||
|
||||
export const Table: FC<Props> = memo(({ data, height, onCellClick, width, columnMinWidth }) => {
|
||||
export const Table: FC<Props> = memo(({ data, height, onCellClick, width, columnMinWidth, noHeader }) => {
|
||||
const theme = useTheme();
|
||||
const [ref, headerRowMeasurements] = useMeasure();
|
||||
const tableStyles = getTableStyles(theme);
|
||||
@ -67,15 +68,17 @@ export const Table: FC<Props> = memo(({ data, height, onCellClick, width, column
|
||||
return (
|
||||
<div {...getTableProps()} className={tableStyles.table}>
|
||||
<CustomScrollbar>
|
||||
<div>
|
||||
{headerGroups.map((headerGroup: any) => (
|
||||
<div className={tableStyles.thead} {...headerGroup.getHeaderGroupProps()} ref={ref}>
|
||||
{headerGroup.headers.map((column: any) =>
|
||||
renderHeaderCell(column, tableStyles.headerCell, data.fields[column.index])
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{!noHeader && (
|
||||
<div>
|
||||
{headerGroups.map((headerGroup: any) => (
|
||||
<div className={tableStyles.thead} {...headerGroup.getHeaderGroupProps()} ref={ref}>
|
||||
{headerGroup.headers.map((column: any) =>
|
||||
renderHeaderCell(column, tableStyles.headerCell, data.fields[column.index])
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<FixedSizeList
|
||||
height={height - headerRowMeasurements.height}
|
||||
itemCount={rows.length}
|
||||
|
@ -1,17 +1,16 @@
|
||||
import React, { FC } from 'react';
|
||||
import { css } from 'emotion';
|
||||
import { Icon, selectThemeVariant, stylesFactory, Tab, TabsBar, useTheme } from '@grafana/ui';
|
||||
import { GrafanaTheme, SelectableValue } from '@grafana/data';
|
||||
import { GrafanaTheme, SelectableValue, PanelData, getValueFormat, formattedValueToString } from '@grafana/data';
|
||||
import { InspectTab } from './PanelInspector';
|
||||
import { PanelModel } from '../../state';
|
||||
|
||||
interface Props {
|
||||
tab: InspectTab;
|
||||
tabs: Array<{ label: string; value: InspectTab }>;
|
||||
stats: { requestTime: number; queries: number; dataSources: number };
|
||||
panelData: PanelData;
|
||||
panel: PanelModel;
|
||||
isExpanded: boolean;
|
||||
|
||||
onSelectTab: (tab: SelectableValue<InspectTab>) => void;
|
||||
onClose: () => void;
|
||||
onToggleExpand: () => void;
|
||||
@ -24,7 +23,7 @@ export const InspectHeader: FC<Props> = ({
|
||||
onClose,
|
||||
onToggleExpand,
|
||||
panel,
|
||||
stats,
|
||||
panelData,
|
||||
isExpanded,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
@ -42,7 +41,7 @@ export const InspectHeader: FC<Props> = ({
|
||||
</div>
|
||||
<div className={styles.titleWrapper}>
|
||||
<h3>{panel.title}</h3>
|
||||
<div>{formatStats(stats)}</div>
|
||||
<div className="muted">{formatStats(panelData)}</div>
|
||||
</div>
|
||||
<TabsBar className={styles.tabsBar}>
|
||||
{tabs.map((t, index) => {
|
||||
@ -95,10 +94,15 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
};
|
||||
});
|
||||
|
||||
function formatStats(stats: { requestTime: number; queries: number; dataSources: number }) {
|
||||
const queries = `${stats.queries} ${stats.queries === 1 ? 'query' : 'queries'}`;
|
||||
const dataSources = `${stats.dataSources} ${stats.dataSources === 1 ? 'data source' : 'data sources'}`;
|
||||
const requestTime = `${stats.requestTime === -1 ? 'N/A' : stats.requestTime}ms`;
|
||||
function formatStats(panelData: PanelData) {
|
||||
const { request } = panelData;
|
||||
if (!request) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return `${queries} - ${dataSources} - ${requestTime}`;
|
||||
const queryCount = request.targets.length;
|
||||
const requestTime = request.endTime ? request.endTime - request.startTime : 0;
|
||||
const formatted = formattedValueToString(getValueFormat('ms')(requestTime));
|
||||
|
||||
return `${queryCount} queries with total query time of ${formatted}`;
|
||||
}
|
||||
|
@ -16,7 +16,9 @@ import {
|
||||
toCSV,
|
||||
DataQueryError,
|
||||
PanelData,
|
||||
DataQuery,
|
||||
getValueFormat,
|
||||
formattedValueToString,
|
||||
QueryResultMetaStat,
|
||||
} from '@grafana/data';
|
||||
import { config } from 'app/core/config';
|
||||
|
||||
@ -51,8 +53,6 @@ interface State {
|
||||
// If the datasource supports custom metadata
|
||||
metaDS?: DataSourceApi;
|
||||
|
||||
stats: { requestTime: number; queries: number; dataSources: number; processingTime: number };
|
||||
|
||||
drawerWidth: string;
|
||||
}
|
||||
|
||||
@ -65,7 +65,6 @@ export class PanelInspector extends PureComponent<Props, State> {
|
||||
selected: 0,
|
||||
tab: props.selectedTab || InspectTab.Data,
|
||||
drawerWidth: '50%',
|
||||
stats: { requestTime: 0, queries: 0, dataSources: 0, processingTime: 0 },
|
||||
};
|
||||
}
|
||||
|
||||
@ -89,23 +88,13 @@ export class PanelInspector extends PureComponent<Props, State> {
|
||||
const error = lastResult.error;
|
||||
|
||||
const targets = lastResult.request?.targets || [];
|
||||
const requestTime = lastResult.request?.endTime ? lastResult.request?.endTime - lastResult.request.startTime : -1;
|
||||
const dataSources = new Set(targets.map(t => t.datasource)).size;
|
||||
const processingTime = lastResult.timings?.dataProcessingTime || -1;
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
for (const frame of data) {
|
||||
const q = queries[frame.refId];
|
||||
|
||||
if (q && frame.meta && frame.meta.custom) {
|
||||
const dataSource = await getDataSourceSrv().get(q.datasource);
|
||||
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;
|
||||
@ -121,12 +110,6 @@ export class PanelInspector extends PureComponent<Props, State> {
|
||||
data,
|
||||
metaDS,
|
||||
tab: error ? InspectTab.Error : prevState.tab,
|
||||
stats: {
|
||||
requestTime,
|
||||
queries: targets.length,
|
||||
dataSources,
|
||||
processingTime,
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
@ -184,7 +167,6 @@ export class PanelInspector extends PureComponent<Props, State> {
|
||||
};
|
||||
});
|
||||
|
||||
// Apply dummy styles
|
||||
const processed = applyFieldOverrides({
|
||||
data,
|
||||
theme: config.theme,
|
||||
@ -251,29 +233,66 @@ export class PanelInspector extends PureComponent<Props, State> {
|
||||
}
|
||||
|
||||
renderStatsTab() {
|
||||
const { stats } = this.state;
|
||||
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 (
|
||||
<table className="filter-table width-30">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Query time</td>
|
||||
<td>{`${stats.requestTime === -1 ? 'N/A' : stats.requestTime + 'ms'}`}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Data processing time</td>
|
||||
<td>{`${
|
||||
stats.processingTime === -1
|
||||
? 'N/A'
|
||||
: Math.round((stats.processingTime + Number.EPSILON) * 100) / 100 + 'ms'
|
||||
}`}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<>
|
||||
{this.renderStatsTable('Stats', stats)}
|
||||
{dataStats.length && this.renderStatsTable('Data source stats', dataStats)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
renderStatsTable(name: string, stats: QueryResultMetaStat[]) {
|
||||
return (
|
||||
<div style={{ paddingBottom: '16px' }}>
|
||||
<div className="section-heading">{name}</div>
|
||||
<table className="filter-table width-30">
|
||||
<tbody>
|
||||
{stats.map(stat => {
|
||||
return (
|
||||
<tr>
|
||||
<td>{stat.title}</td>
|
||||
<td style={{ textAlign: 'right' }}>{formatStat(stat.value, stat.unit)}</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
drawerHeader = () => {
|
||||
const { tab, last, stats } = this.state;
|
||||
const { tab, last } = this.state;
|
||||
const error = last?.error;
|
||||
const tabs = [];
|
||||
|
||||
@ -296,7 +315,7 @@ export class PanelInspector extends PureComponent<Props, State> {
|
||||
<InspectHeader
|
||||
tabs={tabs}
|
||||
tab={tab}
|
||||
stats={stats}
|
||||
panelData={last}
|
||||
onSelectTab={this.onSelectTab}
|
||||
onClose={this.onDismiss}
|
||||
panel={this.props.panel}
|
||||
@ -327,6 +346,14 @@ export class PanelInspector extends PureComponent<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
function formatStat(value: any, unit?: string): string {
|
||||
if (unit) {
|
||||
return formattedValueToString(getValueFormat(unit)(value));
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
const getStyles = stylesFactory(() => {
|
||||
return {
|
||||
toolbar: css`
|
||||
|
@ -12,6 +12,7 @@ import { PanelChromeAngular } from './PanelChromeAngular';
|
||||
|
||||
// Actions
|
||||
import { initDashboardPanel } from '../state/actions';
|
||||
import { updateLocation } from 'app/core/reducers/location';
|
||||
|
||||
// Types
|
||||
import { PanelModel, DashboardModel } from '../state';
|
||||
@ -33,6 +34,7 @@ export interface ConnectedProps {
|
||||
|
||||
export interface DispatchProps {
|
||||
initDashboardPanel: typeof initDashboardPanel;
|
||||
updateLocation: typeof updateLocation;
|
||||
}
|
||||
|
||||
export type Props = OwnProps & ConnectedProps & DispatchProps;
|
||||
@ -72,7 +74,7 @@ export class DashboardPanelUnconnected extends PureComponent<Props, State> {
|
||||
};
|
||||
|
||||
renderPanel(plugin: PanelPlugin) {
|
||||
const { dashboard, panel, isFullscreen, isInView, isInEditMode } = this.props;
|
||||
const { dashboard, panel, isFullscreen, isInView, isInEditMode, updateLocation } = this.props;
|
||||
|
||||
return (
|
||||
<AutoSizer>
|
||||
@ -105,6 +107,7 @@ export class DashboardPanelUnconnected extends PureComponent<Props, State> {
|
||||
isInEditMode={isInEditMode}
|
||||
width={width}
|
||||
height={height}
|
||||
updateLocation={updateLocation}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
@ -170,6 +173,6 @@ const mapStateToProps: MapStateToProps<ConnectedProps, OwnProps, StoreState> = (
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps: MapDispatchToProps<DispatchProps, OwnProps> = { initDashboardPanel };
|
||||
const mapDispatchToProps: MapDispatchToProps<DispatchProps, OwnProps> = { initDashboardPanel, updateLocation };
|
||||
|
||||
export const DashboardPanel = connect(mapStateToProps, mapDispatchToProps)(DashboardPanelUnconnected);
|
||||
|
@ -11,6 +11,7 @@ import { applyPanelTimeOverrides } from 'app/features/dashboard/utils/panel';
|
||||
import { profiler } from 'app/core/profiler';
|
||||
import { getProcessedDataFrames } from '../state/runRequest';
|
||||
import config from 'app/core/config';
|
||||
import { updateLocation } from 'app/core/actions';
|
||||
// Types
|
||||
import { DashboardModel, PanelModel } from '../state';
|
||||
import { PANEL_BORDER } from 'app/core/constants';
|
||||
@ -36,6 +37,7 @@ export interface Props {
|
||||
isInEditMode?: boolean;
|
||||
width: number;
|
||||
height: number;
|
||||
updateLocation: typeof updateLocation;
|
||||
}
|
||||
|
||||
export interface State {
|
||||
@ -43,8 +45,6 @@ export interface State {
|
||||
renderCounter: number;
|
||||
errorMessage?: string;
|
||||
refreshWhenInView: boolean;
|
||||
|
||||
// Current state of all events
|
||||
data: PanelData;
|
||||
}
|
||||
|
||||
@ -312,7 +312,7 @@ export class PanelChrome extends PureComponent<Props, State> {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { dashboard, panel, isFullscreen, width, height } = this.props;
|
||||
const { dashboard, panel, isFullscreen, width, height, updateLocation } = this.props;
|
||||
const { errorMessage, data } = this.state;
|
||||
const { transparent } = panel;
|
||||
|
||||
@ -328,14 +328,14 @@ export class PanelChrome extends PureComponent<Props, State> {
|
||||
<PanelHeader
|
||||
panel={panel}
|
||||
dashboard={dashboard}
|
||||
timeInfo={data.request ? data.request.timeInfo : undefined}
|
||||
title={panel.title}
|
||||
description={panel.description}
|
||||
scopedVars={panel.scopedVars}
|
||||
links={panel.links}
|
||||
error={errorMessage}
|
||||
isFullscreen={isFullscreen}
|
||||
isLoading={data.state === LoadingState.Loading}
|
||||
data={data}
|
||||
updateLocation={updateLocation}
|
||||
/>
|
||||
<ErrorBoundary>
|
||||
{({ error }) => {
|
||||
|
@ -17,6 +17,7 @@ import config from 'app/core/config';
|
||||
import { DashboardModel, PanelModel } from '../state';
|
||||
import { StoreState } from 'app/types';
|
||||
import { LoadingState, DefaultTimeRange, PanelData, PanelPlugin, PanelEvents } from '@grafana/data';
|
||||
import { updateLocation } from 'app/core/actions';
|
||||
import { PANEL_BORDER } from 'app/core/constants';
|
||||
|
||||
interface OwnProps {
|
||||
@ -35,6 +36,7 @@ interface ConnectedProps {
|
||||
|
||||
interface DispatchProps {
|
||||
setPanelAngularComponent: typeof setPanelAngularComponent;
|
||||
updateLocation: typeof updateLocation;
|
||||
}
|
||||
|
||||
export type Props = OwnProps & ConnectedProps & DispatchProps;
|
||||
@ -215,7 +217,7 @@ export class PanelChromeAngularUnconnected extends PureComponent<Props, State> {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { dashboard, panel, isFullscreen, plugin, angularComponent } = this.props;
|
||||
const { dashboard, panel, isFullscreen, plugin, angularComponent, updateLocation } = this.props;
|
||||
const { errorMessage, data, alertState } = this.state;
|
||||
const { transparent } = panel;
|
||||
|
||||
@ -238,7 +240,6 @@ export class PanelChromeAngularUnconnected extends PureComponent<Props, State> {
|
||||
<PanelHeader
|
||||
panel={panel}
|
||||
dashboard={dashboard}
|
||||
timeInfo={data.request ? data.request.timeInfo : undefined}
|
||||
title={panel.title}
|
||||
description={panel.description}
|
||||
scopedVars={panel.scopedVars}
|
||||
@ -246,7 +247,8 @@ export class PanelChromeAngularUnconnected extends PureComponent<Props, State> {
|
||||
links={panel.links}
|
||||
error={errorMessage}
|
||||
isFullscreen={isFullscreen}
|
||||
isLoading={data.state === LoadingState.Loading}
|
||||
data={data}
|
||||
updateLocation={updateLocation}
|
||||
/>
|
||||
<div className={panelContentClassNames}>
|
||||
<div ref={element => (this.element = element)} className="panel-height-helper" />
|
||||
@ -262,6 +264,6 @@ const mapStateToProps: MapStateToProps<ConnectedProps, OwnProps, StoreState> = (
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps: MapDispatchToProps<DispatchProps, OwnProps> = { setPanelAngularComponent };
|
||||
const mapDispatchToProps: MapDispatchToProps<DispatchProps, OwnProps> = { setPanelAngularComponent, updateLocation };
|
||||
|
||||
export const PanelChromeAngular = connect(mapStateToProps, mapDispatchToProps)(PanelChromeAngularUnconnected);
|
||||
|
@ -1,9 +1,9 @@
|
||||
import React, { Component } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { isEqual } from 'lodash';
|
||||
import { DataLink, ScopedVars, PanelMenuItem } from '@grafana/data';
|
||||
import { DataLink, ScopedVars, PanelMenuItem, PanelData, LoadingState, QueryResultMetaNotice } from '@grafana/data';
|
||||
import { AngularComponent } from '@grafana/runtime';
|
||||
import { ClickOutsideWrapper } from '@grafana/ui';
|
||||
import { ClickOutsideWrapper, Tooltip } from '@grafana/ui';
|
||||
import { e2e } from '@grafana/e2e';
|
||||
|
||||
import PanelHeaderCorner from './PanelHeaderCorner';
|
||||
@ -14,11 +14,11 @@ import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
|
||||
import { PanelModel } from 'app/features/dashboard/state/PanelModel';
|
||||
import { getPanelLinksSupplier } from 'app/features/panel/panellinks/linkSuppliers';
|
||||
import { getPanelMenu } from 'app/features/dashboard/utils/getPanelMenu';
|
||||
import { updateLocation } from 'app/core/actions';
|
||||
|
||||
export interface Props {
|
||||
panel: PanelModel;
|
||||
dashboard: DashboardModel;
|
||||
timeInfo?: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
scopedVars?: ScopedVars;
|
||||
@ -26,7 +26,8 @@ export interface Props {
|
||||
links?: DataLink[];
|
||||
error?: string;
|
||||
isFullscreen: boolean;
|
||||
isLoading: boolean;
|
||||
data: PanelData;
|
||||
updateLocation: typeof updateLocation;
|
||||
}
|
||||
|
||||
interface ClickCoordinates {
|
||||
@ -92,8 +93,35 @@ export class PanelHeader extends Component<Props, State> {
|
||||
);
|
||||
}
|
||||
|
||||
openInspect = (e: React.SyntheticEvent, tab: string) => {
|
||||
const { updateLocation, panel } = this.props;
|
||||
|
||||
e.stopPropagation();
|
||||
|
||||
updateLocation({
|
||||
query: { inspect: panel.id, tab },
|
||||
partial: true,
|
||||
});
|
||||
};
|
||||
|
||||
renderNotice = (notice: QueryResultMetaNotice) => {
|
||||
return (
|
||||
<Tooltip content={notice.text} key={notice.severity}>
|
||||
{notice.inspect ? (
|
||||
<div className="panel-info-notice" onClick={e => this.openInspect(e, notice.inspect)}>
|
||||
<span className="fa fa-info-circle" style={{ marginRight: '8px', cursor: 'pointer' }} />
|
||||
</div>
|
||||
) : (
|
||||
<a className="panel-info-notice" href={notice.url} target="_blank">
|
||||
<span className="fa fa-info-circle" style={{ marginRight: '8px', cursor: 'pointer' }} />
|
||||
</a>
|
||||
)}
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { panel, timeInfo, scopedVars, error, isFullscreen, isLoading } = this.props;
|
||||
const { panel, scopedVars, error, isFullscreen, data } = this.props;
|
||||
const { menuItems } = this.state;
|
||||
const title = templateSrv.replaceWithText(panel.title, scopedVars);
|
||||
|
||||
@ -102,9 +130,20 @@ export class PanelHeader extends Component<Props, State> {
|
||||
'grid-drag-handle': !isFullscreen,
|
||||
});
|
||||
|
||||
// dedupe on severity
|
||||
const notices: Record<string, QueryResultMetaNotice> = {};
|
||||
|
||||
for (const series of data.series) {
|
||||
if (series.meta && series.meta.notices) {
|
||||
for (const notice of series.meta.notices) {
|
||||
notices[notice.severity] = notice;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{isLoading && this.renderLoadingState()}
|
||||
{data.state === LoadingState.Loading && this.renderLoadingState()}
|
||||
<div className={panelHeaderClass}>
|
||||
<PanelHeaderCorner
|
||||
panel={panel}
|
||||
@ -121,6 +160,7 @@ export class PanelHeader extends Component<Props, State> {
|
||||
aria-label={e2e.pages.Dashboard.Panels.Panel.selectors.title(title)}
|
||||
>
|
||||
<div className="panel-title">
|
||||
{Object.values(notices).map(this.renderNotice)}
|
||||
<span className="icon-gf panel-alert-icon" />
|
||||
<span className="panel-title-text">
|
||||
{title} <span className="fa fa-caret-down panel-menu-toggle" />
|
||||
@ -130,9 +170,9 @@ export class PanelHeader extends Component<Props, State> {
|
||||
<PanelHeaderMenu items={menuItems} />
|
||||
</ClickOutsideWrapper>
|
||||
)}
|
||||
{timeInfo && (
|
||||
{data.request && data.request.timeInfo && (
|
||||
<span className="panel-time-info">
|
||||
<i className="fa fa-clock-o" /> {timeInfo}
|
||||
<i className="fa fa-clock-o" /> {data.request.timeInfo}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
@ -1,8 +1,12 @@
|
||||
import { css, cx } from 'emotion';
|
||||
import React, { PureComponent } from 'react';
|
||||
import { MetadataInspectorProps, DataFrame } from '@grafana/data';
|
||||
import { MetadataInspectorProps } from '@grafana/data';
|
||||
import { GraphiteDatasource } from './datasource';
|
||||
import { GraphiteQuery, GraphiteOptions, MetricTankMeta, MetricTankResultMeta } from './types';
|
||||
import { parseSchemaRetentions } from './meta';
|
||||
import { GraphiteQuery, GraphiteOptions, MetricTankSeriesMeta } from './types';
|
||||
import { parseSchemaRetentions, getRollupNotice, getRuntimeConsolidationNotice } from './meta';
|
||||
import { stylesFactory } from '@grafana/ui';
|
||||
import { config } from 'app/core/config';
|
||||
import kbn from 'app/core/utils/kbn';
|
||||
|
||||
export type Props = MetadataInspectorProps<GraphiteDatasource, GraphiteQuery, GraphiteOptions>;
|
||||
|
||||
@ -11,48 +15,159 @@ export interface State {
|
||||
}
|
||||
|
||||
export class MetricTankMetaInspector extends PureComponent<Props, State> {
|
||||
state = { index: 0 };
|
||||
renderMeta(meta: MetricTankSeriesMeta, key: string) {
|
||||
const styles = getStyles();
|
||||
const buckets = parseSchemaRetentions(meta['schema-retentions']);
|
||||
const rollupNotice = getRollupNotice([meta]);
|
||||
const runtimeNotice = getRuntimeConsolidationNotice([meta]);
|
||||
const normFunc = (meta['consolidate-normfetch'] || '').replace('Consolidator', '');
|
||||
|
||||
let totalSeconds = 0;
|
||||
|
||||
for (const bucket of buckets) {
|
||||
totalSeconds += kbn.interval_to_seconds(bucket.retention);
|
||||
}
|
||||
|
||||
renderInfo = (info: MetricTankResultMeta, frame: DataFrame) => {
|
||||
const buckets = parseSchemaRetentions(info['schema-retentions']);
|
||||
return (
|
||||
<div>
|
||||
<h3>Info</h3>
|
||||
<table>
|
||||
<tbody>
|
||||
{buckets.map(row => (
|
||||
<tr key={row.interval}>
|
||||
<td>{row.interval} </td>
|
||||
<td>{row.retention} </td>
|
||||
<td>{row.chunkspan} </td>
|
||||
<td>{row.numchunks} </td>
|
||||
<td>{row.ready} </td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<pre>{JSON.stringify(info, null, 2)}</pre>
|
||||
<div className={styles.metaItem} key={key}>
|
||||
<div className={styles.metaItemHeader}>Schema: {meta['schema-name']}</div>
|
||||
<div className={styles.metaItemBody}>
|
||||
<div className={styles.step}>
|
||||
<div className={styles.stepHeading}>Step 1: Fetch</div>
|
||||
<div className={styles.stepDescription}>
|
||||
First data is fetched, either from raw data archive or a rollup archive
|
||||
</div>
|
||||
|
||||
{rollupNotice && <p>{rollupNotice.text}</p>}
|
||||
{!rollupNotice && <p>No rollup archive was used</p>}
|
||||
|
||||
<div>
|
||||
{buckets.map((bucket, index) => {
|
||||
const bucketLength = kbn.interval_to_seconds(bucket.retention);
|
||||
const lengthPercent = (bucketLength / totalSeconds) * 100;
|
||||
const isActive = index === meta['archive-read'];
|
||||
|
||||
return (
|
||||
<div key={bucket.retention} className={styles.bucket}>
|
||||
<div className={styles.bucketInterval}>{bucket.interval}</div>
|
||||
<div
|
||||
className={cx(styles.bucketRetention, { [styles.bucketRetentionActive]: isActive })}
|
||||
style={{ flexGrow: lengthPercent }}
|
||||
/>
|
||||
<div style={{ flexGrow: 100 - lengthPercent }}>{bucket.retention}</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.step}>
|
||||
<div className={styles.stepHeading}>Step 2: Normalization</div>
|
||||
<div className={styles.stepDescription}>
|
||||
Normalization happens when series with different intervals between points are combined.
|
||||
</div>
|
||||
|
||||
{meta['aggnum-norm'] > 1 && <p>Normalization did occur using {normFunc}</p>}
|
||||
{meta['aggnum-norm'] === 1 && <p>No normalization was needed</p>}
|
||||
</div>
|
||||
|
||||
<div className={styles.step}>
|
||||
<div className={styles.stepHeading}>Step 3: Runtime consolidation</div>
|
||||
<div className={styles.stepDescription}>
|
||||
If there are too many data points at this point Metrictank will consolidate them down to below max data
|
||||
points (set in queries tab).
|
||||
</div>
|
||||
|
||||
{runtimeNotice && <p>{runtimeNotice.text}</p>}
|
||||
{!runtimeNotice && <p>No runtime consolidation</p>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const { data } = this.props;
|
||||
if (!data || !data.length) {
|
||||
return <div>No Metadata</div>;
|
||||
|
||||
// away to dedupe them
|
||||
const seriesMetas: Record<string, MetricTankSeriesMeta> = {};
|
||||
|
||||
for (const series of data) {
|
||||
if (series.meta && series.meta.custom) {
|
||||
for (const metaItem of series.meta.custom.seriesMetaList as MetricTankSeriesMeta[]) {
|
||||
// key is to dedupe as many series will have identitical meta
|
||||
const key = `${metaItem['schema-name']}-${metaItem['archive-read']}`;
|
||||
seriesMetas[key] = metaItem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const frame = data[this.state.index];
|
||||
const meta = frame.meta?.custom as MetricTankMeta;
|
||||
if (!meta || !meta.info) {
|
||||
return <>No Metadatata on DataFrame</>;
|
||||
if (Object.keys(seriesMetas).length === 0) {
|
||||
return <div>No response meta data</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h3>MetricTank Request</h3>
|
||||
<pre>{JSON.stringify(meta.request, null, 2)}</pre>
|
||||
{meta.info.map(info => this.renderInfo(info, frame))}
|
||||
<h2 className="page-heading">Aggregation & rollup</h2>
|
||||
{Object.keys(seriesMetas).map(key => this.renderMeta(seriesMetas[key], key))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getStyles = stylesFactory(() => {
|
||||
const { theme } = config;
|
||||
const borderColor = theme.isDark ? theme.colors.gray25 : theme.colors.gray85;
|
||||
const background = theme.isDark ? theme.colors.dark1 : theme.colors.white;
|
||||
const headerBg = theme.isDark ? theme.colors.gray15 : theme.colors.gray85;
|
||||
|
||||
return {
|
||||
metaItem: css`
|
||||
background: ${background};
|
||||
border: 1px solid ${borderColor};
|
||||
margin-bottom: ${theme.spacing.md};
|
||||
`,
|
||||
metaItemHeader: css`
|
||||
background: ${headerBg};
|
||||
padding: ${theme.spacing.xs} ${theme.spacing.md};
|
||||
font-size: ${theme.typography.size.md};
|
||||
`,
|
||||
metaItemBody: css`
|
||||
padding: ${theme.spacing.md};
|
||||
`,
|
||||
stepHeading: css`
|
||||
font-size: ${theme.typography.size.md};
|
||||
`,
|
||||
stepDescription: css`
|
||||
font-size: ${theme.typography.size.sm};
|
||||
color: ${theme.colors.textWeak};
|
||||
margin-bottom: ${theme.spacing.sm};
|
||||
`,
|
||||
step: css`
|
||||
margin-bottom: ${theme.spacing.lg};
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
`,
|
||||
bucket: css`
|
||||
display: flex;
|
||||
margin-bottom: ${theme.spacing.sm};
|
||||
border-radius: ${theme.border.radius.md};
|
||||
`,
|
||||
bucketInterval: css`
|
||||
flex-grow: 0;
|
||||
width: 60px;
|
||||
`,
|
||||
bucketRetention: css`
|
||||
background: linear-gradient(0deg, ${theme.colors.blue85}, ${theme.colors.blue95});
|
||||
text-align: center;
|
||||
color: ${theme.colors.white};
|
||||
margin-right: ${theme.spacing.md};
|
||||
border-radius: ${theme.border.radius.md};
|
||||
`,
|
||||
bucketRetentionActive: css`
|
||||
background: linear-gradient(0deg, ${theme.colors.greenBase}, ${theme.colors.greenShade});
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
@ -1,10 +0,0 @@
|
||||
import { css } from 'emotion';
|
||||
|
||||
const styles = {
|
||||
helpbtn: css`
|
||||
margin-left: 8px;
|
||||
margin-top: 5px;
|
||||
`,
|
||||
};
|
||||
|
||||
export default styles;
|
@ -1,8 +1,11 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { DataSourceHttpSettings, FormLabel, Button, Select } from '@grafana/ui';
|
||||
import { DataSourcePluginOptionsEditorProps, onUpdateDatasourceJsonDataOptionSelect } from '@grafana/data';
|
||||
import { DataSourceHttpSettings, FormLabel, Select, Switch } from '@grafana/ui';
|
||||
import {
|
||||
DataSourcePluginOptionsEditorProps,
|
||||
onUpdateDatasourceJsonDataOptionSelect,
|
||||
onUpdateDatasourceJsonDataOptionChecked,
|
||||
} from '@grafana/data';
|
||||
import { GraphiteOptions, GraphiteType } from '../types';
|
||||
import styles from './ConfigEditor.styles';
|
||||
|
||||
const graphiteVersions = [
|
||||
{ label: '0.9.x', value: '0.9' },
|
||||
@ -17,22 +20,27 @@ const graphiteTypes = Object.entries(GraphiteType).map(([label, value]) => ({
|
||||
|
||||
export type Props = DataSourcePluginOptionsEditorProps<GraphiteOptions>;
|
||||
|
||||
interface State {
|
||||
showMetricTankHelp: boolean;
|
||||
}
|
||||
|
||||
export class ConfigEditor extends PureComponent<Props, State> {
|
||||
export class ConfigEditor extends PureComponent<Props> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
showMetricTankHelp: false,
|
||||
};
|
||||
}
|
||||
|
||||
renderTypeHelp = () => {
|
||||
return (
|
||||
<p>
|
||||
There are different types of Graphite compatible backends. Here you can specify the type you are using. If you
|
||||
are using{' '}
|
||||
<a href="https://github.com/grafana/metrictank" className="pointer" target="_blank">
|
||||
Metrictank
|
||||
</a>{' '}
|
||||
then select that here. This will enable Metrictank specific features like query processing meta data. Metrictank
|
||||
is a multi-tenant timeseries engine for Graphite and friends.
|
||||
</p>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { options, onOptionsChange } = this.props;
|
||||
const { showMetricTankHelp } = this.state;
|
||||
|
||||
const currentVersion =
|
||||
graphiteVersions.find(item => item.value === options.jsonData.graphiteVersion) ?? graphiteVersions[2];
|
||||
@ -61,39 +69,25 @@ export class ConfigEditor extends PureComponent<Props, State> {
|
||||
</div>
|
||||
<div className="gf-form-inline">
|
||||
<div className="gf-form">
|
||||
<FormLabel>Type</FormLabel>
|
||||
<FormLabel tooltip={this.renderTypeHelp}>Type</FormLabel>
|
||||
<Select
|
||||
options={graphiteTypes}
|
||||
value={graphiteTypes.find(type => type.value === options.jsonData.graphiteType)}
|
||||
width={8}
|
||||
onChange={onUpdateDatasourceJsonDataOptionSelect(this.props, 'graphiteType')}
|
||||
/>
|
||||
|
||||
<div className={styles.helpbtn}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
this.setState((prevState: State) => ({ showMetricTankHelp: !prevState.showMetricTankHelp }))
|
||||
}
|
||||
>
|
||||
Help <i className={showMetricTankHelp ? 'fa fa-caret-down' : 'fa fa-caret-right'} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{showMetricTankHelp && (
|
||||
<div className="grafana-info-box m-t-2">
|
||||
<div className="alert-body">
|
||||
<p>
|
||||
There are different types of Graphite compatible backends. Here you can specify the type you are
|
||||
using. If you are using{' '}
|
||||
<a href="https://github.com/grafana/metrictank" className="pointer" target="_blank">
|
||||
Metrictank
|
||||
</a>{' '}
|
||||
then select that here. This will enable Metrictank specific features like query processing meta data.
|
||||
Metrictank is a multi-tenant timeseries engine for Graphite and friends.
|
||||
</p>
|
||||
{options.jsonData.graphiteType === GraphiteType.Metrictank && (
|
||||
<div className="gf-form-inline">
|
||||
<div className="gf-form">
|
||||
<Switch
|
||||
label="Rollup indicator"
|
||||
labelClass={'width-10'}
|
||||
tooltip="Shows up as an info icon in panel headers when data is aggregated"
|
||||
checked={options.jsonData.rollupIndicatorEnabled}
|
||||
onChange={onUpdateDatasourceJsonDataOptionChecked(this.props, 'rollupIndicatorEnabled')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
@ -7,14 +7,16 @@ import {
|
||||
DataQueryRequest,
|
||||
toDataFrame,
|
||||
DataSourceApi,
|
||||
QueryResultMetaStat,
|
||||
} from '@grafana/data';
|
||||
import { isVersionGtOrEq, SemVersion } from 'app/core/utils/version';
|
||||
import gfunc from './gfunc';
|
||||
import { getBackendSrv } from '@grafana/runtime';
|
||||
import { TemplateSrv } from 'app/features/templating/template_srv';
|
||||
//Types
|
||||
import { GraphiteOptions, GraphiteQuery, GraphiteType } from './types';
|
||||
// Types
|
||||
import { GraphiteOptions, GraphiteQuery, GraphiteType, MetricTankRequestMeta } from './types';
|
||||
import { getSearchFilterScopedVar } from '../../../features/templating/variable';
|
||||
import { getRollupNotice, getRuntimeConsolidationNotice } from 'app/plugins/datasource/graphite/meta';
|
||||
|
||||
export class GraphiteDatasource extends DataSourceApi<GraphiteQuery, GraphiteOptions> {
|
||||
basicAuth: string;
|
||||
@ -23,6 +25,7 @@ export class GraphiteDatasource extends DataSourceApi<GraphiteQuery, GraphiteOpt
|
||||
graphiteVersion: any;
|
||||
supportsTags: boolean;
|
||||
isMetricTank: boolean;
|
||||
rollupIndicatorEnabled: boolean;
|
||||
cacheTimeout: any;
|
||||
withCredentials: boolean;
|
||||
funcDefs: any = null;
|
||||
@ -39,6 +42,7 @@ export class GraphiteDatasource extends DataSourceApi<GraphiteQuery, GraphiteOpt
|
||||
this.isMetricTank = instanceSettings.jsonData.graphiteType === GraphiteType.Metrictank;
|
||||
this.supportsTags = supportsTags(this.graphiteVersion);
|
||||
this.cacheTimeout = instanceSettings.cacheTimeout;
|
||||
this.rollupIndicatorEnabled = instanceSettings.jsonData.rollupIndicatorEnabled;
|
||||
this.withCredentials = instanceSettings.withCredentials;
|
||||
this.funcDefs = null;
|
||||
this.funcDefsPromise = null;
|
||||
@ -108,33 +112,71 @@ export class GraphiteDatasource extends DataSourceApi<GraphiteQuery, GraphiteOpt
|
||||
if (!result || !result.data) {
|
||||
return { data };
|
||||
}
|
||||
|
||||
// Series are either at the root or under a node called 'series'
|
||||
const series = result.data.series || result.data;
|
||||
|
||||
if (!_.isArray(series)) {
|
||||
throw { message: 'Missing series in result', data: result };
|
||||
}
|
||||
|
||||
for (let i = 0; i < series.length; i++) {
|
||||
const s = series[i];
|
||||
|
||||
for (let y = 0; y < s.datapoints.length; y++) {
|
||||
s.datapoints[y][1] *= 1000;
|
||||
}
|
||||
|
||||
const frame = toDataFrame(s);
|
||||
|
||||
// Metrictank metadata
|
||||
if (s.meta) {
|
||||
frame.meta = {
|
||||
custom: {
|
||||
request: result.data.meta, // info for the whole request
|
||||
info: s.meta, // Array of metadata
|
||||
requestMetaList: result.data.meta, // info for the whole request
|
||||
seriesMetaList: s.meta, // Array of metadata
|
||||
},
|
||||
};
|
||||
|
||||
if (this.rollupIndicatorEnabled) {
|
||||
const rollupNotice = getRollupNotice(s.meta);
|
||||
const runtimeNotice = getRuntimeConsolidationNotice(s.meta);
|
||||
|
||||
if (rollupNotice) {
|
||||
frame.meta.notices = [rollupNotice];
|
||||
} else if (runtimeNotice) {
|
||||
frame.meta.notices = [runtimeNotice];
|
||||
}
|
||||
}
|
||||
|
||||
// only add the request stats to the first frame
|
||||
if (i === 0 && result.data.meta.stats) {
|
||||
frame.meta.stats = this.getRequestStats(result.data.meta);
|
||||
}
|
||||
}
|
||||
|
||||
data.push(frame);
|
||||
}
|
||||
|
||||
return { data };
|
||||
};
|
||||
|
||||
getRequestStats(meta: MetricTankRequestMeta): QueryResultMetaStat[] {
|
||||
const stats: QueryResultMetaStat[] = [];
|
||||
|
||||
for (const key in meta.stats) {
|
||||
let unit: string | undefined = undefined;
|
||||
|
||||
if (key.endsWith('.ms')) {
|
||||
unit = 'ms';
|
||||
}
|
||||
|
||||
stats.push({ title: key, value: meta.stats[key], unit });
|
||||
}
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
parseTags(tagString: string) {
|
||||
let tags: string[] = [];
|
||||
tags = tagString.split(',');
|
||||
@ -278,7 +320,7 @@ export class GraphiteDatasource extends DataSourceApi<GraphiteQuery, GraphiteOpt
|
||||
return date.unix();
|
||||
}
|
||||
|
||||
metricFindQuery(query: string, optionalOptions: any) {
|
||||
metricFindQuery(query: string, optionalOptions?: any) {
|
||||
const options: any = optionalOptions || {};
|
||||
let interpolatedQuery = this.templateSrv.replace(
|
||||
query,
|
||||
@ -573,7 +615,7 @@ export class GraphiteDatasource extends DataSourceApi<GraphiteQuery, GraphiteOpt
|
||||
return getBackendSrv().datasourceRequest(options);
|
||||
}
|
||||
|
||||
buildGraphiteParams(options: any, scopedVars: ScopedVars): string[] {
|
||||
buildGraphiteParams(options: any, scopedVars?: ScopedVars): string[] {
|
||||
const graphiteOptions = ['from', 'until', 'rawData', 'format', 'maxDataPoints', 'cacheTimeout'];
|
||||
const cleanOptions = [],
|
||||
targets: any = {};
|
||||
|
@ -1,7 +1,5 @@
|
||||
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",
|
||||
}
|
||||
import { MetricTankSeriesMeta } from './types';
|
||||
import { QueryResultMetaNotice } from '@grafana/data';
|
||||
|
||||
// https://github.com/grafana/metrictank/blob/master/scripts/config/storage-schemas.conf#L15-L46
|
||||
|
||||
@ -19,6 +17,7 @@ function toInteger(val?: string): number | undefined {
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function toBooleanOrTimestamp(val?: string): number | boolean | undefined {
|
||||
if (val) {
|
||||
if (val === 'true') {
|
||||
@ -32,6 +31,44 @@ function toBooleanOrTimestamp(val?: string): number | boolean | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function getRollupNotice(metaList: MetricTankSeriesMeta[]): QueryResultMetaNotice | null {
|
||||
for (const meta of metaList) {
|
||||
const archiveIndex = meta['archive-read'];
|
||||
|
||||
if (archiveIndex > 0) {
|
||||
const schema = parseSchemaRetentions(meta['schema-retentions']);
|
||||
const intervalString = schema[archiveIndex].interval;
|
||||
const func = meta['consolidate-normfetch'].replace('Consolidator', '');
|
||||
|
||||
return {
|
||||
text: `Data is rolled up, aggregated over ${intervalString} using ${func} function`,
|
||||
severity: 'info',
|
||||
inspect: 'meta',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getRuntimeConsolidationNotice(metaList: MetricTankSeriesMeta[]): QueryResultMetaNotice | null {
|
||||
for (const meta of metaList) {
|
||||
const runtimeNr = meta['aggnum-rc'];
|
||||
|
||||
if (runtimeNr > 0) {
|
||||
const func = meta['consolidate-rc'].replace('Consolidator', '');
|
||||
|
||||
return {
|
||||
text: `Data is runtime consolidated, ${runtimeNr} datapoints combined using ${func} function`,
|
||||
severity: 'info',
|
||||
inspect: 'meta',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function parseSchemaRetentions(spec: string): RetentionInfo[] {
|
||||
if (!spec) {
|
||||
return [];
|
||||
|
@ -10,19 +10,94 @@ jest.mock('@grafana/runtime', () => ({
|
||||
getBackendSrv: () => backendSrv,
|
||||
}));
|
||||
|
||||
interface Context {
|
||||
templateSrv: TemplateSrv;
|
||||
ds: GraphiteDatasource;
|
||||
}
|
||||
|
||||
describe('graphiteDatasource', () => {
|
||||
const datasourceRequestMock = jest.spyOn(backendSrv, 'datasourceRequest');
|
||||
|
||||
const ctx: any = {
|
||||
// @ts-ignore
|
||||
templateSrv: new TemplateSrv(),
|
||||
instanceSettings: { url: 'url', name: 'graphiteProd', jsonData: {} },
|
||||
};
|
||||
let ctx = {} as Context;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
ctx.instanceSettings.url = '/api/datasources/proxy/1';
|
||||
ctx.ds = new GraphiteDatasource(ctx.instanceSettings, ctx.templateSrv);
|
||||
|
||||
const instanceSettings = {
|
||||
url: '/api/datasources/proxy/1',
|
||||
name: 'graphiteProd',
|
||||
jsonData: {
|
||||
rollupIndicatorEnabled: true,
|
||||
},
|
||||
};
|
||||
const templateSrv = new TemplateSrv();
|
||||
const ds = new GraphiteDatasource(instanceSettings, templateSrv);
|
||||
ctx = { templateSrv, ds };
|
||||
});
|
||||
|
||||
describe('convertResponseToDataFrames', () => {
|
||||
it('should transform regular result', () => {
|
||||
const result = ctx.ds.convertResponseToDataFrames({
|
||||
data: {
|
||||
meta: {
|
||||
stats: {
|
||||
'executeplan.cache-hit-partial.count': 5,
|
||||
'executeplan.cache-hit.count': 10,
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
target: 'seriesA',
|
||||
datapoints: [
|
||||
[100, 200],
|
||||
[101, 201],
|
||||
],
|
||||
meta: [
|
||||
{
|
||||
'aggnum-norm': 1,
|
||||
'aggnum-rc': 7,
|
||||
'archive-interval': 3600,
|
||||
'archive-read': 1,
|
||||
'consolidate-normfetch': 'AverageConsolidator',
|
||||
'consolidate-rc': 'AverageConsolidator',
|
||||
count: 1,
|
||||
'schema-name': 'wpUsageMetrics',
|
||||
'schema-retentions': '1h:35d:6h:2,2h:2y:6h:2',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
target: 'seriesB',
|
||||
meta: [
|
||||
{
|
||||
'aggnum-norm': 1,
|
||||
'aggnum-rc': 0,
|
||||
'archive-interval': 3600,
|
||||
'archive-read': 0,
|
||||
'consolidate-normfetch': 'AverageConsolidator',
|
||||
'consolidate-rc': 'NoneConsolidator',
|
||||
count: 1,
|
||||
'schema-name': 'wpUsageMetrics',
|
||||
'schema-retentions': '1h:35d:6h:2,2h:2y:6h:2',
|
||||
},
|
||||
],
|
||||
datapoints: [
|
||||
[200, 300],
|
||||
[201, 301],
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.data.length).toBe(2);
|
||||
expect(result.data[0].name).toBe('seriesA');
|
||||
expect(result.data[1].name).toBe('seriesB');
|
||||
expect(result.data[0].length).toBe(2);
|
||||
expect(result.data[0].meta.notices.length).toBe(1);
|
||||
expect(result.data[0].meta.notices[0].text).toBe('Data is rolled up, aggregated over 2h using Average function');
|
||||
expect(result.data[1].meta.notices).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('When querying graphite with one target using query editor target spec', () => {
|
||||
@ -53,7 +128,7 @@ describe('graphiteDatasource', () => {
|
||||
});
|
||||
});
|
||||
|
||||
await ctx.ds.query(query).then((data: any) => {
|
||||
await ctx.ds.query(query as any).then((data: any) => {
|
||||
results = data;
|
||||
});
|
||||
});
|
||||
@ -233,7 +308,7 @@ describe('graphiteDatasource', () => {
|
||||
|
||||
describe('when formatting targets', () => {
|
||||
it('does not attempt to glob for one variable', () => {
|
||||
ctx.ds.templateSrv.init([
|
||||
ctx.templateSrv.init([
|
||||
{
|
||||
type: 'query',
|
||||
name: 'metric',
|
||||
@ -248,7 +323,7 @@ describe('graphiteDatasource', () => {
|
||||
});
|
||||
|
||||
it('globs for more than one variable', () => {
|
||||
ctx.ds.templateSrv.init([
|
||||
ctx.templateSrv.init([
|
||||
{
|
||||
type: 'query',
|
||||
name: 'metric',
|
||||
@ -259,6 +334,7 @@ describe('graphiteDatasource', () => {
|
||||
const results = ctx.ds.buildGraphiteParams({
|
||||
targets: [{ target: 'my.[[metric]].*' }],
|
||||
});
|
||||
|
||||
expect(results).toStrictEqual(['target=my.%7Ba%2Cb%7D.*', 'format=json']);
|
||||
});
|
||||
});
|
||||
@ -352,7 +428,7 @@ describe('graphiteDatasource', () => {
|
||||
});
|
||||
|
||||
it('/metrics/find should be POST', () => {
|
||||
ctx.ds.templateSrv.init([
|
||||
ctx.templateSrv.init([
|
||||
{
|
||||
type: 'query',
|
||||
name: 'foo',
|
||||
|
@ -7,6 +7,7 @@ export interface GraphiteQuery extends DataQuery {
|
||||
export interface GraphiteOptions extends DataSourceJsonData {
|
||||
graphiteVersion: string;
|
||||
graphiteType: GraphiteType;
|
||||
rollupIndicatorEnabled?: boolean;
|
||||
}
|
||||
|
||||
export enum GraphiteType {
|
||||
@ -15,10 +16,10 @@ export enum GraphiteType {
|
||||
}
|
||||
|
||||
export interface MetricTankRequestMeta {
|
||||
[key: string]: any; // TODO -- fill this with real values from metrictank
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface MetricTankResultMeta {
|
||||
export interface MetricTankSeriesMeta {
|
||||
'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;
|
||||
@ -32,5 +33,5 @@ export interface MetricTankResultMeta {
|
||||
|
||||
export interface MetricTankMeta {
|
||||
request: MetricTankRequestMeta;
|
||||
info: MetricTankResultMeta[];
|
||||
info: MetricTankSeriesMeta[];
|
||||
}
|
||||
|
@ -1,6 +1,15 @@
|
||||
import React from 'react';
|
||||
import { mount, ReactWrapper } from 'enzyme';
|
||||
import { PanelData, dateMath, TimeRange, VizOrientation, PanelProps, LoadingState, dateTime } from '@grafana/data';
|
||||
import {
|
||||
PanelData,
|
||||
dateMath,
|
||||
TimeRange,
|
||||
VizOrientation,
|
||||
PanelProps,
|
||||
LoadingState,
|
||||
dateTime,
|
||||
toDataFrame,
|
||||
} from '@grafana/data';
|
||||
import { BarGaugeDisplayMode } from '@grafana/ui';
|
||||
|
||||
import { BarGaugePanel } from './BarGaugePanel';
|
||||
@ -19,6 +28,27 @@ describe('BarGaugePanel', () => {
|
||||
expect(displayValue).toBe('No data');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when there is data', () => {
|
||||
const wrapper = createBarGaugePanelWithData({
|
||||
series: [
|
||||
toDataFrame({
|
||||
target: 'test',
|
||||
datapoints: [
|
||||
[100, 1000],
|
||||
[100, 200],
|
||||
],
|
||||
}),
|
||||
],
|
||||
timeRange: createTimeRange(),
|
||||
state: LoadingState.Done,
|
||||
});
|
||||
|
||||
it('should render with title "No data"', () => {
|
||||
const displayValue = wrapper.find('div.bar-gauge__value').text();
|
||||
expect(displayValue).toBe('100');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function createTimeRange(): TimeRange {
|
||||
|
@ -6,7 +6,7 @@ import { NewsOptions, DEFAULT_FEED_URL } from './types';
|
||||
const PROXY_PREFIX = 'https://cors-anywhere.herokuapp.com/';
|
||||
|
||||
interface State {
|
||||
feedUrl: string;
|
||||
feedUrl?: string;
|
||||
}
|
||||
|
||||
export class NewsPanelEditor extends PureComponent<PanelEditorProps<NewsOptions>, State> {
|
||||
@ -41,27 +41,29 @@ export class NewsPanelEditor extends PureComponent<PanelEditorProps<NewsOptions>
|
||||
return (
|
||||
<>
|
||||
<PanelOptionsGroup title="Feed">
|
||||
<div className="gf-form">
|
||||
<FormField
|
||||
label="URL"
|
||||
labelWidth={7}
|
||||
inputWidth={30}
|
||||
value={feedUrl || ''}
|
||||
placeholder={DEFAULT_FEED_URL}
|
||||
onChange={this.onFeedUrlChange}
|
||||
tooltip="Only RSS feed formats are supported (not Atom)."
|
||||
onBlur={this.onUpdatePanel}
|
||||
/>
|
||||
</div>
|
||||
{suggestProxy && (
|
||||
<div>
|
||||
<br />
|
||||
<div>If the feed is unable to connect, consider a CORS proxy</div>
|
||||
<Button variant="inverse" onClick={this.onSetProxyPrefix}>
|
||||
Use Proxy
|
||||
</Button>
|
||||
<>
|
||||
<div className="gf-form">
|
||||
<FormField
|
||||
label="URL"
|
||||
labelWidth={7}
|
||||
inputWidth={30}
|
||||
value={feedUrl || ''}
|
||||
placeholder={DEFAULT_FEED_URL}
|
||||
onChange={this.onFeedUrlChange}
|
||||
tooltip="Only RSS feed formats are supported (not Atom)."
|
||||
onBlur={this.onUpdatePanel}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{suggestProxy && (
|
||||
<div>
|
||||
<br />
|
||||
<div>If the feed is unable to connect, consider a CORS proxy</div>
|
||||
<Button variant="inverse" onClick={this.onSetProxyPrefix}>
|
||||
Use Proxy
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
</PanelOptionsGroup>
|
||||
</>
|
||||
);
|
||||
|
@ -175,7 +175,8 @@ class SingleStatCtrl extends MetricsPanelCtrl {
|
||||
}
|
||||
|
||||
const distinct = getDistinctNames(frames);
|
||||
let fieldInfo = distinct.byName[panel.tableColumn]; //
|
||||
let fieldInfo: FieldInfo | undefined = distinct.byName[panel.tableColumn];
|
||||
|
||||
this.fieldNames = distinct.names;
|
||||
|
||||
if (!fieldInfo) {
|
||||
|
Loading…
Reference in New Issue
Block a user