changedTransactions.tsx 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752
  1. import {Fragment, useCallback, useState} from 'react';
  2. import styled from '@emotion/styled';
  3. import type {Location} from 'history';
  4. import type {Client} from 'sentry/api';
  5. import Feature from 'sentry/components/acl/feature';
  6. import {Button} from 'sentry/components/button';
  7. import {HeaderTitleLegend} from 'sentry/components/charts/styles';
  8. import Count from 'sentry/components/count';
  9. import DropdownLink from 'sentry/components/dropdownLink';
  10. import Duration from 'sentry/components/duration';
  11. import EmptyStateWarning from 'sentry/components/emptyStateWarning';
  12. import {RadioLineItem} from 'sentry/components/forms/controls/radioGroup';
  13. import IdBadge from 'sentry/components/idBadge';
  14. import Link from 'sentry/components/links/link';
  15. import LoadingIndicator from 'sentry/components/loadingIndicator';
  16. import MenuItem from 'sentry/components/menuItem';
  17. import type {CursorHandler} from 'sentry/components/pagination';
  18. import Pagination from 'sentry/components/pagination';
  19. import Panel from 'sentry/components/panels/panel';
  20. import QuestionTooltip from 'sentry/components/questionTooltip';
  21. import Radio from 'sentry/components/radio';
  22. import {Tooltip} from 'sentry/components/tooltip';
  23. import {IconArrow, IconEllipsis} from 'sentry/icons';
  24. import {t} from 'sentry/locale';
  25. import {space} from 'sentry/styles/space';
  26. import type {Organization} from 'sentry/types/organization';
  27. import type {AvatarProject, Project} from 'sentry/types/project';
  28. import {trackAnalytics} from 'sentry/utils/analytics';
  29. import {browserHistory} from 'sentry/utils/browserHistory';
  30. import getDuration from 'sentry/utils/duration/getDuration';
  31. import {formatPercentage} from 'sentry/utils/number/formatPercentage';
  32. import {useMetricsCardinalityContext} from 'sentry/utils/performance/contexts/metricsCardinality';
  33. import TrendsDiscoverQuery from 'sentry/utils/performance/trends/trendsDiscoverQuery';
  34. import {decodeScalar} from 'sentry/utils/queryString';
  35. import {MutableSearch} from 'sentry/utils/tokenizeSearch';
  36. import useApi from 'sentry/utils/useApi';
  37. import withOrganization from 'sentry/utils/withOrganization';
  38. import withProjects from 'sentry/utils/withProjects';
  39. import {
  40. DisplayModes,
  41. transactionSummaryRouteWithQuery,
  42. } from 'sentry/views/performance/transactionSummary/utils';
  43. import {PerformanceChangeExplorer} from 'sentry/views/performance/trends/changeExplorer';
  44. import getSelectedQueryKey from 'sentry/views/performance/trends/utils/getSelectedQueryKey';
  45. import {getSelectedTransaction} from 'sentry/views/performance/utils/getSelectedTransaction';
  46. import Chart from './chart';
  47. import type {
  48. NormalizedTrendsTransaction,
  49. TrendFunctionField,
  50. TrendParameter,
  51. TrendParameterColumn,
  52. TrendsStats,
  53. TrendView,
  54. } from './types';
  55. import {TrendChangeType} from './types';
  56. import {
  57. getCurrentTrendFunction,
  58. getCurrentTrendParameter,
  59. getTrendProjectId,
  60. modifyTrendView,
  61. normalizeTrends,
  62. transformDeltaSpread,
  63. transformValueDelta,
  64. trendCursorNames,
  65. trendToColor,
  66. } from './utils';
  67. type Props = {
  68. location: Location;
  69. organization: Organization;
  70. projects: Project[];
  71. setError: (msg: string | undefined) => void;
  72. trendChangeType: TrendChangeType;
  73. trendView: TrendView;
  74. previousTrendColumn?: TrendParameterColumn;
  75. previousTrendFunction?: TrendFunctionField;
  76. withBreakpoint?: boolean;
  77. };
  78. type TrendsCursorQuery = {
  79. improvedCursor?: string;
  80. regressionCursor?: string;
  81. };
  82. const makeTrendsCursorHandler =
  83. (trendChangeType: TrendChangeType): CursorHandler =>
  84. (cursor, path, query) => {
  85. const cursorQuery = {} as TrendsCursorQuery;
  86. if (trendChangeType === TrendChangeType.IMPROVED) {
  87. cursorQuery.improvedCursor = cursor;
  88. } else if (trendChangeType === TrendChangeType.REGRESSION) {
  89. cursorQuery.regressionCursor = cursor;
  90. }
  91. const selectedQueryKey = getSelectedQueryKey(trendChangeType);
  92. delete query[selectedQueryKey];
  93. browserHistory.push({
  94. pathname: path,
  95. query: {...query, ...cursorQuery},
  96. });
  97. };
  98. function getChartTitle(trendChangeType: TrendChangeType): string {
  99. switch (trendChangeType) {
  100. case TrendChangeType.IMPROVED:
  101. return t('Most Improved Transactions');
  102. case TrendChangeType.REGRESSION:
  103. return t('Most Regressed Transactions');
  104. default:
  105. throw new Error('No trend type passed');
  106. }
  107. }
  108. function handleChangeSelected(
  109. location: Location,
  110. organization: Organization,
  111. trendChangeType: TrendChangeType
  112. ) {
  113. return function updateSelected(transaction?: NormalizedTrendsTransaction) {
  114. const selectedQueryKey = getSelectedQueryKey(trendChangeType);
  115. const query = {
  116. ...location.query,
  117. };
  118. if (!transaction) {
  119. delete query[selectedQueryKey];
  120. } else {
  121. query[selectedQueryKey] = transaction
  122. ? `${transaction.transaction}-${transaction.project}`
  123. : undefined;
  124. }
  125. browserHistory.push({
  126. pathname: location.pathname,
  127. query,
  128. });
  129. trackAnalytics('performance_views.trends.widget_interaction', {
  130. organization,
  131. widget_type: trendChangeType,
  132. });
  133. };
  134. }
  135. enum FilterSymbols {
  136. GREATER_THAN_EQUALS = '>=',
  137. LESS_THAN_EQUALS = '<=',
  138. }
  139. function handleFilterTransaction(location: Location, transaction: string) {
  140. const queryString = decodeScalar(location.query.query);
  141. const conditions = new MutableSearch(queryString ?? '');
  142. conditions.addFilterValues('!transaction', [transaction]);
  143. const query = conditions.formatString();
  144. browserHistory.push({
  145. pathname: location.pathname,
  146. query: {
  147. ...location.query,
  148. query: String(query).trim(),
  149. },
  150. });
  151. }
  152. function handleFilterDuration(
  153. location: Location,
  154. organization: Organization,
  155. value: number,
  156. symbol: FilterSymbols,
  157. trendChangeType: TrendChangeType,
  158. projects: Project[],
  159. projectIds: Readonly<number[]>
  160. ) {
  161. const durationTag = getCurrentTrendParameter(location, projects, projectIds).column;
  162. const queryString = decodeScalar(location.query.query);
  163. const conditions = new MutableSearch(queryString ?? '');
  164. const existingValues = conditions.getFilterValues(durationTag);
  165. const alternateSymbol = symbol === FilterSymbols.GREATER_THAN_EQUALS ? '>' : '<';
  166. if (existingValues) {
  167. existingValues.forEach(existingValue => {
  168. if (existingValue.startsWith(symbol) || existingValue.startsWith(alternateSymbol)) {
  169. conditions.removeFilterValue(durationTag, existingValue);
  170. }
  171. });
  172. }
  173. conditions.addFilterValues(durationTag, [`${symbol}${value}`]);
  174. const query = conditions.formatString();
  175. browserHistory.push({
  176. pathname: location.pathname,
  177. query: {
  178. ...location.query,
  179. query: String(query).trim(),
  180. },
  181. });
  182. trackAnalytics('performance_views.trends.change_duration', {
  183. organization,
  184. widget_type: getChartTitle(trendChangeType),
  185. value: `${symbol}${value}`,
  186. });
  187. }
  188. function ChangedTransactions(props: Props) {
  189. const {
  190. location,
  191. trendChangeType,
  192. previousTrendFunction,
  193. previousTrendColumn,
  194. organization,
  195. projects,
  196. setError,
  197. withBreakpoint,
  198. } = props;
  199. const api = useApi();
  200. const {isLoading: isCardinalityCheckLoading, outcome} = useMetricsCardinalityContext();
  201. const canUseMetricsTrends = withBreakpoint && !outcome?.forceTransactionsOnly;
  202. const trendView = props.trendView.clone();
  203. const chartTitle = getChartTitle(trendChangeType);
  204. modifyTrendView(trendView, location, trendChangeType, projects, canUseMetricsTrends);
  205. const onCursor = makeTrendsCursorHandler(trendChangeType);
  206. const cursor = decodeScalar(location.query[trendCursorNames[trendChangeType]]);
  207. const paginationAnalyticsEvent = (direction: string) => {
  208. trackAnalytics('performance_views.trends.widget_pagination', {
  209. organization,
  210. direction,
  211. widget_type: getChartTitle(trendChangeType),
  212. });
  213. };
  214. return (
  215. <TrendsDiscoverQuery
  216. eventView={trendView}
  217. orgSlug={organization.slug}
  218. location={location}
  219. trendChangeType={trendChangeType}
  220. cursor={cursor}
  221. limit={5}
  222. setError={error => setError(error?.message)}
  223. withBreakpoint={canUseMetricsTrends}
  224. >
  225. {({isLoading, trendsData, pageLinks}) => {
  226. const trendFunction = getCurrentTrendFunction(location);
  227. const trendParameter = getCurrentTrendParameter(
  228. location,
  229. projects,
  230. trendView.project
  231. );
  232. const events = normalizeTrends(trendsData?.events?.data || []);
  233. const selectedTransaction = getSelectedTransaction(
  234. location,
  235. trendChangeType,
  236. events
  237. );
  238. const statsData = trendsData?.stats || {};
  239. const transactionsList = events?.slice ? events.slice(0, 5) : [];
  240. const currentTrendFunction =
  241. isLoading && previousTrendFunction
  242. ? previousTrendFunction
  243. : trendFunction.field;
  244. const currentTrendColumn =
  245. isLoading && previousTrendColumn ? previousTrendColumn : trendParameter.column;
  246. const titleTooltipContent = t(
  247. 'This compares the baseline (%s) of the past with the present.',
  248. trendFunction.legendLabel
  249. );
  250. return (
  251. <TransactionsListContainer data-test-id="changed-transactions">
  252. <TrendsTransactionPanel>
  253. <StyledHeaderTitleLegend>
  254. {chartTitle}
  255. <QuestionTooltip size="sm" position="top" title={titleTooltipContent} />
  256. </StyledHeaderTitleLegend>
  257. {isLoading || isCardinalityCheckLoading ? (
  258. <LoadingIndicator
  259. style={{
  260. margin: '237px auto',
  261. }}
  262. />
  263. ) : (
  264. <Fragment>
  265. {transactionsList.length ? (
  266. <Fragment>
  267. <ChartContainer>
  268. <Chart
  269. statsData={statsData}
  270. query={trendView.query}
  271. project={trendView.project}
  272. environment={trendView.environment}
  273. start={trendView.start}
  274. end={trendView.end}
  275. statsPeriod={trendView.statsPeriod}
  276. transaction={selectedTransaction}
  277. isLoading={isLoading}
  278. {...props}
  279. />
  280. </ChartContainer>
  281. {transactionsList.map((transaction, index) => (
  282. <TrendsListItem
  283. api={api}
  284. currentTrendFunction={currentTrendFunction}
  285. currentTrendColumn={currentTrendColumn}
  286. trendView={trendView}
  287. organization={organization}
  288. transaction={transaction}
  289. key={transaction.transaction}
  290. index={index}
  291. trendChangeType={trendChangeType}
  292. transactions={transactionsList}
  293. location={location}
  294. projects={projects}
  295. statsData={statsData}
  296. handleSelectTransaction={handleChangeSelected(
  297. location,
  298. organization,
  299. trendChangeType
  300. )}
  301. isLoading={isLoading}
  302. trendParameter={trendParameter}
  303. />
  304. ))}
  305. </Fragment>
  306. ) : (
  307. <StyledEmptyStateWarning small>
  308. {t('No results')}
  309. </StyledEmptyStateWarning>
  310. )}
  311. </Fragment>
  312. )}
  313. </TrendsTransactionPanel>
  314. <Pagination
  315. pageLinks={pageLinks}
  316. onCursor={onCursor}
  317. paginationAnalyticsEvent={paginationAnalyticsEvent}
  318. />
  319. </TransactionsListContainer>
  320. );
  321. }}
  322. </TrendsDiscoverQuery>
  323. );
  324. }
  325. type TrendsListItemProps = {
  326. api: Client;
  327. currentTrendColumn: string;
  328. currentTrendFunction: string;
  329. handleSelectTransaction: (transaction: NormalizedTrendsTransaction) => void;
  330. index: number;
  331. isLoading: boolean;
  332. location: Location;
  333. organization: Organization;
  334. projects: Project[];
  335. statsData: TrendsStats;
  336. transaction: NormalizedTrendsTransaction;
  337. transactions: NormalizedTrendsTransaction[];
  338. trendChangeType: TrendChangeType;
  339. trendParameter: TrendParameter;
  340. trendView: TrendView;
  341. };
  342. function TrendsListItem(props: TrendsListItemProps) {
  343. const {
  344. transaction,
  345. transactions,
  346. trendChangeType,
  347. currentTrendFunction,
  348. currentTrendColumn,
  349. index,
  350. location,
  351. organization,
  352. projects,
  353. handleSelectTransaction,
  354. trendView,
  355. statsData,
  356. isLoading,
  357. trendParameter,
  358. } = props;
  359. const color = trendToColor[trendChangeType].default;
  360. const [openedTransaction, setOpenedTransaction] = useState<null | string>(null);
  361. const selectedTransaction = getSelectedTransaction(
  362. location,
  363. trendChangeType,
  364. transactions
  365. );
  366. const isSelected = selectedTransaction === transaction;
  367. const project = projects.find(
  368. ({slug}) => slug === transaction.project
  369. ) as AvatarProject;
  370. const currentPeriodValue = transaction.aggregate_range_2;
  371. const previousPeriodValue = transaction.aggregate_range_1;
  372. const absolutePercentChange = formatPercentage(
  373. Math.abs(transaction.trend_percentage - 1),
  374. 0
  375. );
  376. const previousDuration = getDuration(
  377. previousPeriodValue / 1000,
  378. previousPeriodValue < 1000 && previousPeriodValue > 10 ? 0 : 2
  379. );
  380. const currentDuration = getDuration(
  381. currentPeriodValue / 1000,
  382. currentPeriodValue < 1000 && currentPeriodValue > 10 ? 0 : 2
  383. );
  384. const percentChangeExplanation = t(
  385. 'Over this period, the %s for %s has %s %s from %s to %s',
  386. currentTrendFunction,
  387. currentTrendColumn,
  388. trendChangeType === TrendChangeType.IMPROVED ? t('decreased') : t('increased'),
  389. absolutePercentChange,
  390. previousDuration,
  391. currentDuration
  392. );
  393. const longestPeriodValue =
  394. trendChangeType === TrendChangeType.IMPROVED
  395. ? previousPeriodValue
  396. : currentPeriodValue;
  397. const longestDuration =
  398. trendChangeType === TrendChangeType.IMPROVED ? previousDuration : currentDuration;
  399. return (
  400. <Fragment>
  401. <ListItemContainer data-test-id={'trends-list-item-' + trendChangeType}>
  402. <ItemRadioContainer color={color}>
  403. {transaction.count_range_1 && transaction.count_range_2 ? (
  404. <Tooltip
  405. title={
  406. <TooltipContent>
  407. <span>{t('Total Events')}</span>
  408. <span>
  409. <Count value={transaction.count_range_1} />
  410. <StyledIconArrow direction="right" size="xs" />
  411. <Count value={transaction.count_range_2} />
  412. </span>
  413. </TooltipContent>
  414. }
  415. >
  416. <RadioLineItem index={index} role="radio">
  417. <Radio
  418. checked={isSelected}
  419. onChange={() => handleSelectTransaction(transaction)}
  420. />
  421. </RadioLineItem>
  422. </Tooltip>
  423. ) : (
  424. <RadioLineItem index={index} role="radio">
  425. <Radio
  426. checked={isSelected}
  427. onChange={() => handleSelectTransaction(transaction)}
  428. />
  429. </RadioLineItem>
  430. )}
  431. </ItemRadioContainer>
  432. <TransactionSummaryLink {...props} onItemClicked={setOpenedTransaction} />
  433. <ItemTransactionPercentage>
  434. <Tooltip title={percentChangeExplanation}>
  435. <Fragment>
  436. {trendChangeType === TrendChangeType.REGRESSION ? '+' : ''}
  437. {formatPercentage(transaction.trend_percentage - 1, 0)}
  438. </Fragment>
  439. </Tooltip>
  440. </ItemTransactionPercentage>
  441. <DropdownLink
  442. caret={false}
  443. anchorRight
  444. title={
  445. <StyledButton
  446. size="xs"
  447. icon={<IconEllipsis data-test-id="trends-item-action" />}
  448. aria-label={t('Actions')}
  449. />
  450. }
  451. >
  452. {!organization.features.includes('performance-new-trends') && (
  453. <Fragment>
  454. <MenuItem
  455. onClick={() =>
  456. handleFilterDuration(
  457. location,
  458. organization,
  459. longestPeriodValue,
  460. FilterSymbols.LESS_THAN_EQUALS,
  461. trendChangeType,
  462. projects,
  463. trendView.project
  464. )
  465. }
  466. >
  467. <MenuAction>{t('Show \u2264 %s', longestDuration)}</MenuAction>
  468. </MenuItem>
  469. <MenuItem
  470. onClick={() =>
  471. handleFilterDuration(
  472. location,
  473. organization,
  474. longestPeriodValue,
  475. FilterSymbols.GREATER_THAN_EQUALS,
  476. trendChangeType,
  477. projects,
  478. trendView.project
  479. )
  480. }
  481. >
  482. <MenuAction>{t('Show \u2265 %s', longestDuration)}</MenuAction>
  483. </MenuItem>
  484. </Fragment>
  485. )}
  486. <MenuItem
  487. onClick={() => handleFilterTransaction(location, transaction.transaction)}
  488. >
  489. <MenuAction>{t('Hide from list')}</MenuAction>
  490. </MenuItem>
  491. </DropdownLink>
  492. <ItemTransactionDurationChange>
  493. {project && (
  494. <Tooltip title={transaction.project}>
  495. <IdBadge avatarSize={16} project={project} hideName />
  496. </Tooltip>
  497. )}
  498. <CompareDurations {...props} />
  499. </ItemTransactionDurationChange>
  500. <ItemTransactionStatus color={color}>
  501. <ValueDelta {...props} />
  502. </ItemTransactionStatus>
  503. </ListItemContainer>
  504. <Feature features="performance-change-explorer">
  505. <PerformanceChangeExplorer
  506. collapsed={openedTransaction === null}
  507. onClose={() => setOpenedTransaction(null)}
  508. transaction={transaction}
  509. trendChangeType={trendChangeType}
  510. trendFunction={currentTrendFunction}
  511. trendView={trendView}
  512. statsData={statsData}
  513. isLoading={isLoading}
  514. organization={organization}
  515. projects={projects}
  516. trendParameter={trendParameter}
  517. location={location}
  518. />
  519. </Feature>
  520. </Fragment>
  521. );
  522. }
  523. export function CompareDurations({
  524. transaction,
  525. }: {
  526. transaction: TrendsListItemProps['transaction'];
  527. }) {
  528. const {fromSeconds, toSeconds, showDigits} = transformDeltaSpread(
  529. transaction.aggregate_range_1,
  530. transaction.aggregate_range_2
  531. );
  532. return (
  533. <DurationChange>
  534. <Duration seconds={fromSeconds} fixedDigits={showDigits ? 1 : 0} abbreviation />
  535. <StyledIconArrow direction="right" size="xs" />
  536. <Duration seconds={toSeconds} fixedDigits={showDigits ? 1 : 0} abbreviation />
  537. </DurationChange>
  538. );
  539. }
  540. function ValueDelta({transaction, trendChangeType}: TrendsListItemProps) {
  541. const {seconds, fixedDigits, changeLabel} = transformValueDelta(
  542. transaction.trend_difference,
  543. trendChangeType
  544. );
  545. return (
  546. <span>
  547. <Duration seconds={seconds} fixedDigits={fixedDigits} abbreviation /> {changeLabel}
  548. </span>
  549. );
  550. }
  551. type TransactionSummaryLinkProps = TrendsListItemProps & {
  552. onItemClicked: React.Dispatch<React.SetStateAction<null | string>>;
  553. };
  554. function TransactionSummaryLink(props: TransactionSummaryLinkProps) {
  555. const {
  556. organization,
  557. trendView: eventView,
  558. transaction,
  559. projects,
  560. location,
  561. currentTrendFunction,
  562. onItemClicked: onTransactionSelection,
  563. } = props;
  564. const summaryView = eventView.clone();
  565. const projectID = getTrendProjectId(transaction, projects);
  566. const target = transactionSummaryRouteWithQuery({
  567. orgSlug: organization.slug,
  568. transaction: String(transaction.transaction),
  569. query: summaryView.generateQueryStringObject(),
  570. projectID,
  571. display: DisplayModes.TREND,
  572. trendFunction: currentTrendFunction,
  573. additionalQuery: {
  574. trendParameter: location.query.trendParameter?.toString(),
  575. },
  576. });
  577. const handleClick = useCallback<React.MouseEventHandler>(
  578. event => {
  579. event.preventDefault();
  580. onTransactionSelection(transaction.transaction);
  581. trackAnalytics('performance_views.performance_change_explorer.open', {
  582. organization,
  583. transaction: transaction.transaction,
  584. });
  585. },
  586. [onTransactionSelection, transaction.transaction, organization]
  587. );
  588. if (organization.features.includes('performance-change-explorer')) {
  589. return (
  590. <ItemTransactionName
  591. to={location}
  592. data-test-id="item-transaction-name"
  593. onClick={handleClick}
  594. >
  595. {transaction.transaction}
  596. </ItemTransactionName>
  597. );
  598. }
  599. return (
  600. <ItemTransactionName to={target} data-test-id="item-transaction-name">
  601. {transaction.transaction}
  602. </ItemTransactionName>
  603. );
  604. }
  605. const TransactionsListContainer = styled('div')`
  606. display: flex;
  607. flex-direction: column;
  608. `;
  609. const TrendsTransactionPanel = styled(Panel)`
  610. margin: 0;
  611. flex-grow: 1;
  612. `;
  613. const ChartContainer = styled('div')`
  614. padding: ${space(3)};
  615. `;
  616. const StyledHeaderTitleLegend = styled(HeaderTitleLegend)`
  617. border-radius: ${p => p.theme.borderRadius};
  618. margin: ${space(2)} ${space(3)};
  619. `;
  620. const StyledButton = styled(Button)`
  621. vertical-align: middle;
  622. `;
  623. const MenuAction = styled('div')<{['data-test-id']?: string}>`
  624. white-space: nowrap;
  625. color: ${p => p.theme.textColor};
  626. `;
  627. MenuAction.defaultProps = {
  628. 'data-test-id': 'menu-action',
  629. };
  630. const StyledEmptyStateWarning = styled(EmptyStateWarning)`
  631. min-height: 300px;
  632. justify-content: center;
  633. `;
  634. const ListItemContainer = styled('div')`
  635. display: grid;
  636. grid-template-columns: 24px auto 100px 30px;
  637. grid-template-rows: repeat(2, auto);
  638. grid-column-gap: ${space(1)};
  639. border-top: 1px solid ${p => p.theme.border};
  640. padding: ${space(1)} ${space(2)};
  641. `;
  642. const ItemRadioContainer = styled('div')`
  643. grid-row: 1/3;
  644. input {
  645. cursor: pointer;
  646. }
  647. input:checked::after {
  648. background-color: ${p => p.color};
  649. }
  650. `;
  651. const ItemTransactionName = styled(Link)`
  652. font-size: ${p => p.theme.fontSizeMedium};
  653. margin-right: ${space(1)};
  654. ${p => p.theme.overflowEllipsis};
  655. `;
  656. const ItemTransactionDurationChange = styled('div')`
  657. display: flex;
  658. align-items: center;
  659. font-size: ${p => p.theme.fontSizeSmall};
  660. `;
  661. const DurationChange = styled('span')`
  662. color: ${p => p.theme.gray300};
  663. margin: 0 ${space(1)};
  664. `;
  665. const ItemTransactionPercentage = styled('div')`
  666. text-align: right;
  667. font-size: ${p => p.theme.fontSizeMedium};
  668. `;
  669. const ItemTransactionStatus = styled('div')`
  670. color: ${p => p.color};
  671. text-align: right;
  672. font-size: ${p => p.theme.fontSizeSmall};
  673. `;
  674. const TooltipContent = styled('div')`
  675. display: flex;
  676. flex-direction: column;
  677. align-items: center;
  678. `;
  679. const StyledIconArrow = styled(IconArrow)`
  680. margin: 0 ${space(1)};
  681. `;
  682. export default withProjects(withOrganization(ChangedTransactions));