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 { DataTransformerConfig } from './transformations';
|
||||||
import { ApplyFieldOverrideOptions } from './fieldOverrides';
|
import { ApplyFieldOverrideOptions } from './fieldOverrides';
|
||||||
|
|
||||||
@ -15,19 +16,40 @@ export enum LoadingState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface QueryResultMeta {
|
export interface QueryResultMeta {
|
||||||
[key: string]: any;
|
/** DatasSource Specific Values */
|
||||||
|
|
||||||
// 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
|
|
||||||
custom?: Record<string, any>;
|
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 {
|
export interface QueryResultBase {
|
||||||
|
@ -16,14 +16,31 @@ export interface PanelPluginMeta extends PluginMeta {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface PanelData {
|
export interface PanelData {
|
||||||
|
/**
|
||||||
|
* State of the data (loading, done, error, streaming)
|
||||||
|
*/
|
||||||
state: LoadingState;
|
state: LoadingState;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Contains data frames with field overrides applied
|
* Contains data frames with field overrides applied
|
||||||
*/
|
*/
|
||||||
series: DataFrame[];
|
series: DataFrame[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request contains the queries and properties sent to the datasource
|
||||||
|
*/
|
||||||
request?: DataQueryRequest;
|
request?: DataQueryRequest;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timing measurements
|
||||||
|
*/
|
||||||
timings?: DataQueryTimings;
|
timings?: DataQueryTimings;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Any query errors
|
||||||
|
*/
|
||||||
error?: DataQueryError;
|
error?: DataQueryError;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Contains the range from the request or a shifted time range if a request uses relative time
|
* 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>(
|
export const onUpdateDatasourceJsonDataOptionChecked = <J, S, K extends keyof J>(
|
||||||
props: DataSourcePluginOptionsEditorProps<J, S>,
|
props: DataSourcePluginOptionsEditorProps<J, S>,
|
||||||
key: K,
|
key: K
|
||||||
val: boolean
|
) => (event: React.SyntheticEvent<HTMLInputElement>) => {
|
||||||
) => (event?: React.SyntheticEvent<HTMLInputElement>) => {
|
updateDatasourcePluginJsonDataOption(props, key, event.currentTarget.checked);
|
||||||
updateDatasourcePluginJsonDataOption(props, key, val);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const onUpdateDatasourceSecureJsonDataOptionSelect = <J, S extends {} = KeyValue>(
|
export const onUpdateDatasourceSecureJsonDataOptionSelect = <J, S extends {} = KeyValue>(
|
||||||
|
@ -37,7 +37,7 @@ export interface Props extends Themeable {
|
|||||||
height: number;
|
height: number;
|
||||||
width: number;
|
width: number;
|
||||||
field: FieldConfig;
|
field: FieldConfig;
|
||||||
display: DisplayProcessor;
|
display?: DisplayProcessor;
|
||||||
value: DisplayValue;
|
value: DisplayValue;
|
||||||
orientation: VizOrientation;
|
orientation: VizOrientation;
|
||||||
itemSpacing?: number;
|
itemSpacing?: number;
|
||||||
|
@ -18,10 +18,11 @@ export interface Props {
|
|||||||
height: number;
|
height: number;
|
||||||
/** Minimal column width specified in pixels */
|
/** Minimal column width specified in pixels */
|
||||||
columnMinWidth?: number;
|
columnMinWidth?: number;
|
||||||
|
noHeader?: boolean;
|
||||||
onCellClick?: TableFilterActionCallback;
|
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 theme = useTheme();
|
||||||
const [ref, headerRowMeasurements] = useMeasure();
|
const [ref, headerRowMeasurements] = useMeasure();
|
||||||
const tableStyles = getTableStyles(theme);
|
const tableStyles = getTableStyles(theme);
|
||||||
@ -67,15 +68,17 @@ export const Table: FC<Props> = memo(({ data, height, onCellClick, width, column
|
|||||||
return (
|
return (
|
||||||
<div {...getTableProps()} className={tableStyles.table}>
|
<div {...getTableProps()} className={tableStyles.table}>
|
||||||
<CustomScrollbar>
|
<CustomScrollbar>
|
||||||
<div>
|
{!noHeader && (
|
||||||
{headerGroups.map((headerGroup: any) => (
|
<div>
|
||||||
<div className={tableStyles.thead} {...headerGroup.getHeaderGroupProps()} ref={ref}>
|
{headerGroups.map((headerGroup: any) => (
|
||||||
{headerGroup.headers.map((column: any) =>
|
<div className={tableStyles.thead} {...headerGroup.getHeaderGroupProps()} ref={ref}>
|
||||||
renderHeaderCell(column, tableStyles.headerCell, data.fields[column.index])
|
{headerGroup.headers.map((column: any) =>
|
||||||
)}
|
renderHeaderCell(column, tableStyles.headerCell, data.fields[column.index])
|
||||||
</div>
|
)}
|
||||||
))}
|
</div>
|
||||||
</div>
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<FixedSizeList
|
<FixedSizeList
|
||||||
height={height - headerRowMeasurements.height}
|
height={height - headerRowMeasurements.height}
|
||||||
itemCount={rows.length}
|
itemCount={rows.length}
|
||||||
|
@ -1,17 +1,16 @@
|
|||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
import { css } from 'emotion';
|
import { css } from 'emotion';
|
||||||
import { Icon, selectThemeVariant, stylesFactory, Tab, TabsBar, useTheme } from '@grafana/ui';
|
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 { InspectTab } from './PanelInspector';
|
||||||
import { PanelModel } from '../../state';
|
import { PanelModel } from '../../state';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
tab: InspectTab;
|
tab: InspectTab;
|
||||||
tabs: Array<{ label: string; value: InspectTab }>;
|
tabs: Array<{ label: string; value: InspectTab }>;
|
||||||
stats: { requestTime: number; queries: number; dataSources: number };
|
panelData: PanelData;
|
||||||
panel: PanelModel;
|
panel: PanelModel;
|
||||||
isExpanded: boolean;
|
isExpanded: boolean;
|
||||||
|
|
||||||
onSelectTab: (tab: SelectableValue<InspectTab>) => void;
|
onSelectTab: (tab: SelectableValue<InspectTab>) => void;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onToggleExpand: () => void;
|
onToggleExpand: () => void;
|
||||||
@ -24,7 +23,7 @@ export const InspectHeader: FC<Props> = ({
|
|||||||
onClose,
|
onClose,
|
||||||
onToggleExpand,
|
onToggleExpand,
|
||||||
panel,
|
panel,
|
||||||
stats,
|
panelData,
|
||||||
isExpanded,
|
isExpanded,
|
||||||
}) => {
|
}) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
@ -42,7 +41,7 @@ export const InspectHeader: FC<Props> = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className={styles.titleWrapper}>
|
<div className={styles.titleWrapper}>
|
||||||
<h3>{panel.title}</h3>
|
<h3>{panel.title}</h3>
|
||||||
<div>{formatStats(stats)}</div>
|
<div className="muted">{formatStats(panelData)}</div>
|
||||||
</div>
|
</div>
|
||||||
<TabsBar className={styles.tabsBar}>
|
<TabsBar className={styles.tabsBar}>
|
||||||
{tabs.map((t, index) => {
|
{tabs.map((t, index) => {
|
||||||
@ -95,10 +94,15 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
function formatStats(stats: { requestTime: number; queries: number; dataSources: number }) {
|
function formatStats(panelData: PanelData) {
|
||||||
const queries = `${stats.queries} ${stats.queries === 1 ? 'query' : 'queries'}`;
|
const { request } = panelData;
|
||||||
const dataSources = `${stats.dataSources} ${stats.dataSources === 1 ? 'data source' : 'data sources'}`;
|
if (!request) {
|
||||||
const requestTime = `${stats.requestTime === -1 ? 'N/A' : stats.requestTime}ms`;
|
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,
|
toCSV,
|
||||||
DataQueryError,
|
DataQueryError,
|
||||||
PanelData,
|
PanelData,
|
||||||
DataQuery,
|
getValueFormat,
|
||||||
|
formattedValueToString,
|
||||||
|
QueryResultMetaStat,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { config } from 'app/core/config';
|
import { config } from 'app/core/config';
|
||||||
|
|
||||||
@ -51,8 +53,6 @@ interface State {
|
|||||||
// If the datasource supports custom metadata
|
// If the datasource supports custom metadata
|
||||||
metaDS?: DataSourceApi;
|
metaDS?: DataSourceApi;
|
||||||
|
|
||||||
stats: { requestTime: number; queries: number; dataSources: number; processingTime: number };
|
|
||||||
|
|
||||||
drawerWidth: string;
|
drawerWidth: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,7 +65,6 @@ export class PanelInspector extends PureComponent<Props, State> {
|
|||||||
selected: 0,
|
selected: 0,
|
||||||
tab: props.selectedTab || InspectTab.Data,
|
tab: props.selectedTab || InspectTab.Data,
|
||||||
drawerWidth: '50%',
|
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 error = lastResult.error;
|
||||||
|
|
||||||
const targets = lastResult.request?.targets || [];
|
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
|
// Find the first DataSource wanting to show custom metadata
|
||||||
if (data && targets.length) {
|
if (data && targets.length) {
|
||||||
const queries: Record<string, DataQuery> = {};
|
|
||||||
|
|
||||||
for (const target of targets) {
|
|
||||||
queries[target.refId] = target;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const frame of data) {
|
for (const frame of data) {
|
||||||
const q = queries[frame.refId];
|
if (frame.meta && frame.meta.custom) {
|
||||||
|
// get data source from first query
|
||||||
if (q && frame.meta && frame.meta.custom) {
|
const dataSource = await getDataSourceSrv().get(targets[0].datasource);
|
||||||
const dataSource = await getDataSourceSrv().get(q.datasource);
|
|
||||||
|
|
||||||
if (dataSource && dataSource.components?.MetadataInspector) {
|
if (dataSource && dataSource.components?.MetadataInspector) {
|
||||||
metaDS = dataSource;
|
metaDS = dataSource;
|
||||||
@ -121,12 +110,6 @@ export class PanelInspector extends PureComponent<Props, State> {
|
|||||||
data,
|
data,
|
||||||
metaDS,
|
metaDS,
|
||||||
tab: error ? InspectTab.Error : prevState.tab,
|
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({
|
const processed = applyFieldOverrides({
|
||||||
data,
|
data,
|
||||||
theme: config.theme,
|
theme: config.theme,
|
||||||
@ -251,29 +233,66 @@ export class PanelInspector extends PureComponent<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderStatsTab() {
|
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 (
|
return (
|
||||||
<table className="filter-table width-30">
|
<>
|
||||||
<tbody>
|
{this.renderStatsTable('Stats', stats)}
|
||||||
<tr>
|
{dataStats.length && this.renderStatsTable('Data source stats', dataStats)}
|
||||||
<td>Query time</td>
|
</>
|
||||||
<td>{`${stats.requestTime === -1 ? 'N/A' : stats.requestTime + 'ms'}`}</td>
|
);
|
||||||
</tr>
|
}
|
||||||
<tr>
|
|
||||||
<td>Data processing time</td>
|
renderStatsTable(name: string, stats: QueryResultMetaStat[]) {
|
||||||
<td>{`${
|
return (
|
||||||
stats.processingTime === -1
|
<div style={{ paddingBottom: '16px' }}>
|
||||||
? 'N/A'
|
<div className="section-heading">{name}</div>
|
||||||
: Math.round((stats.processingTime + Number.EPSILON) * 100) / 100 + 'ms'
|
<table className="filter-table width-30">
|
||||||
}`}</td>
|
<tbody>
|
||||||
</tr>
|
{stats.map(stat => {
|
||||||
</tbody>
|
return (
|
||||||
</table>
|
<tr>
|
||||||
|
<td>{stat.title}</td>
|
||||||
|
<td style={{ textAlign: 'right' }}>{formatStat(stat.value, stat.unit)}</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
drawerHeader = () => {
|
drawerHeader = () => {
|
||||||
const { tab, last, stats } = this.state;
|
const { tab, last } = this.state;
|
||||||
const error = last?.error;
|
const error = last?.error;
|
||||||
const tabs = [];
|
const tabs = [];
|
||||||
|
|
||||||
@ -296,7 +315,7 @@ export class PanelInspector extends PureComponent<Props, State> {
|
|||||||
<InspectHeader
|
<InspectHeader
|
||||||
tabs={tabs}
|
tabs={tabs}
|
||||||
tab={tab}
|
tab={tab}
|
||||||
stats={stats}
|
panelData={last}
|
||||||
onSelectTab={this.onSelectTab}
|
onSelectTab={this.onSelectTab}
|
||||||
onClose={this.onDismiss}
|
onClose={this.onDismiss}
|
||||||
panel={this.props.panel}
|
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(() => {
|
const getStyles = stylesFactory(() => {
|
||||||
return {
|
return {
|
||||||
toolbar: css`
|
toolbar: css`
|
||||||
|
@ -12,6 +12,7 @@ import { PanelChromeAngular } from './PanelChromeAngular';
|
|||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
import { initDashboardPanel } from '../state/actions';
|
import { initDashboardPanel } from '../state/actions';
|
||||||
|
import { updateLocation } from 'app/core/reducers/location';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
import { PanelModel, DashboardModel } from '../state';
|
import { PanelModel, DashboardModel } from '../state';
|
||||||
@ -33,6 +34,7 @@ export interface ConnectedProps {
|
|||||||
|
|
||||||
export interface DispatchProps {
|
export interface DispatchProps {
|
||||||
initDashboardPanel: typeof initDashboardPanel;
|
initDashboardPanel: typeof initDashboardPanel;
|
||||||
|
updateLocation: typeof updateLocation;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Props = OwnProps & ConnectedProps & DispatchProps;
|
export type Props = OwnProps & ConnectedProps & DispatchProps;
|
||||||
@ -72,7 +74,7 @@ export class DashboardPanelUnconnected extends PureComponent<Props, State> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
renderPanel(plugin: PanelPlugin) {
|
renderPanel(plugin: PanelPlugin) {
|
||||||
const { dashboard, panel, isFullscreen, isInView, isInEditMode } = this.props;
|
const { dashboard, panel, isFullscreen, isInView, isInEditMode, updateLocation } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AutoSizer>
|
<AutoSizer>
|
||||||
@ -105,6 +107,7 @@ export class DashboardPanelUnconnected extends PureComponent<Props, State> {
|
|||||||
isInEditMode={isInEditMode}
|
isInEditMode={isInEditMode}
|
||||||
width={width}
|
width={width}
|
||||||
height={height}
|
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);
|
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 { profiler } from 'app/core/profiler';
|
||||||
import { getProcessedDataFrames } from '../state/runRequest';
|
import { getProcessedDataFrames } from '../state/runRequest';
|
||||||
import config from 'app/core/config';
|
import config from 'app/core/config';
|
||||||
|
import { updateLocation } from 'app/core/actions';
|
||||||
// Types
|
// Types
|
||||||
import { DashboardModel, PanelModel } from '../state';
|
import { DashboardModel, PanelModel } from '../state';
|
||||||
import { PANEL_BORDER } from 'app/core/constants';
|
import { PANEL_BORDER } from 'app/core/constants';
|
||||||
@ -36,6 +37,7 @@ export interface Props {
|
|||||||
isInEditMode?: boolean;
|
isInEditMode?: boolean;
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
|
updateLocation: typeof updateLocation;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface State {
|
export interface State {
|
||||||
@ -43,8 +45,6 @@ export interface State {
|
|||||||
renderCounter: number;
|
renderCounter: number;
|
||||||
errorMessage?: string;
|
errorMessage?: string;
|
||||||
refreshWhenInView: boolean;
|
refreshWhenInView: boolean;
|
||||||
|
|
||||||
// Current state of all events
|
|
||||||
data: PanelData;
|
data: PanelData;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -312,7 +312,7 @@ export class PanelChrome extends PureComponent<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { dashboard, panel, isFullscreen, width, height } = this.props;
|
const { dashboard, panel, isFullscreen, width, height, updateLocation } = this.props;
|
||||||
const { errorMessage, data } = this.state;
|
const { errorMessage, data } = this.state;
|
||||||
const { transparent } = panel;
|
const { transparent } = panel;
|
||||||
|
|
||||||
@ -328,14 +328,14 @@ export class PanelChrome extends PureComponent<Props, State> {
|
|||||||
<PanelHeader
|
<PanelHeader
|
||||||
panel={panel}
|
panel={panel}
|
||||||
dashboard={dashboard}
|
dashboard={dashboard}
|
||||||
timeInfo={data.request ? data.request.timeInfo : undefined}
|
|
||||||
title={panel.title}
|
title={panel.title}
|
||||||
description={panel.description}
|
description={panel.description}
|
||||||
scopedVars={panel.scopedVars}
|
scopedVars={panel.scopedVars}
|
||||||
links={panel.links}
|
links={panel.links}
|
||||||
error={errorMessage}
|
error={errorMessage}
|
||||||
isFullscreen={isFullscreen}
|
isFullscreen={isFullscreen}
|
||||||
isLoading={data.state === LoadingState.Loading}
|
data={data}
|
||||||
|
updateLocation={updateLocation}
|
||||||
/>
|
/>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
{({ error }) => {
|
{({ error }) => {
|
||||||
|
@ -17,6 +17,7 @@ import config from 'app/core/config';
|
|||||||
import { DashboardModel, PanelModel } from '../state';
|
import { DashboardModel, PanelModel } from '../state';
|
||||||
import { StoreState } from 'app/types';
|
import { StoreState } from 'app/types';
|
||||||
import { LoadingState, DefaultTimeRange, PanelData, PanelPlugin, PanelEvents } from '@grafana/data';
|
import { LoadingState, DefaultTimeRange, PanelData, PanelPlugin, PanelEvents } from '@grafana/data';
|
||||||
|
import { updateLocation } from 'app/core/actions';
|
||||||
import { PANEL_BORDER } from 'app/core/constants';
|
import { PANEL_BORDER } from 'app/core/constants';
|
||||||
|
|
||||||
interface OwnProps {
|
interface OwnProps {
|
||||||
@ -35,6 +36,7 @@ interface ConnectedProps {
|
|||||||
|
|
||||||
interface DispatchProps {
|
interface DispatchProps {
|
||||||
setPanelAngularComponent: typeof setPanelAngularComponent;
|
setPanelAngularComponent: typeof setPanelAngularComponent;
|
||||||
|
updateLocation: typeof updateLocation;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Props = OwnProps & ConnectedProps & DispatchProps;
|
export type Props = OwnProps & ConnectedProps & DispatchProps;
|
||||||
@ -215,7 +217,7 @@ export class PanelChromeAngularUnconnected extends PureComponent<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
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 { errorMessage, data, alertState } = this.state;
|
||||||
const { transparent } = panel;
|
const { transparent } = panel;
|
||||||
|
|
||||||
@ -238,7 +240,6 @@ export class PanelChromeAngularUnconnected extends PureComponent<Props, State> {
|
|||||||
<PanelHeader
|
<PanelHeader
|
||||||
panel={panel}
|
panel={panel}
|
||||||
dashboard={dashboard}
|
dashboard={dashboard}
|
||||||
timeInfo={data.request ? data.request.timeInfo : undefined}
|
|
||||||
title={panel.title}
|
title={panel.title}
|
||||||
description={panel.description}
|
description={panel.description}
|
||||||
scopedVars={panel.scopedVars}
|
scopedVars={panel.scopedVars}
|
||||||
@ -246,7 +247,8 @@ export class PanelChromeAngularUnconnected extends PureComponent<Props, State> {
|
|||||||
links={panel.links}
|
links={panel.links}
|
||||||
error={errorMessage}
|
error={errorMessage}
|
||||||
isFullscreen={isFullscreen}
|
isFullscreen={isFullscreen}
|
||||||
isLoading={data.state === LoadingState.Loading}
|
data={data}
|
||||||
|
updateLocation={updateLocation}
|
||||||
/>
|
/>
|
||||||
<div className={panelContentClassNames}>
|
<div className={panelContentClassNames}>
|
||||||
<div ref={element => (this.element = element)} className="panel-height-helper" />
|
<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);
|
export const PanelChromeAngular = connect(mapStateToProps, mapDispatchToProps)(PanelChromeAngularUnconnected);
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { isEqual } from 'lodash';
|
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 { AngularComponent } from '@grafana/runtime';
|
||||||
import { ClickOutsideWrapper } from '@grafana/ui';
|
import { ClickOutsideWrapper, Tooltip } from '@grafana/ui';
|
||||||
import { e2e } from '@grafana/e2e';
|
import { e2e } from '@grafana/e2e';
|
||||||
|
|
||||||
import PanelHeaderCorner from './PanelHeaderCorner';
|
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 { PanelModel } from 'app/features/dashboard/state/PanelModel';
|
||||||
import { getPanelLinksSupplier } from 'app/features/panel/panellinks/linkSuppliers';
|
import { getPanelLinksSupplier } from 'app/features/panel/panellinks/linkSuppliers';
|
||||||
import { getPanelMenu } from 'app/features/dashboard/utils/getPanelMenu';
|
import { getPanelMenu } from 'app/features/dashboard/utils/getPanelMenu';
|
||||||
|
import { updateLocation } from 'app/core/actions';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
panel: PanelModel;
|
panel: PanelModel;
|
||||||
dashboard: DashboardModel;
|
dashboard: DashboardModel;
|
||||||
timeInfo?: string;
|
|
||||||
title?: string;
|
title?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
scopedVars?: ScopedVars;
|
scopedVars?: ScopedVars;
|
||||||
@ -26,7 +26,8 @@ export interface Props {
|
|||||||
links?: DataLink[];
|
links?: DataLink[];
|
||||||
error?: string;
|
error?: string;
|
||||||
isFullscreen: boolean;
|
isFullscreen: boolean;
|
||||||
isLoading: boolean;
|
data: PanelData;
|
||||||
|
updateLocation: typeof updateLocation;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ClickCoordinates {
|
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() {
|
render() {
|
||||||
const { panel, timeInfo, scopedVars, error, isFullscreen, isLoading } = this.props;
|
const { panel, scopedVars, error, isFullscreen, data } = this.props;
|
||||||
const { menuItems } = this.state;
|
const { menuItems } = this.state;
|
||||||
const title = templateSrv.replaceWithText(panel.title, scopedVars);
|
const title = templateSrv.replaceWithText(panel.title, scopedVars);
|
||||||
|
|
||||||
@ -102,9 +130,20 @@ export class PanelHeader extends Component<Props, State> {
|
|||||||
'grid-drag-handle': !isFullscreen,
|
'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 (
|
return (
|
||||||
<>
|
<>
|
||||||
{isLoading && this.renderLoadingState()}
|
{data.state === LoadingState.Loading && this.renderLoadingState()}
|
||||||
<div className={panelHeaderClass}>
|
<div className={panelHeaderClass}>
|
||||||
<PanelHeaderCorner
|
<PanelHeaderCorner
|
||||||
panel={panel}
|
panel={panel}
|
||||||
@ -121,6 +160,7 @@ export class PanelHeader extends Component<Props, State> {
|
|||||||
aria-label={e2e.pages.Dashboard.Panels.Panel.selectors.title(title)}
|
aria-label={e2e.pages.Dashboard.Panels.Panel.selectors.title(title)}
|
||||||
>
|
>
|
||||||
<div className="panel-title">
|
<div className="panel-title">
|
||||||
|
{Object.values(notices).map(this.renderNotice)}
|
||||||
<span className="icon-gf panel-alert-icon" />
|
<span className="icon-gf panel-alert-icon" />
|
||||||
<span className="panel-title-text">
|
<span className="panel-title-text">
|
||||||
{title} <span className="fa fa-caret-down panel-menu-toggle" />
|
{title} <span className="fa fa-caret-down panel-menu-toggle" />
|
||||||
@ -130,9 +170,9 @@ export class PanelHeader extends Component<Props, State> {
|
|||||||
<PanelHeaderMenu items={menuItems} />
|
<PanelHeaderMenu items={menuItems} />
|
||||||
</ClickOutsideWrapper>
|
</ClickOutsideWrapper>
|
||||||
)}
|
)}
|
||||||
{timeInfo && (
|
{data.request && data.request.timeInfo && (
|
||||||
<span className="panel-time-info">
|
<span className="panel-time-info">
|
||||||
<i className="fa fa-clock-o" /> {timeInfo}
|
<i className="fa fa-clock-o" /> {data.request.timeInfo}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,8 +1,12 @@
|
|||||||
|
import { css, cx } from 'emotion';
|
||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import { MetadataInspectorProps, DataFrame } from '@grafana/data';
|
import { MetadataInspectorProps } from '@grafana/data';
|
||||||
import { GraphiteDatasource } from './datasource';
|
import { GraphiteDatasource } from './datasource';
|
||||||
import { GraphiteQuery, GraphiteOptions, MetricTankMeta, MetricTankResultMeta } from './types';
|
import { GraphiteQuery, GraphiteOptions, MetricTankSeriesMeta } from './types';
|
||||||
import { parseSchemaRetentions } from './meta';
|
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>;
|
export type Props = MetadataInspectorProps<GraphiteDatasource, GraphiteQuery, GraphiteOptions>;
|
||||||
|
|
||||||
@ -11,48 +15,159 @@ export interface State {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class MetricTankMetaInspector extends PureComponent<Props, 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 (
|
return (
|
||||||
<div>
|
<div className={styles.metaItem} key={key}>
|
||||||
<h3>Info</h3>
|
<div className={styles.metaItemHeader}>Schema: {meta['schema-name']}</div>
|
||||||
<table>
|
<div className={styles.metaItemBody}>
|
||||||
<tbody>
|
<div className={styles.step}>
|
||||||
{buckets.map(row => (
|
<div className={styles.stepHeading}>Step 1: Fetch</div>
|
||||||
<tr key={row.interval}>
|
<div className={styles.stepDescription}>
|
||||||
<td>{row.interval} </td>
|
First data is fetched, either from raw data archive or a rollup archive
|
||||||
<td>{row.retention} </td>
|
</div>
|
||||||
<td>{row.chunkspan} </td>
|
|
||||||
<td>{row.numchunks} </td>
|
{rollupNotice && <p>{rollupNotice.text}</p>}
|
||||||
<td>{row.ready} </td>
|
{!rollupNotice && <p>No rollup archive was used</p>}
|
||||||
</tr>
|
|
||||||
))}
|
<div>
|
||||||
</tbody>
|
{buckets.map((bucket, index) => {
|
||||||
</table>
|
const bucketLength = kbn.interval_to_seconds(bucket.retention);
|
||||||
<pre>{JSON.stringify(info, null, 2)}</pre>
|
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>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { data } = this.props;
|
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];
|
if (Object.keys(seriesMetas).length === 0) {
|
||||||
const meta = frame.meta?.custom as MetricTankMeta;
|
return <div>No response meta data</div>;
|
||||||
if (!meta || !meta.info) {
|
|
||||||
return <>No Metadatata on DataFrame</>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h3>MetricTank Request</h3>
|
<h2 className="page-heading">Aggregation & rollup</h2>
|
||||||
<pre>{JSON.stringify(meta.request, null, 2)}</pre>
|
{Object.keys(seriesMetas).map(key => this.renderMeta(seriesMetas[key], key))}
|
||||||
{meta.info.map(info => this.renderInfo(info, frame))}
|
|
||||||
</div>
|
</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 React, { PureComponent } from 'react';
|
||||||
import { DataSourceHttpSettings, FormLabel, Button, Select } from '@grafana/ui';
|
import { DataSourceHttpSettings, FormLabel, Select, Switch } from '@grafana/ui';
|
||||||
import { DataSourcePluginOptionsEditorProps, onUpdateDatasourceJsonDataOptionSelect } from '@grafana/data';
|
import {
|
||||||
|
DataSourcePluginOptionsEditorProps,
|
||||||
|
onUpdateDatasourceJsonDataOptionSelect,
|
||||||
|
onUpdateDatasourceJsonDataOptionChecked,
|
||||||
|
} from '@grafana/data';
|
||||||
import { GraphiteOptions, GraphiteType } from '../types';
|
import { GraphiteOptions, GraphiteType } from '../types';
|
||||||
import styles from './ConfigEditor.styles';
|
|
||||||
|
|
||||||
const graphiteVersions = [
|
const graphiteVersions = [
|
||||||
{ label: '0.9.x', value: '0.9' },
|
{ label: '0.9.x', value: '0.9' },
|
||||||
@ -17,22 +20,27 @@ const graphiteTypes = Object.entries(GraphiteType).map(([label, value]) => ({
|
|||||||
|
|
||||||
export type Props = DataSourcePluginOptionsEditorProps<GraphiteOptions>;
|
export type Props = DataSourcePluginOptionsEditorProps<GraphiteOptions>;
|
||||||
|
|
||||||
interface State {
|
export class ConfigEditor extends PureComponent<Props> {
|
||||||
showMetricTankHelp: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ConfigEditor extends PureComponent<Props, State> {
|
|
||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
super(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() {
|
render() {
|
||||||
const { options, onOptionsChange } = this.props;
|
const { options, onOptionsChange } = this.props;
|
||||||
const { showMetricTankHelp } = this.state;
|
|
||||||
|
|
||||||
const currentVersion =
|
const currentVersion =
|
||||||
graphiteVersions.find(item => item.value === options.jsonData.graphiteVersion) ?? graphiteVersions[2];
|
graphiteVersions.find(item => item.value === options.jsonData.graphiteVersion) ?? graphiteVersions[2];
|
||||||
@ -61,39 +69,25 @@ export class ConfigEditor extends PureComponent<Props, State> {
|
|||||||
</div>
|
</div>
|
||||||
<div className="gf-form-inline">
|
<div className="gf-form-inline">
|
||||||
<div className="gf-form">
|
<div className="gf-form">
|
||||||
<FormLabel>Type</FormLabel>
|
<FormLabel tooltip={this.renderTypeHelp}>Type</FormLabel>
|
||||||
<Select
|
<Select
|
||||||
options={graphiteTypes}
|
options={graphiteTypes}
|
||||||
value={graphiteTypes.find(type => type.value === options.jsonData.graphiteType)}
|
value={graphiteTypes.find(type => type.value === options.jsonData.graphiteType)}
|
||||||
width={8}
|
width={8}
|
||||||
onChange={onUpdateDatasourceJsonDataOptionSelect(this.props, 'graphiteType')}
|
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>
|
||||||
</div>
|
</div>
|
||||||
{showMetricTankHelp && (
|
{options.jsonData.graphiteType === GraphiteType.Metrictank && (
|
||||||
<div className="grafana-info-box m-t-2">
|
<div className="gf-form-inline">
|
||||||
<div className="alert-body">
|
<div className="gf-form">
|
||||||
<p>
|
<Switch
|
||||||
There are different types of Graphite compatible backends. Here you can specify the type you are
|
label="Rollup indicator"
|
||||||
using. If you are using{' '}
|
labelClass={'width-10'}
|
||||||
<a href="https://github.com/grafana/metrictank" className="pointer" target="_blank">
|
tooltip="Shows up as an info icon in panel headers when data is aggregated"
|
||||||
Metrictank
|
checked={options.jsonData.rollupIndicatorEnabled}
|
||||||
</a>{' '}
|
onChange={onUpdateDatasourceJsonDataOptionChecked(this.props, 'rollupIndicatorEnabled')}
|
||||||
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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -7,14 +7,16 @@ import {
|
|||||||
DataQueryRequest,
|
DataQueryRequest,
|
||||||
toDataFrame,
|
toDataFrame,
|
||||||
DataSourceApi,
|
DataSourceApi,
|
||||||
|
QueryResultMetaStat,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { isVersionGtOrEq, SemVersion } from 'app/core/utils/version';
|
import { isVersionGtOrEq, SemVersion } from 'app/core/utils/version';
|
||||||
import gfunc from './gfunc';
|
import gfunc from './gfunc';
|
||||||
import { getBackendSrv } from '@grafana/runtime';
|
import { getBackendSrv } from '@grafana/runtime';
|
||||||
import { TemplateSrv } from 'app/features/templating/template_srv';
|
import { TemplateSrv } from 'app/features/templating/template_srv';
|
||||||
//Types
|
// Types
|
||||||
import { GraphiteOptions, GraphiteQuery, GraphiteType } from './types';
|
import { GraphiteOptions, GraphiteQuery, GraphiteType, MetricTankRequestMeta } from './types';
|
||||||
import { getSearchFilterScopedVar } from '../../../features/templating/variable';
|
import { getSearchFilterScopedVar } from '../../../features/templating/variable';
|
||||||
|
import { getRollupNotice, getRuntimeConsolidationNotice } from 'app/plugins/datasource/graphite/meta';
|
||||||
|
|
||||||
export class GraphiteDatasource extends DataSourceApi<GraphiteQuery, GraphiteOptions> {
|
export class GraphiteDatasource extends DataSourceApi<GraphiteQuery, GraphiteOptions> {
|
||||||
basicAuth: string;
|
basicAuth: string;
|
||||||
@ -23,6 +25,7 @@ export class GraphiteDatasource extends DataSourceApi<GraphiteQuery, GraphiteOpt
|
|||||||
graphiteVersion: any;
|
graphiteVersion: any;
|
||||||
supportsTags: boolean;
|
supportsTags: boolean;
|
||||||
isMetricTank: boolean;
|
isMetricTank: boolean;
|
||||||
|
rollupIndicatorEnabled: boolean;
|
||||||
cacheTimeout: any;
|
cacheTimeout: any;
|
||||||
withCredentials: boolean;
|
withCredentials: boolean;
|
||||||
funcDefs: any = null;
|
funcDefs: any = null;
|
||||||
@ -39,6 +42,7 @@ export class GraphiteDatasource extends DataSourceApi<GraphiteQuery, GraphiteOpt
|
|||||||
this.isMetricTank = instanceSettings.jsonData.graphiteType === GraphiteType.Metrictank;
|
this.isMetricTank = instanceSettings.jsonData.graphiteType === GraphiteType.Metrictank;
|
||||||
this.supportsTags = supportsTags(this.graphiteVersion);
|
this.supportsTags = supportsTags(this.graphiteVersion);
|
||||||
this.cacheTimeout = instanceSettings.cacheTimeout;
|
this.cacheTimeout = instanceSettings.cacheTimeout;
|
||||||
|
this.rollupIndicatorEnabled = instanceSettings.jsonData.rollupIndicatorEnabled;
|
||||||
this.withCredentials = instanceSettings.withCredentials;
|
this.withCredentials = instanceSettings.withCredentials;
|
||||||
this.funcDefs = null;
|
this.funcDefs = null;
|
||||||
this.funcDefsPromise = null;
|
this.funcDefsPromise = null;
|
||||||
@ -108,33 +112,71 @@ export class GraphiteDatasource extends DataSourceApi<GraphiteQuery, GraphiteOpt
|
|||||||
if (!result || !result.data) {
|
if (!result || !result.data) {
|
||||||
return { data };
|
return { data };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Series are either at the root or under a node called 'series'
|
// Series are either at the root or under a node called 'series'
|
||||||
const series = result.data.series || result.data;
|
const series = result.data.series || result.data;
|
||||||
|
|
||||||
if (!_.isArray(series)) {
|
if (!_.isArray(series)) {
|
||||||
throw { message: 'Missing series in result', data: result };
|
throw { message: 'Missing series in result', data: result };
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0; i < series.length; i++) {
|
for (let i = 0; i < series.length; i++) {
|
||||||
const s = series[i];
|
const s = series[i];
|
||||||
|
|
||||||
for (let y = 0; y < s.datapoints.length; y++) {
|
for (let y = 0; y < s.datapoints.length; y++) {
|
||||||
s.datapoints[y][1] *= 1000;
|
s.datapoints[y][1] *= 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
const frame = toDataFrame(s);
|
const frame = toDataFrame(s);
|
||||||
|
|
||||||
// Metrictank metadata
|
// Metrictank metadata
|
||||||
if (s.meta) {
|
if (s.meta) {
|
||||||
frame.meta = {
|
frame.meta = {
|
||||||
custom: {
|
custom: {
|
||||||
request: result.data.meta, // info for the whole request
|
requestMetaList: result.data.meta, // info for the whole request
|
||||||
info: s.meta, // Array of metadata
|
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);
|
data.push(frame);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { data };
|
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) {
|
parseTags(tagString: string) {
|
||||||
let tags: string[] = [];
|
let tags: string[] = [];
|
||||||
tags = tagString.split(',');
|
tags = tagString.split(',');
|
||||||
@ -278,7 +320,7 @@ export class GraphiteDatasource extends DataSourceApi<GraphiteQuery, GraphiteOpt
|
|||||||
return date.unix();
|
return date.unix();
|
||||||
}
|
}
|
||||||
|
|
||||||
metricFindQuery(query: string, optionalOptions: any) {
|
metricFindQuery(query: string, optionalOptions?: any) {
|
||||||
const options: any = optionalOptions || {};
|
const options: any = optionalOptions || {};
|
||||||
let interpolatedQuery = this.templateSrv.replace(
|
let interpolatedQuery = this.templateSrv.replace(
|
||||||
query,
|
query,
|
||||||
@ -573,7 +615,7 @@ export class GraphiteDatasource extends DataSourceApi<GraphiteQuery, GraphiteOpt
|
|||||||
return getBackendSrv().datasourceRequest(options);
|
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 graphiteOptions = ['from', 'until', 'rawData', 'format', 'maxDataPoints', 'cacheTimeout'];
|
||||||
const cleanOptions = [],
|
const cleanOptions = [],
|
||||||
targets: any = {};
|
targets: any = {};
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
export interface MetricTankResultMeta {
|
import { MetricTankSeriesMeta } from './types';
|
||||||
'schema-name': string;
|
import { QueryResultMetaNotice } from '@grafana/data';
|
||||||
'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
|
// 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;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function toBooleanOrTimestamp(val?: string): number | boolean | undefined {
|
function toBooleanOrTimestamp(val?: string): number | boolean | undefined {
|
||||||
if (val) {
|
if (val) {
|
||||||
if (val === 'true') {
|
if (val === 'true') {
|
||||||
@ -32,6 +31,44 @@ function toBooleanOrTimestamp(val?: string): number | boolean | undefined {
|
|||||||
return 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[] {
|
export function parseSchemaRetentions(spec: string): RetentionInfo[] {
|
||||||
if (!spec) {
|
if (!spec) {
|
||||||
return [];
|
return [];
|
||||||
|
@ -10,19 +10,94 @@ jest.mock('@grafana/runtime', () => ({
|
|||||||
getBackendSrv: () => backendSrv,
|
getBackendSrv: () => backendSrv,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
interface Context {
|
||||||
|
templateSrv: TemplateSrv;
|
||||||
|
ds: GraphiteDatasource;
|
||||||
|
}
|
||||||
|
|
||||||
describe('graphiteDatasource', () => {
|
describe('graphiteDatasource', () => {
|
||||||
const datasourceRequestMock = jest.spyOn(backendSrv, 'datasourceRequest');
|
const datasourceRequestMock = jest.spyOn(backendSrv, 'datasourceRequest');
|
||||||
|
|
||||||
const ctx: any = {
|
let ctx = {} as Context;
|
||||||
// @ts-ignore
|
|
||||||
templateSrv: new TemplateSrv(),
|
|
||||||
instanceSettings: { url: 'url', name: 'graphiteProd', jsonData: {} },
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.clearAllMocks();
|
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', () => {
|
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;
|
results = data;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -233,7 +308,7 @@ describe('graphiteDatasource', () => {
|
|||||||
|
|
||||||
describe('when formatting targets', () => {
|
describe('when formatting targets', () => {
|
||||||
it('does not attempt to glob for one variable', () => {
|
it('does not attempt to glob for one variable', () => {
|
||||||
ctx.ds.templateSrv.init([
|
ctx.templateSrv.init([
|
||||||
{
|
{
|
||||||
type: 'query',
|
type: 'query',
|
||||||
name: 'metric',
|
name: 'metric',
|
||||||
@ -248,7 +323,7 @@ describe('graphiteDatasource', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('globs for more than one variable', () => {
|
it('globs for more than one variable', () => {
|
||||||
ctx.ds.templateSrv.init([
|
ctx.templateSrv.init([
|
||||||
{
|
{
|
||||||
type: 'query',
|
type: 'query',
|
||||||
name: 'metric',
|
name: 'metric',
|
||||||
@ -259,6 +334,7 @@ describe('graphiteDatasource', () => {
|
|||||||
const results = ctx.ds.buildGraphiteParams({
|
const results = ctx.ds.buildGraphiteParams({
|
||||||
targets: [{ target: 'my.[[metric]].*' }],
|
targets: [{ target: 'my.[[metric]].*' }],
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(results).toStrictEqual(['target=my.%7Ba%2Cb%7D.*', 'format=json']);
|
expect(results).toStrictEqual(['target=my.%7Ba%2Cb%7D.*', 'format=json']);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -352,7 +428,7 @@ describe('graphiteDatasource', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('/metrics/find should be POST', () => {
|
it('/metrics/find should be POST', () => {
|
||||||
ctx.ds.templateSrv.init([
|
ctx.templateSrv.init([
|
||||||
{
|
{
|
||||||
type: 'query',
|
type: 'query',
|
||||||
name: 'foo',
|
name: 'foo',
|
||||||
|
@ -7,6 +7,7 @@ export interface GraphiteQuery extends DataQuery {
|
|||||||
export interface GraphiteOptions extends DataSourceJsonData {
|
export interface GraphiteOptions extends DataSourceJsonData {
|
||||||
graphiteVersion: string;
|
graphiteVersion: string;
|
||||||
graphiteType: GraphiteType;
|
graphiteType: GraphiteType;
|
||||||
|
rollupIndicatorEnabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum GraphiteType {
|
export enum GraphiteType {
|
||||||
@ -15,10 +16,10 @@ export enum GraphiteType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface MetricTankRequestMeta {
|
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-name': string;
|
||||||
'schema-retentions': string; //"1s:35d:20min:5:1542274085,1min:38d:2h:1:true,10min:120d:6h:1:true,2h:2y:6h:2",
|
'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-read': number;
|
||||||
@ -32,5 +33,5 @@ export interface MetricTankResultMeta {
|
|||||||
|
|
||||||
export interface MetricTankMeta {
|
export interface MetricTankMeta {
|
||||||
request: MetricTankRequestMeta;
|
request: MetricTankRequestMeta;
|
||||||
info: MetricTankResultMeta[];
|
info: MetricTankSeriesMeta[];
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,15 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { mount, ReactWrapper } from 'enzyme';
|
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 { BarGaugeDisplayMode } from '@grafana/ui';
|
||||||
|
|
||||||
import { BarGaugePanel } from './BarGaugePanel';
|
import { BarGaugePanel } from './BarGaugePanel';
|
||||||
@ -19,6 +28,27 @@ describe('BarGaugePanel', () => {
|
|||||||
expect(displayValue).toBe('No data');
|
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 {
|
function createTimeRange(): TimeRange {
|
||||||
|
@ -6,7 +6,7 @@ import { NewsOptions, DEFAULT_FEED_URL } from './types';
|
|||||||
const PROXY_PREFIX = 'https://cors-anywhere.herokuapp.com/';
|
const PROXY_PREFIX = 'https://cors-anywhere.herokuapp.com/';
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
feedUrl: string;
|
feedUrl?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class NewsPanelEditor extends PureComponent<PanelEditorProps<NewsOptions>, State> {
|
export class NewsPanelEditor extends PureComponent<PanelEditorProps<NewsOptions>, State> {
|
||||||
@ -41,27 +41,29 @@ export class NewsPanelEditor extends PureComponent<PanelEditorProps<NewsOptions>
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PanelOptionsGroup title="Feed">
|
<PanelOptionsGroup title="Feed">
|
||||||
<div className="gf-form">
|
<>
|
||||||
<FormField
|
<div className="gf-form">
|
||||||
label="URL"
|
<FormField
|
||||||
labelWidth={7}
|
label="URL"
|
||||||
inputWidth={30}
|
labelWidth={7}
|
||||||
value={feedUrl || ''}
|
inputWidth={30}
|
||||||
placeholder={DEFAULT_FEED_URL}
|
value={feedUrl || ''}
|
||||||
onChange={this.onFeedUrlChange}
|
placeholder={DEFAULT_FEED_URL}
|
||||||
tooltip="Only RSS feed formats are supported (not Atom)."
|
onChange={this.onFeedUrlChange}
|
||||||
onBlur={this.onUpdatePanel}
|
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>
|
</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>
|
</PanelOptionsGroup>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -175,7 +175,8 @@ class SingleStatCtrl extends MetricsPanelCtrl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const distinct = getDistinctNames(frames);
|
const distinct = getDistinctNames(frames);
|
||||||
let fieldInfo = distinct.byName[panel.tableColumn]; //
|
let fieldInfo: FieldInfo | undefined = distinct.byName[panel.tableColumn];
|
||||||
|
|
||||||
this.fieldNames = distinct.names;
|
this.fieldNames = distinct.names;
|
||||||
|
|
||||||
if (!fieldInfo) {
|
if (!fieldInfo) {
|
||||||
|
Loading…
Reference in New Issue
Block a user