replaySearchAlert.tsx 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
  1. import {useMemo} from 'react';
  2. import styled from '@emotion/styled';
  3. import Alert from 'sentry/components/alert';
  4. import {Button} from 'sentry/components/button';
  5. import {IconClose, IconInfo} from 'sentry/icons';
  6. import {t, tct} from 'sentry/locale';
  7. import {space} from 'sentry/styles/space';
  8. import type {Project, ProjectSdkUpdates} from 'sentry/types';
  9. import {decodeScalar} from 'sentry/utils/queryString';
  10. import {MutableSearch} from 'sentry/utils/tokenizeSearch';
  11. import useDismissAlert from 'sentry/utils/useDismissAlert';
  12. import {useLocation} from 'sentry/utils/useLocation';
  13. import useOrganization from 'sentry/utils/useOrganization';
  14. import usePageFilters from 'sentry/utils/usePageFilters';
  15. import useProjects from 'sentry/utils/useProjects';
  16. import {useProjectSdkUpdates} from 'sentry/utils/useProjectSdkUpdates';
  17. import {semverCompare} from 'sentry/utils/versions';
  18. const MIN_REPLAY_CLICK_SDK = '7.44.0';
  19. const LOCAL_STORAGE_KEY = 'replay-search-min-sdk-alert-dismissed';
  20. // exported for testing
  21. export function ReplaySearchAlert() {
  22. const {selection} = usePageFilters();
  23. const projects = useProjects();
  24. const location = useLocation();
  25. const organization = useOrganization();
  26. const sdkUpdates = useProjectSdkUpdates({
  27. organization,
  28. projectId: null,
  29. });
  30. const {dismiss: handleDismiss, isDismissed} = useDismissAlert({
  31. key: LOCAL_STORAGE_KEY,
  32. });
  33. const conditions = useMemo(() => {
  34. return new MutableSearch(decodeScalar(location.query.query, ''));
  35. }, [location.query.query]);
  36. const hasReplayClick = conditions.getFilterKeys().some(k => k.startsWith('click.'));
  37. if (sdkUpdates.type !== 'resolved') {
  38. return null;
  39. }
  40. const selectedProjectsWithSdkUpdates = sdkUpdates.data?.reduce((acc, sdkUpdate) => {
  41. if (!selection.projects.includes(Number(sdkUpdate.projectId))) {
  42. return acc;
  43. }
  44. const project = projects.projects.find(p => p.id === sdkUpdate.projectId);
  45. // should never really happen but making ts happy
  46. if (!project) {
  47. return acc;
  48. }
  49. acc.push({
  50. project,
  51. sdkUpdate,
  52. });
  53. return acc;
  54. }, [] as Array<{project: Project; sdkUpdate: ProjectSdkUpdates}>);
  55. const doesNotMeetMinSDK =
  56. selectedProjectsWithSdkUpdates &&
  57. selectedProjectsWithSdkUpdates.length > 0 &&
  58. selectedProjectsWithSdkUpdates.every(({sdkUpdate}) => {
  59. return semverCompare(sdkUpdate.sdkVersion, MIN_REPLAY_CLICK_SDK) === -1;
  60. });
  61. if (!doesNotMeetMinSDK) {
  62. return null;
  63. }
  64. if (hasReplayClick) {
  65. return (
  66. <Alert data-test-id="min-sdk-alert">
  67. <AlertContent>
  68. <IconInfo />
  69. <AlertText>
  70. {tct(
  71. 'Search field [click] requires a minimum SDK version of >= [minSdkVersion].',
  72. {
  73. click: <strong>'click'</strong>,
  74. minSdkVersion: <strong>{MIN_REPLAY_CLICK_SDK}</strong>,
  75. }
  76. )}
  77. </AlertText>
  78. </AlertContent>
  79. </Alert>
  80. );
  81. }
  82. if (isDismissed) {
  83. return null;
  84. }
  85. return (
  86. <Alert data-test-id="min-sdk-alert">
  87. <AlertContent>
  88. <IconInfo />
  89. <AlertText>
  90. {tct(
  91. 'Search for dom elements clicked during a replay by using our new search key [click]. Sadly, it requires an SDK version >= [version]',
  92. {
  93. click: <strong>{`'click'`}</strong>,
  94. version: <strong>{MIN_REPLAY_CLICK_SDK}</strong>,
  95. }
  96. )}
  97. </AlertText>
  98. <Button
  99. priority="link"
  100. size="sm"
  101. icon={<IconClose size="xs" />}
  102. aria-label={t('Close Alert')}
  103. onClick={handleDismiss}
  104. />
  105. </AlertContent>
  106. </Alert>
  107. );
  108. }
  109. const AlertContent = styled('div')`
  110. display: flex;
  111. justify-content: space-between;
  112. gap: ${space(1)};
  113. `;
  114. const AlertText = styled('div')`
  115. flex-grow: 1;
  116. `;