metricsExtractionRuleForm.tsx 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779
  1. import {Fragment, useCallback, useId, useMemo, useState} from 'react';
  2. import {components} from 'react-select';
  3. import {css, useTheme} from '@emotion/react';
  4. import styled from '@emotion/styled';
  5. import {Observer} from 'mobx-react';
  6. import Alert from 'sentry/components/alert';
  7. import {Button} from 'sentry/components/button';
  8. import SearchBar from 'sentry/components/events/searchBar';
  9. import SelectField from 'sentry/components/forms/fields/selectField';
  10. import Form, {type FormProps} from 'sentry/components/forms/form';
  11. import FormField from 'sentry/components/forms/formField';
  12. import type FormModel from 'sentry/components/forms/model';
  13. import ExternalLink from 'sentry/components/links/externalLink';
  14. import {Tooltip} from 'sentry/components/tooltip';
  15. import {IconAdd, IconClose, IconQuestion, IconWarning} from 'sentry/icons';
  16. import {t, tct} from 'sentry/locale';
  17. import {space} from 'sentry/styles/space';
  18. import type {SelectValue} from 'sentry/types/core';
  19. import type {MetricAggregation, MetricsExtractionCondition} from 'sentry/types/metrics';
  20. import {DiscoverDatasets} from 'sentry/utils/discover/types';
  21. import useOrganization from 'sentry/utils/useOrganization';
  22. import {SpanIndexedField, SpanMetricsField} from 'sentry/views/insights/types';
  23. import {useSpanFieldSupportedTags} from 'sentry/views/performance/utils/useSpanFieldSupportedTags';
  24. import {openExtractionRuleEditModal} from 'sentry/views/settings/projectMetrics/metricsExtractionRuleEditModal';
  25. import {useMetricsExtractionRules} from 'sentry/views/settings/projectMetrics/utils/useMetricsExtractionRules';
  26. export type AggregateGroup = 'count' | 'count_unique' | 'min_max' | 'percentiles';
  27. export interface FormData {
  28. aggregates: AggregateGroup[];
  29. conditions: MetricsExtractionCondition[];
  30. spanAttribute: string | null;
  31. tags: string[];
  32. unit: string;
  33. }
  34. interface Props extends Omit<FormProps, 'onSubmit'> {
  35. initialData: FormData;
  36. projectId: string | number;
  37. cardinality?: Record<string, number>;
  38. isEdit?: boolean;
  39. onSubmit?: (
  40. data: FormData,
  41. onSubmitSuccess: (data: FormData) => void,
  42. onSubmitError: (error: any) => void,
  43. event: React.FormEvent,
  44. model: FormModel
  45. ) => void;
  46. }
  47. const HIGH_CARDINALITY_TAGS = new Set([
  48. SpanIndexedField.HTTP_RESPONSE_CONTENT_LENGTH,
  49. SpanIndexedField.SPAN_DURATION,
  50. SpanIndexedField.SPAN_SELF_TIME,
  51. SpanIndexedField.SPAN_GROUP,
  52. SpanIndexedField.ID,
  53. SpanIndexedField.SPAN_AI_PIPELINE_GROUP,
  54. SpanIndexedField.TRANSACTION_ID,
  55. SpanIndexedField.PROJECT_ID,
  56. SpanIndexedField.PROFILE_ID,
  57. SpanIndexedField.REPLAY_ID,
  58. SpanIndexedField.TIMESTAMP,
  59. SpanIndexedField.USER,
  60. SpanIndexedField.USER_ID,
  61. SpanIndexedField.USER_EMAIL,
  62. SpanIndexedField.USER_USERNAME,
  63. SpanIndexedField.INP,
  64. SpanIndexedField.INP_SCORE,
  65. SpanIndexedField.INP_SCORE_WEIGHT,
  66. SpanIndexedField.TOTAL_SCORE,
  67. SpanIndexedField.CACHE_ITEM_SIZE,
  68. SpanIndexedField.MESSAGING_MESSAGE_ID,
  69. SpanIndexedField.MESSAGING_MESSAGE_BODY_SIZE,
  70. SpanIndexedField.MESSAGING_MESSAGE_RECEIVE_LATENCY,
  71. SpanIndexedField.MESSAGING_MESSAGE_RETRY_COUNT,
  72. SpanMetricsField.AI_TOTAL_TOKENS_USED,
  73. SpanMetricsField.AI_PROMPT_TOKENS_USED,
  74. SpanMetricsField.AI_COMPLETION_TOKENS_USED,
  75. SpanMetricsField.AI_INPUT_MESSAGES,
  76. SpanMetricsField.HTTP_DECODED_RESPONSE_CONTENT_LENGTH,
  77. SpanMetricsField.HTTP_RESPONSE_TRANSFER_SIZE,
  78. SpanMetricsField.CACHE_ITEM_SIZE,
  79. SpanMetricsField.CACHE_KEY,
  80. SpanMetricsField.THREAD_ID,
  81. SpanMetricsField.SENTRY_FRAMES_FROZEN,
  82. SpanMetricsField.SENTRY_FRAMES_SLOW,
  83. SpanMetricsField.SENTRY_FRAMES_TOTAL,
  84. SpanMetricsField.FRAMES_DELAY,
  85. SpanMetricsField.URL_FULL,
  86. SpanMetricsField.USER_AGENT_ORIGINAL,
  87. SpanMetricsField.FRAMES_DELAY,
  88. ]);
  89. const isHighCardinalityTag = (tag: string): boolean => {
  90. return HIGH_CARDINALITY_TAGS.has(tag as SpanIndexedField);
  91. };
  92. const AGGREGATE_OPTIONS: {label: string; value: AggregateGroup}[] = [
  93. {
  94. label: t('count'),
  95. value: 'count',
  96. },
  97. {
  98. label: t('count_unique'),
  99. value: 'count_unique',
  100. },
  101. {
  102. label: t('min, max, sum, avg'),
  103. value: 'min_max',
  104. },
  105. {
  106. label: t('percentiles'),
  107. value: 'percentiles',
  108. },
  109. ];
  110. export function explodeAggregateGroup(group: AggregateGroup): MetricAggregation[] {
  111. switch (group) {
  112. case 'count':
  113. return ['count'];
  114. case 'count_unique':
  115. return ['count_unique'];
  116. case 'min_max':
  117. return ['min', 'max', 'sum', 'avg'];
  118. case 'percentiles':
  119. return ['p50', 'p75', 'p90', 'p95', 'p99'];
  120. default:
  121. throw new Error(`Unknown aggregate group: ${group}`);
  122. }
  123. }
  124. export function aggregatesToGroups(aggregates: MetricAggregation[]): AggregateGroup[] {
  125. const groups: AggregateGroup[] = [];
  126. if (aggregates.includes('count')) {
  127. groups.push('count');
  128. }
  129. if (aggregates.includes('count_unique')) {
  130. groups.push('count_unique');
  131. }
  132. const minMaxAggregates = new Set<MetricAggregation>(['min', 'max', 'sum', 'avg']);
  133. if (aggregates.find(aggregate => minMaxAggregates.has(aggregate))) {
  134. groups.push('min_max');
  135. }
  136. const percentileAggregates = new Set<MetricAggregation>([
  137. 'p50',
  138. 'p75',
  139. 'p90',
  140. 'p95',
  141. 'p99',
  142. ]);
  143. if (aggregates.find(aggregate => percentileAggregates.has(aggregate))) {
  144. groups.push('percentiles');
  145. }
  146. return groups;
  147. }
  148. let currentTempId = 0;
  149. function createTempId(): number {
  150. currentTempId -= 1;
  151. return currentTempId;
  152. }
  153. export function createCondition(): MetricsExtractionCondition {
  154. return {
  155. value: '',
  156. // id and mris will be set by the backend after creation
  157. id: createTempId(),
  158. mris: [],
  159. };
  160. }
  161. const SUPPORTED_UNITS = [
  162. 'none',
  163. 'nanosecond',
  164. 'microsecond',
  165. 'millisecond',
  166. 'second',
  167. 'minute',
  168. 'hour',
  169. 'day',
  170. 'week',
  171. 'ratio',
  172. 'percent',
  173. 'bit',
  174. 'byte',
  175. 'kibibyte',
  176. 'kilobyte',
  177. 'mebibyte',
  178. 'megabyte',
  179. 'gibibyte',
  180. 'gigabyte',
  181. 'tebibyte',
  182. 'terabyte',
  183. 'pebibyte',
  184. 'petabyte',
  185. 'exbibyte',
  186. 'exabyte',
  187. ] as const;
  188. const isSupportedUnit = (unit: string): unit is (typeof SUPPORTED_UNITS)[number] => {
  189. return SUPPORTED_UNITS.includes(unit as (typeof SUPPORTED_UNITS)[number]);
  190. };
  191. const EMPTY_SET = new Set<never>();
  192. const SPAN_SEARCH_CONFIG = {
  193. booleanKeys: EMPTY_SET,
  194. dateKeys: EMPTY_SET,
  195. durationKeys: EMPTY_SET,
  196. numericKeys: EMPTY_SET,
  197. percentageKeys: EMPTY_SET,
  198. sizeKeys: EMPTY_SET,
  199. textOperatorKeys: EMPTY_SET,
  200. disallowFreeText: true,
  201. disallowWildcard: true,
  202. disallowNegation: true,
  203. };
  204. const FIXED_UNITS_BY_ATTRIBUTE: Record<string, (typeof SUPPORTED_UNITS)[number]> = {
  205. [SpanIndexedField.SPAN_DURATION]: 'millisecond',
  206. };
  207. export function MetricsExtractionRuleForm({
  208. isEdit,
  209. projectId,
  210. onSubmit,
  211. cardinality,
  212. ...props
  213. }: Props) {
  214. const organization = useOrganization();
  215. const [customAttributes, setCustomAttributes] = useState<string[]>(() => {
  216. const {spanAttribute, tags} = props.initialData;
  217. return [...new Set(spanAttribute ? [...tags, spanAttribute] : tags)];
  218. });
  219. const [customUnit, setCustomUnit] = useState<string | null>(() => {
  220. const {unit} = props.initialData;
  221. return unit && !isSupportedUnit(unit) ? unit : null;
  222. });
  223. const [isUnitDisabled, setIsUnitDisabled] = useState(() => {
  224. const {spanAttribute} = props.initialData;
  225. return !!(spanAttribute && spanAttribute in FIXED_UNITS_BY_ATTRIBUTE);
  226. });
  227. const {data: extractionRules} = useMetricsExtractionRules({
  228. orgId: organization.slug,
  229. projectId: projectId,
  230. });
  231. const tags = useSpanFieldSupportedTags({projects: [Number(projectId)]});
  232. // TODO(aknaus): Make this nicer
  233. const supportedTags = useMemo(() => {
  234. const copy = {...tags};
  235. delete copy.has;
  236. return copy;
  237. }, [tags]);
  238. const allAttributeOptions = useMemo(() => {
  239. let keys = Object.keys(supportedTags);
  240. if (customAttributes.length) {
  241. keys = [...new Set(keys.concat(customAttributes))];
  242. }
  243. return keys.sort((a, b) => a.localeCompare(b));
  244. }, [customAttributes, supportedTags]);
  245. const attributeOptions = useMemo(() => {
  246. const disabledKeys = new Set(extractionRules?.map(rule => rule.spanAttribute) || []);
  247. return (
  248. allAttributeOptions
  249. .map<SelectValue<string>>(key => {
  250. const disabledRule = disabledKeys.has(key)
  251. ? extractionRules?.find(rule => rule.spanAttribute === key)
  252. : undefined;
  253. return {
  254. label: key,
  255. value: key,
  256. disabled: disabledKeys.has(key),
  257. tooltip: disabledKeys.has(key)
  258. ? tct(
  259. 'This attribute is already in use. Please select another one or [link:edit the existing metric].',
  260. {
  261. link: disabledRule ? (
  262. <Button
  263. priority="link"
  264. aria-label={t('Edit %s metric', disabledRule.spanAttribute)}
  265. onClick={() => {
  266. openExtractionRuleEditModal({
  267. metricExtractionRule: disabledRule,
  268. });
  269. }}
  270. />
  271. ) : null,
  272. }
  273. )
  274. : undefined,
  275. tooltipOptions: {position: 'left', isHoverable: true},
  276. };
  277. })
  278. // Sort disabled attributes to bottom
  279. .sort((a, b) => Number(a.disabled) - Number(b.disabled))
  280. );
  281. }, [allAttributeOptions, extractionRules]);
  282. const tagOptions = useMemo(() => {
  283. return allAttributeOptions.map<SelectValue<string>>(option => ({
  284. label: option,
  285. value: option,
  286. disabled: isHighCardinalityTag(option),
  287. tooltip: isHighCardinalityTag(option)
  288. ? t('This tag has high cardinality.')
  289. : undefined,
  290. tooltipOptions: {position: 'left'},
  291. }));
  292. }, [allAttributeOptions]);
  293. const unitOptions = useMemo(() => {
  294. const options: SelectValue<string>[] = SUPPORTED_UNITS.map(unit => ({
  295. label: unit + (unit === 'none' ? '' : 's'),
  296. value: unit,
  297. }));
  298. if (customUnit) {
  299. options.push({
  300. label: customUnit,
  301. value: customUnit,
  302. });
  303. }
  304. return options;
  305. }, [customUnit]);
  306. const handleSubmit = useCallback(
  307. (
  308. data: Record<string, any>,
  309. onSubmitSuccess: (data: Record<string, any>) => void,
  310. onSubmitError: (error: any) => void,
  311. event: React.FormEvent,
  312. model: FormModel
  313. ) => {
  314. const errors: Record<string, [string]> = {};
  315. if (!data.spanAttribute) {
  316. errors.spanAttribute = [t('Span attribute is required.')];
  317. }
  318. if (!data.aggregates.length) {
  319. errors.aggregates = [t('At least one aggregate is required.')];
  320. }
  321. if (Object.keys(errors).length) {
  322. onSubmitError({responseJSON: errors});
  323. return;
  324. }
  325. onSubmit?.(data as FormData, onSubmitSuccess, onSubmitError, event, model);
  326. },
  327. [onSubmit]
  328. );
  329. const isNewCustomSpanAttribute = useCallback((value?: string) => {
  330. if (!value) {
  331. return false;
  332. }
  333. return !attributeOptions.some(option => option.value === value);
  334. // attributeOptions is being mutated when a new custom attribute is created
  335. }, []); // eslint-disable-line react-hooks/exhaustive-deps
  336. const isCardinalityLimited = useMemo(() => {
  337. if (!cardinality) {
  338. return false;
  339. }
  340. const {conditions} = props.initialData;
  341. return conditions.some(condition =>
  342. condition.mris.some(conditionMri => cardinality[conditionMri] > 0)
  343. );
  344. }, [cardinality, props.initialData]);
  345. return (
  346. <Form onSubmit={onSubmit && handleSubmit} {...props}>
  347. {({model}) => (
  348. <Fragment>
  349. <SpanAttributeUnitWrapper>
  350. <SelectField
  351. inline={false}
  352. stacked
  353. name="spanAttribute"
  354. options={attributeOptions}
  355. disabled={isEdit}
  356. label={
  357. <TooltipIconLabel
  358. isHoverable
  359. label={t('Measure')}
  360. help={tct(
  361. 'Define the span attribute you want to track. Learn how to instrument custom attributes in [link:our docs].',
  362. {
  363. // TODO(telemetry-experience): add the correct link here once we have it!!!
  364. link: (
  365. <ExternalLink href="https://docs.sentry.io/product/explore/metrics/" />
  366. ),
  367. }
  368. )}
  369. />
  370. }
  371. onCreateOption={value => {
  372. setCustomAttributes(curr => [...curr, value]);
  373. model.setValue('spanAttribute', value);
  374. }}
  375. components={{
  376. MenuList: (
  377. menuListProps: React.ComponentProps<typeof components.MenuList>
  378. ) => {
  379. return (
  380. <MenuList
  381. {...menuListProps}
  382. info={tct(
  383. 'Select an attribute or create one. [link:See how to instrument a custom attribute.]',
  384. {
  385. link: (
  386. <ExternalLink href="https://docs.sentry.io/product/explore/metrics/metrics-set-up/" />
  387. ),
  388. }
  389. )}
  390. />
  391. );
  392. },
  393. }}
  394. placeholder={t('Select span attribute')}
  395. creatable
  396. onChange={value => {
  397. model.setValue('spanAttribute', value);
  398. if (value in FIXED_UNITS_BY_ATTRIBUTE) {
  399. model.setValue('unit', FIXED_UNITS_BY_ATTRIBUTE[value]);
  400. setIsUnitDisabled(true);
  401. } else {
  402. setIsUnitDisabled(false);
  403. }
  404. }}
  405. required
  406. />
  407. <StyledFieldConnector>in</StyledFieldConnector>
  408. <SelectField
  409. inline={false}
  410. stacked
  411. allowEmpty
  412. name="unit"
  413. options={unitOptions}
  414. disabled={isUnitDisabled}
  415. placeholder={t('Select unit')}
  416. creatable
  417. onCreateOption={value => {
  418. setCustomUnit(value);
  419. model.setValue('unit', value);
  420. }}
  421. css={css`
  422. min-width: 150px;
  423. `}
  424. />
  425. </SpanAttributeUnitWrapper>
  426. <SelectField
  427. inline={false}
  428. stacked
  429. name="aggregates"
  430. required
  431. options={AGGREGATE_OPTIONS}
  432. placeholder={t('Select aggregations')}
  433. label={
  434. <TooltipIconLabel
  435. isHoverable
  436. label={t('Aggregate')}
  437. help={tct(
  438. 'Select the aggregations you’d like to view. For more information, read [link:our docs]',
  439. {
  440. // TODO(telemetry-experience): add the correct link here once we have it!!!
  441. link: (
  442. <ExternalLink href="https://docs.sentry.io/product/explore/metrics/" />
  443. ),
  444. }
  445. )}
  446. />
  447. }
  448. multiple
  449. />
  450. <SelectField
  451. inline={false}
  452. stacked
  453. name="tags"
  454. aria-label={t('Select tags')}
  455. options={tagOptions}
  456. multiple
  457. placeholder={t('Select tags')}
  458. label={
  459. <Fragment>
  460. {isCardinalityLimited && (
  461. <Tooltip
  462. title={t(
  463. 'One of the selected tags is exceeding the cardinality limit. Remove tags or add more conditions to receive accurate data.'
  464. )}
  465. >
  466. <StyledIconWarning size="xs" color="yellow300" />
  467. </Tooltip>
  468. )}
  469. <TooltipIconLabel
  470. label={t('Group by')}
  471. help={t(
  472. 'Select the tags that can be used to group and filter the metric. Tag values have to be non-numeric.'
  473. )}
  474. />
  475. </Fragment>
  476. }
  477. creatable
  478. onCreateOption={value => {
  479. setCustomAttributes(curr => [...curr, value]);
  480. const currentTags = model.getValue('tags') as string[];
  481. model.setValue('tags', [...currentTags, value]);
  482. }}
  483. components={{
  484. MenuList: (
  485. menuListProps: React.ComponentProps<typeof components.MenuList>
  486. ) => {
  487. return (
  488. <MenuList
  489. {...menuListProps}
  490. info={tct(
  491. 'Select a tag or create one. [link:See how to instrument a custom tag.]',
  492. {
  493. link: (
  494. <ExternalLink href="https://docs.sentry.io/product/explore/metrics/metrics-set-up/" />
  495. ),
  496. }
  497. )}
  498. />
  499. );
  500. },
  501. }}
  502. />
  503. <FormField
  504. stacked
  505. label={
  506. <TooltipIconLabel
  507. label={t('Filters')}
  508. help={t(
  509. 'Define filters to narrow down the metric to a specific set of spans.'
  510. )}
  511. />
  512. }
  513. name="conditions"
  514. inline={false}
  515. hasControlState={false}
  516. flexibleControlStateSize
  517. >
  518. {({onChange, initialData, value}) => {
  519. const conditions = (value ||
  520. initialData ||
  521. []) as MetricsExtractionCondition[];
  522. const handleChange = (queryString: string, index: number) => {
  523. onChange(
  524. conditions.toSpliced(index, 1, {
  525. ...conditions[index],
  526. value: queryString,
  527. }),
  528. {}
  529. );
  530. };
  531. return (
  532. <Fragment>
  533. <ConditionsWrapper hasDelete={value.length > 1}>
  534. {conditions.map((condition, index) => {
  535. const hasSiblings = conditions.length > 1;
  536. return (
  537. <Fragment key={condition.id}>
  538. <SearchWrapper hasPrefix={hasSiblings}>
  539. {hasSiblings ? (
  540. <ConditionSymbol>{index + 1}</ConditionSymbol>
  541. ) : null}
  542. <SearchBarWithId
  543. {...SPAN_SEARCH_CONFIG}
  544. searchSource="metrics-extraction"
  545. query={condition.value}
  546. onSearch={(queryString: string) =>
  547. handleChange(queryString, index)
  548. }
  549. onClose={(queryString: string, {validSearch}) => {
  550. if (validSearch) {
  551. handleChange(queryString, index);
  552. }
  553. }}
  554. placeholder={t('Add span attributes')}
  555. organization={organization}
  556. supportedTags={supportedTags}
  557. excludedTags={[]}
  558. dataset={DiscoverDatasets.SPANS_INDEXED}
  559. projectIds={[Number(projectId)]}
  560. hasRecentSearches={false}
  561. savedSearchType={undefined}
  562. useFormWrapper={false}
  563. />
  564. </SearchWrapper>
  565. {value.length > 1 && (
  566. <Button
  567. aria-label={t('Remove Filter')}
  568. onClick={() => onChange(conditions.toSpliced(index, 1), {})}
  569. icon={<IconClose />}
  570. />
  571. )}
  572. </Fragment>
  573. );
  574. })}
  575. </ConditionsWrapper>
  576. <ConditionsButtonBar>
  577. <Button
  578. size="sm"
  579. onClick={() => onChange([...conditions, createCondition()], {})}
  580. icon={<IconAdd />}
  581. >
  582. {t('Add Filter')}
  583. </Button>
  584. </ConditionsButtonBar>
  585. </Fragment>
  586. );
  587. }}
  588. </FormField>
  589. <Observer>
  590. {() => {
  591. if (!isEdit && isNewCustomSpanAttribute(model.getValue('spanAttribute'))) {
  592. return (
  593. <Alert type="info" showIcon>
  594. {tct(
  595. 'You want to track a custom attribute, so if you haven’t already, please [link:add it to your span data].',
  596. {
  597. link: (
  598. <ExternalLink href="https://docs.sentry.io/product/explore/metrics/metrics-set-up/" />
  599. ),
  600. }
  601. )}
  602. </Alert>
  603. );
  604. }
  605. if (isEdit && model.formChanged) {
  606. return (
  607. <Alert type="info" showIcon>
  608. {t('The changes you made will only be reflected on future data.')}
  609. </Alert>
  610. );
  611. }
  612. return null;
  613. }}
  614. </Observer>
  615. </Fragment>
  616. )}
  617. </Form>
  618. );
  619. }
  620. function MenuList({
  621. children,
  622. info,
  623. ...props
  624. }: React.ComponentProps<typeof components.MenuList> & {info: React.ReactNode}) {
  625. const theme = useTheme();
  626. return (
  627. <components.MenuList {...props}>
  628. <div
  629. css={css`
  630. /* The padding must align with the values specified for the option in the forms/controls/selectOption component */
  631. padding: ${space(1)};
  632. padding-left: calc(${space(0.5)} + ${space(1.5)});
  633. color: ${theme.gray300};
  634. `}
  635. >
  636. {info}
  637. </div>
  638. {children}
  639. </components.MenuList>
  640. );
  641. }
  642. function TooltipIconLabel({
  643. label,
  644. help,
  645. isHoverable,
  646. }: {
  647. help: React.ReactNode;
  648. label: React.ReactNode;
  649. isHoverable?: boolean;
  650. }) {
  651. return (
  652. <TooltipIconLabelWrapper>
  653. {label}
  654. <Tooltip title={help} isHoverable={isHoverable}>
  655. <IconQuestion size="sm" color="gray200" />
  656. </Tooltip>
  657. </TooltipIconLabelWrapper>
  658. );
  659. }
  660. const TooltipIconLabelWrapper = styled('span')`
  661. display: inline-flex;
  662. font-weight: bold;
  663. color: ${p => p.theme.gray300};
  664. gap: ${space(0.5)};
  665. & > span {
  666. margin-top: 1px;
  667. }
  668. & > span:hover {
  669. cursor: pointer;
  670. }
  671. `;
  672. const StyledFieldConnector = styled('div')`
  673. color: ${p => p.theme.gray300};
  674. padding-bottom: ${space(1)};
  675. `;
  676. const SpanAttributeUnitWrapper = styled('div')`
  677. display: flex;
  678. align-items: flex-end;
  679. gap: ${space(1)};
  680. padding-bottom: ${space(2)};
  681. & > div:first-child {
  682. flex: 1;
  683. padding-bottom: 0;
  684. }
  685. `;
  686. function SearchBarWithId(props: React.ComponentProps<typeof SearchBar>) {
  687. const id = useId();
  688. return <SearchBar id={id} {...props} />;
  689. }
  690. const ConditionsWrapper = styled('div')<{hasDelete: boolean}>`
  691. display: grid;
  692. align-items: center;
  693. gap: ${space(1)};
  694. ${p =>
  695. p.hasDelete
  696. ? `
  697. grid-template-columns: 1fr min-content;
  698. `
  699. : `
  700. grid-template-columns: 1fr;
  701. `}
  702. `;
  703. const SearchWrapper = styled('div')<{hasPrefix?: boolean}>`
  704. display: grid;
  705. gap: ${space(1)};
  706. align-items: center;
  707. grid-template-columns: ${p => (p.hasPrefix ? 'max-content' : '')} 1fr;
  708. `;
  709. const ConditionSymbol = styled('div')`
  710. background-color: ${p => p.theme.purple100};
  711. color: ${p => p.theme.purple400};
  712. text-align: center;
  713. align-content: center;
  714. height: ${space(3)};
  715. width: ${space(3)};
  716. border-radius: 50%;
  717. `;
  718. const StyledIconWarning = styled(IconWarning)`
  719. margin: 0 ${space(0.5)};
  720. `;
  721. const ConditionsButtonBar = styled('div')`
  722. margin-top: ${space(1)};
  723. `;