import {Fragment, useMemo} from 'react'; import styled from '@emotion/styled'; import uniqBy from 'lodash/uniqBy'; import {Alert} from 'sentry/components/alert'; import ExternalLink from 'sentry/components/links/externalLink'; import {space} from 'sentry/styles/space'; import {CodeOwner, RepositoryProjectPathConfig} from 'sentry/types'; type CodeOwnerErrorKeys = keyof CodeOwner['errors']; function ErrorMessage({ message, values, link, linkValue, }: { link: string; linkValue: React.ReactNode; message: string; values: string[]; }) { return ( {message} {values.join(', ')} {linkValue} ); } function ErrorMessageList({ message, values, linkFunction, linkValueFunction, }: { linkFunction: (s: string) => string; linkValueFunction: (s: string) => string; message: string; values: string[]; }) { return ( {message} {values.map((value, index) => ( {value} {linkValueFunction(value)} ))} ); } interface CodeOwnerErrorsProps { codeowners: CodeOwner[]; orgSlug: string; projectSlug: string; } export function CodeOwnerErrors({ codeowners, orgSlug, projectSlug, }: CodeOwnerErrorsProps) { const filteredCodeowners = useMemo(() => { const owners = codeowners.filter(({errors}) => { // Remove codeowners files with no errors return Object.values(errors).some(values => values.length); }); // Uniq errors return uniqBy(owners, codeowner => JSON.stringify(codeowner.errors)); }, [codeowners]); const errMessage = ( codeMapping: RepositoryProjectPathConfig, type: CodeOwnerErrorKeys, values: string[] ) => { switch (type) { case 'missing_external_teams': return ( ); case 'missing_external_users': return ( ); case 'missing_user_emails': return ( ); case 'teams_without_access': return ( `/settings/${orgSlug}/teams/${value.slice(1)}/projects/` } linkValueFunction={value => `Configure ${value} Permissions`} /> ); case 'users_without_access': return ( `/settings/${orgSlug}/members/?query=${email}`} linkValueFunction={() => `Configure Member Settings`} /> ); default: return null; } }; return ( {filteredCodeowners.map(({id, codeMapping, errors}) => { const errorPairs = Object.entries(errors).filter( ([_, values]) => values.length ) as Array<[CodeOwnerErrorKeys, string[]]>; const errorCount = errorPairs.reduce( (acc, [_, values]) => acc + values.length, 0 ); return ( {errorPairs.map(([type, values]) => ( {errMessage(codeMapping!, type, values)} ))} } > {`There were ${errorCount} ownership issues within Sentry on the latest sync with the CODEOWNERS file`} ); })} ); } const AlertContentContainer = styled('div')` overflow-y: auto; max-height: 350px; `; const ErrorContainer = styled('div')` display: grid; grid-template-areas: 'message cta'; grid-template-columns: 2fr 1fr; gap: ${space(2)}; padding: ${space(1.5)} 0; `; const ErrorInlineContainer = styled(ErrorContainer)` gap: ${space(1.5)}; grid-template-columns: 1fr 2fr; align-items: center; padding: 0; `; const ErrorMessageContainer = styled('div')` grid-area: message; display: grid; gap: ${space(1.5)}; `; const ErrorMessageListContainer = styled('div')` grid-column: message / cta-end; gap: ${space(1.5)}; `; const ErrorCtaContainer = styled('div')` grid-area: cta; justify-self: flex-end; text-align: right; line-height: 1.5; `;