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:
Torkel Ödegaard 2022-02-07 13:32:36 +01:00 committed by GitHub
parent d9d1f8520e
commit f92ab9bc72
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 352 additions and 105 deletions

View File

@ -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',

View File

@ -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' },

View File

@ -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');
}); });

View File

@ -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 () => {

View File

@ -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>
); );
}); });

View File

@ -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';

View File

@ -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 };
} }

View File

@ -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>
</> </>
); );
}); });

View File

@ -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,
}), }),

View File

@ -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: [],

View File

@ -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)}`,
}),
};
};

View File

@ -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[];

View File

@ -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;
} }