codeownerErrors.tsx 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  1. import {Fragment} from 'react';
  2. import styled from '@emotion/styled';
  3. import {Alert} from 'sentry/components/alert';
  4. import ExternalLink from 'sentry/components/links/externalLink';
  5. import {space} from 'sentry/styles/space';
  6. import {CodeOwner, RepositoryProjectPathConfig} from 'sentry/types';
  7. type CodeOwnerErrorKeys = keyof CodeOwner['errors'];
  8. function ErrorMessage({
  9. message,
  10. values,
  11. link,
  12. linkValue,
  13. }: {
  14. link: string;
  15. linkValue: React.ReactNode;
  16. message: string;
  17. values: string[];
  18. }) {
  19. return (
  20. <Fragment>
  21. <ErrorMessageContainer>
  22. <span>{message}</span>
  23. <b>{values.join(', ')}</b>
  24. </ErrorMessageContainer>
  25. <ErrorCtaContainer>
  26. <ExternalLink href={link}>{linkValue}</ExternalLink>
  27. </ErrorCtaContainer>
  28. </Fragment>
  29. );
  30. }
  31. function ErrorMessageList({
  32. message,
  33. values,
  34. linkFunction,
  35. linkValueFunction,
  36. }: {
  37. linkFunction: (s: string) => string;
  38. linkValueFunction: (s: string) => string;
  39. message: string;
  40. values: string[];
  41. }) {
  42. return (
  43. <Fragment>
  44. <ErrorMessageContainer>
  45. <span>{message}</span>
  46. </ErrorMessageContainer>
  47. <ErrorMessageListContainer>
  48. {values.map((value, index) => (
  49. <ErrorInlineContainer key={index}>
  50. <b>{value}</b>
  51. <ErrorCtaContainer>
  52. <ExternalLink href={linkFunction(value)} key={index}>
  53. {linkValueFunction(value)}
  54. </ExternalLink>
  55. </ErrorCtaContainer>
  56. </ErrorInlineContainer>
  57. ))}
  58. </ErrorMessageListContainer>
  59. </Fragment>
  60. );
  61. }
  62. interface CodeOwnerErrorsProps {
  63. codeowners: CodeOwner[];
  64. orgSlug: string;
  65. projectSlug: string;
  66. }
  67. export function CodeOwnerErrors({
  68. codeowners,
  69. orgSlug,
  70. projectSlug,
  71. }: CodeOwnerErrorsProps) {
  72. const errMessage = (
  73. codeMapping: RepositoryProjectPathConfig,
  74. type: CodeOwnerErrorKeys,
  75. values: string[]
  76. ) => {
  77. switch (type) {
  78. case 'missing_external_teams':
  79. return (
  80. <ErrorMessage
  81. message="There’s a problem linking teams and members from an integration"
  82. values={values}
  83. link={`/settings/${orgSlug}/integrations/${codeMapping?.provider?.slug}/${codeMapping?.integrationId}/?tab=teamMappings`}
  84. linkValue="Configure Team Mappings"
  85. />
  86. );
  87. case 'missing_external_users':
  88. return (
  89. <ErrorMessage
  90. message={`The following usernames do not have an association in the organization: ${orgSlug}`}
  91. values={values}
  92. link={`/settings/${orgSlug}/integrations/${codeMapping?.provider?.slug}/${codeMapping?.integrationId}/?tab=userMappings`}
  93. linkValue="Configure User Mappings"
  94. />
  95. );
  96. case 'missing_user_emails':
  97. return (
  98. <ErrorMessage
  99. message={`The following emails do not have an Sentry user in the organization: ${orgSlug}`}
  100. values={values}
  101. link={`/settings/${orgSlug}/members/`}
  102. linkValue="Invite Users"
  103. />
  104. );
  105. case 'teams_without_access':
  106. return (
  107. <ErrorMessageList
  108. message={`The following team do not have access to the project: ${projectSlug}`}
  109. values={values}
  110. linkFunction={value =>
  111. `/settings/${orgSlug}/teams/${value.slice(1)}/projects/`
  112. }
  113. linkValueFunction={value => `Configure ${value} Permissions`}
  114. />
  115. );
  116. case 'users_without_access':
  117. return (
  118. <ErrorMessageList
  119. message={`The following users are not on a team that has access to the project: ${projectSlug}`}
  120. values={values}
  121. linkFunction={email => `/settings/${orgSlug}/members/?query=${email}`}
  122. linkValueFunction={() => `Configure Member Settings`}
  123. />
  124. );
  125. default:
  126. return null;
  127. }
  128. };
  129. return (
  130. <Fragment>
  131. {codeowners
  132. .filter(({errors}) => Object.values(errors).some(values => values.length))
  133. .map(({id, codeMapping, errors}) => {
  134. const errorPairs = Object.entries(errors).filter(
  135. ([_, values]) => values.length
  136. ) as Array<[CodeOwnerErrorKeys, string[]]>;
  137. const errorCount = errorPairs.reduce(
  138. (acc, [_, values]) => acc + values.length,
  139. 0
  140. );
  141. return (
  142. <Alert
  143. key={id}
  144. type="error"
  145. showIcon
  146. expand={
  147. <AlertContentContainer key="container">
  148. {errorPairs.map(([type, values]) => (
  149. <ErrorContainer key={`${id}-${type}`}>
  150. {errMessage(codeMapping!, type, values)}
  151. </ErrorContainer>
  152. ))}
  153. </AlertContentContainer>
  154. }
  155. >
  156. {`There were ${errorCount} ownership issues within Sentry on the latest sync with the CODEOWNERS file`}
  157. </Alert>
  158. );
  159. })}
  160. </Fragment>
  161. );
  162. }
  163. const AlertContentContainer = styled('div')`
  164. overflow-y: auto;
  165. max-height: 350px;
  166. `;
  167. const ErrorContainer = styled('div')`
  168. display: grid;
  169. grid-template-areas: 'message cta';
  170. grid-template-columns: 2fr 1fr;
  171. gap: ${space(2)};
  172. padding: ${space(1.5)} 0;
  173. `;
  174. const ErrorInlineContainer = styled(ErrorContainer)`
  175. gap: ${space(1.5)};
  176. grid-template-columns: 1fr 2fr;
  177. align-items: center;
  178. padding: 0;
  179. `;
  180. const ErrorMessageContainer = styled('div')`
  181. grid-area: message;
  182. display: grid;
  183. gap: ${space(1.5)};
  184. `;
  185. const ErrorMessageListContainer = styled('div')`
  186. grid-column: message / cta-end;
  187. gap: ${space(1.5)};
  188. `;
  189. const ErrorCtaContainer = styled('div')`
  190. grid-area: cta;
  191. justify-self: flex-end;
  192. text-align: right;
  193. line-height: 1.5;
  194. `;