content.tsx 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745
  1. import {Fragment, useCallback, useEffect, useMemo, useState} from 'react';
  2. import {useTheme} from '@emotion/react';
  3. import styled from '@emotion/styled';
  4. import debounce from 'lodash/debounce';
  5. import omit from 'lodash/omit';
  6. import {Alert} from 'sentry/components/alert';
  7. import {Button} from 'sentry/components/button';
  8. import Count from 'sentry/components/count';
  9. import EmptyStateWarning from 'sentry/components/emptyStateWarning';
  10. import * as Layout from 'sentry/components/layouts/thirds';
  11. import LoadingIndicator from 'sentry/components/loadingIndicator';
  12. import {DatePageFilter} from 'sentry/components/organizations/datePageFilter';
  13. import {EnvironmentPageFilter} from 'sentry/components/organizations/environmentPageFilter';
  14. import PageFilterBar from 'sentry/components/organizations/pageFilterBar';
  15. import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse';
  16. import Panel from 'sentry/components/panels/panel';
  17. import PanelHeader from 'sentry/components/panels/panelHeader';
  18. import PanelItem from 'sentry/components/panels/panelItem';
  19. import PerformanceDuration from 'sentry/components/performanceDuration';
  20. import {IconChevron} from 'sentry/icons/iconChevron';
  21. import {IconClose} from 'sentry/icons/iconClose';
  22. import {t, tct} from 'sentry/locale';
  23. import {space} from 'sentry/styles/space';
  24. import type {PageFilters} from 'sentry/types/core';
  25. import type {MRI} from 'sentry/types/metrics';
  26. import type {Organization} from 'sentry/types/organization';
  27. import {trackAnalytics} from 'sentry/utils/analytics';
  28. import {browserHistory} from 'sentry/utils/browserHistory';
  29. import {getFormattedMQL} from 'sentry/utils/metrics';
  30. import {useApiQuery} from 'sentry/utils/queryClient';
  31. import {decodeInteger, decodeList, decodeScalar} from 'sentry/utils/queryString';
  32. import {useLocation} from 'sentry/utils/useLocation';
  33. import useOrganization from 'sentry/utils/useOrganization';
  34. import usePageFilters from 'sentry/utils/usePageFilters';
  35. import * as ModuleLayout from 'sentry/views/performance/moduleLayout';
  36. import {type Field, FIELDS, SORTS} from './data';
  37. import {
  38. BREAKDOWN_SLICES,
  39. ProjectRenderer,
  40. SpanBreakdownSliceRenderer,
  41. SpanDescriptionRenderer,
  42. SpanIdRenderer,
  43. SpanTimeRenderer,
  44. TraceBreakdownContainer,
  45. TraceBreakdownRenderer,
  46. TraceIdRenderer,
  47. TraceIssuesRenderer,
  48. } from './fieldRenderers';
  49. import {TracesChart} from './tracesChart';
  50. import {TracesSearchBar} from './tracesSearchBar';
  51. import {
  52. areQueriesEmpty,
  53. getSecondaryNameFromSpan,
  54. getStylingSliceName,
  55. normalizeTraces,
  56. } from './utils';
  57. const DEFAULT_PER_PAGE = 50;
  58. export function Content() {
  59. const location = useLocation();
  60. const queries = useMemo(() => {
  61. return decodeList(location.query.query);
  62. }, [location.query.query]);
  63. const limit = useMemo(() => {
  64. return decodeInteger(location.query.perPage, DEFAULT_PER_PAGE);
  65. }, [location.query.perPage]);
  66. const removeMetric = useCallback(() => {
  67. browserHistory.push({
  68. ...location,
  69. query: omit(location.query, [
  70. 'mri',
  71. 'metricsOp',
  72. 'metricsQuery',
  73. 'metricsMax',
  74. 'metricsMin',
  75. ]),
  76. });
  77. }, [location]);
  78. const metricsMax = decodeScalar(location.query.metricsMax);
  79. const metricsMin = decodeScalar(location.query.metricsMin);
  80. const metricsOp = decodeScalar(location.query.metricsOp);
  81. const metricsQuery = decodeScalar(location.query.metricsQuery);
  82. const mri = decodeScalar(location.query.mri);
  83. const handleSearch = useCallback(
  84. (searchIndex: number, searchQuery: string) => {
  85. const newQueries = [...queries];
  86. if (newQueries.length === 0) {
  87. // In the odd case someone wants to add search bars before any query has been made, we add both the default one shown and a new one.
  88. newQueries[0] = '';
  89. }
  90. newQueries[searchIndex] = searchQuery;
  91. browserHistory.push({
  92. ...location,
  93. query: {
  94. ...location.query,
  95. cursor: undefined,
  96. query: typeof searchQuery === 'string' ? newQueries : queries,
  97. },
  98. });
  99. },
  100. [location, queries]
  101. );
  102. const handleClearSearch = useCallback(
  103. (searchIndex: number) => {
  104. const newQueries = [...queries];
  105. if (typeof newQueries[searchIndex] !== undefined) {
  106. delete newQueries[searchIndex];
  107. browserHistory.push({
  108. ...location,
  109. query: {
  110. ...location.query,
  111. cursor: undefined,
  112. query: newQueries,
  113. },
  114. });
  115. return true;
  116. }
  117. return false;
  118. },
  119. [location, queries]
  120. );
  121. const hasMetric = metricsOp && mri;
  122. const traces = useTraces<Field>({
  123. fields: [
  124. ...FIELDS,
  125. ...SORTS.map(field =>
  126. field.startsWith('-') ? (field.substring(1) as Field) : (field as Field)
  127. ),
  128. ],
  129. limit,
  130. query: queries,
  131. sort: SORTS,
  132. mri: hasMetric ? mri : undefined,
  133. metricsMax: hasMetric ? metricsMax : undefined,
  134. metricsMin: hasMetric ? metricsMin : undefined,
  135. metricsOp: hasMetric ? metricsOp : undefined,
  136. metricsQuery: hasMetric ? metricsQuery : undefined,
  137. });
  138. const isLoading = traces.isFetching;
  139. const isError = !isLoading && traces.isError;
  140. const isEmpty = !isLoading && !isError && (traces?.data?.data?.length ?? 0) === 0;
  141. const data = normalizeTraces(!isLoading && !isError ? traces?.data?.data : undefined);
  142. return (
  143. <LayoutMain fullWidth>
  144. <PageFilterBar condensed>
  145. <EnvironmentPageFilter />
  146. <DatePageFilter defaultPeriod="2h" />
  147. </PageFilterBar>
  148. {hasMetric && (
  149. <StyledAlert
  150. type="info"
  151. showIcon
  152. trailingItems={<StyledCloseButton onClick={removeMetric} />}
  153. >
  154. {tct('The metric query [metricQuery] is filtering the results below.', {
  155. metricQuery: (
  156. <strong>
  157. {getFormattedMQL({mri: mri as MRI, op: metricsOp, query: metricsQuery})}
  158. </strong>
  159. ),
  160. })}
  161. </StyledAlert>
  162. )}
  163. {isError && typeof traces.error?.responseJSON?.detail === 'string' ? (
  164. <StyledAlert type="error" showIcon>
  165. {traces.error?.responseJSON?.detail}
  166. </StyledAlert>
  167. ) : null}
  168. <TracesSearchBar
  169. queries={queries}
  170. handleSearch={handleSearch}
  171. handleClearSearch={handleClearSearch}
  172. />
  173. <ModuleLayout.Full>
  174. <TracesChart />
  175. </ModuleLayout.Full>
  176. <StyledPanel>
  177. <TracePanelContent>
  178. <StyledPanelHeader align="left" lightText>
  179. {t('Trace ID')}
  180. </StyledPanelHeader>
  181. <StyledPanelHeader align="left" lightText>
  182. {t('Trace Root')}
  183. </StyledPanelHeader>
  184. <StyledPanelHeader align="right" lightText>
  185. {areQueriesEmpty(queries) ? t('Total Spans') : t('Matching Spans')}
  186. </StyledPanelHeader>
  187. <StyledPanelHeader align="left" lightText>
  188. {t('Timeline')}
  189. </StyledPanelHeader>
  190. <StyledPanelHeader align="right" lightText>
  191. {t('Duration')}
  192. </StyledPanelHeader>
  193. <StyledPanelHeader align="right" lightText>
  194. {t('Timestamp')}
  195. </StyledPanelHeader>
  196. <StyledPanelHeader align="right" lightText>
  197. {t('Issues')}
  198. </StyledPanelHeader>
  199. {isLoading && (
  200. <StyledPanelItem span={7} overflow>
  201. <LoadingIndicator />
  202. </StyledPanelItem>
  203. )}
  204. {isError && ( // TODO: need an error state
  205. <StyledPanelItem span={7} overflow>
  206. <EmptyStateWarning withIcon />
  207. </StyledPanelItem>
  208. )}
  209. {isEmpty && (
  210. <StyledPanelItem span={7} overflow>
  211. <EmptyStateWarning withIcon>
  212. <EmptyStateText size="fontSizeExtraLarge">
  213. {t('No results found')}
  214. </EmptyStateText>
  215. <EmptyStateText size="fontSizeMedium">
  216. {t('There are no traces that match the conditions above.')}
  217. <br />
  218. {t('Try adjusting your filters starting with your time range.')}
  219. </EmptyStateText>
  220. </EmptyStateWarning>
  221. </StyledPanelItem>
  222. )}
  223. {data?.map((trace, i) => (
  224. <TraceRow key={trace.trace} trace={trace} defaultExpanded={i === 0} />
  225. ))}
  226. </TracePanelContent>
  227. </StyledPanel>
  228. </LayoutMain>
  229. );
  230. }
  231. function TraceRow({
  232. defaultExpanded,
  233. trace,
  234. }: {
  235. defaultExpanded;
  236. trace: TraceResult<Field>;
  237. }) {
  238. const [expanded, setExpanded] = useState<boolean>(defaultExpanded);
  239. const [highlightedSliceName, _setHighlightedSliceName] = useState('');
  240. const location = useLocation();
  241. const organization = useOrganization();
  242. const queries = useMemo(() => {
  243. return decodeList(location.query.query);
  244. }, [location.query.query]);
  245. const setHighlightedSliceName = useMemo(
  246. () =>
  247. debounce(sliceName => _setHighlightedSliceName(sliceName), 100, {
  248. leading: true,
  249. }),
  250. [_setHighlightedSliceName]
  251. );
  252. const onClickExpand = useCallback(() => setExpanded(e => !e), [setExpanded]);
  253. return (
  254. <Fragment>
  255. <StyledPanelItem align="center" center onClick={onClickExpand}>
  256. <Button
  257. icon={<IconChevron size="xs" direction={expanded ? 'down' : 'right'} />}
  258. aria-label={t('Toggle trace details')}
  259. aria-expanded={expanded}
  260. size="zero"
  261. borderless
  262. onClick={() =>
  263. trackAnalytics('trace_explorer.toggle_trace_details', {
  264. organization,
  265. expanded,
  266. })
  267. }
  268. />
  269. <TraceIdRenderer
  270. traceId={trace.trace}
  271. timestamp={trace.spans[0].timestamp}
  272. onClick={() =>
  273. trackAnalytics('trace_explorer.open_trace', {
  274. organization,
  275. })
  276. }
  277. location={location}
  278. />
  279. </StyledPanelItem>
  280. <StyledPanelItem align="left" overflow>
  281. <Description>
  282. {trace.project ? (
  283. <ProjectRenderer projectSlug={trace.project} hideName />
  284. ) : null}
  285. {trace.name ? (
  286. <WrappingText>{trace.name}</WrappingText>
  287. ) : (
  288. <EmptyValueContainer>{t('Missing Trace Root')}</EmptyValueContainer>
  289. )}
  290. </Description>
  291. </StyledPanelItem>
  292. <StyledPanelItem align="right">
  293. {areQueriesEmpty(queries) ? (
  294. <Count value={trace.numSpans} />
  295. ) : (
  296. tct('[numerator][space]of[space][denominator]', {
  297. numerator: <Count value={trace.matchingSpans} />,
  298. denominator: <Count value={trace.numSpans} />,
  299. space: <Fragment>&nbsp;</Fragment>,
  300. })
  301. )}
  302. </StyledPanelItem>
  303. <BreakdownPanelItem
  304. align="right"
  305. highlightedSliceName={highlightedSliceName}
  306. onMouseLeave={() => setHighlightedSliceName('')}
  307. >
  308. <TraceBreakdownRenderer
  309. trace={trace}
  310. setHighlightedSliceName={setHighlightedSliceName}
  311. />
  312. </BreakdownPanelItem>
  313. <StyledPanelItem align="right">
  314. <PerformanceDuration milliseconds={trace.duration} abbreviation />
  315. </StyledPanelItem>
  316. <StyledPanelItem align="right">
  317. <SpanTimeRenderer timestamp={trace.end} tooltipShowSeconds />
  318. </StyledPanelItem>
  319. <StyledPanelItem align="right">
  320. <TraceIssuesRenderer
  321. trace={trace}
  322. onClick={() =>
  323. trackAnalytics('trace_explorer.open_in_issues', {
  324. organization,
  325. })
  326. }
  327. />
  328. </StyledPanelItem>
  329. {expanded && (
  330. <SpanTable
  331. spans={trace.spans}
  332. trace={trace}
  333. setHighlightedSliceName={setHighlightedSliceName}
  334. />
  335. )}
  336. </Fragment>
  337. );
  338. }
  339. function SpanTable({
  340. spans,
  341. trace,
  342. setHighlightedSliceName,
  343. }: {
  344. setHighlightedSliceName: (sliceName: string) => void;
  345. spans: SpanResult<Field>[];
  346. trace: TraceResult<Field>;
  347. }) {
  348. const location = useLocation();
  349. const organization = useOrganization();
  350. const queries = useMemo(() => {
  351. return decodeList(location.query.query);
  352. }, [location.query.query]);
  353. return (
  354. <SpanTablePanelItem span={7} overflow>
  355. <StyledPanel>
  356. <SpanPanelContent>
  357. <StyledPanelHeader align="left" lightText>
  358. {t('Span ID')}
  359. </StyledPanelHeader>
  360. <StyledPanelHeader align="left" lightText>
  361. {t('Span Description')}
  362. </StyledPanelHeader>
  363. <StyledPanelHeader align="right" lightText />
  364. <StyledPanelHeader align="right" lightText>
  365. {t('Span Duration')}
  366. </StyledPanelHeader>
  367. <StyledPanelHeader align="right" lightText>
  368. {t('Timestamp')}
  369. </StyledPanelHeader>
  370. {spans.map(span => (
  371. <SpanRow
  372. organization={organization}
  373. key={span.id}
  374. span={span}
  375. trace={trace}
  376. setHighlightedSliceName={setHighlightedSliceName}
  377. />
  378. ))}
  379. {spans.length < trace.matchingSpans && (
  380. <MoreMatchingSpans span={5}>
  381. {tct('[more][space]more [matching]spans can be found in the trace.', {
  382. more: <Count value={trace.matchingSpans - spans.length} />,
  383. space: <Fragment>&nbsp;</Fragment>,
  384. matching: areQueriesEmpty(queries) ? '' : 'matching ',
  385. })}
  386. </MoreMatchingSpans>
  387. )}
  388. </SpanPanelContent>
  389. </StyledPanel>
  390. </SpanTablePanelItem>
  391. );
  392. }
  393. function SpanRow({
  394. organization,
  395. span,
  396. trace,
  397. setHighlightedSliceName,
  398. }: {
  399. organization: Organization;
  400. setHighlightedSliceName: (sliceName: string) => void;
  401. span: SpanResult<Field>;
  402. trace: TraceResult<Field>;
  403. }) {
  404. const theme = useTheme();
  405. return (
  406. <Fragment>
  407. <StyledSpanPanelItem align="right">
  408. <SpanIdRenderer
  409. projectSlug={span.project}
  410. transactionId={span['transaction.id']}
  411. spanId={span.id}
  412. traceId={trace.trace}
  413. timestamp={span.timestamp}
  414. onClick={() =>
  415. trackAnalytics('trace_explorer.open_trace_span', {
  416. organization,
  417. })
  418. }
  419. />
  420. </StyledSpanPanelItem>
  421. <StyledSpanPanelItem align="left" overflow>
  422. <SpanDescriptionRenderer span={span} />
  423. </StyledSpanPanelItem>
  424. <StyledSpanPanelItem align="right" onMouseLeave={() => setHighlightedSliceName('')}>
  425. <TraceBreakdownContainer>
  426. <SpanBreakdownSliceRenderer
  427. sliceName={span.project}
  428. sliceSecondaryName={getSecondaryNameFromSpan(span)}
  429. sliceStart={Math.ceil(span['precise.start_ts'] * 1000)}
  430. sliceEnd={Math.floor(span['precise.finish_ts'] * 1000)}
  431. trace={trace}
  432. theme={theme}
  433. onMouseEnter={() =>
  434. setHighlightedSliceName(
  435. getStylingSliceName(span.project, getSecondaryNameFromSpan(span)) ?? ''
  436. )
  437. }
  438. />
  439. </TraceBreakdownContainer>
  440. </StyledSpanPanelItem>
  441. <StyledSpanPanelItem align="right">
  442. <PerformanceDuration milliseconds={span['span.duration']} abbreviation />
  443. </StyledSpanPanelItem>
  444. <StyledSpanPanelItem align="right">
  445. <SpanTimeRenderer
  446. timestamp={span['precise.finish_ts'] * 1000}
  447. tooltipShowSeconds
  448. />
  449. </StyledSpanPanelItem>
  450. </Fragment>
  451. );
  452. }
  453. export type SpanResult<F extends string> = Record<F, any>;
  454. export interface TraceResult<F extends string> {
  455. breakdowns: TraceBreakdownResult[];
  456. duration: number;
  457. end: number;
  458. matchingSpans: number;
  459. name: string | null;
  460. numErrors: number;
  461. numOccurrences: number;
  462. numSpans: number;
  463. project: string | null;
  464. slices: number;
  465. spans: SpanResult<F>[];
  466. start: number;
  467. trace: string;
  468. }
  469. interface TraceBreakdownBase {
  470. duration: number; // Contains the accurate duration for display. Start and end may be quantized.
  471. end: number;
  472. opCategory: string | null;
  473. sdkName: string | null;
  474. sliceEnd: number;
  475. sliceStart: number;
  476. sliceWidth: number;
  477. start: number;
  478. }
  479. type TraceBreakdownProject = TraceBreakdownBase & {
  480. kind: 'project';
  481. project: string;
  482. };
  483. type TraceBreakdownMissing = TraceBreakdownBase & {
  484. kind: 'missing';
  485. project: null;
  486. };
  487. export type TraceBreakdownResult = TraceBreakdownProject | TraceBreakdownMissing;
  488. interface TraceResults<F extends string> {
  489. data: TraceResult<F>[];
  490. meta: any;
  491. }
  492. interface UseTracesOptions<F extends string> {
  493. fields: F[];
  494. datetime?: PageFilters['datetime'];
  495. enabled?: boolean;
  496. limit?: number;
  497. metricsMax?: string;
  498. metricsMin?: string;
  499. metricsOp?: string;
  500. metricsQuery?: string;
  501. mri?: string;
  502. query?: string | string[];
  503. sort?: string[];
  504. suggestedQuery?: string;
  505. }
  506. function useTraces<F extends string>({
  507. fields,
  508. datetime,
  509. enabled,
  510. limit,
  511. mri,
  512. metricsMax,
  513. metricsMin,
  514. metricsOp,
  515. metricsQuery,
  516. query,
  517. suggestedQuery,
  518. sort,
  519. }: UseTracesOptions<F>) {
  520. const organization = useOrganization();
  521. const {selection} = usePageFilters();
  522. const path = `/organizations/${organization.slug}/traces/`;
  523. const endpointOptions = {
  524. query: {
  525. project: selection.projects,
  526. environment: selection.environments,
  527. ...(datetime ?? normalizeDateTimeParams(selection.datetime)),
  528. field: fields,
  529. query,
  530. suggestedQuery,
  531. sort,
  532. per_page: limit,
  533. breakdownSlices: BREAKDOWN_SLICES,
  534. maxSpansPerTrace: 10,
  535. mri,
  536. metricsMax,
  537. metricsMin,
  538. metricsOp,
  539. metricsQuery,
  540. },
  541. };
  542. const serializedEndpointOptions = JSON.stringify(endpointOptions);
  543. let queries: string[] = [];
  544. if (Array.isArray(query)) {
  545. queries = query;
  546. } else if (query !== undefined) {
  547. queries = [query];
  548. }
  549. useEffect(() => {
  550. trackAnalytics('trace_explorer.search_request', {
  551. organization,
  552. queries,
  553. });
  554. // `queries` is already included as a dep in serializedEndpointOptions
  555. // eslint-disable-next-line react-hooks/exhaustive-deps
  556. }, [serializedEndpointOptions, organization]);
  557. const result = useApiQuery<TraceResults<F>>([path, endpointOptions], {
  558. staleTime: 0,
  559. refetchOnWindowFocus: false,
  560. retry: false,
  561. enabled,
  562. });
  563. useEffect(() => {
  564. if (result.status === 'success') {
  565. trackAnalytics('trace_explorer.search_success', {
  566. organization,
  567. queries,
  568. has_data: result.data.data.length > 0,
  569. });
  570. } else if (result.status === 'error') {
  571. const response = result.error.responseJSON;
  572. const error =
  573. typeof response?.detail === 'string'
  574. ? response?.detail
  575. : response?.detail?.message;
  576. trackAnalytics('trace_explorer.search_failure', {
  577. organization,
  578. queries,
  579. error: error ?? '',
  580. });
  581. }
  582. // result.status is tied to result.data. No need to explicitly
  583. // include result.data as an additional dep.
  584. // eslint-disable-next-line react-hooks/exhaustive-deps
  585. }, [serializedEndpointOptions, result.status, organization]);
  586. return result;
  587. }
  588. const LayoutMain = styled(Layout.Main)`
  589. display: flex;
  590. flex-direction: column;
  591. gap: ${space(2)};
  592. `;
  593. const StyledPanel = styled(Panel)`
  594. margin-bottom: 0px;
  595. `;
  596. const TracePanelContent = styled('div')`
  597. width: 100%;
  598. display: grid;
  599. grid-template-columns: repeat(1, min-content) auto repeat(2, min-content) 85px 85px 66px;
  600. `;
  601. const SpanPanelContent = styled('div')`
  602. width: 100%;
  603. display: grid;
  604. grid-template-columns: repeat(1, min-content) auto repeat(1, min-content) 133px 85px;
  605. `;
  606. const StyledPanelHeader = styled(PanelHeader)<{align: 'left' | 'right'}>`
  607. white-space: nowrap;
  608. justify-content: ${p => (p.align === 'left' ? 'flex-start' : 'flex-end')};
  609. `;
  610. const EmptyStateText = styled('div')<{size: 'fontSizeExtraLarge' | 'fontSizeMedium'}>`
  611. color: ${p => p.theme.gray300};
  612. font-size: ${p => p.theme[p.size]};
  613. `;
  614. const Description = styled('div')`
  615. ${p => p.theme.overflowEllipsis};
  616. display: flex;
  617. flex-direction: row;
  618. align-items: center;
  619. gap: ${space(1)};
  620. `;
  621. const StyledPanelItem = styled(PanelItem)<{
  622. align?: 'left' | 'center' | 'right';
  623. overflow?: boolean;
  624. span?: number;
  625. }>`
  626. align-items: center;
  627. padding: ${space(1)} ${space(2)};
  628. ${p => (p.align === 'left' ? 'justify-content: flex-start;' : null)}
  629. ${p => (p.align === 'right' ? 'justify-content: flex-end;' : null)}
  630. ${p => (p.overflow ? p.theme.overflowEllipsis : null)};
  631. ${p =>
  632. p.align === 'center'
  633. ? `
  634. justify-content: space-around;`
  635. : p.align === 'left' || p.align === 'right'
  636. ? `text-align: ${p.align};`
  637. : undefined}
  638. ${p => p.span && `grid-column: auto / span ${p.span};`}
  639. white-space: nowrap;
  640. `;
  641. const MoreMatchingSpans = styled(StyledPanelItem)`
  642. color: ${p => p.theme.gray300};
  643. `;
  644. const WrappingText = styled('div')`
  645. width: 100%;
  646. ${p => p.theme.overflowEllipsis};
  647. `;
  648. const StyledSpanPanelItem = styled(StyledPanelItem)`
  649. &:nth-child(10n + 1),
  650. &:nth-child(10n + 2),
  651. &:nth-child(10n + 3),
  652. &:nth-child(10n + 4),
  653. &:nth-child(10n + 5) {
  654. background-color: ${p => p.theme.backgroundSecondary};
  655. }
  656. `;
  657. const SpanTablePanelItem = styled(StyledPanelItem)`
  658. background-color: ${p => p.theme.gray100};
  659. `;
  660. const BreakdownPanelItem = styled(StyledPanelItem)<{highlightedSliceName: string}>`
  661. ${p =>
  662. p.highlightedSliceName
  663. ? `--highlightedSlice-${p.highlightedSliceName}-opacity: 1.0;
  664. --highlightedSlice-${p.highlightedSliceName}-saturate: saturate(1.0) contrast(1.0);
  665. --highlightedSlice-${p.highlightedSliceName}-transform: translateY(0px);
  666. `
  667. : null}
  668. ${p =>
  669. p.highlightedSliceName
  670. ? `
  671. --defaultSlice-opacity: 1.0;
  672. --defaultSlice-saturate: saturate(0.7) contrast(0.9) brightness(1.2);
  673. --defaultSlice-transform: translateY(0px);
  674. `
  675. : `
  676. --defaultSlice-opacity: 1.0;
  677. --defaultSlice-saturate: saturate(1.0) contrast(1.0);
  678. --defaultSlice-transform: translateY(0px);
  679. `}
  680. `;
  681. const EmptyValueContainer = styled('span')`
  682. color: ${p => p.theme.gray300};
  683. `;
  684. const StyledAlert = styled(Alert)`
  685. margin-bottom: 0;
  686. `;
  687. const StyledCloseButton = styled(IconClose)`
  688. cursor: pointer;
  689. `;