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(
|
||||
({ query, datasource, onChange, onRunQuery }) => {
|
||||
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.',
|
||||
},
|
||||
{ value: 'both', label: 'Both', description: 'Run an Instant query and a Range query.' },
|
||||
];
|
||||
|
||||
const rangeOptions = getQueryTypeOptions(true);
|
||||
const prevQuery = usePrevious(query);
|
||||
|
||||
const onExemplarChange = useCallback(
|
||||
@ -53,17 +44,7 @@ export const PromExploreExtraField: React.FC<PromExploreExtraFieldProps> = memo(
|
||||
}
|
||||
}
|
||||
|
||||
function onQueryTypeChange(queryType: string) {
|
||||
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);
|
||||
}
|
||||
const onQueryTypeChange = getQueryTypeChangeHandler(query, onChange);
|
||||
|
||||
return (
|
||||
<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';
|
||||
|
||||
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 = {
|
||||
extraFieldEditor: 'prom-editor-extra-field',
|
||||
stepField: 'prom-editor-extra-field-step',
|
||||
|
@ -13,7 +13,7 @@ import { PromQueryEditorProps } from './types';
|
||||
|
||||
const { Switch } = LegacyForms;
|
||||
|
||||
const FORMAT_OPTIONS: Array<SelectableValue<string>> = [
|
||||
export const FORMAT_OPTIONS: Array<SelectableValue<string>> = [
|
||||
{ label: 'Time series', value: 'time_series' },
|
||||
{ label: 'Table', value: 'table' },
|
||||
{ label: 'Heatmap', value: 'heatmap' },
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { PromQueryModeller } from './PromQueryModeller';
|
||||
import { PromOperationId } from './types';
|
||||
|
||||
describe('PromQueryModeller', () => {
|
||||
const modeller = new PromQueryModeller();
|
||||
@ -41,7 +42,7 @@ describe('PromQueryModeller', () => {
|
||||
modeller.renderQuery({
|
||||
metric: 'metric',
|
||||
labels: [],
|
||||
operations: [{ id: 'histogram_quantile', params: [0.86] }],
|
||||
operations: [{ id: PromOperationId.HistogramQuantile, params: [0.86] }],
|
||||
})
|
||||
).toBe('histogram_quantile(0.86, metric)');
|
||||
});
|
||||
@ -51,7 +52,7 @@ describe('PromQueryModeller', () => {
|
||||
modeller.renderQuery({
|
||||
metric: 'metric',
|
||||
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")');
|
||||
});
|
||||
@ -94,7 +95,7 @@ describe('PromQueryModeller', () => {
|
||||
modeller.renderQuery({
|
||||
metric: 'metric',
|
||||
labels: [{ label: 'pod', op: '=', value: 'A' }],
|
||||
operations: [{ id: 'rate', params: ['auto'] }],
|
||||
operations: [{ id: PromOperationId.Rate, params: ['auto'] }],
|
||||
})
|
||||
).toBe('rate(metric{pod="A"}[$__rate_interval])');
|
||||
});
|
||||
@ -104,7 +105,7 @@ describe('PromQueryModeller', () => {
|
||||
modeller.renderQuery({
|
||||
metric: 'metric',
|
||||
labels: [{ label: 'pod', op: '=', value: 'A' }],
|
||||
operations: [{ id: 'increase', params: ['auto'] }],
|
||||
operations: [{ id: PromOperationId.Increase, params: ['auto'] }],
|
||||
})
|
||||
).toBe('increase(metric{pod="A"}[$__rate_interval])');
|
||||
});
|
||||
@ -114,7 +115,7 @@ describe('PromQueryModeller', () => {
|
||||
modeller.renderQuery({
|
||||
metric: 'metric',
|
||||
labels: [{ label: 'pod', op: '=', value: 'A' }],
|
||||
operations: [{ id: 'rate', params: ['10m'] }],
|
||||
operations: [{ id: PromOperationId.Rate, params: ['10m'] }],
|
||||
})
|
||||
).toBe('rate(metric{pod="A"}[10m])');
|
||||
});
|
||||
@ -124,7 +125,7 @@ describe('PromQueryModeller', () => {
|
||||
modeller.renderQuery({
|
||||
metric: 'metric',
|
||||
labels: [],
|
||||
operations: [{ id: '__multiply_by', params: [1000] }],
|
||||
operations: [{ id: PromOperationId.MultiplyBy, params: [1000] }],
|
||||
})
|
||||
).toBe('metric * 1000');
|
||||
});
|
||||
|
@ -47,8 +47,6 @@ const bugQuery: PromVisualQuery = {
|
||||
describe('PromQueryBuilder', () => {
|
||||
it('shows empty just with metric selected', async () => {
|
||||
setup();
|
||||
// One should be select another query preview
|
||||
expect(screen.getAllByText('random_metric').length).toBe(2);
|
||||
// Add label
|
||||
expect(screen.getByLabelText('Add')).toBeInTheDocument();
|
||||
expect(screen.getByLabelText('Add operation')).toBeInTheDocument();
|
||||
@ -67,9 +65,6 @@ describe('PromQueryBuilder', () => {
|
||||
expect(screen.getByText('Binary operations')).toBeInTheDocument();
|
||||
expect(screen.getByText('Operator')).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 () => {
|
||||
|
@ -3,12 +3,11 @@ import { MetricSelect } from './MetricSelect';
|
||||
import { PromVisualQuery } from '../types';
|
||||
import { LabelFilters } from '../shared/LabelFilters';
|
||||
import { OperationList } from '../shared/OperationList';
|
||||
import { EditorRows, EditorRow } from '@grafana/experimental';
|
||||
import { EditorRow } from '@grafana/experimental';
|
||||
import { PrometheusDatasource } from '../../datasource';
|
||||
import { NestedQueryList } from './NestedQueryList';
|
||||
import { promQueryModeller } from '../PromQueryModeller';
|
||||
import { QueryBuilderLabelFilter } from '../shared/types';
|
||||
import { QueryPreview } from './QueryPreview';
|
||||
import { DataSourceApi, SelectableValue } from '@grafana/data';
|
||||
import { OperationsEditorRow } from '../shared/OperationsEditorRow';
|
||||
|
||||
@ -20,7 +19,7 @@ export interface Props {
|
||||
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[]) => {
|
||||
onChange({ ...query, labels });
|
||||
};
|
||||
@ -78,7 +77,7 @@ export const PromQueryBuilder = React.memo<Props>(({ datasource, query, onChange
|
||||
};
|
||||
|
||||
return (
|
||||
<EditorRows>
|
||||
<>
|
||||
<EditorRow>
|
||||
<MetricSelect
|
||||
query={query}
|
||||
@ -108,12 +107,7 @@ export const PromQueryBuilder = React.memo<Props>(({ datasource, query, onChange
|
||||
<NestedQueryList query={query} datasource={datasource} onChange={onChange} onRunQuery={onRunQuery} />
|
||||
)}
|
||||
</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 { EmptyLanguageProviderMock } from '../../language_provider.mock';
|
||||
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
|
||||
jest.mock('../../components/monaco-query-field/MonacoQueryFieldWrapper', () => {
|
||||
@ -82,17 +84,44 @@ describe('PromQueryEditorSelector', () => {
|
||||
switchToMode(QueryEditorMode.Builder);
|
||||
expect(onChange).toBeCalledWith({
|
||||
refId: 'A',
|
||||
expr: '',
|
||||
expr: defaultQuery.expr,
|
||||
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 () => {
|
||||
const { onChange } = renderWithMode(QueryEditorMode.Builder);
|
||||
switchToMode(QueryEditorMode.Code);
|
||||
expect(onChange).toBeCalledWith({
|
||||
refId: 'A',
|
||||
expr: '',
|
||||
expr: defaultQuery.expr,
|
||||
editorMode: QueryEditorMode.Code,
|
||||
});
|
||||
});
|
||||
@ -102,25 +131,21 @@ describe('PromQueryEditorSelector', () => {
|
||||
switchToMode(QueryEditorMode.Explain);
|
||||
expect(onChange).toBeCalledWith({
|
||||
refId: 'A',
|
||||
expr: '',
|
||||
expr: defaultQuery.expr,
|
||||
editorMode: QueryEditorMode.Explain,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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();
|
||||
render(
|
||||
<PromQueryEditorSelector
|
||||
{...defaultProps}
|
||||
onChange={onChange}
|
||||
query={{
|
||||
refId: 'A',
|
||||
expr: '',
|
||||
editorMode: mode,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
render(<PromQueryEditorSelector {...defaultProps} query={query} onChange={onChange} />);
|
||||
return { onChange };
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { CoreApp, GrafanaTheme2, LoadingState } from '@grafana/data';
|
||||
import { EditorHeader, FlexItem, InlineSelect, Space } from '@grafana/experimental';
|
||||
import { GrafanaTheme2, LoadingState } from '@grafana/data';
|
||||
import { EditorHeader, EditorRows, FlexItem, InlineSelect, Space } from '@grafana/experimental';
|
||||
import { Button, useStyles2 } from '@grafana/ui';
|
||||
import React, { SyntheticEvent, useCallback, useState } from 'react';
|
||||
import { PromQueryEditor } from '../../components/PromQueryEditor';
|
||||
@ -12,6 +12,8 @@ import { QueryEditorMode } from '../shared/types';
|
||||
import { getDefaultEmptyQuery, PromVisualQuery } from '../types';
|
||||
import { PromQueryBuilder } from './PromQueryBuilder';
|
||||
import { PromQueryBuilderExplained } from './PromQueryBuilderExplained';
|
||||
import { PromQueryBuilderOptions } from './PromQueryBuilderOptions';
|
||||
import { QueryPreview } from './QueryPreview';
|
||||
|
||||
export const PromQueryEditorSelector = React.memo<PromQueryEditorProps>((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;
|
||||
onChange({ ...query, instant: isEnabled, exemplar: false });
|
||||
onRunQuery();
|
||||
};
|
||||
|
||||
const onExemplarChange = (event: SyntheticEvent<HTMLInputElement>) => {
|
||||
const isEnabled = event.currentTarget.checked;
|
||||
onChange({ ...query, exemplar: isEnabled });
|
||||
onChange({ ...query, editorPreview: isEnabled });
|
||||
onRunQuery();
|
||||
};
|
||||
|
||||
// If no expr (ie new query) then default to builder
|
||||
const editorMode = query.editorMode ?? (query.expr ? QueryEditorMode.Code : QueryEditorMode.Builder);
|
||||
const showExemplarSwitch = props.app !== CoreApp.UnifiedAlerting && !query.instant;
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -67,10 +62,6 @@ export const PromQueryEditorSelector = React.memo<PromQueryEditorProps>((props)
|
||||
>
|
||||
Run query
|
||||
</Button>
|
||||
<QueryHeaderSwitch label="Instant" value={query.instant} onChange={onInstantChange} />
|
||||
{showExemplarSwitch && (
|
||||
<QueryHeaderSwitch label="Exemplars" value={query.exemplar} onChange={onExemplarChange} />
|
||||
)}
|
||||
{editorMode === QueryEditorMode.Builder && (
|
||||
<>
|
||||
<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} />
|
||||
</EditorHeader>
|
||||
<Space v={0.5} />
|
||||
{editorMode === QueryEditorMode.Code && <PromQueryEditor {...props} />}
|
||||
{editorMode === QueryEditorMode.Builder && (
|
||||
<PromQueryBuilder
|
||||
query={visualQuery}
|
||||
datasource={props.datasource}
|
||||
onChange={onChangeViewModel}
|
||||
onRunQuery={props.onRunQuery}
|
||||
/>
|
||||
)}
|
||||
{editorMode === QueryEditorMode.Explain && <PromQueryBuilderExplained query={visualQuery} />}
|
||||
<EditorRows>
|
||||
{editorMode === QueryEditorMode.Code && <PromQueryEditor {...props} />}
|
||||
{editorMode === QueryEditorMode.Builder && (
|
||||
<>
|
||||
<PromQueryBuilder
|
||||
query={visualQuery}
|
||||
datasource={props.datasource}
|
||||
onChange={onChangeViewModel}
|
||||
onRunQuery={props.onRunQuery}
|
||||
/>
|
||||
{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 { promQueryModeller } from '../PromQueryModeller';
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { EditorField, EditorFieldGroup } from '@grafana/experimental';
|
||||
import { EditorField, EditorFieldGroup, EditorRow } from '@grafana/experimental';
|
||||
import Prism from 'prismjs';
|
||||
import { promqlGrammar } from '../../promql';
|
||||
|
||||
@ -18,22 +18,23 @@ export function QueryPreview({ query }: Props) {
|
||||
const hightlighted = Prism.highlight(promQueryModeller.renderQuery(query), promqlGrammar, 'promql');
|
||||
|
||||
return (
|
||||
<EditorFieldGroup>
|
||||
<EditorField label="Query text">
|
||||
<div
|
||||
className={cx(styles.editorField, 'prism-syntax-highlight')}
|
||||
aria-label="selector"
|
||||
dangerouslySetInnerHTML={{ __html: hightlighted }}
|
||||
/>
|
||||
</EditorField>
|
||||
</EditorFieldGroup>
|
||||
<EditorRow>
|
||||
<EditorFieldGroup>
|
||||
<EditorField label="Preview">
|
||||
<div
|
||||
className={cx(styles.editorField, 'prism-syntax-highlight')}
|
||||
aria-label="selector"
|
||||
dangerouslySetInnerHTML={{ __html: hightlighted }}
|
||||
/>
|
||||
</EditorField>
|
||||
</EditorFieldGroup>
|
||||
</EditorRow>
|
||||
);
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
editorField: css({
|
||||
padding: theme.spacing(0.25, 1),
|
||||
fontFamily: theme.typography.fontFamilyMonospace,
|
||||
fontSize: theme.typography.bodySmall.fontSize,
|
||||
}),
|
||||
|
@ -10,12 +10,12 @@ import {
|
||||
QueryBuilderOperationParamDef,
|
||||
VisualQueryModeller,
|
||||
} from './shared/types';
|
||||
import { PromVisualQuery, PromVisualQueryOperationCategory } from './types';
|
||||
import { PromOperationId, PromVisualQuery, PromVisualQueryOperationCategory } from './types';
|
||||
|
||||
export function getOperationDefinitions(): QueryBuilderOperationDef[] {
|
||||
const list: QueryBuilderOperationDef[] = [
|
||||
{
|
||||
id: 'histogram_quantile',
|
||||
id: PromOperationId.HistogramQuantile,
|
||||
name: 'Histogram quantile',
|
||||
params: [{ name: 'Quantile', type: 'number', options: [0.99, 0.95, 0.9, 0.75, 0.5, 0.25] }],
|
||||
defaultParams: [0.9],
|
||||
@ -24,7 +24,7 @@ export function getOperationDefinitions(): QueryBuilderOperationDef[] {
|
||||
addOperationHandler: defaultAddOperationHandler,
|
||||
},
|
||||
{
|
||||
id: 'label_replace',
|
||||
id: PromOperationId.LabelReplace,
|
||||
name: 'Label replace',
|
||||
params: [
|
||||
{ name: 'Destination label', type: 'string' },
|
||||
@ -38,7 +38,7 @@ export function getOperationDefinitions(): QueryBuilderOperationDef[] {
|
||||
addOperationHandler: defaultAddOperationHandler,
|
||||
},
|
||||
{
|
||||
id: 'ln',
|
||||
id: PromOperationId.Ln,
|
||||
name: 'Ln',
|
||||
params: [],
|
||||
defaultParams: [],
|
||||
@ -46,15 +46,15 @@ export function getOperationDefinitions(): QueryBuilderOperationDef[] {
|
||||
renderer: functionRendererLeft,
|
||||
addOperationHandler: defaultAddOperationHandler,
|
||||
},
|
||||
createRangeFunction('changes'),
|
||||
createRangeFunction('rate'),
|
||||
createRangeFunction('irate'),
|
||||
createRangeFunction('increase'),
|
||||
createRangeFunction('delta'),
|
||||
createRangeFunction(PromOperationId.Changes),
|
||||
createRangeFunction(PromOperationId.Rate),
|
||||
createRangeFunction(PromOperationId.Irate),
|
||||
createRangeFunction(PromOperationId.Increase),
|
||||
createRangeFunction(PromOperationId.Delta),
|
||||
// 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
|
||||
{
|
||||
id: '__multiply_by',
|
||||
id: PromOperationId.MultiplyBy,
|
||||
name: 'Multiply by scalar',
|
||||
params: [{ name: 'Factor', type: 'number' }],
|
||||
defaultParams: [2],
|
||||
@ -63,7 +63,7 @@ export function getOperationDefinitions(): QueryBuilderOperationDef[] {
|
||||
addOperationHandler: defaultAddOperationHandler,
|
||||
},
|
||||
{
|
||||
id: '__divide_by',
|
||||
id: PromOperationId.DivideBy,
|
||||
name: 'Divide by scalar',
|
||||
params: [{ name: 'Factor', type: 'number' }],
|
||||
defaultParams: [2],
|
||||
@ -72,7 +72,7 @@ export function getOperationDefinitions(): QueryBuilderOperationDef[] {
|
||||
addOperationHandler: defaultAddOperationHandler,
|
||||
},
|
||||
{
|
||||
id: '__nested_query',
|
||||
id: PromOperationId.NestedQuery,
|
||||
name: 'Binary operation with query',
|
||||
params: [],
|
||||
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',
|
||||
}
|
||||
|
||||
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 {
|
||||
name: string;
|
||||
operations: QueryBuilderOperation[];
|
||||
|
@ -18,7 +18,10 @@ export interface PromQuery extends DataQuery {
|
||||
requestId?: string;
|
||||
showingGraph?: boolean;
|
||||
showingTable?: boolean;
|
||||
/** Code, Builder or Explain */
|
||||
editorMode?: QueryEditorMode;
|
||||
/** Controls if the query preview is shown */
|
||||
editorPreview?: boolean;
|
||||
/** Temporary until we have a parser */
|
||||
visualQuery?: PromVisualQuery;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user