metricsExtractionRuleForm.tsx 23 KB

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