content.tsx 23 KB

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