queryField.tsx 25 KB

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