import {Fragment} from 'react'; import styled from '@emotion/styled'; import {addErrorMessage} from 'sentry/actionCreators/indicator'; import type {ModalRenderProps} from 'sentry/actionCreators/modal'; import {Alert} from 'sentry/components/alert'; import {Button} from 'sentry/components/button'; import DeprecatedAsyncComponent from 'sentry/components/deprecatedAsyncComponent'; import SelectField from 'sentry/components/forms/fields/selectField'; import Form from 'sentry/components/forms/form'; import Link from 'sentry/components/links/link'; import LoadingIndicator from 'sentry/components/loadingIndicator'; import Panel from 'sentry/components/panels/panel'; import PanelBody from 'sentry/components/panels/panelBody'; import {IconCheckmark, IconNot} from 'sentry/icons'; import {t, tct} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import type { CodeOwner, CodeownersFile, Integration, Organization, Project, RepositoryProjectPathConfig, } from 'sentry/types'; import {getIntegrationIcon} from 'sentry/utils/integrationUtil'; type Props = { organization: Organization; project: Project; onSave?: (data: CodeOwner) => void; } & ModalRenderProps & DeprecatedAsyncComponent['props']; type State = { codeMappingId: string | null; codeMappings: RepositoryProjectPathConfig[]; codeownersFile: CodeownersFile | null; error: boolean; errorJSON: {raw?: string} | null; integrations: Integration[]; isLoading: boolean; } & DeprecatedAsyncComponent['state']; class AddCodeOwnerModal extends DeprecatedAsyncComponent { getDefaultState() { return { ...super.getDefaultState(), codeownersFile: null, codeMappingId: null, isLoading: false, error: false, errorJSON: null, }; } getEndpoints(): ReturnType { const {organization, project} = this.props; const endpoints: ReturnType = [ [ 'codeMappings', `/organizations/${organization.slug}/code-mappings/`, {query: {project: project.id}}, ], [ 'integrations', `/organizations/${organization.slug}/integrations/`, {query: {features: ['codeowners']}}, ], ]; return endpoints; } fetchFile = async (codeMappingId: string) => { const {organization} = this.props; this.setState({ codeMappingId, codeownersFile: null, error: false, errorJSON: null, isLoading: true, }); try { const data: CodeownersFile = await this.api.requestPromise( `/organizations/${organization.slug}/code-mappings/${codeMappingId}/codeowners/`, { method: 'GET', } ); this.setState({codeownersFile: data, isLoading: false}); } catch (_err) { this.setState({isLoading: false}); } }; addFile = async () => { const {organization, project} = this.props; const {codeownersFile, codeMappingId, codeMappings} = this.state; if (codeownersFile) { const postData: { codeMappingId: string | null; raw: string; } = { codeMappingId, raw: codeownersFile.raw, }; try { const data = await this.api.requestPromise( `/projects/${organization.slug}/${project.slug}/codeowners/`, { method: 'POST', data: postData, } ); const codeMapping = codeMappings.find( mapping => mapping.id === codeMappingId?.toString() ); this.handleAddedFile({...data, codeMapping}); } catch (err) { if (err.responseJSON.raw) { this.setState({error: true, errorJSON: err.responseJSON, isLoading: false}); } else { addErrorMessage(Object.values(err.responseJSON).flat().join(' ')); } } } }; handleAddedFile(data: CodeOwner) { this.props.onSave?.(data); this.props.closeModal(); } sourceFile(codeownersFile: CodeownersFile) { return ( {codeownersFile.filepath} ); } errorMessage(baseUrl) { const {errorJSON, codeMappingId, codeMappings} = this.state; const codeMapping = codeMappings.find(mapping => mapping.id === codeMappingId); const {integrationId, provider} = codeMapping as RepositoryProjectPathConfig; const errActors = errorJSON?.raw?.[0].split('\n').map((el, i) =>

{el}

); return ( {errActors} {codeMapping && (

{tct( 'Configure [userMappingsLink:User Mappings] or [teamMappingsLink:Team Mappings] for any missing associations.', { userMappingsLink: ( ), teamMappingsLink: ( ), } )}

)} {tct( '[addAndSkip:Add and Skip Missing Associations] will add your codeowner file and skip any rules that having missing associations. You can add associations later for any skipped rules.', {addAndSkip: Add and Skip Missing Associations} )}
); } noSourceFile() { const {codeMappingId, isLoading} = this.state; if (isLoading) { return ( ); } if (!codeMappingId) { return null; } return ( {codeMappingId ? ( {t('No codeowner file found.')} ) : null} ); } renderBody() { const {Header, Body, Footer} = this.props; const {codeownersFile, error, errorJSON, codeMappings, integrations} = this.state; const {organization} = this.props; const baseUrl = `/settings/${organization.slug}/integrations`; return (
{t('Add Code Owner File')}
{!codeMappings.length ? ( !integrations.length ? (
{t('Install a GitHub or GitLab integration to use this feature.')}
) : (
{t( "Configure code mapping to add your CODEOWNERS file. Select the integration you'd like to use for mapping:" )}
{integrations.map(integration => ( ))}
) ) : null} {codeMappings.length > 0 && (
({ value: cm.id, label: cm.repoName, }))} onChange={this.fetchFile} required inline={false} flexibleControlStateSize stacked /> {codeownersFile ? this.sourceFile(codeownersFile) : this.noSourceFile()} {error && errorJSON && this.errorMessage(baseUrl)} )}
); } } export default AddCodeOwnerModal; export {AddCodeOwnerModal}; const StyledSelectField = styled(SelectField)` border-bottom: None; padding-right: 16px; `; const FileResult = styled('div')` width: inherit; `; const NoSourceFileBody = styled(PanelBody)` display: grid; padding: 12px; grid-template-columns: 30px 1fr; align-items: center; `; const SourceFileBody = styled(PanelBody)` display: grid; padding: 12px; grid-template-columns: 30px 1fr 100px; align-items: center; `; const IntegrationsList = styled('div')` display: grid; gap: ${space(1)}; justify-items: center; margin-top: ${space(2)}; `; const IntegrationName = styled('p')` padding-left: 10px; `; const Container = styled('div')` display: flex; justify-content: center; `;