metricsExtractionRuleForm.tsx 24 KB

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