mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
9b6552c7b4
commit
4ab191b612
@ -43,7 +43,7 @@ export const Input = React.forwardRef<HTMLInputElement, Props>((props, ref) => {
|
|||||||
const styles = getInputStyles({ theme, invalid: !!invalid, width });
|
const styles = getInputStyles({ theme, invalid: !!invalid, width });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cx(styles.wrapper, className)}>
|
<div className={cx(styles.wrapper, className)} data-testid={'input-wrapper'}>
|
||||||
{!!addonBefore && <div className={styles.addon}>{addonBefore}</div>}
|
{!!addonBefore && <div className={styles.addon}>{addonBefore}</div>}
|
||||||
|
|
||||||
<div className={styles.inputWrapper}>
|
<div className={styles.inputWrapper}>
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { GrafanaTheme2, toOption } from '@grafana/data';
|
import { GrafanaTheme2, toOption } from '@grafana/data';
|
||||||
import { EditorRows, FlexItem } from '@grafana/experimental';
|
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 React from 'react';
|
||||||
import { PrometheusDatasource } from '../../datasource';
|
import { PrometheusDatasource } from '../../datasource';
|
||||||
|
import { AutoSizeInput } from '../shared/AutoSizeInput';
|
||||||
import { PromVisualQueryBinary } from '../types';
|
import { PromVisualQueryBinary } from '../types';
|
||||||
import { PromQueryBuilder } from './PromQueryBuilder';
|
import { PromQueryBuilder } from './PromQueryBuilder';
|
||||||
|
|
||||||
@ -36,10 +37,10 @@ export const NestedQuery = React.memo<Props>(({ nestedQuery, index, datasource,
|
|||||||
/>
|
/>
|
||||||
<div className={styles.name}>Vector matches</div>
|
<div className={styles.name}>Vector matches</div>
|
||||||
|
|
||||||
<Input
|
<AutoSizeInput
|
||||||
width={20}
|
minWidth={20}
|
||||||
defaultValue={nestedQuery.vectorMatches}
|
defaultValue={nestedQuery.vectorMatches}
|
||||||
onBlur={(evt) => {
|
onCommitChange={(evt) => {
|
||||||
onChange(index, {
|
onChange(index, {
|
||||||
...nestedQuery,
|
...nestedQuery,
|
||||||
vectorMatches: evt.currentTarget.value,
|
vectorMatches: evt.currentTarget.value,
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
import React, { SyntheticEvent } from 'react';
|
import React, { SyntheticEvent } from 'react';
|
||||||
import { EditorRow, EditorField } from '@grafana/experimental';
|
import { EditorRow, EditorField } from '@grafana/experimental';
|
||||||
import { CoreApp, SelectableValue } from '@grafana/data';
|
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 { QueryOptionGroup } from '../shared/QueryOptionGroup';
|
||||||
import { PromQuery } from '../../types';
|
import { PromQuery } from '../../types';
|
||||||
import { FORMAT_OPTIONS, INTERVAL_FACTOR_OPTIONS } from '../../components/PromQueryEditor';
|
import { FORMAT_OPTIONS, INTERVAL_FACTOR_OPTIONS } from '../../components/PromQueryEditor';
|
||||||
import { getQueryTypeChangeHandler, getQueryTypeOptions } from '../../components/PromExploreExtraField';
|
import { getQueryTypeChangeHandler, getQueryTypeOptions } from '../../components/PromExploreExtraField';
|
||||||
import { getLegendModeLabel, PromQueryLegendEditor } from './PromQueryLegendEditor';
|
import { getLegendModeLabel, PromQueryLegendEditor } from './PromQueryLegendEditor';
|
||||||
|
import { AutoSizeInput } from '../shared/AutoSizeInput';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
query: PromQuery;
|
query: PromQuery;
|
||||||
@ -21,7 +22,7 @@ export const PromQueryBuilderOptions = React.memo<Props>(({ query, app, onChange
|
|||||||
onRunQuery();
|
onRunQuery();
|
||||||
};
|
};
|
||||||
|
|
||||||
const onChangeStep = (evt: React.FocusEvent<HTMLInputElement>) => {
|
const onChangeStep = (evt: React.FormEvent<HTMLInputElement>) => {
|
||||||
onChange({ ...query, interval: evt.currentTarget.value });
|
onChange({ ...query, interval: evt.currentTarget.value });
|
||||||
onRunQuery();
|
onRunQuery();
|
||||||
};
|
};
|
||||||
@ -57,12 +58,12 @@ export const PromQueryBuilderOptions = React.memo<Props>(({ query, app, onChange
|
|||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Input
|
<AutoSizeInput
|
||||||
type="text"
|
type="text"
|
||||||
aria-label="Set lower limit for the step parameter"
|
aria-label="Set lower limit for the step parameter"
|
||||||
placeholder={'auto'}
|
placeholder={'auto'}
|
||||||
width={10}
|
minWidth={10}
|
||||||
onBlur={onChangeStep}
|
onCommitChange={onChangeStep}
|
||||||
defaultValue={query.interval}
|
defaultValue={query.interval}
|
||||||
/>
|
/>
|
||||||
</EditorField>
|
</EditorField>
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import React, { useRef } from 'react';
|
import React, { useRef } from 'react';
|
||||||
import { EditorField } from '@grafana/experimental';
|
import { EditorField } from '@grafana/experimental';
|
||||||
import { SelectableValue } from '@grafana/data';
|
import { SelectableValue } from '@grafana/data';
|
||||||
import { Input, Select } from '@grafana/ui';
|
import { Select } from '@grafana/ui';
|
||||||
import { LegendFormatMode, PromQuery } from '../../types';
|
import { LegendFormatMode, PromQuery } from '../../types';
|
||||||
|
import { AutoSizeInput } from '../shared/AutoSizeInput';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
query: PromQuery;
|
query: PromQuery;
|
||||||
@ -27,7 +28,7 @@ export const PromQueryLegendEditor = React.memo<Props>(({ query, onChange, onRun
|
|||||||
const mode = getLegendMode(query.legendFormat);
|
const mode = getLegendMode(query.legendFormat);
|
||||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||||
|
|
||||||
const onLegendFormatChanged = (evt: React.FocusEvent<HTMLInputElement>) => {
|
const onLegendFormatChanged = (evt: React.FormEvent<HTMLInputElement>) => {
|
||||||
let legendFormat = evt.currentTarget.value;
|
let legendFormat = evt.currentTarget.value;
|
||||||
if (legendFormat.length === 0) {
|
if (legendFormat.length === 0) {
|
||||||
legendFormat = LegendFormatMode.Auto;
|
legendFormat = LegendFormatMode.Auto;
|
||||||
@ -62,12 +63,12 @@ export const PromQueryLegendEditor = React.memo<Props>(({ query, onChange, onRun
|
|||||||
>
|
>
|
||||||
<>
|
<>
|
||||||
{mode === LegendFormatMode.Custom && (
|
{mode === LegendFormatMode.Custom && (
|
||||||
<Input
|
<AutoSizeInput
|
||||||
id="legendFormat"
|
id="legendFormat"
|
||||||
width={22}
|
minWidth={22}
|
||||||
placeholder="auto"
|
placeholder="auto"
|
||||||
defaultValue={query.legendFormat}
|
defaultValue={query.legendFormat}
|
||||||
onBlur={onLegendFormatChanged}
|
onCommitChange={onLegendFormatChanged}
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
@ -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');
|
||||||
|
});
|
||||||
|
});
|
@ -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';
|
@ -1,7 +1,8 @@
|
|||||||
import { SelectableValue, toOption } from '@grafana/data';
|
import { SelectableValue, toOption } from '@grafana/data';
|
||||||
import { Input, Select } from '@grafana/ui';
|
import { 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 { AutoSizeInput } from './AutoSizeInput';
|
||||||
import { getOperationParamId } from './operationUtils';
|
import { getOperationParamId } from './operationUtils';
|
||||||
|
|
||||||
export function getOperationParamEditor(
|
export function getOperationParamEditor(
|
||||||
@ -20,18 +21,10 @@ export function getOperationParamEditor(
|
|||||||
|
|
||||||
function SimpleInputParamEditor(props: QueryBuilderOperationParamEditorProps) {
|
function SimpleInputParamEditor(props: QueryBuilderOperationParamEditorProps) {
|
||||||
return (
|
return (
|
||||||
<Input
|
<AutoSizeInput
|
||||||
id={getOperationParamId(props.operationIndex, props.index)}
|
id={getOperationParamId(props.operationIndex, props.index)}
|
||||||
defaultValue={props.value ?? ''}
|
defaultValue={props.value}
|
||||||
onKeyDown={(evt) => {
|
onCommitChange={(evt) => {
|
||||||
if (evt.key === 'Enter') {
|
|
||||||
if (evt.currentTarget.value !== props.value) {
|
|
||||||
props.onChange(props.index, evt.currentTarget.value);
|
|
||||||
}
|
|
||||||
props.onRunQuery();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onBlur={(evt) => {
|
|
||||||
props.onChange(props.index, evt.currentTarget.value);
|
props.onChange(props.index, evt.currentTarget.value);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
Loading…
Reference in New Issue
Block a user