changedTransactions.tsx 22 KB

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