Prometheus: Create AutoSizeInput with dynamic width (#45601)

* Autosize input for dynamic width

* Update

* Refactoring to use measureText util instead

* removed react fragment tags

* Add tests

* Use AutoSize input in legend, step and nested queries vector matcher

* Update

* Remove unused imports

Co-authored-by: Torkel Ödegaard <torkel@grafana.com>
This commit is contained in:
Ivana Huckova 2022-02-28 10:52:58 +01:00 committed by GitHub
parent 9b6552c7b4
commit 4ab191b612
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 145 additions and 27 deletions

View File

@ -43,7 +43,7 @@ export const Input = React.forwardRef<HTMLInputElement, Props>((props, ref) => {
const styles = getInputStyles({ theme, invalid: !!invalid, width });
return (
<div className={cx(styles.wrapper, className)}>
<div className={cx(styles.wrapper, className)} data-testid={'input-wrapper'}>
{!!addonBefore && <div className={styles.addon}>{addonBefore}</div>}
<div className={styles.inputWrapper}>

View File

@ -1,9 +1,10 @@
import { css } from '@emotion/css';
import { GrafanaTheme2, toOption } from '@grafana/data';
import { EditorRows, FlexItem } from '@grafana/experimental';
import { IconButton, Input, Select, useStyles2 } from '@grafana/ui';
import { IconButton, Select, useStyles2 } from '@grafana/ui';
import React from 'react';
import { PrometheusDatasource } from '../../datasource';
import { AutoSizeInput } from '../shared/AutoSizeInput';
import { PromVisualQueryBinary } from '../types';
import { PromQueryBuilder } from './PromQueryBuilder';
@ -36,10 +37,10 @@ export const NestedQuery = React.memo<Props>(({ nestedQuery, index, datasource,
/>
<div className={styles.name}>Vector matches</div>
<Input
width={20}
<AutoSizeInput
minWidth={20}
defaultValue={nestedQuery.vectorMatches}
onBlur={(evt) => {
onCommitChange={(evt) => {
onChange(index, {
...nestedQuery,
vectorMatches: evt.currentTarget.value,

View File

@ -1,12 +1,13 @@
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 { RadioButtonGroup, Select, Switch } from '@grafana/ui';
import { QueryOptionGroup } from '../shared/QueryOptionGroup';
import { PromQuery } from '../../types';
import { FORMAT_OPTIONS, INTERVAL_FACTOR_OPTIONS } from '../../components/PromQueryEditor';
import { getQueryTypeChangeHandler, getQueryTypeOptions } from '../../components/PromExploreExtraField';
import { getLegendModeLabel, PromQueryLegendEditor } from './PromQueryLegendEditor';
import { AutoSizeInput } from '../shared/AutoSizeInput';
export interface Props {
query: PromQuery;
@ -21,7 +22,7 @@ export const PromQueryBuilderOptions = React.memo<Props>(({ query, app, onChange
onRunQuery();
};
const onChangeStep = (evt: React.FocusEvent<HTMLInputElement>) => {
const onChangeStep = (evt: React.FormEvent<HTMLInputElement>) => {
onChange({ ...query, interval: evt.currentTarget.value });
onRunQuery();
};
@ -57,12 +58,12 @@ export const PromQueryBuilderOptions = React.memo<Props>(({ query, app, onChange
</>
}
>
<Input
<AutoSizeInput
type="text"
aria-label="Set lower limit for the step parameter"
placeholder={'auto'}
width={10}
onBlur={onChangeStep}
minWidth={10}
onCommitChange={onChangeStep}
defaultValue={query.interval}
/>
</EditorField>

View File

@ -1,8 +1,9 @@
import React, { useRef } from 'react';
import { EditorField } from '@grafana/experimental';
import { SelectableValue } from '@grafana/data';
import { Input, Select } from '@grafana/ui';
import { Select } from '@grafana/ui';
import { LegendFormatMode, PromQuery } from '../../types';
import { AutoSizeInput } from '../shared/AutoSizeInput';
export interface Props {
query: PromQuery;
@ -27,7 +28,7 @@ export const PromQueryLegendEditor = React.memo<Props>(({ query, onChange, onRun
const mode = getLegendMode(query.legendFormat);
const inputRef = useRef<HTMLInputElement | null>(null);
const onLegendFormatChanged = (evt: React.FocusEvent<HTMLInputElement>) => {
const onLegendFormatChanged = (evt: React.FormEvent<HTMLInputElement>) => {
let legendFormat = evt.currentTarget.value;
if (legendFormat.length === 0) {
legendFormat = LegendFormatMode.Auto;
@ -62,12 +63,12 @@ export const PromQueryLegendEditor = React.memo<Props>(({ query, onChange, onRun
>
<>
{mode === LegendFormatMode.Custom && (
<Input
<AutoSizeInput
id="legendFormat"
width={22}
minWidth={22}
placeholder="auto"
defaultValue={query.legendFormat}
onBlur={onLegendFormatChanged}
onCommitChange={onLegendFormatChanged}
ref={inputRef}
/>
)}

View File

@ -0,0 +1,51 @@
import React from 'react';
import { screen, render, fireEvent } from '@testing-library/react';
import { AutoSizeInput } from './AutoSizeInput';
jest.mock('@grafana/ui', () => {
const original = jest.requireActual('@grafana/ui');
const mockedUi = { ...original };
// Mocking measureText
mockedUi.measureText = (text: string, fontSize: number) => {
return { width: text.length * fontSize };
};
return mockedUi;
});
describe('AutoSizeInput', () => {
it('should have default minWidth when empty', () => {
render(<AutoSizeInput />);
const input: HTMLInputElement = screen.getByTestId('autosize-input');
const inputWrapper: HTMLDivElement = screen.getByTestId('input-wrapper');
fireEvent.change(input, { target: { value: '' } });
expect(input.value).toBe('');
expect(getComputedStyle(inputWrapper).width).toBe('80px');
});
it('should have default minWidth for short content', () => {
render(<AutoSizeInput />);
const input: HTMLInputElement = screen.getByTestId('autosize-input');
const inputWrapper: HTMLDivElement = screen.getByTestId('input-wrapper');
fireEvent.change(input, { target: { value: 'foo' } });
expect(input.value).toBe('foo');
expect(getComputedStyle(inputWrapper).width).toBe('80px');
});
it('should change width for long content', () => {
render(<AutoSizeInput />);
const input: HTMLInputElement = screen.getByTestId('autosize-input');
const inputWrapper: HTMLDivElement = screen.getByTestId('input-wrapper');
fireEvent.change(input, { target: { value: 'very very long value' } });
expect(getComputedStyle(inputWrapper).width).toBe('304px');
});
});

View File

@ -0,0 +1,71 @@
import { Input, measureText } from '@grafana/ui';
import { Props as InputProps } from '@grafana/ui/src/components/Input/Input';
import React, { useEffect } from 'react';
export interface Props extends InputProps {
/** Sets the min-width to a multiple of 8px. Default value is 10*/
minWidth?: number;
/** Sets the max-width to a multiple of 8px.*/
maxWidth?: number;
/** onChange function that will be run on onBlur and onKeyPress with enter*/
onCommitChange?: (event: React.FormEvent<HTMLInputElement>) => void;
}
export const AutoSizeInput = React.forwardRef<HTMLInputElement, Props>((props, ref) => {
const { defaultValue = '', minWidth = 10, maxWidth, onCommitChange, onKeyDown, onBlur, ...restProps } = props;
const [value, setValue] = React.useState(defaultValue);
const [inputWidth, setInputWidth] = React.useState(minWidth);
useEffect(() => {
setInputWidth(getWidthFor(value.toString(), minWidth, maxWidth));
}, [value, maxWidth, minWidth]);
return (
<Input
{...restProps}
ref={ref}
value={value.toString()}
onChange={(event) => {
setValue(event.currentTarget.value);
}}
width={inputWidth}
onBlur={(event) => {
if (onCommitChange) {
onCommitChange(event);
}
if (onBlur) {
onBlur(event);
}
}}
onKeyDown={(event) => {
if (event.key === 'Enter' && onCommitChange) {
onCommitChange(event);
}
if (onKeyDown) {
onKeyDown(event);
}
}}
data-testid={'autosize-input'}
/>
);
});
function getWidthFor(value: string, minWidth: number, maxWidth: number | undefined): number {
if (!value) {
return minWidth;
}
const extraSpace = 3;
const realWidth = measureText(value.toString(), 14).width / 8 + extraSpace;
if (minWidth && realWidth < minWidth) {
return minWidth;
}
if (maxWidth && realWidth > maxWidth) {
return realWidth;
}
return realWidth;
}
AutoSizeInput.displayName = 'AutoSizeInput';

View File

@ -1,7 +1,8 @@
import { SelectableValue, toOption } from '@grafana/data';
import { Input, Select } from '@grafana/ui';
import { Select } from '@grafana/ui';
import React, { ComponentType } from 'react';
import { QueryBuilderOperationParamDef, QueryBuilderOperationParamEditorProps } from '../shared/types';
import { AutoSizeInput } from './AutoSizeInput';
import { getOperationParamId } from './operationUtils';
export function getOperationParamEditor(
@ -20,18 +21,10 @@ export function getOperationParamEditor(
function SimpleInputParamEditor(props: QueryBuilderOperationParamEditorProps) {
return (
<Input
<AutoSizeInput
id={getOperationParamId(props.operationIndex, props.index)}
defaultValue={props.value ?? ''}
onKeyDown={(evt) => {
if (evt.key === 'Enter') {
if (evt.currentTarget.value !== props.value) {
props.onChange(props.index, evt.currentTarget.value);
}
props.onRunQuery();
}
}}
onBlur={(evt) => {
defaultValue={props.value}
onCommitChange={(evt) => {
props.onChange(props.index, evt.currentTarget.value);
}}
/>