mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Prometheus: Adds missing options to the new query builder (#44915)
* Prometheus: Query builder legend format, first working state * Added format option * Working options section * Wrapping stuff up removing old stuff * Fixed ts issues * Review fixes * Added unit tests for preview toggle
This commit is contained in:
parent
d9d1f8520e
commit
f92ab9bc72
@ -16,16 +16,7 @@ export interface PromExploreExtraFieldProps {
|
|||||||
|
|
||||||
export const PromExploreExtraField: React.FC<PromExploreExtraFieldProps> = memo(
|
export const PromExploreExtraField: React.FC<PromExploreExtraFieldProps> = memo(
|
||||||
({ query, datasource, onChange, onRunQuery }) => {
|
({ query, datasource, onChange, onRunQuery }) => {
|
||||||
const rangeOptions = [
|
const rangeOptions = getQueryTypeOptions(true);
|
||||||
{ value: 'range', label: 'Range', description: 'Run query over a range of time.' },
|
|
||||||
{
|
|
||||||
value: 'instant',
|
|
||||||
label: 'Instant',
|
|
||||||
description: 'Run query against a single point in time. For this query, the "To" time is used.',
|
|
||||||
},
|
|
||||||
{ value: 'both', label: 'Both', description: 'Run an Instant query and a Range query.' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const prevQuery = usePrevious(query);
|
const prevQuery = usePrevious(query);
|
||||||
|
|
||||||
const onExemplarChange = useCallback(
|
const onExemplarChange = useCallback(
|
||||||
@ -53,17 +44,7 @@ export const PromExploreExtraField: React.FC<PromExploreExtraFieldProps> = memo(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onQueryTypeChange(queryType: string) {
|
const onQueryTypeChange = getQueryTypeChangeHandler(query, onChange);
|
||||||
let nextQuery;
|
|
||||||
if (queryType === 'instant') {
|
|
||||||
nextQuery = { ...query, instant: true, range: false };
|
|
||||||
} else if (queryType === 'range') {
|
|
||||||
nextQuery = { ...query, instant: false, range: true };
|
|
||||||
} else {
|
|
||||||
nextQuery = { ...query, instant: true, range: true };
|
|
||||||
}
|
|
||||||
onChange(nextQuery);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div aria-label="Prometheus extra field" className="gf-form-inline" data-testid={testIds.extraFieldEditor}>
|
<div aria-label="Prometheus extra field" className="gf-form-inline" data-testid={testIds.extraFieldEditor}>
|
||||||
@ -123,6 +104,35 @@ export const PromExploreExtraField: React.FC<PromExploreExtraFieldProps> = memo(
|
|||||||
|
|
||||||
PromExploreExtraField.displayName = 'PromExploreExtraField';
|
PromExploreExtraField.displayName = 'PromExploreExtraField';
|
||||||
|
|
||||||
|
export function getQueryTypeOptions(includeBoth: boolean) {
|
||||||
|
const rangeOptions = [
|
||||||
|
{ value: 'range', label: 'Range', description: 'Run query over a range of time' },
|
||||||
|
{
|
||||||
|
value: 'instant',
|
||||||
|
label: 'Instant',
|
||||||
|
description: 'Run query against a single point in time. For this query, the "To" time is used',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
if (includeBoth) {
|
||||||
|
rangeOptions.push({ value: 'both', label: 'Both', description: 'Run an Instant query and a Range query' });
|
||||||
|
}
|
||||||
|
|
||||||
|
return rangeOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getQueryTypeChangeHandler(query: PromQuery, onChange: (update: PromQuery) => void) {
|
||||||
|
return (queryType: string) => {
|
||||||
|
if (queryType === 'instant') {
|
||||||
|
onChange({ ...query, instant: true, range: false });
|
||||||
|
} else if (queryType === 'range') {
|
||||||
|
onChange({ ...query, instant: false, range: true });
|
||||||
|
} else {
|
||||||
|
onChange({ ...query, instant: true, range: true });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export const testIds = {
|
export const testIds = {
|
||||||
extraFieldEditor: 'prom-editor-extra-field',
|
extraFieldEditor: 'prom-editor-extra-field',
|
||||||
stepField: 'prom-editor-extra-field-step',
|
stepField: 'prom-editor-extra-field-step',
|
||||||
|
@ -13,7 +13,7 @@ import { PromQueryEditorProps } from './types';
|
|||||||
|
|
||||||
const { Switch } = LegacyForms;
|
const { Switch } = LegacyForms;
|
||||||
|
|
||||||
const FORMAT_OPTIONS: Array<SelectableValue<string>> = [
|
export const FORMAT_OPTIONS: Array<SelectableValue<string>> = [
|
||||||
{ label: 'Time series', value: 'time_series' },
|
{ label: 'Time series', value: 'time_series' },
|
||||||
{ label: 'Table', value: 'table' },
|
{ label: 'Table', value: 'table' },
|
||||||
{ label: 'Heatmap', value: 'heatmap' },
|
{ label: 'Heatmap', value: 'heatmap' },
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { PromQueryModeller } from './PromQueryModeller';
|
import { PromQueryModeller } from './PromQueryModeller';
|
||||||
|
import { PromOperationId } from './types';
|
||||||
|
|
||||||
describe('PromQueryModeller', () => {
|
describe('PromQueryModeller', () => {
|
||||||
const modeller = new PromQueryModeller();
|
const modeller = new PromQueryModeller();
|
||||||
@ -41,7 +42,7 @@ describe('PromQueryModeller', () => {
|
|||||||
modeller.renderQuery({
|
modeller.renderQuery({
|
||||||
metric: 'metric',
|
metric: 'metric',
|
||||||
labels: [],
|
labels: [],
|
||||||
operations: [{ id: 'histogram_quantile', params: [0.86] }],
|
operations: [{ id: PromOperationId.HistogramQuantile, params: [0.86] }],
|
||||||
})
|
})
|
||||||
).toBe('histogram_quantile(0.86, metric)');
|
).toBe('histogram_quantile(0.86, metric)');
|
||||||
});
|
});
|
||||||
@ -51,7 +52,7 @@ describe('PromQueryModeller', () => {
|
|||||||
modeller.renderQuery({
|
modeller.renderQuery({
|
||||||
metric: 'metric',
|
metric: 'metric',
|
||||||
labels: [],
|
labels: [],
|
||||||
operations: [{ id: 'label_replace', params: ['server', '$1', 'instance', 'as(.*)d'] }],
|
operations: [{ id: PromOperationId.LabelReplace, params: ['server', '$1', 'instance', 'as(.*)d'] }],
|
||||||
})
|
})
|
||||||
).toBe('label_replace(metric, "server", "$1", "instance", "as(.*)d")');
|
).toBe('label_replace(metric, "server", "$1", "instance", "as(.*)d")');
|
||||||
});
|
});
|
||||||
@ -94,7 +95,7 @@ describe('PromQueryModeller', () => {
|
|||||||
modeller.renderQuery({
|
modeller.renderQuery({
|
||||||
metric: 'metric',
|
metric: 'metric',
|
||||||
labels: [{ label: 'pod', op: '=', value: 'A' }],
|
labels: [{ label: 'pod', op: '=', value: 'A' }],
|
||||||
operations: [{ id: 'rate', params: ['auto'] }],
|
operations: [{ id: PromOperationId.Rate, params: ['auto'] }],
|
||||||
})
|
})
|
||||||
).toBe('rate(metric{pod="A"}[$__rate_interval])');
|
).toBe('rate(metric{pod="A"}[$__rate_interval])');
|
||||||
});
|
});
|
||||||
@ -104,7 +105,7 @@ describe('PromQueryModeller', () => {
|
|||||||
modeller.renderQuery({
|
modeller.renderQuery({
|
||||||
metric: 'metric',
|
metric: 'metric',
|
||||||
labels: [{ label: 'pod', op: '=', value: 'A' }],
|
labels: [{ label: 'pod', op: '=', value: 'A' }],
|
||||||
operations: [{ id: 'increase', params: ['auto'] }],
|
operations: [{ id: PromOperationId.Increase, params: ['auto'] }],
|
||||||
})
|
})
|
||||||
).toBe('increase(metric{pod="A"}[$__rate_interval])');
|
).toBe('increase(metric{pod="A"}[$__rate_interval])');
|
||||||
});
|
});
|
||||||
@ -114,7 +115,7 @@ describe('PromQueryModeller', () => {
|
|||||||
modeller.renderQuery({
|
modeller.renderQuery({
|
||||||
metric: 'metric',
|
metric: 'metric',
|
||||||
labels: [{ label: 'pod', op: '=', value: 'A' }],
|
labels: [{ label: 'pod', op: '=', value: 'A' }],
|
||||||
operations: [{ id: 'rate', params: ['10m'] }],
|
operations: [{ id: PromOperationId.Rate, params: ['10m'] }],
|
||||||
})
|
})
|
||||||
).toBe('rate(metric{pod="A"}[10m])');
|
).toBe('rate(metric{pod="A"}[10m])');
|
||||||
});
|
});
|
||||||
@ -124,7 +125,7 @@ describe('PromQueryModeller', () => {
|
|||||||
modeller.renderQuery({
|
modeller.renderQuery({
|
||||||
metric: 'metric',
|
metric: 'metric',
|
||||||
labels: [],
|
labels: [],
|
||||||
operations: [{ id: '__multiply_by', params: [1000] }],
|
operations: [{ id: PromOperationId.MultiplyBy, params: [1000] }],
|
||||||
})
|
})
|
||||||
).toBe('metric * 1000');
|
).toBe('metric * 1000');
|
||||||
});
|
});
|
||||||
|
@ -47,8 +47,6 @@ const bugQuery: PromVisualQuery = {
|
|||||||
describe('PromQueryBuilder', () => {
|
describe('PromQueryBuilder', () => {
|
||||||
it('shows empty just with metric selected', async () => {
|
it('shows empty just with metric selected', async () => {
|
||||||
setup();
|
setup();
|
||||||
// One should be select another query preview
|
|
||||||
expect(screen.getAllByText('random_metric').length).toBe(2);
|
|
||||||
// Add label
|
// Add label
|
||||||
expect(screen.getByLabelText('Add')).toBeInTheDocument();
|
expect(screen.getByLabelText('Add')).toBeInTheDocument();
|
||||||
expect(screen.getByLabelText('Add operation')).toBeInTheDocument();
|
expect(screen.getByLabelText('Add operation')).toBeInTheDocument();
|
||||||
@ -67,9 +65,6 @@ describe('PromQueryBuilder', () => {
|
|||||||
expect(screen.getByText('Binary operations')).toBeInTheDocument();
|
expect(screen.getByText('Binary operations')).toBeInTheDocument();
|
||||||
expect(screen.getByText('Operator')).toBeInTheDocument();
|
expect(screen.getByText('Operator')).toBeInTheDocument();
|
||||||
expect(screen.getByText('Vector matches')).toBeInTheDocument();
|
expect(screen.getByText('Vector matches')).toBeInTheDocument();
|
||||||
expect(screen.getByLabelText('selector').textContent).toBe(
|
|
||||||
'sum by(instance, job) (rate(random_metric{instance="localhost:9090"}[$__rate_interval])) / sum by(app) (metric2{foo="bar"})'
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('tries to load metrics without labels', async () => {
|
it('tries to load metrics without labels', async () => {
|
||||||
|
@ -3,12 +3,11 @@ import { MetricSelect } from './MetricSelect';
|
|||||||
import { PromVisualQuery } from '../types';
|
import { PromVisualQuery } from '../types';
|
||||||
import { LabelFilters } from '../shared/LabelFilters';
|
import { LabelFilters } from '../shared/LabelFilters';
|
||||||
import { OperationList } from '../shared/OperationList';
|
import { OperationList } from '../shared/OperationList';
|
||||||
import { EditorRows, EditorRow } from '@grafana/experimental';
|
import { EditorRow } from '@grafana/experimental';
|
||||||
import { PrometheusDatasource } from '../../datasource';
|
import { PrometheusDatasource } from '../../datasource';
|
||||||
import { NestedQueryList } from './NestedQueryList';
|
import { NestedQueryList } from './NestedQueryList';
|
||||||
import { promQueryModeller } from '../PromQueryModeller';
|
import { promQueryModeller } from '../PromQueryModeller';
|
||||||
import { QueryBuilderLabelFilter } from '../shared/types';
|
import { QueryBuilderLabelFilter } from '../shared/types';
|
||||||
import { QueryPreview } from './QueryPreview';
|
|
||||||
import { DataSourceApi, SelectableValue } from '@grafana/data';
|
import { DataSourceApi, SelectableValue } from '@grafana/data';
|
||||||
import { OperationsEditorRow } from '../shared/OperationsEditorRow';
|
import { OperationsEditorRow } from '../shared/OperationsEditorRow';
|
||||||
|
|
||||||
@ -20,7 +19,7 @@ export interface Props {
|
|||||||
nested?: boolean;
|
nested?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PromQueryBuilder = React.memo<Props>(({ datasource, query, onChange, onRunQuery, nested }) => {
|
export const PromQueryBuilder = React.memo<Props>(({ datasource, query, onChange, onRunQuery }) => {
|
||||||
const onChangeLabels = (labels: QueryBuilderLabelFilter[]) => {
|
const onChangeLabels = (labels: QueryBuilderLabelFilter[]) => {
|
||||||
onChange({ ...query, labels });
|
onChange({ ...query, labels });
|
||||||
};
|
};
|
||||||
@ -78,7 +77,7 @@ export const PromQueryBuilder = React.memo<Props>(({ datasource, query, onChange
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EditorRows>
|
<>
|
||||||
<EditorRow>
|
<EditorRow>
|
||||||
<MetricSelect
|
<MetricSelect
|
||||||
query={query}
|
query={query}
|
||||||
@ -108,12 +107,7 @@ export const PromQueryBuilder = React.memo<Props>(({ datasource, query, onChange
|
|||||||
<NestedQueryList query={query} datasource={datasource} onChange={onChange} onRunQuery={onRunQuery} />
|
<NestedQueryList query={query} datasource={datasource} onChange={onChange} onRunQuery={onRunQuery} />
|
||||||
)}
|
)}
|
||||||
</OperationsEditorRow>
|
</OperationsEditorRow>
|
||||||
{!nested && (
|
</>
|
||||||
<EditorRow>
|
|
||||||
<QueryPreview query={query} />
|
|
||||||
</EditorRow>
|
|
||||||
)}
|
|
||||||
</EditorRows>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -0,0 +1,119 @@
|
|||||||
|
import React, { SyntheticEvent } from 'react';
|
||||||
|
import { EditorRow, EditorField } from '@grafana/experimental';
|
||||||
|
import { CoreApp, SelectableValue } from '@grafana/data';
|
||||||
|
import { Input, RadioButtonGroup, Select, Switch } from '@grafana/ui';
|
||||||
|
import { QueryOptionGroup } from '../shared/QueryOptionGroup';
|
||||||
|
import { PromQuery } from '../../types';
|
||||||
|
import { FORMAT_OPTIONS } from '../../components/PromQueryEditor';
|
||||||
|
import { getQueryTypeChangeHandler, getQueryTypeOptions } from '../../components/PromExploreExtraField';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
query: PromQuery;
|
||||||
|
app?: CoreApp;
|
||||||
|
onChange: (update: PromQuery) => void;
|
||||||
|
onRunQuery: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PromQueryBuilderOptions = React.memo<Props>(({ query, app, onChange, onRunQuery }) => {
|
||||||
|
const formatOption = FORMAT_OPTIONS.find((option) => option.value === query.format) || FORMAT_OPTIONS[0];
|
||||||
|
|
||||||
|
const onChangeFormat = (value: SelectableValue<string>) => {
|
||||||
|
onChange({ ...query, format: value.value });
|
||||||
|
onRunQuery();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onLegendFormatChanged = (evt: React.FocusEvent<HTMLInputElement>) => {
|
||||||
|
onChange({ ...query, legendFormat: evt.currentTarget.value });
|
||||||
|
onRunQuery();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onChangeStep = (evt: React.FocusEvent<HTMLInputElement>) => {
|
||||||
|
onChange({ ...query, interval: evt.currentTarget.value });
|
||||||
|
onRunQuery();
|
||||||
|
};
|
||||||
|
|
||||||
|
const queryTypeOptions = getQueryTypeOptions(false);
|
||||||
|
const onQueryTypeChange = getQueryTypeChangeHandler(query, onChange);
|
||||||
|
|
||||||
|
const onExemplarChange = (event: SyntheticEvent<HTMLInputElement>) => {
|
||||||
|
const isEnabled = event.currentTarget.checked;
|
||||||
|
onChange({ ...query, exemplar: isEnabled });
|
||||||
|
onRunQuery();
|
||||||
|
};
|
||||||
|
|
||||||
|
const showExemplarSwitch = app !== CoreApp.UnifiedAlerting && !query.instant;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EditorRow>
|
||||||
|
<QueryOptionGroup title="Options" collapsedInfo={getCollapsedInfo(query, formatOption)}>
|
||||||
|
<EditorField
|
||||||
|
label="Legend"
|
||||||
|
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."
|
||||||
|
>
|
||||||
|
<Input placeholder="auto" defaultValue={query.legendFormat} onBlur={onLegendFormatChanged} />
|
||||||
|
</EditorField>
|
||||||
|
<EditorField
|
||||||
|
label="Min step"
|
||||||
|
tooltip={
|
||||||
|
<>
|
||||||
|
An additional lower limit for the step parameter of the Prometheus query and for the{' '}
|
||||||
|
<code>$__interval</code> and <code>$__rate_interval</code> variables.
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
aria-label="Set lower limit for the step parameter"
|
||||||
|
placeholder={'auto'}
|
||||||
|
width={10}
|
||||||
|
onBlur={onChangeStep}
|
||||||
|
defaultValue={query.interval}
|
||||||
|
/>
|
||||||
|
</EditorField>
|
||||||
|
|
||||||
|
<EditorField label="Format">
|
||||||
|
<Select value={formatOption} allowCustomValue onChange={onChangeFormat} options={FORMAT_OPTIONS} />
|
||||||
|
</EditorField>
|
||||||
|
<EditorField label="Type">
|
||||||
|
<RadioButtonGroup
|
||||||
|
options={queryTypeOptions}
|
||||||
|
value={query.range && query.instant ? 'both' : query.instant ? 'instant' : 'range'}
|
||||||
|
onChange={onQueryTypeChange}
|
||||||
|
/>
|
||||||
|
</EditorField>
|
||||||
|
{showExemplarSwitch && (
|
||||||
|
<EditorField label="Exemplars">
|
||||||
|
<Switch value={query.exemplar} onChange={onExemplarChange} />
|
||||||
|
</EditorField>
|
||||||
|
)}
|
||||||
|
</QueryOptionGroup>
|
||||||
|
</EditorRow>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
function getCollapsedInfo(query: PromQuery, formatOption: SelectableValue<string>): string[] {
|
||||||
|
const items: string[] = [];
|
||||||
|
|
||||||
|
if (query.legendFormat) {
|
||||||
|
items.push(`Legend: ${query.legendFormat}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
items.push(`Format: ${formatOption.label}`);
|
||||||
|
|
||||||
|
if (query.interval) {
|
||||||
|
items.push(`Step ${query.interval}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.instant) {
|
||||||
|
items.push(`Instant: true`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.exemplar) {
|
||||||
|
items.push(`Exemplars: true`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
PromQueryBuilderOptions.displayName = 'PromQueryBuilderOptions';
|
@ -6,6 +6,8 @@ import { PrometheusDatasource } from '../../datasource';
|
|||||||
import { QueryEditorMode } from '../shared/types';
|
import { QueryEditorMode } from '../shared/types';
|
||||||
import { EmptyLanguageProviderMock } from '../../language_provider.mock';
|
import { EmptyLanguageProviderMock } from '../../language_provider.mock';
|
||||||
import PromQlLanguageProvider from '../../language_provider';
|
import PromQlLanguageProvider from '../../language_provider';
|
||||||
|
import { cloneDeep, defaultsDeep } from 'lodash';
|
||||||
|
import { PromQuery } from '../../types';
|
||||||
|
|
||||||
// We need to mock this because it seems jest has problem importing monaco in tests
|
// We need to mock this because it seems jest has problem importing monaco in tests
|
||||||
jest.mock('../../components/monaco-query-field/MonacoQueryFieldWrapper', () => {
|
jest.mock('../../components/monaco-query-field/MonacoQueryFieldWrapper', () => {
|
||||||
@ -82,17 +84,44 @@ describe('PromQueryEditorSelector', () => {
|
|||||||
switchToMode(QueryEditorMode.Builder);
|
switchToMode(QueryEditorMode.Builder);
|
||||||
expect(onChange).toBeCalledWith({
|
expect(onChange).toBeCalledWith({
|
||||||
refId: 'A',
|
refId: 'A',
|
||||||
expr: '',
|
expr: defaultQuery.expr,
|
||||||
editorMode: QueryEditorMode.Builder,
|
editorMode: QueryEditorMode.Builder,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Can enable preview', async () => {
|
||||||
|
const { onChange } = renderWithMode(QueryEditorMode.Builder);
|
||||||
|
expect(screen.queryByLabelText('selector')).not.toBeInTheDocument();
|
||||||
|
|
||||||
|
screen.getByLabelText('Preview').click();
|
||||||
|
|
||||||
|
expect(onChange).toBeCalledWith({
|
||||||
|
refId: 'A',
|
||||||
|
expr: defaultQuery.expr,
|
||||||
|
editorMode: QueryEditorMode.Builder,
|
||||||
|
editorPreview: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should show preview', async () => {
|
||||||
|
renderWithProps({
|
||||||
|
editorPreview: true,
|
||||||
|
editorMode: QueryEditorMode.Builder,
|
||||||
|
visualQuery: {
|
||||||
|
metric: 'my_metric',
|
||||||
|
labels: [],
|
||||||
|
operations: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(screen.getByLabelText('selector').textContent).toBe('my_metric');
|
||||||
|
});
|
||||||
|
|
||||||
it('changes to code mode', async () => {
|
it('changes to code mode', async () => {
|
||||||
const { onChange } = renderWithMode(QueryEditorMode.Builder);
|
const { onChange } = renderWithMode(QueryEditorMode.Builder);
|
||||||
switchToMode(QueryEditorMode.Code);
|
switchToMode(QueryEditorMode.Code);
|
||||||
expect(onChange).toBeCalledWith({
|
expect(onChange).toBeCalledWith({
|
||||||
refId: 'A',
|
refId: 'A',
|
||||||
expr: '',
|
expr: defaultQuery.expr,
|
||||||
editorMode: QueryEditorMode.Code,
|
editorMode: QueryEditorMode.Code,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -102,25 +131,21 @@ describe('PromQueryEditorSelector', () => {
|
|||||||
switchToMode(QueryEditorMode.Explain);
|
switchToMode(QueryEditorMode.Explain);
|
||||||
expect(onChange).toBeCalledWith({
|
expect(onChange).toBeCalledWith({
|
||||||
refId: 'A',
|
refId: 'A',
|
||||||
expr: '',
|
expr: defaultQuery.expr,
|
||||||
editorMode: QueryEditorMode.Explain,
|
editorMode: QueryEditorMode.Explain,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function renderWithMode(mode: QueryEditorMode) {
|
function renderWithMode(mode: QueryEditorMode) {
|
||||||
|
return renderWithProps({ editorMode: mode } as any);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderWithProps(overrides?: Partial<PromQuery>) {
|
||||||
|
const query = defaultsDeep(overrides ?? {}, cloneDeep(defaultQuery));
|
||||||
const onChange = jest.fn();
|
const onChange = jest.fn();
|
||||||
render(
|
|
||||||
<PromQueryEditorSelector
|
render(<PromQueryEditorSelector {...defaultProps} query={query} onChange={onChange} />);
|
||||||
{...defaultProps}
|
|
||||||
onChange={onChange}
|
|
||||||
query={{
|
|
||||||
refId: 'A',
|
|
||||||
expr: '',
|
|
||||||
editorMode: mode,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
return { onChange };
|
return { onChange };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { CoreApp, GrafanaTheme2, LoadingState } from '@grafana/data';
|
import { GrafanaTheme2, LoadingState } from '@grafana/data';
|
||||||
import { EditorHeader, FlexItem, InlineSelect, Space } from '@grafana/experimental';
|
import { EditorHeader, EditorRows, FlexItem, InlineSelect, Space } from '@grafana/experimental';
|
||||||
import { Button, useStyles2 } from '@grafana/ui';
|
import { Button, useStyles2 } from '@grafana/ui';
|
||||||
import React, { SyntheticEvent, useCallback, useState } from 'react';
|
import React, { SyntheticEvent, useCallback, useState } from 'react';
|
||||||
import { PromQueryEditor } from '../../components/PromQueryEditor';
|
import { PromQueryEditor } from '../../components/PromQueryEditor';
|
||||||
@ -12,6 +12,8 @@ import { QueryEditorMode } from '../shared/types';
|
|||||||
import { getDefaultEmptyQuery, PromVisualQuery } from '../types';
|
import { getDefaultEmptyQuery, PromVisualQuery } from '../types';
|
||||||
import { PromQueryBuilder } from './PromQueryBuilder';
|
import { PromQueryBuilder } from './PromQueryBuilder';
|
||||||
import { PromQueryBuilderExplained } from './PromQueryBuilderExplained';
|
import { PromQueryBuilderExplained } from './PromQueryBuilderExplained';
|
||||||
|
import { PromQueryBuilderOptions } from './PromQueryBuilderOptions';
|
||||||
|
import { QueryPreview } from './QueryPreview';
|
||||||
|
|
||||||
export const PromQueryEditorSelector = React.memo<PromQueryEditorProps>((props) => {
|
export const PromQueryEditorSelector = React.memo<PromQueryEditorProps>((props) => {
|
||||||
const { query, onChange, onRunQuery, data } = props;
|
const { query, onChange, onRunQuery, data } = props;
|
||||||
@ -36,21 +38,14 @@ export const PromQueryEditorSelector = React.memo<PromQueryEditorProps>((props)
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const onInstantChange = (event: SyntheticEvent<HTMLInputElement>) => {
|
const onQueryPreviewChange = (event: SyntheticEvent<HTMLInputElement>) => {
|
||||||
const isEnabled = event.currentTarget.checked;
|
const isEnabled = event.currentTarget.checked;
|
||||||
onChange({ ...query, instant: isEnabled, exemplar: false });
|
onChange({ ...query, editorPreview: isEnabled });
|
||||||
onRunQuery();
|
|
||||||
};
|
|
||||||
|
|
||||||
const onExemplarChange = (event: SyntheticEvent<HTMLInputElement>) => {
|
|
||||||
const isEnabled = event.currentTarget.checked;
|
|
||||||
onChange({ ...query, exemplar: isEnabled });
|
|
||||||
onRunQuery();
|
onRunQuery();
|
||||||
};
|
};
|
||||||
|
|
||||||
// If no expr (ie new query) then default to builder
|
// If no expr (ie new query) then default to builder
|
||||||
const editorMode = query.editorMode ?? (query.expr ? QueryEditorMode.Code : QueryEditorMode.Builder);
|
const editorMode = query.editorMode ?? (query.expr ? QueryEditorMode.Code : QueryEditorMode.Builder);
|
||||||
const showExemplarSwitch = props.app !== CoreApp.UnifiedAlerting && !query.instant;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -67,10 +62,6 @@ export const PromQueryEditorSelector = React.memo<PromQueryEditorProps>((props)
|
|||||||
>
|
>
|
||||||
Run query
|
Run query
|
||||||
</Button>
|
</Button>
|
||||||
<QueryHeaderSwitch label="Instant" value={query.instant} onChange={onInstantChange} />
|
|
||||||
{showExemplarSwitch && (
|
|
||||||
<QueryHeaderSwitch label="Exemplars" value={query.exemplar} onChange={onExemplarChange} />
|
|
||||||
)}
|
|
||||||
{editorMode === QueryEditorMode.Builder && (
|
{editorMode === QueryEditorMode.Builder && (
|
||||||
<>
|
<>
|
||||||
<InlineSelect
|
<InlineSelect
|
||||||
@ -87,19 +78,31 @@ export const PromQueryEditorSelector = React.memo<PromQueryEditorProps>((props)
|
|||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
<QueryHeaderSwitch
|
||||||
|
label="Preview"
|
||||||
|
value={query.editorPreview}
|
||||||
|
onChange={onQueryPreviewChange}
|
||||||
|
disabled={editorMode !== QueryEditorMode.Builder}
|
||||||
|
/>
|
||||||
<QueryEditorModeToggle mode={editorMode} onChange={onEditorModeChange} />
|
<QueryEditorModeToggle mode={editorMode} onChange={onEditorModeChange} />
|
||||||
</EditorHeader>
|
</EditorHeader>
|
||||||
<Space v={0.5} />
|
<Space v={0.5} />
|
||||||
{editorMode === QueryEditorMode.Code && <PromQueryEditor {...props} />}
|
<EditorRows>
|
||||||
{editorMode === QueryEditorMode.Builder && (
|
{editorMode === QueryEditorMode.Code && <PromQueryEditor {...props} />}
|
||||||
<PromQueryBuilder
|
{editorMode === QueryEditorMode.Builder && (
|
||||||
query={visualQuery}
|
<>
|
||||||
datasource={props.datasource}
|
<PromQueryBuilder
|
||||||
onChange={onChangeViewModel}
|
query={visualQuery}
|
||||||
onRunQuery={props.onRunQuery}
|
datasource={props.datasource}
|
||||||
/>
|
onChange={onChangeViewModel}
|
||||||
)}
|
onRunQuery={props.onRunQuery}
|
||||||
{editorMode === QueryEditorMode.Explain && <PromQueryBuilderExplained query={visualQuery} />}
|
/>
|
||||||
|
{query.editorPreview && <QueryPreview query={visualQuery} />}
|
||||||
|
<PromQueryBuilderOptions query={query} app={props.app} onChange={onChange} onRunQuery={onRunQuery} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{editorMode === QueryEditorMode.Explain && <PromQueryBuilderExplained query={visualQuery} />}
|
||||||
|
</EditorRows>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -4,7 +4,7 @@ import { useTheme2 } from '@grafana/ui';
|
|||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
import { promQueryModeller } from '../PromQueryModeller';
|
import { promQueryModeller } from '../PromQueryModeller';
|
||||||
import { css, cx } from '@emotion/css';
|
import { css, cx } from '@emotion/css';
|
||||||
import { EditorField, EditorFieldGroup } from '@grafana/experimental';
|
import { EditorField, EditorFieldGroup, EditorRow } from '@grafana/experimental';
|
||||||
import Prism from 'prismjs';
|
import Prism from 'prismjs';
|
||||||
import { promqlGrammar } from '../../promql';
|
import { promqlGrammar } from '../../promql';
|
||||||
|
|
||||||
@ -18,22 +18,23 @@ export function QueryPreview({ query }: Props) {
|
|||||||
const hightlighted = Prism.highlight(promQueryModeller.renderQuery(query), promqlGrammar, 'promql');
|
const hightlighted = Prism.highlight(promQueryModeller.renderQuery(query), promqlGrammar, 'promql');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EditorFieldGroup>
|
<EditorRow>
|
||||||
<EditorField label="Query text">
|
<EditorFieldGroup>
|
||||||
<div
|
<EditorField label="Preview">
|
||||||
className={cx(styles.editorField, 'prism-syntax-highlight')}
|
<div
|
||||||
aria-label="selector"
|
className={cx(styles.editorField, 'prism-syntax-highlight')}
|
||||||
dangerouslySetInnerHTML={{ __html: hightlighted }}
|
aria-label="selector"
|
||||||
/>
|
dangerouslySetInnerHTML={{ __html: hightlighted }}
|
||||||
</EditorField>
|
/>
|
||||||
</EditorFieldGroup>
|
</EditorField>
|
||||||
|
</EditorFieldGroup>
|
||||||
|
</EditorRow>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const getStyles = (theme: GrafanaTheme2) => {
|
const getStyles = (theme: GrafanaTheme2) => {
|
||||||
return {
|
return {
|
||||||
editorField: css({
|
editorField: css({
|
||||||
padding: theme.spacing(0.25, 1),
|
|
||||||
fontFamily: theme.typography.fontFamilyMonospace,
|
fontFamily: theme.typography.fontFamilyMonospace,
|
||||||
fontSize: theme.typography.bodySmall.fontSize,
|
fontSize: theme.typography.bodySmall.fontSize,
|
||||||
}),
|
}),
|
||||||
|
@ -10,12 +10,12 @@ import {
|
|||||||
QueryBuilderOperationParamDef,
|
QueryBuilderOperationParamDef,
|
||||||
VisualQueryModeller,
|
VisualQueryModeller,
|
||||||
} from './shared/types';
|
} from './shared/types';
|
||||||
import { PromVisualQuery, PromVisualQueryOperationCategory } from './types';
|
import { PromOperationId, PromVisualQuery, PromVisualQueryOperationCategory } from './types';
|
||||||
|
|
||||||
export function getOperationDefinitions(): QueryBuilderOperationDef[] {
|
export function getOperationDefinitions(): QueryBuilderOperationDef[] {
|
||||||
const list: QueryBuilderOperationDef[] = [
|
const list: QueryBuilderOperationDef[] = [
|
||||||
{
|
{
|
||||||
id: 'histogram_quantile',
|
id: PromOperationId.HistogramQuantile,
|
||||||
name: 'Histogram quantile',
|
name: 'Histogram quantile',
|
||||||
params: [{ name: 'Quantile', type: 'number', options: [0.99, 0.95, 0.9, 0.75, 0.5, 0.25] }],
|
params: [{ name: 'Quantile', type: 'number', options: [0.99, 0.95, 0.9, 0.75, 0.5, 0.25] }],
|
||||||
defaultParams: [0.9],
|
defaultParams: [0.9],
|
||||||
@ -24,7 +24,7 @@ export function getOperationDefinitions(): QueryBuilderOperationDef[] {
|
|||||||
addOperationHandler: defaultAddOperationHandler,
|
addOperationHandler: defaultAddOperationHandler,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'label_replace',
|
id: PromOperationId.LabelReplace,
|
||||||
name: 'Label replace',
|
name: 'Label replace',
|
||||||
params: [
|
params: [
|
||||||
{ name: 'Destination label', type: 'string' },
|
{ name: 'Destination label', type: 'string' },
|
||||||
@ -38,7 +38,7 @@ export function getOperationDefinitions(): QueryBuilderOperationDef[] {
|
|||||||
addOperationHandler: defaultAddOperationHandler,
|
addOperationHandler: defaultAddOperationHandler,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'ln',
|
id: PromOperationId.Ln,
|
||||||
name: 'Ln',
|
name: 'Ln',
|
||||||
params: [],
|
params: [],
|
||||||
defaultParams: [],
|
defaultParams: [],
|
||||||
@ -46,15 +46,15 @@ export function getOperationDefinitions(): QueryBuilderOperationDef[] {
|
|||||||
renderer: functionRendererLeft,
|
renderer: functionRendererLeft,
|
||||||
addOperationHandler: defaultAddOperationHandler,
|
addOperationHandler: defaultAddOperationHandler,
|
||||||
},
|
},
|
||||||
createRangeFunction('changes'),
|
createRangeFunction(PromOperationId.Changes),
|
||||||
createRangeFunction('rate'),
|
createRangeFunction(PromOperationId.Rate),
|
||||||
createRangeFunction('irate'),
|
createRangeFunction(PromOperationId.Irate),
|
||||||
createRangeFunction('increase'),
|
createRangeFunction(PromOperationId.Increase),
|
||||||
createRangeFunction('delta'),
|
createRangeFunction(PromOperationId.Delta),
|
||||||
// Not sure about this one. It could also be a more generic "Simple math operation" where user specifies
|
// Not sure about this one. It could also be a more generic "Simple math operation" where user specifies
|
||||||
// both the operator and the operand in a single input
|
// both the operator and the operand in a single input
|
||||||
{
|
{
|
||||||
id: '__multiply_by',
|
id: PromOperationId.MultiplyBy,
|
||||||
name: 'Multiply by scalar',
|
name: 'Multiply by scalar',
|
||||||
params: [{ name: 'Factor', type: 'number' }],
|
params: [{ name: 'Factor', type: 'number' }],
|
||||||
defaultParams: [2],
|
defaultParams: [2],
|
||||||
@ -63,7 +63,7 @@ export function getOperationDefinitions(): QueryBuilderOperationDef[] {
|
|||||||
addOperationHandler: defaultAddOperationHandler,
|
addOperationHandler: defaultAddOperationHandler,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '__divide_by',
|
id: PromOperationId.DivideBy,
|
||||||
name: 'Divide by scalar',
|
name: 'Divide by scalar',
|
||||||
params: [{ name: 'Factor', type: 'number' }],
|
params: [{ name: 'Factor', type: 'number' }],
|
||||||
defaultParams: [2],
|
defaultParams: [2],
|
||||||
@ -72,7 +72,7 @@ export function getOperationDefinitions(): QueryBuilderOperationDef[] {
|
|||||||
addOperationHandler: defaultAddOperationHandler,
|
addOperationHandler: defaultAddOperationHandler,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '__nested_query',
|
id: PromOperationId.NestedQuery,
|
||||||
name: 'Binary operation with query',
|
name: 'Binary operation with query',
|
||||||
params: [],
|
params: [],
|
||||||
defaultParams: [],
|
defaultParams: [],
|
||||||
|
@ -0,0 +1,82 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { css } from '@emotion/css';
|
||||||
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
|
import { Stack } from '@grafana/experimental';
|
||||||
|
import { Icon, useStyles2 } from '@grafana/ui';
|
||||||
|
import { useToggle } from 'react-use';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
title: string;
|
||||||
|
collapsedInfo: string[];
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function QueryOptionGroup({ title, children, collapsedInfo }: Props) {
|
||||||
|
const [isOpen, toggleOpen] = useToggle(false);
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack gap={0} direction="column">
|
||||||
|
<div className={styles.header} onClick={toggleOpen} title="Click to edit options">
|
||||||
|
<div className={styles.toggle}>
|
||||||
|
<Icon name={isOpen ? 'angle-down' : 'angle-right'} />
|
||||||
|
</div>
|
||||||
|
<h6 className={styles.title}>{title}</h6>
|
||||||
|
{!isOpen && (
|
||||||
|
<div className={styles.description}>
|
||||||
|
{collapsedInfo.map((x, i) => (
|
||||||
|
<span key={i}>{x}</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{isOpen && <div className={styles.body}>{children}</div>}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStyles = (theme: GrafanaTheme2) => {
|
||||||
|
return {
|
||||||
|
switchLabel: css({
|
||||||
|
color: theme.colors.text.secondary,
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: theme.typography.bodySmall.fontSize,
|
||||||
|
'&:hover': {
|
||||||
|
color: theme.colors.text.primary,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
header: css({
|
||||||
|
display: 'flex',
|
||||||
|
cursor: 'pointer',
|
||||||
|
alignItems: 'baseline',
|
||||||
|
color: theme.colors.text.primary,
|
||||||
|
'&:hover': {
|
||||||
|
background: theme.colors.emphasize(theme.colors.background.primary, 0.03),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
title: css({
|
||||||
|
flexGrow: 1,
|
||||||
|
overflow: 'hidden',
|
||||||
|
fontSize: theme.typography.bodySmall.fontSize,
|
||||||
|
fontWeight: theme.typography.fontWeightMedium,
|
||||||
|
margin: 0,
|
||||||
|
}),
|
||||||
|
description: css({
|
||||||
|
color: theme.colors.text.secondary,
|
||||||
|
fontSize: theme.typography.bodySmall.fontSize,
|
||||||
|
paddingLeft: theme.spacing(2),
|
||||||
|
gap: theme.spacing(2),
|
||||||
|
display: 'flex',
|
||||||
|
}),
|
||||||
|
body: css({
|
||||||
|
display: 'flex',
|
||||||
|
paddingTop: theme.spacing(2),
|
||||||
|
gap: theme.spacing(2),
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
}),
|
||||||
|
toggle: css({
|
||||||
|
color: theme.colors.text.secondary,
|
||||||
|
marginRight: `${theme.spacing(1)}`,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
};
|
@ -20,6 +20,20 @@ export enum PromVisualQueryOperationCategory {
|
|||||||
BinaryOps = 'Binary operations',
|
BinaryOps = 'Binary operations',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum PromOperationId {
|
||||||
|
HistogramQuantile = 'histogram_quantile',
|
||||||
|
LabelReplace = 'label_replace',
|
||||||
|
Ln = 'ln',
|
||||||
|
Changes = 'changes',
|
||||||
|
Rate = 'rate',
|
||||||
|
Irate = 'irate',
|
||||||
|
Increase = 'increase',
|
||||||
|
Delta = 'delta',
|
||||||
|
MultiplyBy = '__multiply_by',
|
||||||
|
DivideBy = '__divide_by',
|
||||||
|
NestedQuery = '__nested_query',
|
||||||
|
}
|
||||||
|
|
||||||
export interface PromQueryPattern {
|
export interface PromQueryPattern {
|
||||||
name: string;
|
name: string;
|
||||||
operations: QueryBuilderOperation[];
|
operations: QueryBuilderOperation[];
|
||||||
|
@ -18,7 +18,10 @@ export interface PromQuery extends DataQuery {
|
|||||||
requestId?: string;
|
requestId?: string;
|
||||||
showingGraph?: boolean;
|
showingGraph?: boolean;
|
||||||
showingTable?: boolean;
|
showingTable?: boolean;
|
||||||
|
/** Code, Builder or Explain */
|
||||||
editorMode?: QueryEditorMode;
|
editorMode?: QueryEditorMode;
|
||||||
|
/** Controls if the query preview is shown */
|
||||||
|
editorPreview?: boolean;
|
||||||
/** Temporary until we have a parser */
|
/** Temporary until we have a parser */
|
||||||
visualQuery?: PromVisualQuery;
|
visualQuery?: PromVisualQuery;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user