mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Dashboard: Use Explore's Prometheus editor in dashboard panel edit (#15364)
* WIP prometheus editor same in panel * Dont use panel in plugin editors * prettiered modified files * Fix step in external link * Prevent exiting edit mode when slate suggestions are shown * Blur behavior and $__interval variable * Remove unused query controller * Basic render test * Chore: Fixes blacklisted import * Refactor: Adds correct start and end time
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
import _ from 'lodash';
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { PrometheusDatasource } from '../datasource';
|
||||
import { PromQuery } from '../types';
|
||||
import { DataQueryRequest, PanelData } from '@grafana/ui';
|
||||
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||
|
||||
interface Props {
|
||||
datasource: PrometheusDatasource;
|
||||
query: PromQuery;
|
||||
panelData: PanelData;
|
||||
}
|
||||
|
||||
interface State {
|
||||
href: string;
|
||||
}
|
||||
|
||||
export default class PromLink extends Component<Props, State> {
|
||||
state: State = { href: null };
|
||||
async componentDidUpdate(prevProps: Props) {
|
||||
if (prevProps.panelData !== this.props.panelData && this.props.panelData.request) {
|
||||
const href = await this.getExternalLink();
|
||||
this.setState({ href });
|
||||
}
|
||||
}
|
||||
|
||||
async getExternalLink(): Promise<string> {
|
||||
const { query, panelData } = this.props;
|
||||
const target = panelData.request.targets.length > 0 ? panelData.request.targets[0] : { datasource: null };
|
||||
const datasourceName = target.datasource;
|
||||
const datasource: PrometheusDatasource = datasourceName
|
||||
? (((await getDatasourceSrv().get(datasourceName)) as any) as PrometheusDatasource)
|
||||
: (this.props.datasource as PrometheusDatasource);
|
||||
|
||||
const range = panelData.request.range;
|
||||
const start = datasource.getPrometheusTime(range.from, false);
|
||||
const end = datasource.getPrometheusTime(range.to, true);
|
||||
const rangeDiff = Math.ceil(end - start);
|
||||
const endTime = range.to.utc().format('YYYY-MM-DD HH:mm');
|
||||
|
||||
const options = {
|
||||
interval: panelData.request.interval,
|
||||
} as DataQueryRequest<PromQuery>;
|
||||
const queryOptions = datasource.createQuery(query, options, start, end);
|
||||
const expr = {
|
||||
'g0.expr': queryOptions.expr,
|
||||
'g0.range_input': rangeDiff + 's',
|
||||
'g0.end_input': endTime,
|
||||
'g0.step_input': queryOptions.step,
|
||||
'g0.tab': 0,
|
||||
};
|
||||
|
||||
const args = _.map(expr, (v: string, k: string) => {
|
||||
return k + '=' + encodeURIComponent(v);
|
||||
}).join('&');
|
||||
return `${datasource.directUrl}/graph?${args}`;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { href } = this.state;
|
||||
return (
|
||||
<a href={href} target="_blank">
|
||||
<i className="fa fa-share-square-o" /> Prometheus
|
||||
</a>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { dateTime } from '@grafana/ui';
|
||||
|
||||
import { PromQueryEditor } from './PromQueryEditor';
|
||||
import { PrometheusDatasource } from '../datasource';
|
||||
import { PromQuery } from '../types';
|
||||
|
||||
jest.mock('app/features/dashboard/services/TimeSrv', () => {
|
||||
return {
|
||||
getTimeSrv: () => ({
|
||||
timeRange: () => ({
|
||||
from: dateTime(),
|
||||
to: dateTime(),
|
||||
}),
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
const setup = (propOverrides?: object) => {
|
||||
const datasourceMock: unknown = {
|
||||
createQuery: jest.fn(q => q),
|
||||
getPrometheusTime: jest.fn((date, roundup) => 123),
|
||||
};
|
||||
const datasource: PrometheusDatasource = datasourceMock as PrometheusDatasource;
|
||||
const onRunQuery = jest.fn();
|
||||
const onChange = jest.fn();
|
||||
const query: PromQuery = { expr: '', refId: 'A' };
|
||||
|
||||
const props: any = {
|
||||
datasource,
|
||||
onChange,
|
||||
onRunQuery,
|
||||
query,
|
||||
};
|
||||
|
||||
Object.assign(props, propOverrides);
|
||||
|
||||
const wrapper = shallow(<PromQueryEditor {...props} />);
|
||||
const instance = wrapper.instance() as PromQueryEditor;
|
||||
|
||||
return {
|
||||
instance,
|
||||
wrapper,
|
||||
};
|
||||
};
|
||||
|
||||
describe('Render PromQueryEditor with basic options', () => {
|
||||
it('should render', () => {
|
||||
const { wrapper } = setup();
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,177 @@
|
||||
import _ from 'lodash';
|
||||
import React, { PureComponent } from 'react';
|
||||
|
||||
// Types
|
||||
import { FormLabel, Select, SelectOptionItem, Switch } from '@grafana/ui';
|
||||
import { QueryEditorProps, DataSourceStatus } from '@grafana/ui/src/types';
|
||||
|
||||
import { PrometheusDatasource } from '../datasource';
|
||||
import { PromQuery, PromOptions } from '../types';
|
||||
|
||||
import PromQueryField from './PromQueryField';
|
||||
import PromLink from './PromLink';
|
||||
|
||||
export type Props = QueryEditorProps<PrometheusDatasource, PromQuery, PromOptions>;
|
||||
|
||||
const FORMAT_OPTIONS: Array<SelectOptionItem<string>> = [
|
||||
{ label: 'Time series', value: 'time_series' },
|
||||
{ label: 'Table', value: 'table' },
|
||||
{ label: 'Heatmap', value: 'heatmap' },
|
||||
];
|
||||
|
||||
const INTERVAL_FACTOR_OPTIONS: Array<SelectOptionItem<number>> = _.map([1, 2, 3, 4, 5, 10], (value: number) => ({
|
||||
value,
|
||||
label: '1/' + value,
|
||||
}));
|
||||
|
||||
interface State {
|
||||
legendFormat: string;
|
||||
formatOption: SelectOptionItem<string>;
|
||||
interval: string;
|
||||
intervalFactorOption: SelectOptionItem<number>;
|
||||
instant: boolean;
|
||||
}
|
||||
|
||||
export class PromQueryEditor extends PureComponent<Props, State> {
|
||||
// Query target to be modified and used for queries
|
||||
query: PromQuery;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
const { query } = props;
|
||||
this.query = query;
|
||||
// Query target properties that are fullu controlled inputs
|
||||
this.state = {
|
||||
// Fully controlled text inputs
|
||||
interval: query.interval,
|
||||
legendFormat: query.legendFormat,
|
||||
// Select options
|
||||
formatOption: FORMAT_OPTIONS.find(option => option.value === query.format) || FORMAT_OPTIONS[0],
|
||||
intervalFactorOption:
|
||||
INTERVAL_FACTOR_OPTIONS.find(option => option.value === query.intervalFactor) || INTERVAL_FACTOR_OPTIONS[0],
|
||||
// Switch options
|
||||
instant: Boolean(query.instant),
|
||||
};
|
||||
}
|
||||
|
||||
onFieldChange = (query: PromQuery, override?) => {
|
||||
this.query.expr = query.expr;
|
||||
};
|
||||
|
||||
onFormatChange = (option: SelectOptionItem<string>) => {
|
||||
this.query.format = option.value;
|
||||
this.setState({ formatOption: option }, this.onRunQuery);
|
||||
};
|
||||
|
||||
onInstantChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const instant = e.target.checked;
|
||||
this.query.instant = instant;
|
||||
this.setState({ instant }, this.onRunQuery);
|
||||
};
|
||||
|
||||
onIntervalChange = (e: React.SyntheticEvent<HTMLInputElement>) => {
|
||||
const interval = e.currentTarget.value;
|
||||
this.query.interval = interval;
|
||||
this.setState({ interval });
|
||||
};
|
||||
|
||||
onIntervalFactorChange = (option: SelectOptionItem<number>) => {
|
||||
this.query.intervalFactor = option.value;
|
||||
this.setState({ intervalFactorOption: option }, this.onRunQuery);
|
||||
};
|
||||
|
||||
onLegendChange = (e: React.SyntheticEvent<HTMLInputElement>) => {
|
||||
const legendFormat = e.currentTarget.value;
|
||||
this.query.legendFormat = legendFormat;
|
||||
this.setState({ legendFormat });
|
||||
};
|
||||
|
||||
onRunQuery = () => {
|
||||
const { query } = this;
|
||||
this.props.onChange(query);
|
||||
this.props.onRunQuery();
|
||||
};
|
||||
|
||||
render() {
|
||||
const { datasource, query, panelData, queryResponse } = this.props;
|
||||
const { formatOption, instant, interval, intervalFactorOption, legendFormat } = this.state;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="gf-form-input" style={{ height: 'initial' }}>
|
||||
<PromQueryField
|
||||
datasource={datasource}
|
||||
query={query}
|
||||
onRunQuery={this.onRunQuery}
|
||||
onChange={this.onFieldChange}
|
||||
history={[]}
|
||||
panelData={panelData}
|
||||
queryResponse={queryResponse}
|
||||
datasourceStatus={DataSourceStatus.Connected} // TODO: replace with real DataSourceStatus
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="gf-form-inline">
|
||||
<div className="gf-form">
|
||||
<FormLabel
|
||||
width={7}
|
||||
tooltip="Controls the name of the time series, using name or pattern. For example
|
||||
{{hostname}} will be replaced with label value for the label hostname."
|
||||
>
|
||||
Legend
|
||||
</FormLabel>
|
||||
<input
|
||||
type="text"
|
||||
className="gf-form-input"
|
||||
placeholder="legend format"
|
||||
value={legendFormat}
|
||||
onChange={this.onLegendChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="gf-form">
|
||||
<FormLabel
|
||||
width={7}
|
||||
tooltip="Leave blank for auto handling based on time range and panel width.
|
||||
Note that the actual dates used in the query will be adjusted
|
||||
to a multiple of the interval step."
|
||||
>
|
||||
Min step
|
||||
</FormLabel>
|
||||
<input
|
||||
type="text"
|
||||
className="gf-form-input width-8"
|
||||
placeholder={interval}
|
||||
onChange={this.onIntervalChange}
|
||||
value={interval}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="gf-form">
|
||||
<div className="gf-form-label">Resolution</div>
|
||||
<Select
|
||||
isSearchable={false}
|
||||
options={INTERVAL_FACTOR_OPTIONS}
|
||||
onChange={this.onIntervalFactorChange}
|
||||
value={intervalFactorOption}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="gf-form">
|
||||
<div className="gf-form-label">Format</div>
|
||||
<Select isSearchable={false} options={FORMAT_OPTIONS} onChange={this.onFormatChange} value={formatOption} />
|
||||
<Switch label="Instant" checked={instant} onChange={this.onInstantChange} />
|
||||
|
||||
<FormLabel width={10} tooltip="Link to Graph in Prometheus">
|
||||
<PromLink
|
||||
datasource={datasource}
|
||||
query={this.query} // Use modified query
|
||||
panelData={panelData}
|
||||
/>
|
||||
</FormLabel>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -13,9 +13,10 @@ import { TypeaheadOutput, HistoryItem } from 'app/types/explore';
|
||||
import { getNextCharacter, getPreviousCousin } from 'app/features/explore/utils/dom';
|
||||
import BracesPlugin from 'app/features/explore/slate-plugins/braces';
|
||||
import QueryField, { TypeaheadInput, QueryFieldState } from 'app/features/explore/QueryField';
|
||||
import { PromQuery, PromContext } from '../types';
|
||||
import { PromQuery, PromContext, PromOptions } from '../types';
|
||||
import { CancelablePromise, makePromiseCancelable } from 'app/core/utils/CancelablePromise';
|
||||
import { DataSourceApi, ExploreQueryFieldProps, DataSourceStatus, QueryHint } from '@grafana/ui';
|
||||
import { ExploreQueryFieldProps, DataSourceStatus, QueryHint, isSeriesData, toLegacyResponseData } from '@grafana/ui';
|
||||
import { PrometheusDatasource } from '../datasource';
|
||||
|
||||
const HISTOGRAM_GROUP = '__histograms__';
|
||||
const METRIC_MARK = 'metric';
|
||||
@@ -101,7 +102,7 @@ interface CascaderOption {
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
interface PromQueryFieldProps extends ExploreQueryFieldProps<DataSourceApi<PromQuery>, PromQuery> {
|
||||
interface PromQueryFieldProps extends ExploreQueryFieldProps<PrometheusDatasource, PromQuery, PromOptions> {
|
||||
history: HistoryItem[];
|
||||
}
|
||||
|
||||
@@ -152,8 +153,9 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: PromQueryFieldProps) {
|
||||
const currentHasSeries = this.props.queryResponse.series && this.props.queryResponse.series.length > 0;
|
||||
if (currentHasSeries && prevProps.queryResponse.series !== this.props.queryResponse.series) {
|
||||
const { queryResponse } = this.props;
|
||||
const currentHasSeries = queryResponse && queryResponse.series && queryResponse.series.length > 0 ? true : false;
|
||||
if (currentHasSeries && prevProps.queryResponse && prevProps.queryResponse.series !== queryResponse.series) {
|
||||
this.refreshHint();
|
||||
}
|
||||
|
||||
@@ -175,11 +177,14 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
|
||||
|
||||
refreshHint = () => {
|
||||
const { datasource, query, queryResponse } = this.props;
|
||||
if (queryResponse.series && queryResponse.series.length === 0) {
|
||||
if (!queryResponse || !queryResponse.series || queryResponse.series.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hints = datasource.getQueryHints(query, queryResponse.series);
|
||||
const result = isSeriesData(queryResponse.series[0])
|
||||
? queryResponse.series.map(toLegacyResponseData)
|
||||
: queryResponse.series;
|
||||
const hints = datasource.getQueryHints(query, result);
|
||||
const hint = hints && hints.length > 0 ? hints[0] : null;
|
||||
this.setState({ hint });
|
||||
};
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Render PromQueryEditor with basic options should render 1`] = `
|
||||
<div>
|
||||
<div
|
||||
className="gf-form-input"
|
||||
style={
|
||||
Object {
|
||||
"height": "initial",
|
||||
}
|
||||
}
|
||||
>
|
||||
<PromQueryField
|
||||
datasource={
|
||||
Object {
|
||||
"createQuery": [MockFunction],
|
||||
"getPrometheusTime": [MockFunction],
|
||||
}
|
||||
}
|
||||
datasourceStatus={0}
|
||||
history={Array []}
|
||||
onChange={[Function]}
|
||||
onRunQuery={[Function]}
|
||||
query={
|
||||
Object {
|
||||
"expr": "",
|
||||
"refId": "A",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="gf-form-inline"
|
||||
>
|
||||
<div
|
||||
className="gf-form"
|
||||
>
|
||||
<Component
|
||||
tooltip="Controls the name of the time series, using name or pattern. For example
|
||||
{{hostname}} will be replaced with label value for the label hostname."
|
||||
width={7}
|
||||
>
|
||||
Legend
|
||||
</Component>
|
||||
<input
|
||||
className="gf-form-input"
|
||||
onChange={[Function]}
|
||||
placeholder="legend format"
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="gf-form"
|
||||
>
|
||||
<Component
|
||||
tooltip="Leave blank for auto handling based on time range and panel width.
|
||||
Note that the actual dates used in the query will be adjusted
|
||||
to a multiple of the interval step."
|
||||
width={7}
|
||||
>
|
||||
Min step
|
||||
</Component>
|
||||
<input
|
||||
className="gf-form-input width-8"
|
||||
onChange={[Function]}
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="gf-form"
|
||||
>
|
||||
<div
|
||||
className="gf-form-label"
|
||||
>
|
||||
Resolution
|
||||
</div>
|
||||
<Select
|
||||
autoFocus={false}
|
||||
backspaceRemovesValue={true}
|
||||
className=""
|
||||
components={
|
||||
Object {
|
||||
"Group": [Function],
|
||||
"IndicatorsContainer": [Function],
|
||||
"MenuList": [Function],
|
||||
"Option": [Function],
|
||||
"SingleValue": [Function],
|
||||
}
|
||||
}
|
||||
isClearable={false}
|
||||
isDisabled={false}
|
||||
isLoading={false}
|
||||
isMulti={false}
|
||||
isSearchable={false}
|
||||
maxMenuHeight={300}
|
||||
onChange={[Function]}
|
||||
openMenuOnFocus={false}
|
||||
options={
|
||||
Array [
|
||||
Object {
|
||||
"label": "1/1",
|
||||
"value": 1,
|
||||
},
|
||||
Object {
|
||||
"label": "1/2",
|
||||
"value": 2,
|
||||
},
|
||||
Object {
|
||||
"label": "1/3",
|
||||
"value": 3,
|
||||
},
|
||||
Object {
|
||||
"label": "1/4",
|
||||
"value": 4,
|
||||
},
|
||||
Object {
|
||||
"label": "1/5",
|
||||
"value": 5,
|
||||
},
|
||||
Object {
|
||||
"label": "1/10",
|
||||
"value": 10,
|
||||
},
|
||||
]
|
||||
}
|
||||
value={
|
||||
Object {
|
||||
"label": "1/1",
|
||||
"value": 1,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="gf-form"
|
||||
>
|
||||
<div
|
||||
className="gf-form-label"
|
||||
>
|
||||
Format
|
||||
</div>
|
||||
<Select
|
||||
autoFocus={false}
|
||||
backspaceRemovesValue={true}
|
||||
className=""
|
||||
components={
|
||||
Object {
|
||||
"Group": [Function],
|
||||
"IndicatorsContainer": [Function],
|
||||
"MenuList": [Function],
|
||||
"Option": [Function],
|
||||
"SingleValue": [Function],
|
||||
}
|
||||
}
|
||||
isClearable={false}
|
||||
isDisabled={false}
|
||||
isLoading={false}
|
||||
isMulti={false}
|
||||
isSearchable={false}
|
||||
maxMenuHeight={300}
|
||||
onChange={[Function]}
|
||||
openMenuOnFocus={false}
|
||||
options={
|
||||
Array [
|
||||
Object {
|
||||
"label": "Time series",
|
||||
"value": "time_series",
|
||||
},
|
||||
Object {
|
||||
"label": "Table",
|
||||
"value": "table",
|
||||
},
|
||||
Object {
|
||||
"label": "Heatmap",
|
||||
"value": "heatmap",
|
||||
},
|
||||
]
|
||||
}
|
||||
value={
|
||||
Object {
|
||||
"label": "Time series",
|
||||
"value": "time_series",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Switch
|
||||
checked={false}
|
||||
label="Instant"
|
||||
onChange={[Function]}
|
||||
/>
|
||||
<Component
|
||||
tooltip="Link to Graph in Prometheus"
|
||||
width={10}
|
||||
>
|
||||
<PromLink
|
||||
datasource={
|
||||
Object {
|
||||
"createQuery": [MockFunction],
|
||||
"getPrometheusTime": [MockFunction],
|
||||
}
|
||||
}
|
||||
query={
|
||||
Object {
|
||||
"expr": "",
|
||||
"refId": "A",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</Component>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
Reference in New Issue
Block a user