codeownerErrors.tsx 6.1 KB

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