123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849 |
- import {Component, createRef} from 'react';
- import type {SingleValueProps} from 'react-select';
- import {components} from 'react-select';
- import styled from '@emotion/styled';
- import cloneDeep from 'lodash/cloneDeep';
- import type {ControlProps} from 'sentry/components/forms/controls/selectControl';
- import SelectControl from 'sentry/components/forms/controls/selectControl';
- import type {InputProps} from 'sentry/components/input';
- import Input from 'sentry/components/input';
- import Tag from 'sentry/components/tag';
- import {Tooltip} from 'sentry/components/tooltip';
- import {IconWarning} from 'sentry/icons';
- import {t} from 'sentry/locale';
- import {pulse} from 'sentry/styles/animations';
- import {space} from 'sentry/styles/space';
- import type {SelectValue} from 'sentry/types';
- import type {
- AggregateParameter,
- AggregationKeyWithAlias,
- Column,
- ColumnType,
- QueryFieldValue,
- ValidateColumnTypes,
- } from 'sentry/utils/discover/fields';
- import {AGGREGATIONS, DEPRECATED_FIELDS} from 'sentry/utils/discover/fields';
- import {SESSIONS_OPERATIONS} from 'sentry/views/dashboards/widgetBuilder/releaseWidget/fields';
- import ArithmeticInput from './arithmeticInput';
- import type {FieldValue, FieldValueColumns} from './types';
- import {FieldValueKind} from './types';
- export type FieldValueOption = SelectValue<FieldValue>;
- type FieldOptions = Record<string, FieldValueOption>;
- // Intermediate type that combines the current column
- // data with the AggregateParameter type.
- type ParameterDescription =
- | {
- dataType: ColumnType;
- kind: 'value';
- required: boolean;
- value: string;
- placeholder?: string;
- }
- | {
- kind: 'column';
- options: FieldValueOption[];
- required: boolean;
- value: FieldValue | null;
- }
- | {
- dataType: string;
- kind: 'dropdown';
- options: SelectValue<string>[];
- required: boolean;
- value: string;
- placeholder?: string;
- };
- type Props = {
- fieldOptions: FieldOptions;
- fieldValue: QueryFieldValue;
- onChange: (fieldValue: QueryFieldValue) => void;
- className?: string;
- disabled?: boolean;
- error?: string;
- /**
- * Function to filter the options that are used as parameters for function/aggregate.
- */
- filterAggregateParameters?: (
- option: FieldValueOption,
- fieldValue?: QueryFieldValue
- ) => boolean;
- /**
- * Filter the options in the primary selector. Useful if you only want to
- * show a subset of selectable items.
- *
- * NOTE: This is different from passing an already filtered fieldOptions
- * list, as tag items in the list may be used as parameters to functions.
- */
- filterPrimaryOptions?: (option: FieldValueOption) => boolean;
- /**
- * The number of columns to render. Columns that do not have a parameter will
- * render an empty parameter placeholder. Leave blank to avoid adding spacers.
- */
- gridColumns?: number;
- hideParameterSelector?: boolean;
- hidePrimarySelector?: boolean;
- /**
- * Whether or not to add labels inside of the input fields, currently only
- * used for the metric alert builder.
- */
- inFieldLabels?: boolean;
- /**
- * This will be displayed in the select if there are no fields
- */
- noFieldsMessage?: string;
- otherColumns?: Column[];
- placeholder?: string;
- /**
- * Whether or not to add the tag explaining the FieldValueKind of each field
- */
- shouldRenderTag?: boolean;
- skipParameterPlaceholder?: boolean;
- takeFocus?: boolean;
- };
- // Type for completing generics in react-select
- type OptionType = {
- label: string;
- value: FieldValue;
- };
- class QueryField extends Component<Props> {
- FieldSelectComponents = {
- SingleValue: ({data, ...props}: SingleValueProps<OptionType>) => {
- return (
- <components.SingleValue data={data} {...props}>
- <span data-test-id="label">{data.label}</span>
- {data.value && this.renderTag(data.value.kind, data.label)}
- </components.SingleValue>
- );
- },
- };
- FieldSelectStyles = {
- singleValue(provided: React.CSSProperties) {
- const custom = {
- display: 'flex',
- justifyContent: 'space-between',
- alignItems: 'center',
- };
- return {...provided, ...custom};
- },
- };
- handleFieldChange = (selected?: FieldValueOption | null) => {
- if (!selected) {
- return;
- }
- const {value} = selected;
- const current = this.props.fieldValue;
- let fieldValue: QueryFieldValue = cloneDeep(this.props.fieldValue);
- switch (value.kind) {
- case FieldValueKind.TAG:
- case FieldValueKind.MEASUREMENT:
- case FieldValueKind.CUSTOM_MEASUREMENT:
- case FieldValueKind.BREAKDOWN:
- case FieldValueKind.FIELD:
- fieldValue = {kind: 'field', field: value.meta.name};
- break;
- case FieldValueKind.NUMERIC_METRICS:
- fieldValue = {
- kind: 'calculatedField',
- field: value.meta.name,
- };
- break;
- case FieldValueKind.FUNCTION:
- if (current.kind === 'function') {
- fieldValue = {
- kind: 'function',
- function: [
- value.meta.name as AggregationKeyWithAlias,
- current.function[1],
- current.function[2],
- current.function[3],
- ],
- };
- } else {
- fieldValue = {
- kind: 'function',
- function: [
- value.meta.name as AggregationKeyWithAlias,
- '',
- undefined,
- undefined,
- ],
- };
- }
- break;
- case FieldValueKind.EQUATION:
- fieldValue = {
- kind: 'equation',
- field: value.meta.name,
- alias: value.meta.name,
- };
- break;
- default:
- throw new Error('Invalid field type found in column picker');
- }
- if (value.kind === FieldValueKind.FUNCTION) {
- value.meta.parameters.forEach((param: AggregateParameter, i: number) => {
- if (fieldValue.kind !== 'function') {
- return;
- }
- if (param.kind === 'column') {
- const field = this.getFieldOrTagOrMeasurementValue(fieldValue.function[i + 1]);
- if (field === null) {
- fieldValue.function[i + 1] = param.defaultValue || '';
- } else if (
- (field.kind === FieldValueKind.FIELD ||
- field.kind === FieldValueKind.TAG ||
- field.kind === FieldValueKind.MEASUREMENT ||
- field.kind === FieldValueKind.CUSTOM_MEASUREMENT ||
- field.kind === FieldValueKind.METRICS ||
- field.kind === FieldValueKind.BREAKDOWN) &&
- validateColumnTypes(param.columnTypes as ValidateColumnTypes, field)
- ) {
- // New function accepts current field.
- fieldValue.function[i + 1] = field.meta.name;
- } else {
- // field does not fit within new function requirements, use the default.
- fieldValue.function[i + 1] = param.defaultValue || '';
- fieldValue.function[i + 2] = undefined;
- fieldValue.function[i + 3] = undefined;
- }
- } else {
- fieldValue.function[i + 1] = param.defaultValue || '';
- }
- });
- if (fieldValue.kind === 'function') {
- if (value.meta.parameters.length === 0) {
- fieldValue.function = [fieldValue.function[0], '', undefined, undefined];
- } else if (value.meta.parameters.length === 1) {
- fieldValue.function[2] = undefined;
- fieldValue.function[3] = undefined;
- } else if (value.meta.parameters.length === 2) {
- fieldValue.function[3] = undefined;
- }
- }
- }
- this.triggerChange(fieldValue);
- };
- handleEquationChange = (value: string) => {
- const newColumn = cloneDeep(this.props.fieldValue);
- if (newColumn.kind === FieldValueKind.EQUATION) {
- newColumn.field = value;
- }
- this.triggerChange(newColumn);
- };
- handleFieldParameterChange = ({value}) => {
- const newColumn = cloneDeep(this.props.fieldValue);
- if (newColumn.kind === 'function') {
- newColumn.function[1] = value.meta.name;
- }
- this.triggerChange(newColumn);
- };
- handleDropdownParameterChange = (index: number) => {
- return (value: SelectValue<string>) => {
- const newColumn = cloneDeep(this.props.fieldValue);
- if (newColumn.kind === 'function') {
- newColumn.function[index] = value.value;
- }
- this.triggerChange(newColumn);
- };
- };
- handleScalarParameterChange = (index: number) => {
- return (value: string) => {
- const newColumn = cloneDeep(this.props.fieldValue);
- if (newColumn.kind === 'function') {
- newColumn.function[index] = value;
- }
- this.triggerChange(newColumn);
- };
- };
- triggerChange(fieldValue: QueryFieldValue) {
- this.props.onChange(fieldValue);
- }
- getFieldOrTagOrMeasurementValue(
- name: string | undefined,
- functions: string[] = []
- ): FieldValue | null {
- const {fieldOptions} = this.props;
- if (name === undefined) {
- return null;
- }
- const fieldName = `field:${name}`;
- if (fieldOptions[fieldName]) {
- return fieldOptions[fieldName].value;
- }
- const measurementName = `measurement:${name}`;
- if (fieldOptions[measurementName]) {
- return fieldOptions[measurementName].value;
- }
- const spanOperationBreakdownName = `span_op_breakdown:${name}`;
- if (fieldOptions[spanOperationBreakdownName]) {
- return fieldOptions[spanOperationBreakdownName].value;
- }
- const equationName = `equation:${name}`;
- if (fieldOptions[equationName]) {
- return fieldOptions[equationName].value;
- }
- const tagName =
- name.indexOf('tags[') === 0
- ? `tag:${name.replace(/tags\[(.*?)\]/, '$1')}`
- : `tag:${name}`;
- if (fieldOptions[tagName]) {
- return fieldOptions[tagName].value;
- }
- if (name.length > 0) {
- // Custom Measurement. Probably not appearing in field options because
- // no metrics found within selected time range
- if (name.startsWith('measurements.')) {
- return {
- kind: FieldValueKind.CUSTOM_MEASUREMENT,
- meta: {
- name,
- dataType: 'number',
- functions,
- },
- };
- }
- // Likely a tag that was deleted but left behind in a saved query
- // Cook up a tag option so select control works.
- return {
- kind: FieldValueKind.TAG,
- meta: {
- name,
- dataType: 'string',
- unknown: true,
- },
- };
- }
- return null;
- }
- getFieldData() {
- let field: FieldValue | null = null;
- const {fieldValue} = this.props;
- let {fieldOptions} = this.props;
- if (fieldValue?.kind === 'function') {
- const funcName = `function:${fieldValue.function[0]}`;
- if (fieldOptions[funcName] !== undefined) {
- field = fieldOptions[funcName].value;
- }
- }
- if (fieldValue?.kind === 'field' || fieldValue?.kind === 'calculatedField') {
- field = this.getFieldOrTagOrMeasurementValue(fieldValue.field);
- fieldOptions = this.appendFieldIfUnknown(fieldOptions, field);
- }
- let parameterDescriptions: ParameterDescription[] = [];
- // Generate options and values for each parameter.
- if (
- field &&
- field.kind === FieldValueKind.FUNCTION &&
- field.meta.parameters.length > 0 &&
- fieldValue?.kind === FieldValueKind.FUNCTION
- ) {
- parameterDescriptions = field.meta.parameters.map(
- (param, index: number): ParameterDescription => {
- if (param.kind === 'column') {
- const fieldParameter = this.getFieldOrTagOrMeasurementValue(
- fieldValue.function[1],
- [fieldValue.function[0]]
- );
- fieldOptions = this.appendFieldIfUnknown(fieldOptions, fieldParameter);
- return {
- kind: 'column',
- value: fieldParameter,
- required: param.required,
- options: Object.values(fieldOptions).filter(
- ({value}) =>
- (value.kind === FieldValueKind.FIELD ||
- value.kind === FieldValueKind.TAG ||
- value.kind === FieldValueKind.MEASUREMENT ||
- value.kind === FieldValueKind.CUSTOM_MEASUREMENT ||
- value.kind === FieldValueKind.METRICS ||
- value.kind === FieldValueKind.BREAKDOWN) &&
- validateColumnTypes(param.columnTypes as ValidateColumnTypes, value)
- ),
- };
- }
- if (param.kind === 'dropdown') {
- return {
- kind: 'dropdown',
- options: param.options,
- dataType: param.dataType,
- required: param.required,
- value:
- (fieldValue.kind === 'function' && fieldValue.function[index + 1]) ||
- param.defaultValue ||
- '',
- };
- }
- return {
- kind: 'value',
- value:
- (fieldValue.kind === 'function' && fieldValue.function[index + 1]) ||
- param.defaultValue ||
- '',
- dataType: param.dataType,
- required: param.required,
- placeholder: param.placeholder,
- };
- }
- );
- }
- return {field, fieldOptions, parameterDescriptions};
- }
- appendFieldIfUnknown(
- fieldOptions: FieldOptions,
- field: FieldValue | null
- ): FieldOptions {
- if (!field) {
- return fieldOptions;
- }
- if (field && field.kind === FieldValueKind.TAG && field.meta.unknown) {
- // Clone the options so we don't mutate other rows.
- fieldOptions = Object.assign({}, fieldOptions);
- fieldOptions[field.meta.name] = {label: field.meta.name, value: field};
- } else if (field && field.kind === FieldValueKind.CUSTOM_MEASUREMENT) {
- fieldOptions = Object.assign({}, fieldOptions);
- fieldOptions[`measurement:${field.meta.name}`] = {
- label: field.meta.name,
- value: field,
- };
- }
- return fieldOptions;
- }
- renderParameterInputs(parameters: ParameterDescription[]): React.ReactNode[] {
- const {
- disabled,
- inFieldLabels,
- filterAggregateParameters,
- hideParameterSelector,
- skipParameterPlaceholder,
- fieldValue,
- } = this.props;
- const inputs = parameters.map((descriptor: ParameterDescription, index: number) => {
- if (descriptor.kind === 'column' && descriptor.options.length > 0) {
- if (hideParameterSelector) {
- return null;
- }
- const aggregateParameters = filterAggregateParameters
- ? descriptor.options.filter(option =>
- filterAggregateParameters(option, fieldValue)
- )
- : descriptor.options;
- aggregateParameters.forEach(opt => {
- opt.trailingItems = this.renderTag(opt.value.kind, String(opt.label));
- });
- return (
- <SelectControl
- key="select"
- name="parameter"
- menuPlacement="auto"
- placeholder={t('Select value')}
- options={aggregateParameters}
- value={descriptor.value}
- required={descriptor.required}
- onChange={this.handleFieldParameterChange}
- inFieldLabel={inFieldLabels ? t('Parameter: ') : undefined}
- disabled={disabled}
- styles={!inFieldLabels ? this.FieldSelectStyles : undefined}
- components={this.FieldSelectComponents}
- />
- );
- }
- if (descriptor.kind === 'value') {
- const inputProps = {
- required: descriptor.required,
- value: descriptor.value,
- onUpdate: this.handleScalarParameterChange(index + 1),
- placeholder: descriptor.placeholder,
- disabled,
- };
- switch (descriptor.dataType) {
- case 'number':
- return (
- <BufferedInput
- name="refinement"
- key="parameter:number"
- type="text"
- inputMode="numeric"
- pattern="[0-9]*(\.[0-9]*)?"
- {...inputProps}
- />
- );
- case 'integer':
- return (
- <BufferedInput
- name="refinement"
- key="parameter:integer"
- type="text"
- inputMode="numeric"
- pattern="[0-9]*"
- {...inputProps}
- />
- );
- default:
- return (
- <BufferedInput
- name="refinement"
- key="parameter:text"
- type="text"
- {...inputProps}
- />
- );
- }
- }
- if (descriptor.kind === 'dropdown') {
- return (
- <SelectControl
- key="dropdown"
- name="dropdown"
- menuPlacement="auto"
- placeholder={t('Select value')}
- options={descriptor.options}
- value={descriptor.value}
- required={descriptor.required}
- onChange={this.handleDropdownParameterChange(index + 1)}
- inFieldLabel={inFieldLabels ? t('Parameter: ') : undefined}
- disabled={disabled}
- />
- );
- }
- throw new Error(`Unknown parameter type encountered for ${this.props.fieldValue}`);
- });
- if (skipParameterPlaceholder) {
- return inputs;
- }
- // Add enough disabled inputs to fill the grid up.
- // We always have 1 input.
- const {gridColumns} = this.props;
- const requiredInputs = (gridColumns ?? inputs.length + 1) - inputs.length - 1;
- if (gridColumns !== undefined && requiredInputs > 0) {
- for (let i = 0; i < requiredInputs; i++) {
- inputs.push(<BlankSpace key={i} data-test-id="blankSpace" />);
- }
- }
- return inputs;
- }
- renderTag(kind: FieldValueKind, label: string) {
- const {shouldRenderTag} = this.props;
- if (shouldRenderTag === false) {
- return null;
- }
- let text, tagType;
- switch (kind) {
- case FieldValueKind.FUNCTION:
- text = 'f(x)';
- tagType = 'success';
- break;
- case FieldValueKind.CUSTOM_MEASUREMENT:
- case FieldValueKind.MEASUREMENT:
- text = 'field';
- tagType = 'highlight';
- break;
- case FieldValueKind.BREAKDOWN:
- text = 'field';
- tagType = 'highlight';
- break;
- case FieldValueKind.TAG:
- text = kind;
- tagType = 'warning';
- break;
- case FieldValueKind.NUMERIC_METRICS:
- text = 'f(x)';
- tagType = 'success';
- break;
- case FieldValueKind.FIELD:
- text = DEPRECATED_FIELDS.includes(label) ? 'deprecated' : 'field';
- tagType = 'highlight';
- break;
- case FieldValueKind.METRICS:
- text = 'metric';
- tagType = 'highlight';
- break;
- default:
- text = kind;
- }
- return <Tag type={tagType}>{text}</Tag>;
- }
- render() {
- const {
- className,
- takeFocus,
- filterPrimaryOptions,
- fieldValue,
- inFieldLabels,
- disabled,
- error,
- hidePrimarySelector,
- gridColumns,
- otherColumns,
- placeholder,
- noFieldsMessage,
- skipParameterPlaceholder,
- } = this.props;
- const {field, fieldOptions, parameterDescriptions} = this.getFieldData();
- const allFieldOptions = filterPrimaryOptions
- ? Object.values(fieldOptions).filter(filterPrimaryOptions)
- : Object.values(fieldOptions);
- allFieldOptions.forEach(opt => {
- opt.trailingItems = this.renderTag(opt.value.kind, String(opt.label));
- });
- const selectProps: ControlProps<FieldValueOption> = {
- name: 'field',
- options: Object.values(allFieldOptions),
- placeholder: placeholder ?? t('(Required)'),
- value: field,
- onChange: this.handleFieldChange,
- inFieldLabel: inFieldLabels ? t('Function: ') : undefined,
- disabled,
- noOptionsMessage: () => noFieldsMessage,
- menuPlacement: 'auto',
- };
- if (takeFocus && field === null) {
- selectProps.autoFocus = true;
- }
- const parameters = this.renderParameterInputs(parameterDescriptions);
- if (fieldValue?.kind === FieldValueKind.EQUATION) {
- return (
- <Container
- className={className}
- gridColumns={1}
- tripleLayout={false}
- error={error !== undefined}
- data-test-id="queryField"
- >
- <ArithmeticInput
- name="arithmetic"
- key="parameter:text"
- type="text"
- required
- value={fieldValue.field}
- onUpdate={this.handleEquationChange}
- options={otherColumns}
- placeholder={t('Equation')}
- />
- {error ? (
- <ArithmeticError title={error}>
- <IconWarning color="errorText" data-test-id="arithmeticErrorWarning" />
- </ArithmeticError>
- ) : null}
- </Container>
- );
- }
- // if there's more than 2 parameters, set gridColumns to 2 so they go onto the next line instead
- const containerColumns =
- parameters.length > 2 ? 2 : gridColumns ? gridColumns : parameters.length + 1;
- let gridColumnsQuantity: undefined | number = undefined;
- if (skipParameterPlaceholder) {
- // if the selected field is a function and has parameters, we would like to display each value in separate columns.
- // Otherwise the field should be displayed in a column, taking up all available space and not displaying the "no parameter" field
- if (fieldValue.kind !== 'function') {
- gridColumnsQuantity = 1;
- } else {
- const operation =
- AGGREGATIONS[fieldValue.function[0]] ??
- SESSIONS_OPERATIONS[fieldValue.function[0]];
- if (operation?.parameters.length > 0) {
- if (containerColumns === 3 && operation.parameters.length === 1) {
- gridColumnsQuantity = 2;
- } else {
- gridColumnsQuantity = containerColumns;
- }
- } else {
- gridColumnsQuantity = 1;
- }
- }
- }
- return (
- <Container
- className={className}
- gridColumns={gridColumnsQuantity ?? containerColumns}
- tripleLayout={gridColumns === 3 && parameters.length > 2}
- data-test-id="queryField"
- >
- {!hidePrimarySelector && (
- <SelectControl
- {...selectProps}
- styles={!inFieldLabels ? this.FieldSelectStyles : undefined}
- components={this.FieldSelectComponents}
- />
- )}
- {parameters}
- </Container>
- );
- }
- }
- function validateColumnTypes(
- columnTypes: ValidateColumnTypes,
- input: FieldValueColumns
- ): boolean {
- if (typeof columnTypes === 'function') {
- return columnTypes({name: input.meta.name, dataType: input.meta.dataType});
- }
- return (columnTypes as string[]).includes(input.meta.dataType);
- }
- const Container = styled('div')<{
- gridColumns: number;
- tripleLayout: boolean;
- error?: boolean;
- }>`
- display: grid;
- ${p =>
- p.tripleLayout
- ? `grid-template-columns: 1fr 2fr;`
- : `grid-template-columns: repeat(${p.gridColumns}, 1fr) ${p.error ? 'auto' : ''};`}
- gap: ${space(1)};
- align-items: center;
- flex-grow: 1;
- `;
- interface BufferedInputProps extends InputProps {
- onUpdate: (value: string) => void;
- value: string;
- }
- type InputState = {value: string};
- /**
- * Because controlled inputs fire onChange on every key stroke,
- * we can't update the QueryField that often as it would re-render
- * the input elements causing focus to be lost.
- *
- * Using a buffered input lets us throttle rendering and enforce data
- * constraints better.
- */
- class BufferedInput extends Component<BufferedInputProps, InputState> {
- constructor(props: BufferedInputProps) {
- super(props);
- this.input = createRef();
- }
- state: InputState = {
- value: this.props.value,
- };
- private input: React.RefObject<HTMLInputElement>;
- get isValid() {
- if (!this.input.current) {
- return true;
- }
- return this.input.current.validity.valid;
- }
- handleBlur = () => {
- if (this.props.required && this.state.value === '') {
- // Handle empty strings separately because we don't pass required
- // to input elements, causing isValid to return true
- this.setState({value: this.props.value});
- } else if (this.isValid) {
- this.props.onUpdate(this.state.value);
- } else {
- this.setState({value: this.props.value});
- }
- };
- handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
- if (this.isValid) {
- this.setState({value: event.target.value});
- }
- };
- render() {
- const {onUpdate: _, ...props} = this.props;
- return (
- <StyledInput
- {...props}
- ref={this.input}
- className="form-control"
- value={this.state.value}
- onChange={this.handleChange}
- onBlur={this.handleBlur}
- />
- );
- }
- }
- // Set a min-width to allow shrinkage in grid.
- const StyledInput = styled(Input)`
- min-width: 50px;
- `;
- const BlankSpace = styled('div')`
- /* Match the height of the select boxes */
- height: ${p => p.theme.form.md.height}px;
- min-width: 50px;
- background: ${p => p.theme.backgroundSecondary};
- border-radius: ${p => p.theme.borderRadius};
- display: flex;
- align-items: center;
- justify-content: center;
- &:after {
- font-size: ${p => p.theme.fontSizeMedium};
- content: '${t('No parameter')}';
- color: ${p => p.theme.subText};
- }
- `;
- const ArithmeticError = styled(Tooltip)`
- color: ${p => p.theme.errorText};
- animation: ${() => pulse(1.15)} 1s ease infinite;
- display: flex;
- `;
- export {QueryField};
|