projectSidebarSection.tsx 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  1. import {Fragment} from 'react';
  2. import styled from '@emotion/styled';
  3. import {motion, Variants} from 'framer-motion';
  4. import {PlatformIcon} from 'platformicons';
  5. import {PlatformKey} from 'sentry/data/platformCategories';
  6. import platforms from 'sentry/data/platforms';
  7. import {IconCheckmark} from 'sentry/icons';
  8. import {t} from 'sentry/locale';
  9. import pulsingIndicatorStyles from 'sentry/styles/pulsingIndicator';
  10. import {space} from 'sentry/styles/space';
  11. import {Project} from 'sentry/types';
  12. import testableTransition from 'sentry/utils/testableTransition';
  13. type Props = {
  14. activeProject: Project | null;
  15. checkProjectHasFirstEvent: (project: Project) => boolean;
  16. projects: Project[];
  17. selectProject: (newProjectId: string) => void;
  18. // A map from selected platform keys to the projects created by onboarding.
  19. selectedPlatformToProjectIdMap: {[key in PlatformKey]?: string};
  20. };
  21. function ProjectSidebarSection({
  22. projects,
  23. activeProject,
  24. selectProject,
  25. checkProjectHasFirstEvent,
  26. selectedPlatformToProjectIdMap,
  27. }: Props) {
  28. const oneProject = (platformOnCreate: string, projectSlug: string) => {
  29. const project = projects.find(p => p.slug === projectSlug);
  30. const platform = project ? project.platform || 'other' : platformOnCreate;
  31. const platformName = platforms.find(p => p.id === platform)?.name ?? '';
  32. const isActive = !!project && activeProject?.id === project.id;
  33. const errorReceived = !!project && checkProjectHasFirstEvent(project);
  34. return (
  35. <ProjectWrapper
  36. key={projectSlug}
  37. isActive={isActive}
  38. onClick={() => project && selectProject(project.id)}
  39. disabled={!project}
  40. >
  41. <StyledPlatformIcon platform={platform} size={36} />
  42. <MiddleWrapper>
  43. <NameWrapper>{platformName}</NameWrapper>
  44. <SubHeader errorReceived={errorReceived} data-test-id="sidebar-error-indicator">
  45. {!project
  46. ? t('Project Deleted')
  47. : errorReceived
  48. ? t('Error Received')
  49. : t('Waiting for error')}
  50. </SubHeader>
  51. </MiddleWrapper>
  52. {errorReceived ? (
  53. <StyledIconCheckmark isCircled color="green400" />
  54. ) : (
  55. isActive && <WaitingIndicator />
  56. )}
  57. </ProjectWrapper>
  58. );
  59. };
  60. return (
  61. <Fragment>
  62. <Title>{t('Projects to Setup')}</Title>
  63. {Object.entries(selectedPlatformToProjectIdMap).map(
  64. ([platformOnCreate, projectSlug]) => {
  65. return oneProject(platformOnCreate, projectSlug);
  66. }
  67. )}
  68. </Fragment>
  69. );
  70. }
  71. export default ProjectSidebarSection;
  72. const Title = styled('span')`
  73. font-size: 12px;
  74. font-weight: 600;
  75. text-transform: uppercase;
  76. margin-left: ${space(2)};
  77. `;
  78. const SubHeader = styled('div')<{errorReceived: boolean}>`
  79. color: ${p => (p.errorReceived ? p.theme.successText : p.theme.pink400)};
  80. `;
  81. const StyledPlatformIcon = styled(PlatformIcon)``;
  82. const ProjectWrapper = styled('div')<{disabled: boolean; isActive: boolean}>`
  83. display: flex;
  84. flex-direction: row;
  85. align-items: center;
  86. background-color: ${p => p.isActive && p.theme.gray100};
  87. padding: ${space(2)};
  88. cursor: pointer;
  89. border-radius: 4px;
  90. user-select: none;
  91. ${p =>
  92. p.disabled &&
  93. `
  94. cursor: not-allowed;
  95. ${StyledPlatformIcon} {
  96. filter: grayscale(1);
  97. }
  98. ${SubHeader} {
  99. color: ${p.theme.gray400};
  100. }
  101. ${Beat} {
  102. color: ${p.theme.gray400};
  103. }
  104. ${NameWrapper} {
  105. text-decoration-line: line-through;
  106. }
  107. `}
  108. `;
  109. const indicatorAnimation: Variants = {
  110. initial: {opacity: 0, y: -10},
  111. animate: {opacity: 1, y: 0},
  112. exit: {opacity: 0, y: 10},
  113. };
  114. const WaitingIndicator = styled(motion.div)`
  115. margin: 0 6px;
  116. flex-shrink: 0;
  117. ${pulsingIndicatorStyles};
  118. background-color: ${p => p.theme.pink300};
  119. `;
  120. const StyledIconCheckmark = styled(IconCheckmark)`
  121. flex-shrink: 0;
  122. `;
  123. WaitingIndicator.defaultProps = {
  124. variants: indicatorAnimation,
  125. transition: testableTransition(),
  126. };
  127. const MiddleWrapper = styled('div')`
  128. margin: 0 ${space(1)};
  129. flex-grow: 1;
  130. overflow: hidden;
  131. `;
  132. const NameWrapper = styled('div')`
  133. overflow: hidden;
  134. white-space: nowrap;
  135. text-overflow: ellipsis;
  136. `;
  137. const Beat = styled('div')<{color: string}>`
  138. color: ${p => p.color};
  139. `;