queryField.tsx 25 KB

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