queries.tsx 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698
  1. import type React from 'react';
  2. import {Fragment, memo, useCallback, useEffect, useMemo, useState} from 'react';
  3. import {css} from '@emotion/react';
  4. import styled from '@emotion/styled';
  5. import debounce from 'lodash/debounce';
  6. import {navigateTo} from 'sentry/actionCreators/navigation';
  7. import {Button} from 'sentry/components/button';
  8. import type {MenuItemProps} from 'sentry/components/dropdownMenu';
  9. import {DropdownMenu} from 'sentry/components/dropdownMenu';
  10. import Input, {type InputProps} from 'sentry/components/input';
  11. import {CreateMetricAlertFeature} from 'sentry/components/metrics/createMetricAlertFeature';
  12. import {EquationInput} from 'sentry/components/metrics/equationInput';
  13. import {EquationSymbol} from 'sentry/components/metrics/equationSymbol';
  14. import {QueryBuilder} from 'sentry/components/metrics/queryBuilder';
  15. import {QueryFieldGroup} from 'sentry/components/metrics/queryFieldGroup';
  16. import {getQuerySymbol, QuerySymbol} from 'sentry/components/metrics/querySymbol';
  17. import {Tooltip} from 'sentry/components/tooltip';
  18. import {DEFAULT_DEBOUNCE_DURATION, SLOW_TOOLTIP_DELAY} from 'sentry/constants';
  19. import {
  20. IconAdd,
  21. IconClose,
  22. IconCopy,
  23. IconDelete,
  24. IconEdit,
  25. IconEllipsis,
  26. IconSettings,
  27. IconSiren,
  28. } from 'sentry/icons';
  29. import {t} from 'sentry/locale';
  30. import {space} from 'sentry/styles/space';
  31. import {isCustomMetric} from 'sentry/utils/metrics';
  32. import {
  33. hasCustomMetrics,
  34. hasMetricAlertFeature,
  35. hasMetricsNewInputs,
  36. } from 'sentry/utils/metrics/features';
  37. import {MetricExpressionType} from 'sentry/utils/metrics/types';
  38. import useOrganization from 'sentry/utils/useOrganization';
  39. import usePageFilters from 'sentry/utils/usePageFilters';
  40. import useRouter from 'sentry/utils/useRouter';
  41. import type {
  42. DashboardMetricsEquation,
  43. DashboardMetricsExpression,
  44. DashboardMetricsQuery,
  45. } from 'sentry/views/dashboards/metrics/types';
  46. import {
  47. formatAlias,
  48. getMetricQueryName,
  49. isVirtualExpression,
  50. } from 'sentry/views/dashboards/metrics/utils';
  51. import {DisplayType} from 'sentry/views/dashboards/types';
  52. import {getCreateAlert} from 'sentry/views/metrics/metricQueryContextMenu';
  53. interface Props {
  54. addEquation: () => void;
  55. addQuery: (index?: number) => void;
  56. displayType: DisplayType;
  57. metricEquations: DashboardMetricsEquation[];
  58. metricQueries: DashboardMetricsQuery[];
  59. onEquationChange: (data: Partial<DashboardMetricsEquation>, index: number) => void;
  60. onQueryChange: (data: Partial<DashboardMetricsQuery>, index: number) => void;
  61. removeEquation: (index: number) => void;
  62. removeQuery: (index: number) => void;
  63. }
  64. export const Queries = memo(function Queries({
  65. displayType,
  66. metricQueries,
  67. metricEquations,
  68. onQueryChange,
  69. onEquationChange,
  70. addQuery,
  71. addEquation,
  72. removeQuery,
  73. removeEquation,
  74. }: Props) {
  75. const {selection} = usePageFilters();
  76. const organization = useOrganization();
  77. const metricsNewInputs = hasMetricsNewInputs(organization);
  78. const availableVariables = useMemo(
  79. () => new Set(metricQueries.map(query => getQuerySymbol(query.id, metricsNewInputs))),
  80. [metricQueries, metricsNewInputs]
  81. );
  82. const handleEditQueryAlias = useCallback(
  83. (index: number) => {
  84. const query = metricQueries[index];
  85. const alias = getMetricQueryName(query);
  86. onQueryChange({alias}, index);
  87. },
  88. [metricQueries, onQueryChange]
  89. );
  90. const handleEditEquationAlias = useCallback(
  91. (index: number) => {
  92. const equation = metricEquations[index];
  93. const alias = getMetricQueryName(equation);
  94. onEquationChange({alias: alias ?? ''}, index);
  95. },
  96. [metricEquations, onEquationChange]
  97. );
  98. const showQuerySymbols = metricQueries.length + metricEquations.length > 1;
  99. const visibleExpressions = [...metricQueries, ...metricEquations].filter(
  100. expression => !expression.isHidden
  101. );
  102. return (
  103. <Fragment>
  104. <ExpressionsWrapper
  105. hasMetricsNewInput={metricsNewInputs}
  106. showQuerySymbols={showQuerySymbols}
  107. >
  108. {metricQueries.map((query, index) => (
  109. <ExpressionWrapper key={query.id} hasMetricsNewInput={metricsNewInputs}>
  110. {showQuerySymbols && (
  111. <QueryToggle
  112. isHidden={query.isHidden}
  113. onChange={isHidden => onQueryChange({isHidden}, index)}
  114. disabled={
  115. (!query.isHidden && visibleExpressions.length === 1) ||
  116. displayType === DisplayType.BIG_NUMBER
  117. }
  118. disabledReason={t('Big Number widgets support only one visible metric')}
  119. queryId={query.id}
  120. type={MetricExpressionType.QUERY}
  121. />
  122. )}
  123. <ExpressionFormWrapper hasMetricsNewInput={metricsNewInputs}>
  124. <ExpressionFormRowWrapper hasMetricsNewInput={metricsNewInputs}>
  125. <WrappedQueryBuilder
  126. index={index}
  127. onQueryChange={onQueryChange}
  128. query={query}
  129. projects={selection.projects}
  130. hasSymbols={showQuerySymbols}
  131. alias={
  132. metricsNewInputs &&
  133. query.alias !== undefined && (
  134. <ExpressionFormRowWrapper hasMetricsNewInput={metricsNewInputs}>
  135. <ExpressionAliasForm
  136. expression={query}
  137. onChange={alias => onQueryChange({alias}, index)}
  138. hasContextMenu
  139. hasMetricsNewInput
  140. />
  141. </ExpressionFormRowWrapper>
  142. )
  143. }
  144. />
  145. <QueryContextMenu
  146. canRemoveQuery={metricQueries.length > 1}
  147. removeQuery={removeQuery}
  148. addQuery={addQuery}
  149. editAlias={handleEditQueryAlias}
  150. queryIndex={index}
  151. metricsQuery={query}
  152. />
  153. </ExpressionFormRowWrapper>
  154. {!metricsNewInputs && query.alias !== undefined && (
  155. <ExpressionFormRowWrapper hasMetricsNewInput={metricsNewInputs}>
  156. <ExpressionAliasForm
  157. expression={query}
  158. onChange={alias => onQueryChange({alias}, index)}
  159. hasContextMenu
  160. />
  161. </ExpressionFormRowWrapper>
  162. )}
  163. </ExpressionFormWrapper>
  164. </ExpressionWrapper>
  165. ))}
  166. {metricEquations.map((equation, index) => (
  167. <ExpressionWrapper key={equation.id} hasMetricsNewInput={metricsNewInputs}>
  168. {showQuerySymbols && (
  169. <QueryToggle
  170. isHidden={equation.isHidden}
  171. onChange={isHidden => onEquationChange({isHidden}, index)}
  172. disabled={
  173. (!equation.isHidden && visibleExpressions.length === 1) ||
  174. displayType === DisplayType.BIG_NUMBER
  175. }
  176. disabledReason={t('Big Number widgets support only one visible metric')}
  177. queryId={equation.id}
  178. type={MetricExpressionType.EQUATION}
  179. />
  180. )}
  181. <ExpressionFormWrapper hasMetricsNewInput={metricsNewInputs}>
  182. <ExpressionFormRowWrapper hasMetricsNewInput={metricsNewInputs}>
  183. <EquationInputWrapper hasMetricsNewInput={metricsNewInputs}>
  184. <EquationInput
  185. onChange={formula => onEquationChange({formula}, index)}
  186. value={equation.formula}
  187. availableVariables={availableVariables}
  188. metricsNewInputs={metricsNewInputs}
  189. />
  190. </EquationInputWrapper>
  191. {equation.alias !== undefined && (
  192. <ExpressionAliasForm
  193. expression={equation}
  194. onChange={alias => onEquationChange({alias}, index)}
  195. />
  196. )}
  197. <EquationContextMenu
  198. removeEquation={removeEquation}
  199. editAlias={handleEditEquationAlias}
  200. equationIndex={index}
  201. />
  202. </ExpressionFormRowWrapper>
  203. </ExpressionFormWrapper>
  204. </ExpressionWrapper>
  205. ))}
  206. </ExpressionsWrapper>
  207. <ButtonBar addQuerySymbolSpacing={showQuerySymbols}>
  208. <Button size="sm" icon={<IconAdd isCircled />} onClick={() => addQuery()}>
  209. {t('Add metric')}
  210. </Button>
  211. {!(displayType === DisplayType.BIG_NUMBER && metricEquations.length > 0) && (
  212. <Button size="sm" icon={<IconAdd isCircled />} onClick={addEquation}>
  213. {t('Add equation')}
  214. </Button>
  215. )}
  216. </ButtonBar>
  217. </Fragment>
  218. );
  219. });
  220. /**
  221. * Wrapper for the QueryBuilder to memoize the onChange handler
  222. */
  223. function WrappedQueryBuilder({
  224. index,
  225. onQueryChange,
  226. projects,
  227. query,
  228. hasSymbols,
  229. alias,
  230. }: {
  231. hasSymbols: boolean;
  232. index: number;
  233. onQueryChange: (data: Partial<DashboardMetricsQuery>, index: number) => void;
  234. projects: number[];
  235. query: DashboardMetricsQuery;
  236. alias?: React.ReactNode;
  237. }) {
  238. const handleChange = useCallback(
  239. (data: Partial<DashboardMetricsQuery>) => {
  240. onQueryChange(data, index);
  241. },
  242. [index, onQueryChange]
  243. );
  244. return (
  245. <QueryBuilder
  246. index={index}
  247. onChange={handleChange}
  248. metricsQuery={query}
  249. projects={projects}
  250. hasSymbols={hasSymbols}
  251. alias={alias}
  252. isModal
  253. />
  254. );
  255. }
  256. interface QueryContextMenuProps {
  257. addQuery: (index: number) => void;
  258. canRemoveQuery: boolean;
  259. editAlias: (index: number) => void;
  260. metricsQuery: DashboardMetricsQuery;
  261. queryIndex: number;
  262. removeQuery: (index: number) => void;
  263. }
  264. function QueryContextMenu({
  265. metricsQuery,
  266. removeQuery,
  267. addQuery,
  268. canRemoveQuery,
  269. queryIndex,
  270. editAlias,
  271. }: QueryContextMenuProps) {
  272. const organization = useOrganization();
  273. const router = useRouter();
  274. const createAlert = useMemo(
  275. () => getCreateAlert(organization, metricsQuery),
  276. [metricsQuery, organization]
  277. );
  278. const items = useMemo<MenuItemProps[]>(() => {
  279. const customMetric = !isCustomMetric({mri: metricsQuery.mri});
  280. const duplicateQueryItem = {
  281. leadingItems: [<IconCopy key="icon" />],
  282. key: 'duplicate',
  283. label: t('Duplicate'),
  284. onAction: () => {
  285. addQuery(queryIndex);
  286. },
  287. };
  288. const addAlertItem = {
  289. leadingItems: [<IconSiren key="icon" />],
  290. key: 'add-alert',
  291. label: <CreateMetricAlertFeature>{t('Create Alert')}</CreateMetricAlertFeature>,
  292. disabled: !createAlert || !hasMetricAlertFeature(organization),
  293. onAction: () => {
  294. createAlert?.();
  295. },
  296. };
  297. const removeQueryItem = {
  298. leadingItems: [<IconClose key="icon" />],
  299. key: 'delete',
  300. label: t('Remove Metric'),
  301. disabled: !canRemoveQuery,
  302. onAction: () => {
  303. removeQuery(queryIndex);
  304. },
  305. };
  306. const aliasItem = {
  307. leadingItems: [<IconEdit key="icon" />],
  308. key: 'alias',
  309. label: t('Add Alias'),
  310. onAction: () => {
  311. editAlias(queryIndex);
  312. },
  313. };
  314. const settingsItem = {
  315. leadingItems: [<IconSettings key="icon" />],
  316. key: 'settings',
  317. label: t('Configure Metric'),
  318. disabled: !customMetric,
  319. onAction: () => {
  320. navigateTo(
  321. `/settings/projects/:projectId/metrics/${encodeURIComponent(metricsQuery.mri)}`,
  322. router
  323. );
  324. },
  325. };
  326. return hasCustomMetrics(organization)
  327. ? [duplicateQueryItem, aliasItem, addAlertItem, removeQueryItem, settingsItem]
  328. : [duplicateQueryItem, aliasItem, removeQueryItem, settingsItem];
  329. }, [
  330. metricsQuery.mri,
  331. createAlert,
  332. organization,
  333. canRemoveQuery,
  334. addQuery,
  335. queryIndex,
  336. removeQuery,
  337. editAlias,
  338. router,
  339. ]);
  340. return (
  341. <DropdownMenu
  342. items={items}
  343. triggerProps={{
  344. 'aria-label': t('Query actions'),
  345. size: 'md',
  346. showChevron: false,
  347. icon: <IconEllipsis direction="down" size="sm" />,
  348. }}
  349. position="bottom-end"
  350. />
  351. );
  352. }
  353. interface EquationContextMenuProps {
  354. editAlias: (index: number) => void;
  355. equationIndex: number;
  356. removeEquation: (index: number) => void;
  357. }
  358. function EquationContextMenu({
  359. equationIndex,
  360. editAlias,
  361. removeEquation,
  362. }: EquationContextMenuProps) {
  363. const items = useMemo<MenuItemProps[]>(() => {
  364. const removeEquationItem = {
  365. leadingItems: [<IconClose key="icon" />],
  366. key: 'delete',
  367. label: t('Remove Equation'),
  368. onAction: () => {
  369. removeEquation(equationIndex);
  370. },
  371. };
  372. const aliasItem = {
  373. leadingItems: [<IconEdit key="icon" />],
  374. key: 'alias',
  375. label: t('Add Alias'),
  376. onAction: () => {
  377. editAlias(equationIndex);
  378. },
  379. };
  380. return [aliasItem, removeEquationItem];
  381. }, [editAlias, equationIndex, removeEquation]);
  382. return (
  383. <DropdownMenu
  384. items={items}
  385. triggerProps={{
  386. 'aria-label': t('Equation actions'),
  387. size: 'md',
  388. showChevron: false,
  389. icon: <IconEllipsis direction="down" size="sm" />,
  390. }}
  391. position="bottom-end"
  392. />
  393. );
  394. }
  395. interface QueryToggleProps {
  396. disabled: boolean;
  397. isHidden: boolean;
  398. onChange: (isHidden: boolean) => void;
  399. queryId: number;
  400. type: MetricExpressionType;
  401. disabledReason?: string;
  402. }
  403. function QueryToggle({
  404. isHidden,
  405. queryId,
  406. disabled,
  407. onChange,
  408. type,
  409. disabledReason,
  410. }: QueryToggleProps) {
  411. const tooltipTitle =
  412. type === MetricExpressionType.QUERY
  413. ? isHidden
  414. ? t('Show metric')
  415. : t('Hide metric')
  416. : isHidden
  417. ? t('Show equation')
  418. : t('Hide equation');
  419. return (
  420. <Tooltip
  421. title={
  422. !disabled
  423. ? tooltipTitle
  424. : disabledReason || t('At least one query must be visible')
  425. }
  426. delay={500}
  427. >
  428. {type === MetricExpressionType.QUERY ? (
  429. <StyledQuerySymbol
  430. isHidden={isHidden}
  431. queryId={queryId}
  432. isClickable={!disabled}
  433. aria-disabled={disabled}
  434. onClick={disabled ? undefined : () => onChange(!isHidden)}
  435. role="button"
  436. aria-label={tooltipTitle}
  437. />
  438. ) : (
  439. <StyledEquationSymbol
  440. isHidden={isHidden}
  441. equationId={queryId}
  442. isClickable={!disabled}
  443. aria-disabled={disabled}
  444. onClick={disabled ? undefined : () => onChange(!isHidden)}
  445. role="button"
  446. aria-label={tooltipTitle}
  447. />
  448. )}
  449. </Tooltip>
  450. );
  451. }
  452. function ExpressionAliasForm({
  453. expression,
  454. onChange,
  455. hasContextMenu,
  456. hasMetricsNewInput,
  457. }: {
  458. expression: DashboardMetricsExpression;
  459. onChange: (alias: string | undefined) => void;
  460. hasContextMenu?: boolean;
  461. hasMetricsNewInput?: boolean;
  462. }) {
  463. const organization = useOrganization();
  464. return (
  465. <ExpressionAliasWrapper
  466. hasOwnRow={hasContextMenu}
  467. hasMetricsNewInput={hasMetricsNewInput}
  468. >
  469. {hasMetricsNewInputs(organization) ? (
  470. <QueryFieldGroup>
  471. <QueryFieldGroup.Label css={fixedWidthLabelCss}>As</QueryFieldGroup.Label>
  472. <div
  473. css={css`
  474. display: flex;
  475. `}
  476. >
  477. <QueryFieldGroup.DebouncedInput
  478. type="text"
  479. value={formatAlias(expression.alias)}
  480. onChange={e => onChange(e.target.value)}
  481. placeholder={t('Add alias')}
  482. />
  483. <QueryFieldGroup.DeleteButton
  484. disabled={isVirtualExpression(expression)}
  485. title={t('Clear Alias')}
  486. onClick={() => onChange(undefined)}
  487. />
  488. </div>
  489. </QueryFieldGroup>
  490. ) : (
  491. <Fragment>
  492. <StyledLabel>as</StyledLabel>
  493. <StyledDebouncedInput
  494. type="text"
  495. value={formatAlias(expression.alias)}
  496. onChange={e => onChange(e.target.value)}
  497. placeholder={t('Add alias')}
  498. />
  499. <Tooltip title={t('Clear alias')} delay={SLOW_TOOLTIP_DELAY}>
  500. <StyledButton
  501. disabled={isVirtualExpression(expression)}
  502. icon={<IconDelete size="xs" />}
  503. aria-label={t('Clear Alias')}
  504. onClick={() => onChange(undefined)}
  505. />
  506. </Tooltip>
  507. </Fragment>
  508. )}
  509. </ExpressionAliasWrapper>
  510. );
  511. }
  512. // TODO: Move this to a shared component
  513. export function DebouncedInput({
  514. onChange,
  515. wait = DEFAULT_DEBOUNCE_DURATION,
  516. ...inputProps
  517. }: InputProps & {wait?: number}) {
  518. const [value, setValue] = useState<string | number | readonly string[] | undefined>(
  519. inputProps.value
  520. );
  521. useEffect(() => {
  522. setValue(inputProps.value);
  523. }, [inputProps.value]);
  524. const handleChange = useMemo(
  525. () =>
  526. debounce((e: React.ChangeEvent<HTMLInputElement>) => {
  527. onChange?.(e);
  528. }, wait),
  529. [onChange, wait]
  530. );
  531. return (
  532. <Input
  533. {...inputProps}
  534. value={value}
  535. onChange={e => {
  536. setValue(e.target.value);
  537. handleChange(e);
  538. }}
  539. />
  540. );
  541. }
  542. const ExpressionsWrapper = styled('div')<{
  543. hasMetricsNewInput: boolean;
  544. showQuerySymbols: boolean;
  545. }>`
  546. ${p =>
  547. p.hasMetricsNewInput &&
  548. css`
  549. display: grid;
  550. gap: ${space(1)};
  551. grid-template-columns: ${p.showQuerySymbols
  552. ? 'min-content 1fr max-content'
  553. : '1fr max-content'};
  554. @media (min-width: ${p.theme.breakpoints.small}) {
  555. grid-template-columns: ${p.showQuerySymbols
  556. ? 'min-content 1fr 1fr max-content'
  557. : '1fr 1fr max-content'};
  558. }
  559. `}
  560. `;
  561. const ExpressionWrapper = styled('div')<{hasMetricsNewInput: boolean}>`
  562. display: flex;
  563. gap: ${space(1)};
  564. padding-bottom: ${space(1)};
  565. ${p => p.hasMetricsNewInput && 'display: contents;'}
  566. `;
  567. const ExpressionFormWrapper = styled('div')<{hasMetricsNewInput: boolean}>`
  568. display: flex;
  569. flex-grow: 1;
  570. flex-direction: column;
  571. gap: ${space(1)};
  572. ${p => p.hasMetricsNewInput && 'display: contents;'}
  573. `;
  574. const ExpressionFormRowWrapper = styled('div')<{hasMetricsNewInput: boolean}>`
  575. display: flex;
  576. gap: ${space(1)};
  577. ${p => p.hasMetricsNewInput && 'display: contents;'}
  578. `;
  579. const StyledQuerySymbol = styled(QuerySymbol)<{isClickable: boolean}>`
  580. ${p => p.isClickable && `cursor: pointer;`}
  581. `;
  582. const StyledEquationSymbol = styled(EquationSymbol)<{isClickable: boolean}>`
  583. ${p => p.isClickable && `cursor: pointer;`}
  584. `;
  585. const ButtonBar = styled('div')<{addQuerySymbolSpacing: boolean}>`
  586. align-items: center;
  587. display: flex;
  588. gap: ${space(2)};
  589. padding: ${space(2)} 0;
  590. ${p =>
  591. p.addQuerySymbolSpacing &&
  592. `
  593. padding-left: ${space(1)};
  594. margin-left: 38px;
  595. `}
  596. `;
  597. const ExpressionAliasWrapper = styled('div')<{
  598. hasMetricsNewInput?: boolean;
  599. hasOwnRow?: boolean;
  600. }>`
  601. display: flex;
  602. flex-basis: 50%;
  603. align-items: center;
  604. padding-bottom: ${space(1)};
  605. /* Add padding for the context menu */
  606. ${p => p.hasOwnRow && `padding-right: 56px;`}
  607. ${p => p.hasOwnRow && `flex-grow: 1;`}
  608. ${p =>
  609. p.hasMetricsNewInput &&
  610. css`
  611. padding: 0;
  612. grid-column: 1/-1;
  613. `}
  614. `;
  615. const StyledLabel = styled('div')`
  616. border: 1px solid ${p => p.theme.border};
  617. border-radius: ${p => p.theme.borderRadius};
  618. padding: ${space(1)} ${space(1.5)};
  619. color: ${p => p.theme.subText};
  620. border-top-right-radius: 0;
  621. border-bottom-right-radius: 0;
  622. border-right: none;
  623. `;
  624. const EquationInputWrapper = styled('div')<{hasMetricsNewInput: boolean}>`
  625. width: 100%;
  626. ${p =>
  627. p.hasMetricsNewInput &&
  628. css`
  629. grid-column-start: 2;
  630. @media (min-width: ${p.theme.breakpoints.small}) {
  631. grid-column-end: 4;
  632. }
  633. `}
  634. `;
  635. const StyledDebouncedInput = styled(DebouncedInput)`
  636. border-radius: 0;
  637. z-index: 1;
  638. `;
  639. const StyledButton = styled(Button)`
  640. border-top-left-radius: 0;
  641. border-bottom-left-radius: 0;
  642. border-left: none;
  643. `;
  644. const fixedWidthLabelCss = css`
  645. width: 95px;
  646. min-width: 95px;
  647. white-space: nowrap;
  648. `;