content.tsx 21 KB

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