queryField.tsx 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845
  1. import {Component, createRef} from 'react';
  2. import {components, SingleValueProps} from 'react-select';
  3. import styled from '@emotion/styled';
  4. import cloneDeep from 'lodash/cloneDeep';
  5. import SelectControl, {
  6. ControlProps,
  7. } from 'sentry/components/forms/controls/selectControl';
  8. import Input, {InputProps} from 'sentry/components/input';
  9. import Tag from 'sentry/components/tag';
  10. import {Tooltip} from 'sentry/components/tooltip';
  11. import {IconWarning} from 'sentry/icons';
  12. import {t} from 'sentry/locale';
  13. import {pulse} from 'sentry/styles/animations';
  14. import {space} from 'sentry/styles/space';
  15. import {SelectValue} from 'sentry/types';
  16. import {
  17. AggregateParameter,
  18. AggregationKeyWithAlias,
  19. AGGREGATIONS,
  20. Column,
  21. ColumnType,
  22. DEPRECATED_FIELDS,
  23. QueryFieldValue,
  24. ValidateColumnTypes,
  25. } from 'sentry/utils/discover/fields';
  26. import {SESSIONS_OPERATIONS} from 'sentry/views/dashboards/widgetBuilder/releaseWidget/fields';
  27. import ArithmeticInput from './arithmeticInput';
  28. import {FieldValue, FieldValueColumns, FieldValueKind} from './types';
  29. export type FieldValueOption = SelectValue<FieldValue>;
  30. type FieldOptions = Record<string, FieldValueOption>;
  31. // Intermediate type that combines the current column
  32. // data with the AggregateParameter type.
  33. type ParameterDescription =
  34. | {
  35. dataType: ColumnType;
  36. kind: 'value';
  37. required: boolean;
  38. value: string;
  39. placeholder?: string;
  40. }
  41. | {
  42. kind: 'column';
  43. options: FieldValueOption[];
  44. required: boolean;
  45. value: FieldValue | null;
  46. }
  47. | {
  48. dataType: string;
  49. kind: 'dropdown';
  50. options: SelectValue<string>[];
  51. required: boolean;
  52. value: string;
  53. placeholder?: string;
  54. };
  55. type Props = {
  56. fieldOptions: FieldOptions;
  57. fieldValue: QueryFieldValue;
  58. onChange: (fieldValue: QueryFieldValue) => void;
  59. className?: string;
  60. disabled?: boolean;
  61. error?: string;
  62. /**
  63. * Function to filter the options that are used as parameters for function/aggregate.
  64. */
  65. filterAggregateParameters?: (
  66. option: FieldValueOption,
  67. fieldValue?: QueryFieldValue
  68. ) => boolean;
  69. /**
  70. * Filter the options in the primary selector. Useful if you only want to
  71. * show a subset of selectable items.
  72. *
  73. * NOTE: This is different from passing an already filtered fieldOptions
  74. * list, as tag items in the list may be used as parameters to functions.
  75. */
  76. filterPrimaryOptions?: (option: FieldValueOption) => boolean;
  77. /**
  78. * The number of columns to render. Columns that do not have a parameter will
  79. * render an empty parameter placeholder. Leave blank to avoid adding spacers.
  80. */
  81. gridColumns?: number;
  82. hideParameterSelector?: boolean;
  83. hidePrimarySelector?: boolean;
  84. /**
  85. * Whether or not to add labels inside of the input fields, currently only
  86. * used for the metric alert builder.
  87. */
  88. inFieldLabels?: boolean;
  89. /**
  90. * This will be displayed in the select if there are no fields
  91. */
  92. noFieldsMessage?: string;
  93. otherColumns?: Column[];
  94. placeholder?: string;
  95. /**
  96. * Whether or not to add the tag explaining the FieldValueKind of each field
  97. */
  98. shouldRenderTag?: boolean;
  99. skipParameterPlaceholder?: boolean;
  100. takeFocus?: boolean;
  101. };
  102. // Type for completing generics in react-select
  103. type OptionType = {
  104. label: string;
  105. value: FieldValue;
  106. };
  107. class QueryField extends Component<Props> {
  108. FieldSelectComponents = {
  109. SingleValue: ({data, ...props}: SingleValueProps<OptionType>) => {
  110. return (
  111. <components.SingleValue data={data} {...props}>
  112. <span data-test-id="label">{data.label}</span>
  113. {data.value && this.renderTag(data.value.kind, data.label)}
  114. </components.SingleValue>
  115. );
  116. },
  117. };
  118. FieldSelectStyles = {
  119. singleValue(provided: React.CSSProperties) {
  120. const custom = {
  121. display: 'flex',
  122. justifyContent: 'space-between',
  123. alignItems: 'center',
  124. };
  125. return {...provided, ...custom};
  126. },
  127. };
  128. handleFieldChange = (selected?: FieldValueOption | null) => {
  129. if (!selected) {
  130. return;
  131. }
  132. const {value} = selected;
  133. const current = this.props.fieldValue;
  134. let fieldValue: QueryFieldValue = cloneDeep(this.props.fieldValue);
  135. switch (value.kind) {
  136. case FieldValueKind.TAG:
  137. case FieldValueKind.MEASUREMENT:
  138. case FieldValueKind.CUSTOM_MEASUREMENT:
  139. case FieldValueKind.BREAKDOWN:
  140. case FieldValueKind.FIELD:
  141. fieldValue = {kind: 'field', field: value.meta.name};
  142. break;
  143. case FieldValueKind.NUMERIC_METRICS:
  144. fieldValue = {
  145. kind: 'calculatedField',
  146. field: value.meta.name,
  147. };
  148. break;
  149. case FieldValueKind.FUNCTION:
  150. if (current.kind === 'function') {
  151. fieldValue = {
  152. kind: 'function',
  153. function: [
  154. value.meta.name as AggregationKeyWithAlias,
  155. current.function[1],
  156. current.function[2],
  157. current.function[3],
  158. ],
  159. };
  160. } else {
  161. fieldValue = {
  162. kind: 'function',
  163. function: [
  164. value.meta.name as AggregationKeyWithAlias,
  165. '',
  166. undefined,
  167. undefined,
  168. ],
  169. };
  170. }
  171. break;
  172. case FieldValueKind.EQUATION:
  173. fieldValue = {
  174. kind: 'equation',
  175. field: value.meta.name,
  176. alias: value.meta.name,
  177. };
  178. break;
  179. default:
  180. throw new Error('Invalid field type found in column picker');
  181. }
  182. if (value.kind === FieldValueKind.FUNCTION) {
  183. value.meta.parameters.forEach((param: AggregateParameter, i: number) => {
  184. if (fieldValue.kind !== 'function') {
  185. return;
  186. }
  187. if (param.kind === 'column') {
  188. const field = this.getFieldOrTagOrMeasurementValue(fieldValue.function[i + 1]);
  189. if (field === null) {
  190. fieldValue.function[i + 1] = param.defaultValue || '';
  191. } else if (
  192. (field.kind === FieldValueKind.FIELD ||
  193. field.kind === FieldValueKind.TAG ||
  194. field.kind === FieldValueKind.MEASUREMENT ||
  195. field.kind === FieldValueKind.CUSTOM_MEASUREMENT ||
  196. field.kind === FieldValueKind.METRICS ||
  197. field.kind === FieldValueKind.BREAKDOWN) &&
  198. validateColumnTypes(param.columnTypes as ValidateColumnTypes, field)
  199. ) {
  200. // New function accepts current field.
  201. fieldValue.function[i + 1] = field.meta.name;
  202. } else {
  203. // field does not fit within new function requirements, use the default.
  204. fieldValue.function[i + 1] = param.defaultValue || '';
  205. fieldValue.function[i + 2] = undefined;
  206. fieldValue.function[i + 3] = undefined;
  207. }
  208. } else {
  209. fieldValue.function[i + 1] = param.defaultValue || '';
  210. }
  211. });
  212. if (fieldValue.kind === 'function') {
  213. if (value.meta.parameters.length === 0) {
  214. fieldValue.function = [fieldValue.function[0], '', undefined, undefined];
  215. } else if (value.meta.parameters.length === 1) {
  216. fieldValue.function[2] = undefined;
  217. fieldValue.function[3] = undefined;
  218. } else if (value.meta.parameters.length === 2) {
  219. fieldValue.function[3] = undefined;
  220. }
  221. }
  222. }
  223. this.triggerChange(fieldValue);
  224. };
  225. handleEquationChange = (value: string) => {
  226. const newColumn = cloneDeep(this.props.fieldValue);
  227. if (newColumn.kind === FieldValueKind.EQUATION) {
  228. newColumn.field = value;
  229. }
  230. this.triggerChange(newColumn);
  231. };
  232. handleFieldParameterChange = ({value}) => {
  233. const newColumn = cloneDeep(this.props.fieldValue);
  234. if (newColumn.kind === 'function') {
  235. newColumn.function[1] = value.meta.name;
  236. }
  237. this.triggerChange(newColumn);
  238. };
  239. handleDropdownParameterChange = (index: number) => {
  240. return (value: SelectValue<string>) => {
  241. const newColumn = cloneDeep(this.props.fieldValue);
  242. if (newColumn.kind === 'function') {
  243. newColumn.function[index] = value.value;
  244. }
  245. this.triggerChange(newColumn);
  246. };
  247. };
  248. handleScalarParameterChange = (index: number) => {
  249. return (value: string) => {
  250. const newColumn = cloneDeep(this.props.fieldValue);
  251. if (newColumn.kind === 'function') {
  252. newColumn.function[index] = value;
  253. }
  254. this.triggerChange(newColumn);
  255. };
  256. };
  257. triggerChange(fieldValue: QueryFieldValue) {
  258. this.props.onChange(fieldValue);
  259. }
  260. getFieldOrTagOrMeasurementValue(
  261. name: string | undefined,
  262. functions: string[] = []
  263. ): FieldValue | null {
  264. const {fieldOptions} = this.props;
  265. if (name === undefined) {
  266. return null;
  267. }
  268. const fieldName = `field:${name}`;
  269. if (fieldOptions[fieldName]) {
  270. return fieldOptions[fieldName].value;
  271. }
  272. const measurementName = `measurement:${name}`;
  273. if (fieldOptions[measurementName]) {
  274. return fieldOptions[measurementName].value;
  275. }
  276. const spanOperationBreakdownName = `span_op_breakdown:${name}`;
  277. if (fieldOptions[spanOperationBreakdownName]) {
  278. return fieldOptions[spanOperationBreakdownName].value;
  279. }
  280. const equationName = `equation:${name}`;
  281. if (fieldOptions[equationName]) {
  282. return fieldOptions[equationName].value;
  283. }
  284. const tagName =
  285. name.indexOf('tags[') === 0
  286. ? `tag:${name.replace(/tags\[(.*?)\]/, '$1')}`
  287. : `tag:${name}`;
  288. if (fieldOptions[tagName]) {
  289. return fieldOptions[tagName].value;
  290. }
  291. if (name.length > 0) {
  292. // Custom Measurement. Probably not appearing in field options because
  293. // no metrics found within selected time range
  294. if (name.startsWith('measurements.')) {
  295. return {
  296. kind: FieldValueKind.CUSTOM_MEASUREMENT,
  297. meta: {
  298. name,
  299. dataType: 'number',
  300. functions,
  301. },
  302. };
  303. }
  304. // Likely a tag that was deleted but left behind in a saved query
  305. // Cook up a tag option so select control works.
  306. return {
  307. kind: FieldValueKind.TAG,
  308. meta: {
  309. name,
  310. dataType: 'string',
  311. unknown: true,
  312. },
  313. };
  314. }
  315. return null;
  316. }
  317. getFieldData() {
  318. let field: FieldValue | null = null;
  319. const {fieldValue} = this.props;
  320. let {fieldOptions} = this.props;
  321. if (fieldValue?.kind === 'function') {
  322. const funcName = `function:${fieldValue.function[0]}`;
  323. if (fieldOptions[funcName] !== undefined) {
  324. field = fieldOptions[funcName].value;
  325. }
  326. }
  327. if (fieldValue?.kind === 'field' || fieldValue?.kind === 'calculatedField') {
  328. field = this.getFieldOrTagOrMeasurementValue(fieldValue.field);
  329. fieldOptions = this.appendFieldIfUnknown(fieldOptions, field);
  330. }
  331. let parameterDescriptions: ParameterDescription[] = [];
  332. // Generate options and values for each parameter.
  333. if (
  334. field &&
  335. field.kind === FieldValueKind.FUNCTION &&
  336. field.meta.parameters.length > 0 &&
  337. fieldValue?.kind === FieldValueKind.FUNCTION
  338. ) {
  339. parameterDescriptions = field.meta.parameters.map(
  340. (param, index: number): ParameterDescription => {
  341. if (param.kind === 'column') {
  342. const fieldParameter = this.getFieldOrTagOrMeasurementValue(
  343. fieldValue.function[1],
  344. [fieldValue.function[0]]
  345. );
  346. fieldOptions = this.appendFieldIfUnknown(fieldOptions, fieldParameter);
  347. return {
  348. kind: 'column',
  349. value: fieldParameter,
  350. required: param.required,
  351. options: Object.values(fieldOptions).filter(
  352. ({value}) =>
  353. (value.kind === FieldValueKind.FIELD ||
  354. value.kind === FieldValueKind.TAG ||
  355. value.kind === FieldValueKind.MEASUREMENT ||
  356. value.kind === FieldValueKind.CUSTOM_MEASUREMENT ||
  357. value.kind === FieldValueKind.METRICS ||
  358. value.kind === FieldValueKind.BREAKDOWN) &&
  359. validateColumnTypes(param.columnTypes as ValidateColumnTypes, value)
  360. ),
  361. };
  362. }
  363. if (param.kind === 'dropdown') {
  364. return {
  365. kind: 'dropdown',
  366. options: param.options,
  367. dataType: param.dataType,
  368. required: param.required,
  369. value:
  370. (fieldValue.kind === 'function' && fieldValue.function[index + 1]) ||
  371. param.defaultValue ||
  372. '',
  373. };
  374. }
  375. return {
  376. kind: 'value',
  377. value:
  378. (fieldValue.kind === 'function' && fieldValue.function[index + 1]) ||
  379. param.defaultValue ||
  380. '',
  381. dataType: param.dataType,
  382. required: param.required,
  383. placeholder: param.placeholder,
  384. };
  385. }
  386. );
  387. }
  388. return {field, fieldOptions, parameterDescriptions};
  389. }
  390. appendFieldIfUnknown(
  391. fieldOptions: FieldOptions,
  392. field: FieldValue | null
  393. ): FieldOptions {
  394. if (!field) {
  395. return fieldOptions;
  396. }
  397. if (field && field.kind === FieldValueKind.TAG && field.meta.unknown) {
  398. // Clone the options so we don't mutate other rows.
  399. fieldOptions = Object.assign({}, fieldOptions);
  400. fieldOptions[field.meta.name] = {label: field.meta.name, value: field};
  401. } else if (field && field.kind === FieldValueKind.CUSTOM_MEASUREMENT) {
  402. fieldOptions = Object.assign({}, fieldOptions);
  403. fieldOptions[`measurement:${field.meta.name}`] = {
  404. label: field.meta.name,
  405. value: field,
  406. };
  407. }
  408. return fieldOptions;
  409. }
  410. renderParameterInputs(parameters: ParameterDescription[]): React.ReactNode[] {
  411. const {
  412. disabled,
  413. inFieldLabels,
  414. filterAggregateParameters,
  415. hideParameterSelector,
  416. skipParameterPlaceholder,
  417. fieldValue,
  418. } = this.props;
  419. const inputs = parameters.map((descriptor: ParameterDescription, index: number) => {
  420. if (descriptor.kind === 'column' && descriptor.options.length > 0) {
  421. if (hideParameterSelector) {
  422. return null;
  423. }
  424. const aggregateParameters = filterAggregateParameters
  425. ? descriptor.options.filter(option =>
  426. filterAggregateParameters(option, fieldValue)
  427. )
  428. : descriptor.options;
  429. aggregateParameters.forEach(opt => {
  430. opt.trailingItems = this.renderTag(opt.value.kind, String(opt.label));
  431. });
  432. return (
  433. <SelectControl
  434. key="select"
  435. name="parameter"
  436. menuPlacement="auto"
  437. placeholder={t('Select value')}
  438. options={aggregateParameters}
  439. value={descriptor.value}
  440. required={descriptor.required}
  441. onChange={this.handleFieldParameterChange}
  442. inFieldLabel={inFieldLabels ? t('Parameter: ') : undefined}
  443. disabled={disabled}
  444. styles={!inFieldLabels ? this.FieldSelectStyles : undefined}
  445. components={this.FieldSelectComponents}
  446. />
  447. );
  448. }
  449. if (descriptor.kind === 'value') {
  450. const inputProps = {
  451. required: descriptor.required,
  452. value: descriptor.value,
  453. onUpdate: this.handleScalarParameterChange(index + 1),
  454. placeholder: descriptor.placeholder,
  455. disabled,
  456. };
  457. switch (descriptor.dataType) {
  458. case 'number':
  459. return (
  460. <BufferedInput
  461. name="refinement"
  462. key="parameter:number"
  463. type="text"
  464. inputMode="numeric"
  465. pattern="[0-9]*(\.[0-9]*)?"
  466. {...inputProps}
  467. />
  468. );
  469. case 'integer':
  470. return (
  471. <BufferedInput
  472. name="refinement"
  473. key="parameter:integer"
  474. type="text"
  475. inputMode="numeric"
  476. pattern="[0-9]*"
  477. {...inputProps}
  478. />
  479. );
  480. default:
  481. return (
  482. <BufferedInput
  483. name="refinement"
  484. key="parameter:text"
  485. type="text"
  486. {...inputProps}
  487. />
  488. );
  489. }
  490. }
  491. if (descriptor.kind === 'dropdown') {
  492. return (
  493. <SelectControl
  494. key="dropdown"
  495. name="dropdown"
  496. menuPlacement="auto"
  497. placeholder={t('Select value')}
  498. options={descriptor.options}
  499. value={descriptor.value}
  500. required={descriptor.required}
  501. onChange={this.handleDropdownParameterChange(index + 1)}
  502. inFieldLabel={inFieldLabels ? t('Parameter: ') : undefined}
  503. disabled={disabled}
  504. />
  505. );
  506. }
  507. throw new Error(`Unknown parameter type encountered for ${this.props.fieldValue}`);
  508. });
  509. if (skipParameterPlaceholder) {
  510. return inputs;
  511. }
  512. // Add enough disabled inputs to fill the grid up.
  513. // We always have 1 input.
  514. const {gridColumns} = this.props;
  515. const requiredInputs = (gridColumns ?? inputs.length + 1) - inputs.length - 1;
  516. if (gridColumns !== undefined && requiredInputs > 0) {
  517. for (let i = 0; i < requiredInputs; i++) {
  518. inputs.push(<BlankSpace key={i} data-test-id="blankSpace" />);
  519. }
  520. }
  521. return inputs;
  522. }
  523. renderTag(kind: FieldValueKind, label: string) {
  524. const {shouldRenderTag} = this.props;
  525. if (shouldRenderTag === false) {
  526. return null;
  527. }
  528. let text, tagType;
  529. switch (kind) {
  530. case FieldValueKind.FUNCTION:
  531. text = 'f(x)';
  532. tagType = 'success';
  533. break;
  534. case FieldValueKind.CUSTOM_MEASUREMENT:
  535. case FieldValueKind.MEASUREMENT:
  536. text = 'field';
  537. tagType = 'highlight';
  538. break;
  539. case FieldValueKind.BREAKDOWN:
  540. text = 'field';
  541. tagType = 'highlight';
  542. break;
  543. case FieldValueKind.TAG:
  544. text = kind;
  545. tagType = 'warning';
  546. break;
  547. case FieldValueKind.NUMERIC_METRICS:
  548. text = 'f(x)';
  549. tagType = 'success';
  550. break;
  551. case FieldValueKind.FIELD:
  552. case FieldValueKind.METRICS:
  553. text = DEPRECATED_FIELDS.includes(label) ? 'deprecated' : 'field';
  554. tagType = 'highlight';
  555. break;
  556. default:
  557. text = kind;
  558. }
  559. return <Tag type={tagType}>{text}</Tag>;
  560. }
  561. render() {
  562. const {
  563. className,
  564. takeFocus,
  565. filterPrimaryOptions,
  566. fieldValue,
  567. inFieldLabels,
  568. disabled,
  569. error,
  570. hidePrimarySelector,
  571. gridColumns,
  572. otherColumns,
  573. placeholder,
  574. noFieldsMessage,
  575. skipParameterPlaceholder,
  576. } = this.props;
  577. const {field, fieldOptions, parameterDescriptions} = this.getFieldData();
  578. const allFieldOptions = filterPrimaryOptions
  579. ? Object.values(fieldOptions).filter(filterPrimaryOptions)
  580. : Object.values(fieldOptions);
  581. allFieldOptions.forEach(opt => {
  582. opt.trailingItems = this.renderTag(opt.value.kind, String(opt.label));
  583. });
  584. const selectProps: ControlProps<FieldValueOption> = {
  585. name: 'field',
  586. options: Object.values(allFieldOptions),
  587. placeholder: placeholder ?? t('(Required)'),
  588. value: field,
  589. onChange: this.handleFieldChange,
  590. inFieldLabel: inFieldLabels ? t('Function: ') : undefined,
  591. disabled,
  592. noOptionsMessage: () => noFieldsMessage,
  593. menuPlacement: 'auto',
  594. };
  595. if (takeFocus && field === null) {
  596. selectProps.autoFocus = true;
  597. }
  598. const parameters = this.renderParameterInputs(parameterDescriptions);
  599. if (fieldValue?.kind === FieldValueKind.EQUATION) {
  600. return (
  601. <Container
  602. className={className}
  603. gridColumns={1}
  604. tripleLayout={false}
  605. error={error !== undefined}
  606. data-test-id="queryField"
  607. >
  608. <ArithmeticInput
  609. name="arithmetic"
  610. key="parameter:text"
  611. type="text"
  612. required
  613. value={fieldValue.field}
  614. onUpdate={this.handleEquationChange}
  615. options={otherColumns}
  616. placeholder={t('Equation')}
  617. />
  618. {error ? (
  619. <ArithmeticError title={error}>
  620. <IconWarning color="errorText" data-test-id="arithmeticErrorWarning" />
  621. </ArithmeticError>
  622. ) : null}
  623. </Container>
  624. );
  625. }
  626. // if there's more than 2 parameters, set gridColumns to 2 so they go onto the next line instead
  627. const containerColumns =
  628. parameters.length > 2 ? 2 : gridColumns ? gridColumns : parameters.length + 1;
  629. let gridColumnsQuantity: undefined | number = undefined;
  630. if (skipParameterPlaceholder) {
  631. // if the selected field is a function and has parameters, we would like to display each value in separate columns.
  632. // Otherwise the field should be displayed in a column, taking up all available space and not displaying the "no parameter" field
  633. if (fieldValue.kind !== 'function') {
  634. gridColumnsQuantity = 1;
  635. } else {
  636. const operation =
  637. AGGREGATIONS[fieldValue.function[0]] ??
  638. SESSIONS_OPERATIONS[fieldValue.function[0]];
  639. if (operation.parameters.length > 0) {
  640. if (containerColumns === 3 && operation.parameters.length === 1) {
  641. gridColumnsQuantity = 2;
  642. } else {
  643. gridColumnsQuantity = containerColumns;
  644. }
  645. } else {
  646. gridColumnsQuantity = 1;
  647. }
  648. }
  649. }
  650. return (
  651. <Container
  652. className={className}
  653. gridColumns={gridColumnsQuantity ?? containerColumns}
  654. tripleLayout={gridColumns === 3 && parameters.length > 2}
  655. data-test-id="queryField"
  656. >
  657. {!hidePrimarySelector && (
  658. <SelectControl
  659. {...selectProps}
  660. styles={!inFieldLabels ? this.FieldSelectStyles : undefined}
  661. components={this.FieldSelectComponents}
  662. />
  663. )}
  664. {parameters}
  665. </Container>
  666. );
  667. }
  668. }
  669. function validateColumnTypes(
  670. columnTypes: ValidateColumnTypes,
  671. input: FieldValueColumns
  672. ): boolean {
  673. if (typeof columnTypes === 'function') {
  674. return columnTypes({name: input.meta.name, dataType: input.meta.dataType});
  675. }
  676. return (columnTypes as string[]).includes(input.meta.dataType);
  677. }
  678. const Container = styled('div')<{
  679. gridColumns: number;
  680. tripleLayout: boolean;
  681. error?: boolean;
  682. }>`
  683. display: grid;
  684. ${p =>
  685. p.tripleLayout
  686. ? `grid-template-columns: 1fr 2fr;`
  687. : `grid-template-columns: repeat(${p.gridColumns}, 1fr) ${p.error ? 'auto' : ''};`}
  688. gap: ${space(1)};
  689. align-items: center;
  690. flex-grow: 1;
  691. `;
  692. interface BufferedInputProps extends InputProps {
  693. onUpdate: (value: string) => void;
  694. value: string;
  695. }
  696. type InputState = {value: string};
  697. /**
  698. * Because controlled inputs fire onChange on every key stroke,
  699. * we can't update the QueryField that often as it would re-render
  700. * the input elements causing focus to be lost.
  701. *
  702. * Using a buffered input lets us throttle rendering and enforce data
  703. * constraints better.
  704. */
  705. class BufferedInput extends Component<BufferedInputProps, InputState> {
  706. constructor(props: BufferedInputProps) {
  707. super(props);
  708. this.input = createRef();
  709. }
  710. state: InputState = {
  711. value: this.props.value,
  712. };
  713. private input: React.RefObject<HTMLInputElement>;
  714. get isValid() {
  715. if (!this.input.current) {
  716. return true;
  717. }
  718. return this.input.current.validity.valid;
  719. }
  720. handleBlur = () => {
  721. if (this.props.required && this.state.value === '') {
  722. // Handle empty strings separately because we don't pass required
  723. // to input elements, causing isValid to return true
  724. this.setState({value: this.props.value});
  725. } else if (this.isValid) {
  726. this.props.onUpdate(this.state.value);
  727. } else {
  728. this.setState({value: this.props.value});
  729. }
  730. };
  731. handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
  732. if (this.isValid) {
  733. this.setState({value: event.target.value});
  734. }
  735. };
  736. render() {
  737. const {onUpdate: _, ...props} = this.props;
  738. return (
  739. <StyledInput
  740. {...props}
  741. ref={this.input}
  742. className="form-control"
  743. value={this.state.value}
  744. onChange={this.handleChange}
  745. onBlur={this.handleBlur}
  746. />
  747. );
  748. }
  749. }
  750. // Set a min-width to allow shrinkage in grid.
  751. const StyledInput = styled(Input)`
  752. min-width: 50px;
  753. `;
  754. const BlankSpace = styled('div')`
  755. /* Match the height of the select boxes */
  756. height: ${p => p.theme.form.md.height}px;
  757. min-width: 50px;
  758. background: ${p => p.theme.backgroundSecondary};
  759. border-radius: ${p => p.theme.borderRadius};
  760. display: flex;
  761. align-items: center;
  762. justify-content: center;
  763. &:after {
  764. font-size: ${p => p.theme.fontSizeMedium};
  765. content: '${t('No parameter')}';
  766. color: ${p => p.theme.subText};
  767. }
  768. `;
  769. const ArithmeticError = styled(Tooltip)`
  770. color: ${p => p.theme.errorText};
  771. animation: ${() => pulse(1.15)} 1s ease infinite;
  772. display: flex;
  773. `;
  774. export {QueryField};