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 type {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)}
))}
}
>
{errorCount === 1
? `There was ${errorCount} ownership issue within Sentry on the latest sync with the CODEOWNERS file`
: `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;
`;