changedTransactions.tsx 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748
  1. import {Fragment, useCallback, useState} from 'react';
  2. import {browserHistory} from 'react-router';
  3. import styled from '@emotion/styled';
  4. import type {Location} from 'history';
  5. import type {Client} from 'sentry/api';
  6. import Feature from 'sentry/components/acl/feature';
  7. import {Button} from 'sentry/components/button';
  8. import {HeaderTitleLegend} from 'sentry/components/charts/styles';
  9. import Count from 'sentry/components/count';
  10. import DropdownLink from 'sentry/components/dropdownLink';
  11. import Duration from 'sentry/components/duration';
  12. import EmptyStateWarning from 'sentry/components/emptyStateWarning';
  13. import {RadioLineItem} from 'sentry/components/forms/controls/radioGroup';
  14. import IdBadge from 'sentry/components/idBadge';
  15. import Link from 'sentry/components/links/link';
  16. import LoadingIndicator from 'sentry/components/loadingIndicator';
  17. import MenuItem from 'sentry/components/menuItem';
  18. import type {CursorHandler} from 'sentry/components/pagination';
  19. import Pagination from 'sentry/components/pagination';
  20. import Panel from 'sentry/components/panels/panel';
  21. import QuestionTooltip from 'sentry/components/questionTooltip';
  22. import Radio from 'sentry/components/radio';
  23. import {Tooltip} from 'sentry/components/tooltip';
  24. import {IconArrow, IconEllipsis} from 'sentry/icons';
  25. import {t} from 'sentry/locale';
  26. import {space} from 'sentry/styles/space';
  27. import type {AvatarProject, Organization, Project} from 'sentry/types';
  28. import {trackAnalytics} from 'sentry/utils/analytics';
  29. import {formatPercentage, getDuration} from 'sentry/utils/formatters';
  30. import {useMetricsCardinalityContext} from 'sentry/utils/performance/contexts/metricsCardinality';
  31. import TrendsDiscoverQuery from 'sentry/utils/performance/trends/trendsDiscoverQuery';
  32. import {decodeScalar} from 'sentry/utils/queryString';
  33. import {MutableSearch} from 'sentry/utils/tokenizeSearch';
  34. import useApi from 'sentry/utils/useApi';
  35. import withOrganization from 'sentry/utils/withOrganization';
  36. import withProjects from 'sentry/utils/withProjects';
  37. import {
  38. DisplayModes,
  39. transactionSummaryRouteWithQuery,
  40. } from 'sentry/views/performance/transactionSummary/utils';
  41. import {PerformanceChangeExplorer} from 'sentry/views/performance/trends/changeExplorer';
  42. import {getSelectedTransaction} from 'sentry/views/performance/utils';
  43. import Chart from './chart';
  44. import type {
  45. NormalizedTrendsTransaction,
  46. TrendFunctionField,
  47. TrendParameter,
  48. TrendParameterColumn,
  49. TrendsStats,
  50. TrendView,
  51. } from './types';
  52. import {TrendChangeType} from './types';
  53. import {
  54. getCurrentTrendFunction,
  55. getCurrentTrendParameter,
  56. getSelectedQueryKey,
  57. getTrendProjectId,
  58. modifyTrendView,
  59. normalizeTrends,
  60. transformDeltaSpread,
  61. transformValueDelta,
  62. trendCursorNames,
  63. trendToColor,
  64. } from './utils';
  65. type Props = {
  66. location: Location;
  67. organization: Organization;
  68. projects: Project[];
  69. setError: (msg: string | undefined) => void;
  70. trendChangeType: TrendChangeType;
  71. trendView: TrendView;
  72. previousTrendColumn?: TrendParameterColumn;
  73. previousTrendFunction?: TrendFunctionField;
  74. withBreakpoint?: boolean;
  75. };
  76. type TrendsCursorQuery = {
  77. improvedCursor?: string;
  78. regressionCursor?: string;
  79. };
  80. const makeTrendsCursorHandler =
  81. (trendChangeType: TrendChangeType): CursorHandler =>
  82. (cursor, path, query) => {
  83. const cursorQuery = {} as TrendsCursorQuery;
  84. if (trendChangeType === TrendChangeType.IMPROVED) {
  85. cursorQuery.improvedCursor = cursor;
  86. } else if (trendChangeType === TrendChangeType.REGRESSION) {
  87. cursorQuery.regressionCursor = cursor;
  88. }
  89. const selectedQueryKey = getSelectedQueryKey(trendChangeType);
  90. delete query[selectedQueryKey];
  91. browserHistory.push({
  92. pathname: path,
  93. query: {...query, ...cursorQuery},
  94. });
  95. };
  96. function getChartTitle(trendChangeType: TrendChangeType): string {
  97. switch (trendChangeType) {
  98. case TrendChangeType.IMPROVED:
  99. return t('Most Improved Transactions');
  100. case TrendChangeType.REGRESSION:
  101. return t('Most Regressed Transactions');
  102. default:
  103. throw new Error('No trend type passed');
  104. }
  105. }
  106. function handleChangeSelected(
  107. location: Location,
  108. organization: Organization,
  109. trendChangeType: TrendChangeType
  110. ) {
  111. return function updateSelected(transaction?: NormalizedTrendsTransaction) {
  112. const selectedQueryKey = getSelectedQueryKey(trendChangeType);
  113. const query = {
  114. ...location.query,
  115. };
  116. if (!transaction) {
  117. delete query[selectedQueryKey];
  118. } else {
  119. query[selectedQueryKey] = transaction
  120. ? `${transaction.transaction}-${transaction.project}`
  121. : undefined;
  122. }
  123. browserHistory.push({
  124. pathname: location.pathname,
  125. query,
  126. });
  127. trackAnalytics('performance_views.trends.widget_interaction', {
  128. organization,
  129. widget_type: trendChangeType,
  130. });
  131. };
  132. }
  133. enum FilterSymbols {
  134. GREATER_THAN_EQUALS = '>=',
  135. LESS_THAN_EQUALS = '<=',
  136. }
  137. function handleFilterTransaction(location: Location, transaction: string) {
  138. const queryString = decodeScalar(location.query.query);
  139. const conditions = new MutableSearch(queryString ?? '');
  140. conditions.addFilterValues('!transaction', [transaction]);
  141. const query = conditions.formatString();
  142. browserHistory.push({
  143. pathname: location.pathname,
  144. query: {
  145. ...location.query,
  146. query: String(query).trim(),
  147. },
  148. });
  149. }
  150. function handleFilterDuration(
  151. location: Location,
  152. organization: Organization,
  153. value: number,
  154. symbol: FilterSymbols,
  155. trendChangeType: TrendChangeType,
  156. projects: Project[],
  157. projectIds: Readonly<number[]>
  158. ) {
  159. const durationTag = getCurrentTrendParameter(location, projects, projectIds).column;
  160. const queryString = decodeScalar(location.query.query);
  161. const conditions = new MutableSearch(queryString ?? '');
  162. const existingValues = conditions.getFilterValues(durationTag);
  163. const alternateSymbol = symbol === FilterSymbols.GREATER_THAN_EQUALS ? '>' : '<';
  164. if (existingValues) {
  165. existingValues.forEach(existingValue => {
  166. if (existingValue.startsWith(symbol) || existingValue.startsWith(alternateSymbol)) {
  167. conditions.removeFilterValue(durationTag, existingValue);
  168. }
  169. });
  170. }
  171. conditions.addFilterValues(durationTag, [`${symbol}${value}`]);
  172. const query = conditions.formatString();
  173. browserHistory.push({
  174. pathname: location.pathname,
  175. query: {
  176. ...location.query,
  177. query: String(query).trim(),
  178. },
  179. });
  180. trackAnalytics('performance_views.trends.change_duration', {
  181. organization,
  182. widget_type: getChartTitle(trendChangeType),
  183. value: `${symbol}${value}`,
  184. });
  185. }
  186. function ChangedTransactions(props: Props) {
  187. const {
  188. location,
  189. trendChangeType,
  190. previousTrendFunction,
  191. previousTrendColumn,
  192. organization,
  193. projects,
  194. setError,
  195. withBreakpoint,
  196. } = props;
  197. const api = useApi();
  198. const {isLoading: isCardinalityCheckLoading, outcome} = useMetricsCardinalityContext();
  199. const canUseMetricsTrends = withBreakpoint && !outcome?.forceTransactionsOnly;
  200. const trendView = props.trendView.clone();
  201. const chartTitle = getChartTitle(trendChangeType);
  202. modifyTrendView(trendView, location, trendChangeType, projects, canUseMetricsTrends);
  203. const onCursor = makeTrendsCursorHandler(trendChangeType);
  204. const cursor = decodeScalar(location.query[trendCursorNames[trendChangeType]]);
  205. const paginationAnalyticsEvent = (direction: string) => {
  206. trackAnalytics('performance_views.trends.widget_pagination', {
  207. organization,
  208. direction,
  209. widget_type: getChartTitle(trendChangeType),
  210. });
  211. };
  212. return (
  213. <TrendsDiscoverQuery
  214. eventView={trendView}
  215. orgSlug={organization.slug}
  216. location={location}
  217. trendChangeType={trendChangeType}
  218. cursor={cursor}
  219. limit={5}
  220. setError={error => setError(error?.message)}
  221. withBreakpoint={canUseMetricsTrends}
  222. >
  223. {({isLoading, trendsData, pageLinks}) => {
  224. const trendFunction = getCurrentTrendFunction(location);
  225. const trendParameter = getCurrentTrendParameter(
  226. location,
  227. projects,
  228. trendView.project
  229. );
  230. const events = normalizeTrends(
  231. (trendsData && trendsData.events && trendsData.events.data) || []
  232. );
  233. const selectedTransaction = getSelectedTransaction(
  234. location,
  235. trendChangeType,
  236. events
  237. );
  238. const statsData = trendsData?.stats || {};
  239. const transactionsList = events && 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(() => {
  578. onTransactionSelection(transaction.transaction);
  579. trackAnalytics('performance_views.performance_change_explorer.open', {
  580. organization,
  581. transaction: transaction.transaction,
  582. });
  583. }, [onTransactionSelection, transaction.transaction, organization]);
  584. if (organization.features.includes('performance-change-explorer')) {
  585. return (
  586. <ItemTransactionName
  587. to=""
  588. data-test-id="item-transaction-name"
  589. onClick={handleClick}
  590. >
  591. {transaction.transaction}
  592. </ItemTransactionName>
  593. );
  594. }
  595. return (
  596. <ItemTransactionName to={target} data-test-id="item-transaction-name">
  597. {transaction.transaction}
  598. </ItemTransactionName>
  599. );
  600. }
  601. const TransactionsListContainer = styled('div')`
  602. display: flex;
  603. flex-direction: column;
  604. `;
  605. const TrendsTransactionPanel = styled(Panel)`
  606. margin: 0;
  607. flex-grow: 1;
  608. `;
  609. const ChartContainer = styled('div')`
  610. padding: ${space(3)};
  611. `;
  612. const StyledHeaderTitleLegend = styled(HeaderTitleLegend)`
  613. border-radius: ${p => p.theme.borderRadius};
  614. margin: ${space(2)} ${space(3)};
  615. `;
  616. const StyledButton = styled(Button)`
  617. vertical-align: middle;
  618. `;
  619. const MenuAction = styled('div')<{['data-test-id']?: string}>`
  620. white-space: nowrap;
  621. color: ${p => p.theme.textColor};
  622. `;
  623. MenuAction.defaultProps = {
  624. 'data-test-id': 'menu-action',
  625. };
  626. const StyledEmptyStateWarning = styled(EmptyStateWarning)`
  627. min-height: 300px;
  628. justify-content: center;
  629. `;
  630. const ListItemContainer = styled('div')`
  631. display: grid;
  632. grid-template-columns: 24px auto 100px 30px;
  633. grid-template-rows: repeat(2, auto);
  634. grid-column-gap: ${space(1)};
  635. border-top: 1px solid ${p => p.theme.border};
  636. padding: ${space(1)} ${space(2)};
  637. `;
  638. const ItemRadioContainer = styled('div')`
  639. grid-row: 1/3;
  640. input {
  641. cursor: pointer;
  642. }
  643. input:checked::after {
  644. background-color: ${p => p.color};
  645. }
  646. `;
  647. const ItemTransactionName = styled(Link)`
  648. font-size: ${p => p.theme.fontSizeMedium};
  649. margin-right: ${space(1)};
  650. ${p => p.theme.overflowEllipsis};
  651. `;
  652. const ItemTransactionDurationChange = styled('div')`
  653. display: flex;
  654. align-items: center;
  655. font-size: ${p => p.theme.fontSizeSmall};
  656. `;
  657. const DurationChange = styled('span')`
  658. color: ${p => p.theme.gray300};
  659. margin: 0 ${space(1)};
  660. `;
  661. const ItemTransactionPercentage = styled('div')`
  662. text-align: right;
  663. font-size: ${p => p.theme.fontSizeMedium};
  664. `;
  665. const ItemTransactionStatus = styled('div')`
  666. color: ${p => p.color};
  667. text-align: right;
  668. font-size: ${p => p.theme.fontSizeSmall};
  669. `;
  670. const TooltipContent = styled('div')`
  671. display: flex;
  672. flex-direction: column;
  673. align-items: center;
  674. `;
  675. const StyledIconArrow = styled(IconArrow)`
  676. margin: 0 ${space(1)};
  677. `;
  678. export default withProjects(withOrganization(ChangedTransactions));