mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Prometheus: Fixes issue adding rest params (#45364)
* Prometheus: Fixing issue adding rest param and adding test * Rebuild when expr changes from outside
This commit is contained in:
parent
02f8e99ca1
commit
916f152a6f
@ -3,10 +3,18 @@ import { Select } from '@grafana/ui';
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { PrometheusDatasource } from '../../datasource';
|
import { PrometheusDatasource } from '../../datasource';
|
||||||
import { promQueryModeller } from '../PromQueryModeller';
|
import { promQueryModeller } from '../PromQueryModeller';
|
||||||
|
import { getOperationParamId } from '../shared/operationUtils';
|
||||||
import { QueryBuilderOperationParamEditorProps } from '../shared/types';
|
import { QueryBuilderOperationParamEditorProps } from '../shared/types';
|
||||||
import { PromVisualQuery } from '../types';
|
import { PromVisualQuery } from '../types';
|
||||||
|
|
||||||
export function LabelParamEditor({ onChange, index, value, query, datasource }: QueryBuilderOperationParamEditorProps) {
|
export function LabelParamEditor({
|
||||||
|
onChange,
|
||||||
|
index,
|
||||||
|
operationIndex,
|
||||||
|
value,
|
||||||
|
query,
|
||||||
|
datasource,
|
||||||
|
}: QueryBuilderOperationParamEditorProps) {
|
||||||
const [state, setState] = useState<{
|
const [state, setState] = useState<{
|
||||||
options?: Array<SelectableValue<any>>;
|
options?: Array<SelectableValue<any>>;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
@ -14,6 +22,7 @@ export function LabelParamEditor({ onChange, index, value, query, datasource }:
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
|
inputId={getOperationParamId(operationIndex, index)}
|
||||||
menuShouldPortal
|
menuShouldPortal
|
||||||
autoFocus={value === '' ? true : undefined}
|
autoFocus={value === '' ? true : undefined}
|
||||||
openMenuOnFocus
|
openMenuOnFocus
|
||||||
|
@ -36,7 +36,7 @@ const bugQuery: PromVisualQuery = {
|
|||||||
labels: [{ label: 'foo', op: '=', value: 'bar' }],
|
labels: [{ label: 'foo', op: '=', value: 'bar' }],
|
||||||
operations: [
|
operations: [
|
||||||
{
|
{
|
||||||
id: '__sum_by',
|
id: '__avg_by',
|
||||||
params: ['app'],
|
params: ['app'],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@ -58,11 +58,12 @@ describe('PromQueryBuilder', () => {
|
|||||||
expect(screen.getByText('random_metric')).toBeInTheDocument();
|
expect(screen.getByText('random_metric')).toBeInTheDocument();
|
||||||
expect(screen.getByText('localhost:9090')).toBeInTheDocument();
|
expect(screen.getByText('localhost:9090')).toBeInTheDocument();
|
||||||
expect(screen.getByText('Rate')).toBeInTheDocument();
|
expect(screen.getByText('Rate')).toBeInTheDocument();
|
||||||
const sumBys = screen.getAllByTestId('operation-wrapper-for-__sum_by');
|
const sumBys = screen.getAllByTestId('operations.1.wrapper');
|
||||||
expect(getByText(sumBys[0], 'instance')).toBeInTheDocument();
|
expect(getByText(sumBys[0], 'instance')).toBeInTheDocument();
|
||||||
expect(getByText(sumBys[0], 'job')).toBeInTheDocument();
|
expect(getByText(sumBys[0], 'job')).toBeInTheDocument();
|
||||||
|
|
||||||
expect(getByText(sumBys[1], 'app')).toBeInTheDocument();
|
const avgBys = screen.getAllByTestId('operations.0.wrapper');
|
||||||
|
expect(getByText(avgBys[1], 'app')).toBeInTheDocument();
|
||||||
expect(screen.getByText('Operator')).toBeInTheDocument();
|
expect(screen.getByText('Operator')).toBeInTheDocument();
|
||||||
expect(screen.getByText('Vector matches')).toBeInTheDocument();
|
expect(screen.getByText('Vector matches')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
@ -1,37 +1,18 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen, waitFor } from '@testing-library/react';
|
||||||
import { PromQueryBuilderContainer } from './PromQueryBuilderContainer';
|
import { PromQueryBuilderContainer } from './PromQueryBuilderContainer';
|
||||||
import { PrometheusDatasource } from '../../datasource';
|
import { PrometheusDatasource } from '../../datasource';
|
||||||
import { EmptyLanguageProviderMock } from '../../language_provider.mock';
|
import { EmptyLanguageProviderMock } from '../../language_provider.mock';
|
||||||
import PromQlLanguageProvider from '../../language_provider';
|
import PromQlLanguageProvider from '../../language_provider';
|
||||||
import { addOperation } from '../shared/OperationList.testUtils';
|
import { addOperation } from '../shared/OperationList.testUtils';
|
||||||
|
import { PromQuery } from '../../types';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
|
import { getOperationParamId } from '../shared/operationUtils';
|
||||||
|
|
||||||
describe('PromQueryBuilderContainer', () => {
|
describe('PromQueryBuilderContainer', () => {
|
||||||
it('translates query between string and model', async () => {
|
it('translates query between string and model', async () => {
|
||||||
const props = {
|
const { props } = setup({ expr: 'rate(metric_test{job="testjob"}[$__rate_interval])' });
|
||||||
query: {
|
|
||||||
expr: 'metric_test{job="testjob"}',
|
|
||||||
refId: 'A',
|
|
||||||
},
|
|
||||||
datasource: new PrometheusDatasource(
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
uid: '',
|
|
||||||
type: 'prometheus',
|
|
||||||
name: 'prom-test',
|
|
||||||
access: 'proxy',
|
|
||||||
url: '',
|
|
||||||
jsonData: {},
|
|
||||||
meta: {} as any,
|
|
||||||
},
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
new EmptyLanguageProviderMock() as unknown as PromQlLanguageProvider
|
|
||||||
),
|
|
||||||
onChange: jest.fn(),
|
|
||||||
onRunQuery: () => {},
|
|
||||||
};
|
|
||||||
render(<PromQueryBuilderContainer {...props} />);
|
|
||||||
expect(screen.getByText('metric_test')).toBeInTheDocument();
|
expect(screen.getByText('metric_test')).toBeInTheDocument();
|
||||||
addOperation('Range functions', 'Rate');
|
addOperation('Range functions', 'Rate');
|
||||||
expect(props.onChange).toBeCalledWith({
|
expect(props.onChange).toBeCalledWith({
|
||||||
@ -39,4 +20,41 @@ describe('PromQueryBuilderContainer', () => {
|
|||||||
refId: 'A',
|
refId: 'A',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Can add rest param', async () => {
|
||||||
|
const { container } = setup({ expr: 'sum(ALERTS)' });
|
||||||
|
userEvent.click(screen.getByTestId('operations.0.add-rest-param'));
|
||||||
|
|
||||||
|
waitFor(() => {
|
||||||
|
expect(container.querySelector(`${getOperationParamId(0, 0)}`)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function setup(queryOverrides: Partial<PromQuery> = {}) {
|
||||||
|
const languageProvider = new EmptyLanguageProviderMock() as unknown as PromQlLanguageProvider;
|
||||||
|
const datasource = new PrometheusDatasource(
|
||||||
|
{
|
||||||
|
url: '',
|
||||||
|
jsonData: {},
|
||||||
|
meta: {} as any,
|
||||||
|
} as any,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
languageProvider
|
||||||
|
);
|
||||||
|
|
||||||
|
const props = {
|
||||||
|
datasource,
|
||||||
|
query: {
|
||||||
|
refId: 'A',
|
||||||
|
expr: '',
|
||||||
|
...queryOverrides,
|
||||||
|
},
|
||||||
|
onRunQuery: jest.fn(),
|
||||||
|
onChange: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const { container } = render(<PromQueryBuilderContainer {...props} />);
|
||||||
|
return { languageProvider, datasource, container, props };
|
||||||
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { PanelData } from '@grafana/data';
|
import { PanelData } from '@grafana/data';
|
||||||
import React from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { PrometheusDatasource } from '../../datasource';
|
import { PrometheusDatasource } from '../../datasource';
|
||||||
import { PromQuery } from '../../types';
|
import { PromQuery } from '../../types';
|
||||||
@ -17,25 +17,40 @@ export interface Props {
|
|||||||
data?: PanelData;
|
data?: PanelData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface State {
|
||||||
|
visQuery?: PromVisualQuery;
|
||||||
|
expr: string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This component is here just to contain the translation logic between string query and the visual query builder model.
|
* This component is here just to contain the translation logic between string query and the visual query builder model.
|
||||||
* @param props
|
|
||||||
* @constructor
|
|
||||||
*/
|
*/
|
||||||
export function PromQueryBuilderContainer(props: Props) {
|
export function PromQueryBuilderContainer(props: Props) {
|
||||||
const { query, onChange, onRunQuery, datasource, data } = props;
|
const { query, onChange, onRunQuery, datasource, data } = props;
|
||||||
|
const [state, setState] = useState<State>({ expr: query.expr });
|
||||||
|
|
||||||
const visQuery = buildVisualQueryFromString(query.expr || '').query;
|
// Only rebuild visual query if expr changes from outside
|
||||||
|
useEffect(() => {
|
||||||
|
if (!state.visQuery || state.expr !== query.expr) {
|
||||||
|
const result = buildVisualQueryFromString(query.expr || '');
|
||||||
|
setState({ visQuery: result.query, expr: query.expr });
|
||||||
|
}
|
||||||
|
}, [query.expr, state.visQuery, state.expr]);
|
||||||
|
|
||||||
const onVisQueryChange = (newVisQuery: PromVisualQuery) => {
|
const onVisQueryChange = (newVisQuery: PromVisualQuery) => {
|
||||||
const rendered = promQueryModeller.renderQuery(newVisQuery);
|
const rendered = promQueryModeller.renderQuery(newVisQuery);
|
||||||
onChange({ ...query, expr: rendered });
|
onChange({ ...query, expr: rendered });
|
||||||
|
setState({ visQuery: newVisQuery, expr: rendered });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!state.visQuery) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PromQueryBuilder
|
<PromQueryBuilder
|
||||||
query={visQuery}
|
query={state.visQuery}
|
||||||
datasource={datasource}
|
datasource={datasource}
|
||||||
onChange={onVisQueryChange}
|
onChange={onVisQueryChange}
|
||||||
onRunQuery={onRunQuery}
|
onRunQuery={onRunQuery}
|
||||||
|
@ -1,12 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { PrometheusDatasource } from '../../datasource';
|
|
||||||
import { PromVisualQuery } from '../types';
|
|
||||||
|
|
||||||
export interface PromQueryBuilderContextType {
|
|
||||||
query: PromVisualQuery;
|
|
||||||
datasource: PrometheusDatasource;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const PromQueryBuilderContext = React.createContext<PromQueryBuilderContextType>(
|
|
||||||
{} as any as PromQueryBuilderContextType
|
|
||||||
);
|
|
@ -14,6 +14,7 @@ import {
|
|||||||
import { OperationInfoButton } from './OperationInfoButton';
|
import { OperationInfoButton } from './OperationInfoButton';
|
||||||
import { OperationName } from './OperationName';
|
import { OperationName } from './OperationName';
|
||||||
import { getOperationParamEditor } from './OperationParamEditor';
|
import { getOperationParamEditor } from './OperationParamEditor';
|
||||||
|
import { getOperationParamId } from './operationUtils';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
operation: QueryBuilderOperation;
|
operation: QueryBuilderOperation;
|
||||||
@ -66,7 +67,9 @@ export function OperationEditor({
|
|||||||
|
|
||||||
operationElements.push(
|
operationElements.push(
|
||||||
<div className={styles.paramRow} key={`${paramIndex}-1`}>
|
<div className={styles.paramRow} key={`${paramIndex}-1`}>
|
||||||
<div className={styles.paramName}>{paramDef.name}</div>
|
<label className={styles.paramName} htmlFor={getOperationParamId(index, paramIndex)}>
|
||||||
|
{paramDef.name}
|
||||||
|
</label>
|
||||||
<div className={styles.paramValue}>
|
<div className={styles.paramValue}>
|
||||||
<Stack gap={0.5} direction="row" alignItems="center" wrap={false}>
|
<Stack gap={0.5} direction="row" alignItems="center" wrap={false}>
|
||||||
<Editor
|
<Editor
|
||||||
@ -74,6 +77,7 @@ export function OperationEditor({
|
|||||||
paramDef={paramDef}
|
paramDef={paramDef}
|
||||||
value={operation.params[paramIndex]}
|
value={operation.params[paramIndex]}
|
||||||
operation={operation}
|
operation={operation}
|
||||||
|
operationIndex={index}
|
||||||
onChange={onParamValueChanged}
|
onChange={onParamValueChanged}
|
||||||
onRunQuery={onRunQuery}
|
onRunQuery={onRunQuery}
|
||||||
query={query}
|
query={query}
|
||||||
@ -81,6 +85,7 @@ export function OperationEditor({
|
|||||||
/>
|
/>
|
||||||
{paramDef.restParam && (operation.params.length > def.params.length || paramDef.optional) && (
|
{paramDef.restParam && (operation.params.length > def.params.length || paramDef.optional) && (
|
||||||
<Button
|
<Button
|
||||||
|
data-testid={`operations.${index}.remove-rest-param`}
|
||||||
size="sm"
|
size="sm"
|
||||||
fill="text"
|
fill="text"
|
||||||
icon="times"
|
icon="times"
|
||||||
@ -100,7 +105,7 @@ export function OperationEditor({
|
|||||||
if (def.params.length > 0) {
|
if (def.params.length > 0) {
|
||||||
const lastParamDef = def.params[def.params.length - 1];
|
const lastParamDef = def.params[def.params.length - 1];
|
||||||
if (lastParamDef.restParam) {
|
if (lastParamDef.restParam) {
|
||||||
restParam = renderAddRestParamButton(lastParamDef, onAddRestParam, operation.params.length, styles);
|
restParam = renderAddRestParamButton(lastParamDef, onAddRestParam, index, operation.params.length, styles);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -111,7 +116,7 @@ export function OperationEditor({
|
|||||||
className={styles.card}
|
className={styles.card}
|
||||||
ref={provided.innerRef}
|
ref={provided.innerRef}
|
||||||
{...provided.draggableProps}
|
{...provided.draggableProps}
|
||||||
data-testid={`operation-wrapper-for-${operation.id}`}
|
data-testid={`operations.${index}.wrapper`}
|
||||||
>
|
>
|
||||||
<div className={styles.header} {...provided.dragHandleProps}>
|
<div className={styles.header} {...provided.dragHandleProps}>
|
||||||
<OperationName
|
<OperationName
|
||||||
@ -151,12 +156,20 @@ export function OperationEditor({
|
|||||||
function renderAddRestParamButton(
|
function renderAddRestParamButton(
|
||||||
paramDef: QueryBuilderOperationParamDef,
|
paramDef: QueryBuilderOperationParamDef,
|
||||||
onAddRestParam: () => void,
|
onAddRestParam: () => void,
|
||||||
|
operationIndex: number,
|
||||||
paramIndex: number,
|
paramIndex: number,
|
||||||
styles: OperationEditorStyles
|
styles: OperationEditorStyles
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<div className={styles.restParam} key={`${paramIndex}-2`}>
|
<div className={styles.restParam} key={`${paramIndex}-2`}>
|
||||||
<Button size="sm" icon="plus" title={`Add ${paramDef.name}`} variant="secondary" onClick={onAddRestParam}>
|
<Button
|
||||||
|
size="sm"
|
||||||
|
icon="plus"
|
||||||
|
title={`Add ${paramDef.name}`}
|
||||||
|
variant="secondary"
|
||||||
|
onClick={onAddRestParam}
|
||||||
|
data-testid={`operations.${operationIndex}.add-rest-param`}
|
||||||
|
>
|
||||||
{paramDef.name}
|
{paramDef.name}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -2,6 +2,7 @@ import { toOption } from '@grafana/data';
|
|||||||
import { Input, Select } from '@grafana/ui';
|
import { Input, Select } from '@grafana/ui';
|
||||||
import React, { ComponentType } from 'react';
|
import React, { ComponentType } from 'react';
|
||||||
import { QueryBuilderOperationParamDef, QueryBuilderOperationParamEditorProps } from '../shared/types';
|
import { QueryBuilderOperationParamDef, QueryBuilderOperationParamEditorProps } from '../shared/types';
|
||||||
|
import { getOperationParamId } from './operationUtils';
|
||||||
|
|
||||||
export function getOperationParamEditor(
|
export function getOperationParamEditor(
|
||||||
paramDef: QueryBuilderOperationParamDef
|
paramDef: QueryBuilderOperationParamDef
|
||||||
@ -20,6 +21,7 @@ export function getOperationParamEditor(
|
|||||||
function SimpleInputParamEditor(props: QueryBuilderOperationParamEditorProps) {
|
function SimpleInputParamEditor(props: QueryBuilderOperationParamEditorProps) {
|
||||||
return (
|
return (
|
||||||
<Input
|
<Input
|
||||||
|
id={getOperationParamId(props.operationIndex, props.index)}
|
||||||
defaultValue={props.value ?? ''}
|
defaultValue={props.value ?? ''}
|
||||||
onKeyDown={(evt) => {
|
onKeyDown={(evt) => {
|
||||||
if (evt.key === 'Enter') {
|
if (evt.key === 'Enter') {
|
||||||
@ -36,7 +38,13 @@ function SimpleInputParamEditor(props: QueryBuilderOperationParamEditorProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SelectInputParamEditor({ paramDef, value, index, onChange }: QueryBuilderOperationParamEditorProps) {
|
function SelectInputParamEditor({
|
||||||
|
paramDef,
|
||||||
|
value,
|
||||||
|
index,
|
||||||
|
operationIndex,
|
||||||
|
onChange,
|
||||||
|
}: QueryBuilderOperationParamEditorProps) {
|
||||||
const selectOptions = paramDef.options!.map((option) => ({
|
const selectOptions = paramDef.options!.map((option) => ({
|
||||||
label: option as string,
|
label: option as string,
|
||||||
value: option as string,
|
value: option as string,
|
||||||
@ -44,6 +52,7 @@ function SelectInputParamEditor({ paramDef, value, index, onChange }: QueryBuild
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
|
id={getOperationParamId(operationIndex, index)}
|
||||||
menuShouldPortal
|
menuShouldPortal
|
||||||
value={toOption(value as string)}
|
value={toOption(value as string)}
|
||||||
options={selectOptions}
|
options={selectOptions}
|
||||||
|
@ -49,3 +49,7 @@ export function defaultAddOperationHandler<T extends QueryWithOperations>(def: Q
|
|||||||
export function getPromAndLokiOperationDisplayName(funcName: string) {
|
export function getPromAndLokiOperationDisplayName(funcName: string) {
|
||||||
return capitalize(funcName.replace(/_/g, ' '));
|
return capitalize(funcName.replace(/_/g, ' '));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getOperationParamId(operationIndex: number, paramIndex: number) {
|
||||||
|
return `operations.${operationIndex}.param.${paramIndex}`;
|
||||||
|
}
|
||||||
|
@ -75,8 +75,10 @@ export interface QueryBuilderOperationEditorProps {
|
|||||||
export interface QueryBuilderOperationParamEditorProps {
|
export interface QueryBuilderOperationParamEditorProps {
|
||||||
value?: QueryBuilderOperationParamValue;
|
value?: QueryBuilderOperationParamValue;
|
||||||
paramDef: QueryBuilderOperationParamDef;
|
paramDef: QueryBuilderOperationParamDef;
|
||||||
|
/** Parameter index */
|
||||||
index: number;
|
index: number;
|
||||||
operation: QueryBuilderOperation;
|
operation: QueryBuilderOperation;
|
||||||
|
operationIndex: number;
|
||||||
query: any;
|
query: any;
|
||||||
datasource: DataSourceApi;
|
datasource: DataSourceApi;
|
||||||
onChange: (index: number, value: QueryBuilderOperationParamValue) => void;
|
onChange: (index: number, value: QueryBuilderOperationParamValue) => void;
|
||||||
|
Loading…
Reference in New Issue
Block a user