Prometheus: Improved the function selector with search (#46084)

* Change to cascader with search

* Don't change to select on backspace

* Fix error on group select

* Simplify getOperationDef

* Set cascader wrapper to fixed width

* Add placeholder

* Fix tests and type errors

* Fix props for backward compatibility

* Add comments
pull/46151/head
Andrej Ocenas 3 years ago committed by GitHub
parent 09f48173fe
commit 6dea7275a6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 40
      packages/grafana-ui/src/components/Cascader/Cascader.tsx
  2. 12
      public/app/plugins/datasource/loki/querybuilder/LokiQueryModeller.test.ts
  3. 12
      public/app/plugins/datasource/loki/querybuilder/operations.ts
  4. 2
      public/app/plugins/datasource/prometheus/querybuilder/PromQueryModeller.ts
  5. 2
      public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilder.test.tsx
  6. 3
      public/app/plugins/datasource/prometheus/querybuilder/operations.ts
  7. 4
      public/app/plugins/datasource/prometheus/querybuilder/shared/LokiAndPromQueryModellerBase.ts
  8. 3
      public/app/plugins/datasource/prometheus/querybuilder/shared/OperationEditor.tsx
  9. 39
      public/app/plugins/datasource/prometheus/querybuilder/shared/OperationList.tsx
  10. 3
      public/app/plugins/datasource/prometheus/querybuilder/shared/OperationListExplained.tsx
  11. 3
      public/app/plugins/datasource/prometheus/querybuilder/shared/OperationName.tsx
  12. 2
      public/app/plugins/datasource/prometheus/querybuilder/shared/types.ts

@ -23,7 +23,16 @@ export interface CascaderProps {
allowCustomValue?: boolean; allowCustomValue?: boolean;
/** A function for formatting the message for custom value creation. Only applies when allowCustomValue is set to true*/ /** A function for formatting the message for custom value creation. Only applies when allowCustomValue is set to true*/
formatCreateLabel?: (val: string) => string; formatCreateLabel?: (val: string) => string;
/** If true all levels are shown in the input by simple concatenating the labels */
displayAllSelectedLevels?: boolean; displayAllSelectedLevels?: boolean;
onBlur?: () => void;
/** When mounted focus automatically on the input */
autoFocus?: boolean;
/** Keep the dropdown open all the time, useful in case whole cascader visibility is controlled by the parent */
alwaysOpen?: boolean;
/** Don't show what is selected in the cascader input/search. Useful when input is used just as search and the
cascader is hidden after selection. */
hideActiveLevelLabel?: boolean;
} }
interface CascaderState { interface CascaderState {
@ -117,12 +126,15 @@ export class Cascader extends React.PureComponent<CascaderProps, CascaderState>
//For rc-cascader //For rc-cascader
onChange = (value: string[], selectedOptions: CascaderOption[]) => { onChange = (value: string[], selectedOptions: CascaderOption[]) => {
const activeLabel = this.props.hideActiveLevelLabel
? ''
: this.props.displayAllSelectedLevels
? selectedOptions.map((option) => option.label).join(this.props.separator || DEFAULT_SEPARATOR)
: selectedOptions[selectedOptions.length - 1].label;
this.setState({ this.setState({
rcValue: value, rcValue: value,
focusCascade: true, focusCascade: true,
activeLabel: this.props.displayAllSelectedLevels activeLabel,
? selectedOptions.map((option) => option.label).join(this.props.separator || DEFAULT_SEPARATOR)
: selectedOptions[selectedOptions.length - 1].label,
}); });
this.props.onSelect(selectedOptions[selectedOptions.length - 1].value); this.props.onSelect(selectedOptions[selectedOptions.length - 1].value);
@ -159,22 +171,19 @@ export class Cascader extends React.PureComponent<CascaderProps, CascaderState>
rcValue: [], rcValue: [],
}); });
} }
this.props.onBlur?.();
}; };
onBlurCascade = () => { onBlurCascade = () => {
this.setState({ this.setState({
focusCascade: false, focusCascade: false,
}); });
this.props.onBlur?.();
}; };
onInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => { onInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if ( if (['ArrowDown', 'ArrowUp', 'Enter', 'ArrowLeft', 'ArrowRight', 'Backspace'].includes(e.key)) {
e.key === 'ArrowDown' ||
e.key === 'ArrowUp' ||
e.key === 'Enter' ||
e.key === 'ArrowLeft' ||
e.key === 'ArrowRight'
) {
return; return;
} }
this.setState({ this.setState({
@ -183,6 +192,14 @@ export class Cascader extends React.PureComponent<CascaderProps, CascaderState>
}); });
}; };
onSelectInputChange = (value: string) => {
if (value === '') {
this.setState({
isSearching: false,
});
}
};
render() { render() {
const { allowCustomValue, formatCreateLabel, placeholder, width, changeOnSelect, options } = this.props; const { allowCustomValue, formatCreateLabel, placeholder, width, changeOnSelect, options } = this.props;
const { focusCascade, isSearching, rcValue, activeLabel } = this.state; const { focusCascade, isSearching, rcValue, activeLabel } = this.state;
@ -203,6 +220,7 @@ export class Cascader extends React.PureComponent<CascaderProps, CascaderState>
onCreateOption={this.onCreateOption} onCreateOption={this.onCreateOption}
formatCreateLabel={formatCreateLabel} formatCreateLabel={formatCreateLabel}
width={width} width={width}
onInputChange={this.onSelectInputChange}
/> />
) : ( ) : (
<RCCascader <RCCascader
@ -212,9 +230,11 @@ export class Cascader extends React.PureComponent<CascaderProps, CascaderState>
value={rcValue.value} value={rcValue.value}
fieldNames={{ label: 'label', value: 'value', children: 'items' }} fieldNames={{ label: 'label', value: 'value', children: 'items' }}
expandIcon={null} expandIcon={null}
open={this.props.alwaysOpen}
> >
<div className={disableDivFocus}> <div className={disableDivFocus}>
<Input <Input
autoFocus={this.props.autoFocus}
width={width} width={width}
placeholder={placeholder} placeholder={placeholder}
onBlur={this.onBlurCascade} onBlur={this.onBlurCascade}

@ -119,7 +119,7 @@ describe('LokiQueryModeller', () => {
operations: [], operations: [],
}; };
const def = modeller.getOperationDef('sum'); const def = modeller.getOperationDef('sum')!;
const result = def.addOperationHandler(def, query, modeller); const result = def.addOperationHandler(def, query, modeller);
expect(result.operations[0].id).toBe('rate'); expect(result.operations[0].id).toBe('rate');
expect(result.operations[1].id).toBe('sum'); expect(result.operations[1].id).toBe('sum');
@ -131,7 +131,7 @@ describe('LokiQueryModeller', () => {
operations: [{ id: 'json', params: [] }], operations: [{ id: 'json', params: [] }],
}; };
const def = modeller.getOperationDef('sum'); const def = modeller.getOperationDef('sum')!;
const result = def.addOperationHandler(def, query, modeller); const result = def.addOperationHandler(def, query, modeller);
expect(result.operations[0].id).toBe('json'); expect(result.operations[0].id).toBe('json');
expect(result.operations[1].id).toBe('rate'); expect(result.operations[1].id).toBe('rate');
@ -144,7 +144,7 @@ describe('LokiQueryModeller', () => {
operations: [{ id: 'rate', params: [] }], operations: [{ id: 'rate', params: [] }],
}; };
const def = modeller.getOperationDef('json'); const def = modeller.getOperationDef('json')!;
const result = def.addOperationHandler(def, query, modeller); const result = def.addOperationHandler(def, query, modeller);
expect(result.operations[0].id).toBe('json'); expect(result.operations[0].id).toBe('json');
expect(result.operations[1].id).toBe('rate'); expect(result.operations[1].id).toBe('rate');
@ -156,7 +156,7 @@ describe('LokiQueryModeller', () => {
operations: [{ id: '__line_contains', params: ['error'] }], operations: [{ id: '__line_contains', params: ['error'] }],
}; };
const def = modeller.getOperationDef('json'); const def = modeller.getOperationDef('json')!;
const result = def.addOperationHandler(def, query, modeller); const result = def.addOperationHandler(def, query, modeller);
expect(result.operations[0].id).toBe('__line_contains'); expect(result.operations[0].id).toBe('__line_contains');
expect(result.operations[1].id).toBe('json'); expect(result.operations[1].id).toBe('json');
@ -168,7 +168,7 @@ describe('LokiQueryModeller', () => {
operations: [{ id: 'json', params: [] }], operations: [{ id: 'json', params: [] }],
}; };
const def = modeller.getOperationDef('__line_contains'); const def = modeller.getOperationDef('__line_contains')!;
const result = def.addOperationHandler(def, query, modeller); const result = def.addOperationHandler(def, query, modeller);
expect(result.operations[0].id).toBe('__line_contains'); expect(result.operations[0].id).toBe('__line_contains');
expect(result.operations[1].id).toBe('json'); expect(result.operations[1].id).toBe('json');
@ -180,7 +180,7 @@ describe('LokiQueryModeller', () => {
operations: [], operations: [],
}; };
const def = modeller.getOperationDef('rate'); const def = modeller.getOperationDef('rate')!;
const result = def.addOperationHandler(def, query, modeller); const result = def.addOperationHandler(def, query, modeller);
expect(result.operations.length).toBe(1); expect(result.operations.length).toBe(1);
}); });

@ -220,7 +220,11 @@ function getIndexOfOrLast(
condition: (def: QueryBuilderOperationDef) => boolean condition: (def: QueryBuilderOperationDef) => boolean
) { ) {
const index = operations.findIndex((x) => { const index = operations.findIndex((x) => {
return condition(queryModeller.getOperationDef(x.id)); const opDef = queryModeller.getOperationDef(x.id);
if (!opDef) {
return false;
}
return condition(opDef);
}); });
return index === -1 ? operations.length : index; return index === -1 ? operations.length : index;
@ -242,7 +246,11 @@ export function addLokiOperation(
case LokiVisualQueryOperationCategory.Aggregations: case LokiVisualQueryOperationCategory.Aggregations:
case LokiVisualQueryOperationCategory.Functions: { case LokiVisualQueryOperationCategory.Functions: {
const rangeVectorFunction = operations.find((x) => { const rangeVectorFunction = operations.find((x) => {
return isRangeVectorFunction(modeller.getOperationDef(x.id)); const opDef = modeller.getOperationDef(x.id);
if (!opDef) {
return false;
}
return isRangeVectorFunction(opDef);
}); });
// If we are adding a function but we have not range vector function yet add one // If we are adding a function but we have not range vector function yet add one

@ -48,7 +48,7 @@ export class PromQueryModeller extends LokiAndPromQueryModellerBase<PromVisualQu
return ( return (
query.operations.find((op) => { query.operations.find((op) => {
const def = this.getOperationDef(op.id); const def = this.getOperationDef(op.id);
return def.category === PromVisualQueryOperationCategory.BinaryOps; return def?.category === PromVisualQueryOperationCategory.BinaryOps;
}) !== undefined }) !== undefined
); );
} }

@ -50,7 +50,7 @@ describe('PromQueryBuilder', () => {
setup(); setup();
// Add label // Add label
expect(screen.getByLabelText('Add')).toBeInTheDocument(); expect(screen.getByLabelText('Add')).toBeInTheDocument();
expect(screen.getByLabelText('Add operation')).toBeInTheDocument(); expect(screen.getByTitle('Add operation')).toBeInTheDocument();
}); });
it('renders all the query sections', async () => { it('renders all the query sections', async () => {

@ -350,7 +350,8 @@ export function addOperationWithRangeVector(
}; };
if (query.operations.length > 0) { if (query.operations.length > 0) {
const firstOp = modeller.getOperationDef(query.operations[0].id); // If operation exists it has to be in the registry so no point to check if it was found
const firstOp = modeller.getOperationDef(query.operations[0].id)!;
if (firstOp.addOperationHandler === addOperationWithRangeVector) { if (firstOp.addOperationHandler === addOperationWithRangeVector) {
return { return {

@ -37,8 +37,8 @@ export abstract class LokiAndPromQueryModellerBase<T extends QueryWithOperations
return this.categories; return this.categories;
} }
getOperationDef(id: string) { getOperationDef(id: string): QueryBuilderOperationDef | undefined {
return this.operationsRegisty.get(id); return this.operationsRegisty.getIfExists(id);
} }
renderOperations(queryString: string, operations: QueryBuilderOperation[]) { renderOperations(queryString: string, operations: QueryBuilderOperation[]) {

@ -39,6 +39,9 @@ export function OperationEditor({
}: Props) { }: Props) {
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
const def = queryModeller.getOperationDef(operation.id); const def = queryModeller.getOperationDef(operation.id);
if (!def) {
return <span>Operation {operation.id} not found</span>;
}
const onParamValueChanged = (paramIdx: number, value: QueryBuilderOperationParamValue) => { const onParamValueChanged = (paramIdx: number, value: QueryBuilderOperationParamValue) => {
const update: QueryBuilderOperation = { ...operation, params: [...operation.params] }; const update: QueryBuilderOperation = { ...operation, params: [...operation.params] };

@ -1,8 +1,8 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { DataSourceApi, GrafanaTheme2 } from '@grafana/data'; import { DataSourceApi, GrafanaTheme2 } from '@grafana/data';
import { Stack } from '@grafana/experimental'; import { Stack } from '@grafana/experimental';
import { ButtonCascader, CascaderOption, useStyles2 } from '@grafana/ui'; import { Button, Cascader, CascaderOption, useStyles2 } from '@grafana/ui';
import React from 'react'; import React, { useState } from 'react';
import { DragDropContext, Droppable, DropResult } from 'react-beautiful-dnd'; import { DragDropContext, Droppable, DropResult } from 'react-beautiful-dnd';
import { QueryBuilderOperation, QueryWithOperations, VisualQueryModeller } from '../shared/types'; import { QueryBuilderOperation, QueryWithOperations, VisualQueryModeller } from '../shared/types';
import { OperationEditor } from './OperationEditor'; import { OperationEditor } from './OperationEditor';
@ -26,6 +26,8 @@ export function OperationList<T extends QueryWithOperations>({
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
const { operations } = query; const { operations } = query;
const [cascaderOpen, setCascaderOpen] = useState(false);
const onOperationChange = (index: number, update: QueryBuilderOperation) => { const onOperationChange = (index: number, update: QueryBuilderOperation) => {
const updatedList = [...operations]; const updatedList = [...operations];
updatedList.splice(index, 1, update); updatedList.splice(index, 1, update);
@ -41,7 +43,7 @@ export function OperationList<T extends QueryWithOperations>({
return { return {
value: category, value: category,
label: category, label: category,
children: queryModeller.getOperationsForCategory(category).map((operation) => ({ items: queryModeller.getOperationsForCategory(category).map((operation) => ({
value: operation.id, value: operation.id,
label: operation.name, label: operation.name,
isLeaf: true, isLeaf: true,
@ -49,9 +51,13 @@ export function OperationList<T extends QueryWithOperations>({
}; };
}); });
const onAddOperation = (value: string[]) => { const onAddOperation = (value: string) => {
const operationDef = queryModeller.getOperationDef(value[1]); const operationDef = queryModeller.getOperationDef(value);
if (!operationDef) {
return;
}
onChange(operationDef.addOperationHandler(operationDef, query, queryModeller)); onChange(operationDef.addOperationHandler(operationDef, query, queryModeller));
setCascaderOpen(false);
}; };
const onDragEnd = (result: DropResult) => { const onDragEnd = (result: DropResult) => {
@ -66,6 +72,10 @@ export function OperationList<T extends QueryWithOperations>({
onChange({ ...query, operations: updatedList }); onChange({ ...query, operations: updatedList });
}; };
const onCascaderBlur = () => {
setCascaderOpen(false);
};
return ( return (
<Stack gap={1} direction="column"> <Stack gap={1} direction="column">
<Stack gap={1}> <Stack gap={1}>
@ -94,15 +104,19 @@ export function OperationList<T extends QueryWithOperations>({
</DragDropContext> </DragDropContext>
)} )}
<div className={styles.addButton}> <div className={styles.addButton}>
<ButtonCascader {cascaderOpen ? (
key="cascader" <Cascader
icon="plus"
options={addOptions} options={addOptions}
onChange={onAddOperation} onSelect={onAddOperation}
variant="secondary" onBlur={onCascaderBlur}
hideDownIcon={true} autoFocus={true}
buttonProps={{ 'aria-label': 'Add operation', title: 'Add operation' }} alwaysOpen={true}
hideActiveLevelLabel={true}
placeholder={'Search'}
/> />
) : (
<Button icon={'plus'} variant={'secondary'} onClick={() => setCascaderOpen(true)} title={'Add operation'} />
)}
</div> </div>
</Stack> </Stack>
</Stack> </Stack>
@ -122,6 +136,7 @@ const getStyles = (theme: GrafanaTheme2) => {
gap: theme.spacing(2), gap: theme.spacing(2),
}), }),
addButton: css({ addButton: css({
width: 150,
paddingBottom: theme.spacing(1), paddingBottom: theme.spacing(1),
}), }),
}; };

@ -14,6 +14,9 @@ export function OperationListExplained<T extends QueryWithOperations>({ query, q
<> <>
{query.operations.map((op, index) => { {query.operations.map((op, index) => {
const def = queryModeller.getOperationDef(op.id); const def = queryModeller.getOperationDef(op.id);
if (!def) {
return `Operation ${op.id} not found`;
}
const title = def.renderer(op, def, '<expr>'); const title = def.renderer(op, def, '<expr>');
const body = def.explainHandler ? def.explainHandler(op, def) : def.documentation ?? 'no docs'; const body = def.explainHandler ? def.explainHandler(op, def) : def.documentation ?? 'no docs';

@ -60,7 +60,8 @@ export const OperationName = React.memo<Props>(({ operation, def, index, onChang
onCloseMenu={onToggleSwitcher} onCloseMenu={onToggleSwitcher}
onChange={(value) => { onChange={(value) => {
if (value.value) { if (value.value) {
const newDef = queryModeller.getOperationDef(value.value.id); // Operation should exist if it is selectable
const newDef = queryModeller.getOperationDef(value.value.id)!;
let changedOp = { ...operation, id: value.value.id }; let changedOp = { ...operation, id: value.value.id };
onChange(index, def.changeTypeHandler ? def.changeTypeHandler(changedOp, newDef) : changedOp); onChange(index, def.changeTypeHandler ? def.changeTypeHandler(changedOp, newDef) : changedOp);
} }

@ -96,5 +96,5 @@ export interface VisualQueryModeller {
getOperationsForCategory(category: string): QueryBuilderOperationDef[]; getOperationsForCategory(category: string): QueryBuilderOperationDef[];
getAlternativeOperations(key: string): QueryBuilderOperationDef[]; getAlternativeOperations(key: string): QueryBuilderOperationDef[];
getCategories(): string[]; getCategories(): string[];
getOperationDef(id: string): QueryBuilderOperationDef; getOperationDef(id: string): QueryBuilderOperationDef | undefined;
} }

Loading…
Cancel
Save