React UI: Graph legend (#6321)

* initial commit

Signed-off-by: blalov <boyko.lalov@tick42.com>
Signed-off-by: Boyko Lalov <boyskila@gmail.com>

* eslint fixes

Signed-off-by: blalov <boyko.lalov@tick42.com>
Signed-off-by: Boyko Lalov <boyskila@gmail.com>

* hover bug fix

Signed-off-by: blalov <boyko.lalov@tick42.com>
Signed-off-by: Boyko Lalov <boyskila@gmail.com>

* refactoring

Signed-off-by: blalov <boyko.lalov@tick42.com>
Signed-off-by: Boyko Lalov <boyskila@gmail.com>

* remove unnecessary check

Signed-off-by: blalov <boyko.lalov@tick42.com>
Signed-off-by: Boyko Lalov <boyskila@gmail.com>

* fix tests

Signed-off-by: blalov <boyko.lalov@tick42.com>
Signed-off-by: Boyko Lalov <boyskila@gmail.com>

* lint fix

https://github.com/prometheus/prometheus/issues/6268
Signed-off-by: blalov <boyko.lalov@tick42.com>
Signed-off-by: Boyko Lalov <boyskila@gmail.com>

* fix typos

Fixes<https://github.com/prometheus/prometheus/issues/6268>

Signed-off-by: blalov <boyko.lalov@tick42.com>
Signed-off-by: Boyko Lalov <boyskila@gmail.com>

* init hover events if can

Fixes: <https://github.com/prometheus/prometheus/issues/6268>

Signed-off-by: blalov <boyko.lalov@tick42.com>
Signed-off-by: Boyko Lalov <boyskila@gmail.com>

* review changes

Signed-off-by: blalov <boyko.lalov@tick42.com>
Signed-off-by: Boyko Lalov <boyskila@gmail.com>

* fix activeIndex bug

Signed-off-by: Boyko Lalov <boyskila@gmail.com>

* extend plot options types

Signed-off-by: Boyko Lalov <boyskila@gmail.com>

* adding more types

Signed-off-by: blalov <boyko.lalov@tick42.com>

* fix branch after wrong force push

Signed-off-by: blalov <boyko.lalov@tick42.com>

* unit test fixes

Signed-off-by: blalov <boyko.lalov@tick42.com>

* remove unused variables

Signed-off-by: blalov <boyko.lalov@tick42.com>
This commit is contained in:
Boyko 2019-11-24 14:14:57 +02:00 committed by Julius Volz
parent cb92a45bf3
commit 731ca08acd
12 changed files with 269 additions and 267 deletions

View file

@ -135,16 +135,31 @@ div.time-input {
}
.graph-legend {
margin: 15px 0 15px 25px;
font-size: 0.8em;
margin: 15px 0 15px 55px;
font-size: 0.75em;
padding: 10px 5px;
display: inline-block;
cursor: pointer;
}
.graph-legend .legend-swatch {
padding: 5px;
height: 5px;
.legend-item {
display: flex;
align-items: center;
padding: 0 5px;
border-radius: 3px;
}
.legend-swatch {
width: 7px;
height: 7px;
outline-offset: 1px;
outline: 1.5px solid #ccc;
margin: 2px 8px 2px 0;
display: inline-block;
}
.legend-item:hover {
background: rgba(0, 0, 0, 0.18);
}
.legend-metric-name {
@ -191,7 +206,6 @@ div.time-input {
display: inline-block;
width: 10px;
height: 10px;
margin: 0 5px 0 0;
}
.add-panel-btn {

View file

@ -3,6 +3,7 @@ import React, { FC, ReactNode } from 'react';
import { Alert, Table } from 'reactstrap';
import SeriesName from './SeriesName';
import { Metric } from './types/types';
export interface QueryResult {
data:
@ -35,10 +36,6 @@ interface RangeSamples {
values: SampleValue[];
}
interface Metric {
[key: string]: string;
}
type SampleValue = [number, string];
const limitSeries = <S extends InstantSample | RangeSamples>(series: S[]): S[] => {

View file

@ -3,48 +3,49 @@ import { shallow } from 'enzyme';
import Graph from './Graph';
import { Alert } from 'reactstrap';
import ReactResizeDetector from 'react-resize-detector';
import Legend from './Legend';
describe('Graph', () => {
[
{
data: null,
color: 'light',
children: 'No data queried yet',
},
{
data: { resultType: 'invalid' },
it('renders an alert if data result type is different than "matrix"', () => {
const props: any = {
data: { resultType: 'invalid', result: [] },
stacked: false,
queryParams: {
startTime: 1572100210000,
endTime: 1572100217898,
resolution: 10,
},
color: 'danger',
children: `Query result is of wrong type '`,
},
{
};
const graph = shallow(<Graph {...props} />);
const alert = graph.find(Alert);
expect(alert.prop('color')).toEqual(props.color);
expect(alert.childAt(0).text()).toEqual(props.children);
});
it('renders an alert if data result empty', () => {
const props: any = {
data: {
resultType: 'matrix',
result: [],
},
color: 'secondary',
children: 'Empty query result',
},
].forEach(testCase => {
it(`renders an alert if data is "${testCase.data}"`, () => {
const props = {
data: testCase.data,
stacked: false,
queryParams: {
startTime: 1572100210000,
endTime: 1572100217898,
resolution: 10,
},
};
const graph = shallow(<Graph {...props} />);
const alert = graph.find(Alert);
expect(alert.prop('color')).toEqual(testCase.color);
expect(alert.childAt(0).text()).toEqual(testCase.children);
});
stacked: false,
queryParams: {
startTime: 1572100210000,
endTime: 1572100217898,
resolution: 10,
},
};
const graph = shallow(<Graph {...props} />);
const alert = graph.find(Alert);
expect(alert.prop('color')).toEqual(props.color);
expect(alert.childAt(0).text()).toEqual(props.children);
});
describe('data is returned', () => {
const props = {
const props: any = {
queryParams: {
startTime: 1572128592,
endTime: 1572130692,
@ -95,14 +96,12 @@ describe('Graph', () => {
const div = graph.find('div').filterWhere(elem => elem.prop('className') === 'graph');
const resize = div.find(ReactResizeDetector);
const innerdiv = div.find('div').filterWhere(elem => elem.prop('className') === 'graph-chart');
const legend = graph.find(Legend);
expect(resize.prop('handleWidth')).toBe(true);
expect(div).toHaveLength(1);
expect(innerdiv).toHaveLength(1);
expect(legend).toHaveLength(1);
});
it('formats tick values correctly', () => {
const graph = new Graph();
const graph = new Graph({ data: { result: [] }, queryParams: {} } as any);
[
{ input: 2e24, output: '2.00Y' },
{ input: 2e23, output: '200.00Z' },
@ -156,9 +155,15 @@ describe('Graph', () => {
{ input: 2e-24, output: '2.00y' },
{ input: 2e-25, output: '0.20y' },
{ input: 2e-26, output: '0.02y' },
].map(function(t) {
].map(t => {
expect(graph.formatValue(t.input)).toBe(t.output);
});
});
describe('Legend', () => {
it('renders a legend', () => {
const graph = shallow(<Graph {...props} />);
expect(graph.find('.graph-legend .legend-item')).toHaveLength(1);
});
});
});
});

View file

@ -3,9 +3,9 @@ import React, { PureComponent } from 'react';
import ReactResizeDetector from 'react-resize-detector';
import { Alert } from 'reactstrap';
import Legend from './Legend';
import { escapeHTML } from './utils/html';
import SeriesName from './SeriesName';
import { Metric, QueryParams } from './types/types';
require('flot');
require('flot/source/jquery.flot.crosshair');
require('flot/source/jquery.flot.legend');
@ -13,84 +13,84 @@ require('flot/source/jquery.flot.time');
require('flot/source/jquery.canvaswrapper');
require('jquery.flot.tooltip');
let graphID = 0;
function getGraphID() {
// TODO: This is ugly.
return graphID++;
}
interface GraphProps {
data: any; // TODO: Type this.
data: {
resultType: string;
result: Array<{ metric: Metric; values: [number, string][] }>;
};
stacked: boolean;
queryParams: {
startTime: number;
endTime: number;
resolution: number;
} | null;
queryParams: QueryParams | null;
}
class Graph extends PureComponent<GraphProps> {
private id: number = getGraphID();
export interface GraphSeries {
labels: { [key: string]: string };
color: string;
normalizedColor: string;
data: (number | null)[][]; // [x,y][]
index: number;
}
interface GraphState {
selectedSeriesIndex: number | null;
hoveredSeriesIndex: number | null;
}
class Graph extends PureComponent<GraphProps, GraphState> {
private chartRef = React.createRef<HTMLDivElement>();
renderLabels(labels: { [key: string]: string }) {
const labelStrings: string[] = [];
for (const label in labels) {
if (label !== '__name__') {
labelStrings.push('<strong>' + label + '</strong>: ' + escapeHTML(labels[label]));
}
}
return '<div class="labels">' + labelStrings.join('<br>') + '</div>';
}
state = {
selectedSeriesIndex: null,
hoveredSeriesIndex: null,
};
formatValue = (y: number | null): string => {
if (y === null) {
return 'null';
}
const abs_y = Math.abs(y);
if (abs_y >= 1e24) {
const absY = Math.abs(y);
if (absY >= 1e24) {
return (y / 1e24).toFixed(2) + 'Y';
} else if (abs_y >= 1e21) {
} else if (absY >= 1e21) {
return (y / 1e21).toFixed(2) + 'Z';
} else if (abs_y >= 1e18) {
} else if (absY >= 1e18) {
return (y / 1e18).toFixed(2) + 'E';
} else if (abs_y >= 1e15) {
} else if (absY >= 1e15) {
return (y / 1e15).toFixed(2) + 'P';
} else if (abs_y >= 1e12) {
} else if (absY >= 1e12) {
return (y / 1e12).toFixed(2) + 'T';
} else if (abs_y >= 1e9) {
} else if (absY >= 1e9) {
return (y / 1e9).toFixed(2) + 'G';
} else if (abs_y >= 1e6) {
} else if (absY >= 1e6) {
return (y / 1e6).toFixed(2) + 'M';
} else if (abs_y >= 1e3) {
} else if (absY >= 1e3) {
return (y / 1e3).toFixed(2) + 'k';
} else if (abs_y >= 1) {
} else if (absY >= 1) {
return y.toFixed(2);
} else if (abs_y === 0) {
} else if (absY === 0) {
return y.toFixed(2);
} else if (abs_y < 1e-23) {
} else if (absY < 1e-23) {
return (y / 1e-24).toFixed(2) + 'y';
} else if (abs_y < 1e-20) {
} else if (absY < 1e-20) {
return (y / 1e-21).toFixed(2) + 'z';
} else if (abs_y < 1e-17) {
} else if (absY < 1e-17) {
return (y / 1e-18).toFixed(2) + 'a';
} else if (abs_y < 1e-14) {
} else if (absY < 1e-14) {
return (y / 1e-15).toFixed(2) + 'f';
} else if (abs_y < 1e-11) {
} else if (absY < 1e-11) {
return (y / 1e-12).toFixed(2) + 'p';
} else if (abs_y < 1e-8) {
} else if (absY < 1e-8) {
return (y / 1e-9).toFixed(2) + 'n';
} else if (abs_y < 1e-5) {
} else if (absY < 1e-5) {
return (y / 1e-6).toFixed(2) + 'µ';
} else if (abs_y < 1e-2) {
} else if (absY < 1e-2) {
return (y / 1e-3).toFixed(2) + 'm';
} else if (abs_y <= 1) {
} else if (absY <= 1) {
return y.toFixed(2);
}
throw Error("couldn't format a value, this is a bug");
};
getOptions(): any {
getOptions(): jquery.flot.plotOptions {
return {
grid: {
hoverable: true,
@ -117,12 +117,22 @@ class Graph extends PureComponent<GraphProps> {
tooltip: {
show: true,
cssClass: 'graph-tooltip',
content: (label: string, xval: number, yval: number, flotItem: any) => {
const series = flotItem.series; // TODO: type this.
const date = '<span class="date">' + new Date(xval).toUTCString() + '</span>';
const swatch = '<span class="detail-swatch" style="background-color: ' + series.color + '"></span>';
const content = swatch + (series.labels.__name__ || 'value') + ': <strong>' + yval + '</strong>';
return date + '<br>' + content + '<br>' + this.renderLabels(series.labels);
content: (_, xval, yval, { series }): string => {
const { labels, color } = series;
return `
<div class="date">${new Date(xval).toUTCString()}</div>
<div>
<span class="detail-swatch" style="background-color: ${color}" />
<span>${labels.__name__ || 'value'}: <strong>${yval}</strong></span>
<div>
<div class="labels mt-1">
${Object.keys(labels)
.map(k =>
k !== '__name__' ? `<div class="mb-1"><strong>${k}</strong>: ${escapeHTML(labels[k])}</div>` : ''
)
.join('')}
</div>
`;
},
defaultTheme: false,
lines: true,
@ -141,15 +151,10 @@ class Graph extends PureComponent<GraphProps> {
// This was adapted from Flot's color generation code.
getColors() {
const colors = [];
const colorPool = ['#edc240', '#afd8f8', '#cb4b4b', '#4da74d', '#9440ed'];
const colorPoolSize = colorPool.length;
let variation = 0;
const neededColors = this.props.data.result.length;
for (let i = 0; i < neededColors; i++) {
const c = ($ as any).color.parse(colorPool[i % colorPoolSize] || '#666');
return this.props.data.result.map((_, i) => {
// Each time we exhaust the colors in the pool we adjust
// a scaling factor used to produce more variations on
// those colors. The factor alternates negative/positive
@ -160,45 +165,46 @@ class Graph extends PureComponent<GraphProps> {
if (i % colorPoolSize === 0 && i) {
if (variation >= 0) {
if (variation < 0.5) {
variation = -variation - 0.2;
} else variation = 0;
} else variation = -variation;
variation = variation < 0.5 ? -variation - 0.2 : 0;
} else {
variation = -variation;
}
}
colors[i] = c.scale('rgb', 1 + variation);
}
return colors;
return $.color.parse(colorPool[i % colorPoolSize] || '#666').scale('rgb', 1 + variation);
});
}
getData() {
getData(): GraphSeries[] {
const colors = this.getColors();
return this.props.data.result.map((ts: any /* TODO: Type this*/, index: number) => {
const { hoveredSeriesIndex } = this.state;
const { stacked, queryParams } = this.props;
const { startTime, endTime, resolution } = queryParams!;
return this.props.data.result.map((ts, index) => {
// Insert nulls for all missing steps.
const data = [];
let pos = 0;
const params = this.props.queryParams!;
for (let t = params.startTime; t <= params.endTime; t += params.resolution) {
for (let t = startTime; t <= endTime; t += resolution) {
// Allow for floating point inaccuracy.
if (ts.values.length > pos && ts.values[pos][0] < t + params.resolution / 100) {
data.push([ts.values[pos][0] * 1000, this.parseValue(ts.values[pos][1])]);
const currentValue = ts.values[pos];
if (ts.values.length > pos && currentValue[0] < t + resolution / 100) {
data.push([currentValue[0] * 1000, this.parseValue(currentValue[1])]);
pos++;
} else {
// TODO: Flot has problems displaying intermittent "null" values when stacked,
// resort to 0 now. In Grafana this works for some reason, figure out how they
// do it.
data.push([t * 1000, this.props.stacked ? 0 : null]);
data.push([t * 1000, stacked ? 0 : null]);
}
}
const { r, g, b } = colors[index];
return {
labels: ts.metric !== null ? ts.metric : {},
data: data,
color: colors[index],
index: index,
color: `rgba(${r}, ${g}, ${b}, ${hoveredSeriesIndex === null || hoveredSeriesIndex === index ? 1 : 0.3})`,
normalizedColor: `rgb(${r}, ${g}, ${b}`,
data,
index,
};
});
}
@ -217,11 +223,10 @@ class Graph extends PureComponent<GraphProps> {
return val;
}
componentDidMount() {
this.plot();
}
componentDidUpdate() {
componentDidUpdate(prevProps: GraphProps) {
if (prevProps.data !== this.props.data) {
this.setState({ selectedSeriesIndex: null });
}
this.plot();
}
@ -229,13 +234,14 @@ class Graph extends PureComponent<GraphProps> {
this.destroyPlot();
}
plot() {
if (this.chartRef.current === null) {
plot = () => {
if (!this.chartRef.current) {
return;
}
const selectedData = this.getData()[this.state.selectedSeriesIndex!];
this.destroyPlot();
$.plot($(this.chartRef.current!), this.getData(), this.getOptions());
}
$.plot($(this.chartRef.current), selectedData ? [selectedData] : this.getData(), this.getOptions());
};
destroyPlot() {
const chart = $(this.chartRef.current!).data('plot');
@ -244,6 +250,17 @@ class Graph extends PureComponent<GraphProps> {
}
}
handleSeriesSelect = (index: number) => () => {
const { selectedSeriesIndex } = this.state;
this.setState({ selectedSeriesIndex: selectedSeriesIndex !== index ? index : null });
};
handleSeriesHover = (index: number) => () => {
this.setState({ hoveredSeriesIndex: index });
};
handleLegendMouseOut = () => this.setState({ hoveredSeriesIndex: null });
render() {
if (this.props.data === null) {
return <Alert color="light">No data queried yet</Alert>;
@ -261,11 +278,28 @@ class Graph extends PureComponent<GraphProps> {
return <Alert color="secondary">Empty query result</Alert>;
}
const { selectedSeriesIndex } = this.state;
const series = this.getData();
const canUseHover = series.length > 1 && selectedSeriesIndex === null;
return (
<div className="graph">
<ReactResizeDetector handleWidth onResize={() => this.plot()} />
<ReactResizeDetector handleWidth onResize={this.plot} />
<div className="graph-chart" ref={this.chartRef} />
<Legend series={this.getData()} />
<div className="graph-legend" onMouseOut={canUseHover ? this.handleLegendMouseOut : undefined}>
{series.map(({ index, normalizedColor, labels }) => (
<div
style={{ opacity: selectedSeriesIndex !== null && index !== selectedSeriesIndex ? 0.7 : 1 }}
onClick={series.length > 1 ? this.handleSeriesSelect(index) : undefined}
onMouseOver={canUseHover ? this.handleSeriesHover(index) : undefined}
key={index}
className="legend-item"
>
<span className="legend-swatch" style={{ backgroundColor: normalizedColor }}></span>
<SeriesName labels={labels} format />
</div>
))}
</div>
</div>
);
}

View file

@ -1,93 +0,0 @@
import * as React from 'react';
import { shallow } from 'enzyme';
import Legend from './Legend';
import SeriesName from './SeriesName';
describe('Legend', () => {
describe('regardless of series', () => {
it('renders a table', () => {
const legend = shallow(<Legend series={[]} />);
expect(legend.type()).toEqual('table');
expect(legend.prop('className')).toEqual('graph-legend');
const tbody = legend.children();
expect(tbody.type()).toEqual('tbody');
});
});
describe('when series is empty', () => {
it('renders props as empty legend table', () => {
const legend = shallow(<Legend series={[]} />);
const tbody = legend.children();
expect(tbody.children()).toHaveLength(0);
});
});
describe('when series has one element', () => {
const legendProps = {
series: [
{
index: 1,
color: 'red',
labels: {
__name__: 'metric_name',
label1: 'value_1',
labeln: 'value_n',
},
},
],
};
it('renders a row of the one series', () => {
const legend = shallow(<Legend {...legendProps} />);
const tbody = legend.children();
expect(tbody.children()).toHaveLength(1);
const row = tbody.find('tr');
expect(row.prop('className')).toEqual('legend-item');
});
it('renders a legend swatch', () => {
const legend = shallow(<Legend {...legendProps} />);
const tbody = legend.children();
const row = tbody.find('tr');
const swatch = row.childAt(0);
expect(swatch.type()).toEqual('td');
expect(swatch.children().prop('className')).toEqual('legend-swatch');
expect(swatch.children().prop('style')).toEqual({
backgroundColor: 'red',
});
});
it('renders a series name', () => {
const legend = shallow(<Legend {...legendProps} />);
const tbody = legend.children();
const row = tbody.find('tr');
const series = row.childAt(1);
expect(series.type()).toEqual('td');
const seriesName = series.find(SeriesName);
expect(seriesName).toHaveLength(1);
expect(seriesName.prop('labels')).toEqual(legendProps.series[0].labels);
expect(seriesName.prop('format')).toBe(true);
});
});
describe('when series has _n_ elements', () => {
const range = Array.from(Array(20).keys());
const legendProps = {
series: range.map(i => ({
index: i,
color: 'red',
labels: {
__name__: `metric_name_${i}`,
label1: 'value_1',
labeln: 'value_n',
},
})),
};
it('renders _n_ rows', () => {
const legend = shallow(<Legend {...legendProps} />);
const tbody = legend.children();
expect(tbody.children()).toHaveLength(20);
const rows = tbody.find('tr');
rows.forEach(row => {
expect(row.prop('className')).toEqual('legend-item');
expect(row.find(SeriesName)).toHaveLength(1);
});
});
});
});

View file

@ -1,28 +0,0 @@
import React, { FC } from 'react';
import SeriesName from './SeriesName';
interface LegendProps {
series: any; // TODO: Type this.
}
const Legend: FC<LegendProps> = ({ series }) => {
return (
<table className="graph-legend">
<tbody>
{series.map((s: any) => (
<tr key={s.index} className="legend-item">
<td>
<div className="legend-swatch" style={{ backgroundColor: s.color }}></div>
</td>
<td>
<SeriesName labels={s.labels} format={true} />
</td>
</tr>
))}
</tbody>
</table>
);
};
export default Legend;

View file

@ -83,11 +83,12 @@ describe('Panel', () => {
};
const graphPanel = mount(<Panel {...props} options={options} />);
const controls = graphPanel.find(GraphControls);
graphPanel.setState({ data: [] });
const graph = graphPanel.find(Graph);
expect(controls.prop('endTime')).toEqual(props.options.endTime);
expect(controls.prop('range')).toEqual(props.options.range);
expect(controls.prop('resolution')).toEqual(props.options.resolution);
expect(controls.prop('stacked')).toEqual(props.options.stacked);
expect(graph.prop('stacked')).toEqual(props.options.stacked);
expect(controls.prop('endTime')).toEqual(options.endTime);
expect(controls.prop('range')).toEqual(options.range);
expect(controls.prop('resolution')).toEqual(options.resolution);
expect(controls.prop('stacked')).toEqual(options.stacked);
expect(graph.prop('stacked')).toEqual(options.stacked);
});
});

View file

@ -11,6 +11,7 @@ import DataTable from './DataTable';
import TimeInput from './TimeInput';
import QueryStatsView, { QueryStats } from './QueryStatsView';
import PathPrefixProps from './PathPrefixProps';
import { QueryParams } from './types/types';
interface PanelProps {
options: PanelOptions;
@ -23,12 +24,7 @@ interface PanelProps {
interface PanelState {
data: any; // TODO: Type data.
lastQueryParams: {
// TODO: Share these with Graph.tsx in a file.
startTime: number;
endTime: number;
resolution: number;
} | null;
lastQueryParams: QueryParams | null;
loading: boolean;
error: string | null;
stats: QueryStats | null;
@ -291,7 +287,11 @@ class Panel extends Component<PanelProps & PathPrefixProps, PanelState> {
onChangeResolution={this.handleChangeResolution}
onChangeStacking={this.handleChangeStacking}
/>
<Graph data={this.state.data} stacked={options.stacked} queryParams={this.state.lastQueryParams} />
{this.state.data ? (
<Graph data={this.state.data} stacked={options.stacked} queryParams={this.state.lastQueryParams} />
) : (
<Alert color="light">No data queried yet</Alert>
)}
</>
)}
</TabPane>

View file

@ -1,6 +1,6 @@
/* eslint @typescript-eslint/camelcase: 0 */
import { ScrapePools, Target, Labels } from '../target';
import { ScrapePools } from '../target';
export const targetGroups: ScrapePools = Object.freeze({
blackbox: {

View file

@ -2,7 +2,6 @@
import { sampleApiResponse } from './__testdata__/testdata';
import { groupTargets, Target, ScrapePools, getColor } from './target';
import { string } from 'prop-types';
describe('groupTargets', () => {
const targets: Target[] = sampleApiResponse.data.activeTargets as Target[];

64
web/ui/react-app/src/types/index.d.ts vendored Normal file
View file

@ -0,0 +1,64 @@
declare namespace jquery.flot {
// eslint-disable-next-line @typescript-eslint/class-name-casing
interface plotOptions extends jquery.flot.plotOptions {
tooltip: {
show?: boolean;
cssClass?: string;
content: (
label: string,
xval: number,
yval: number,
flotItem: jquery.flot.item & {
series: {
labels: { [key: string]: string };
color: string;
data: (number | null)[][]; // [x,y][]
index: number;
};
}
) => string | string;
xDateFormat?: string;
yDateFormat?: string;
monthNames?: string;
dayNames?: string;
shifts?: {
x: number;
y: number;
};
defaultTheme?: boolean;
lines?: boolean;
onHover?: () => string;
$compat?: boolean;
};
crosshair: Partial<jquery.flot.axisOptions, 'mode' | 'color'>;
xaxis: { [K in keyof jquery.flot.axisOptions]: jquery.flot.axisOptions[K] } & {
showTicks: boolean;
showMinorTicks: boolean;
timeBase: 'milliseconds';
};
series: { [K in keyof jquery.flot.seriesOptions]: jq.flot.seriesOptions[K] } & {
stack: boolean;
};
}
}
interface Color {
r: number;
g: number;
b: number;
a: number;
add: (c: string, d: number) => Color;
scale: (c: string, f: number) => Color;
toString: () => string;
normalize: () => Color;
clone: () => Color;
}
interface JQueryStatic {
color: {
extract: (el: JQuery<HTMLElement>, css?: CSSStyleDeclaration) => Color;
make: (r?: number, g?: number, b?: number, a?: number) => Color;
parse: (c: string) => Color;
scale: () => Color;
};
}

View file

@ -0,0 +1,9 @@
export interface Metric {
[key: string]: string;
}
export interface QueryParams {
startTime: number;
endTime: number;
resolution: number;
}