import {Fragment} from 'react'; import type {WithRouterProps} from 'react-router'; import styled from '@emotion/styled'; import {Button} from 'sentry/components/button'; import Confirm from 'sentry/components/confirm'; import DeprecatedAsyncComponent from 'sentry/components/deprecatedAsyncComponent'; import Pagination from 'sentry/components/pagination'; import {PanelTable} from 'sentry/components/panels/panelTable'; import QuestionTooltip from 'sentry/components/questionTooltip'; import {IconAdd, IconArrow, IconDelete} from 'sentry/icons'; import {t, tct} from 'sentry/locale'; import PluginIcon from 'sentry/plugins/components/pluginIcon'; import {space} from 'sentry/styles/space'; import type { ExternalActorMapping, ExternalActorMappingOrSuggestion, ExternalActorSuggestion, Integration, Organization, } from 'sentry/types'; import {isExternalActorMapping} from 'sentry/utils/integrationUtil'; import {capitalize} from 'sentry/utils/string/capitalize'; // eslint-disable-next-line no-restricted-imports import withSentryRouter from 'sentry/utils/withSentryRouter'; import IntegrationExternalMappingForm from './integrationExternalMappingForm'; type CodeOwnersAssociationMappings = { [projectSlug: string]: { associations: { [externalName: string]: string; }; errors: { [errorKey: string]: string; }; }; }; type Props = DeprecatedAsyncComponent['props'] & WithRouterProps & Pick< IntegrationExternalMappingForm['props'], | 'dataEndpoint' | 'getBaseFormEndpoint' | 'sentryNamesMapper' | 'onResults' | 'defaultOptions' > & { integration: Integration; mappings: ExternalActorMapping[]; onCreate: (mapping?: ExternalActorMappingOrSuggestion) => void; onDelete: (mapping: ExternalActorMapping) => void; organization: Organization; type: 'team' | 'user'; pageLinks?: string; }; type State = DeprecatedAsyncComponent['state'] & { associationMappings: CodeOwnersAssociationMappings; newlyAssociatedMappings: ExternalActorMapping[]; }; class IntegrationExternalMappings extends DeprecatedAsyncComponent { getDefaultState(): State { return { ...super.getDefaultState(), associationMappings: {}, newlyAssociatedMappings: [], }; } getEndpoints(): ReturnType { const {organization, integration} = this.props; return [ [ 'associationMappings', `/organizations/${organization.slug}/codeowners-associations/`, {query: {provider: integration.provider.key}}, ], ]; } get isFirstPage(): boolean { const {cursor} = this.props.location.query; return cursor ? cursor?.split(':')[1] === '0' : true; } get unassociatedMappings(): ExternalActorSuggestion[] { const {type} = this.props; const {associationMappings} = this.state; const errorKey = `missing_external_${type}s`; const unassociatedMappings = Object.values(associationMappings).reduce( (map, {errors}) => { return new Set([...map, ...errors[errorKey]]); }, new Set() ); return Array.from(unassociatedMappings).map(externalName => ({externalName})); } get allMappings(): ExternalActorMappingOrSuggestion[] { const {mappings} = this.props; if (!this.isFirstPage) { return mappings; } const {newlyAssociatedMappings} = this.state; const inlineMappings = this.unassociatedMappings.map(mapping => { // If this mapping has been changed, replace it with the new version from its change's response // The new version will be used in IntegrationExternalMappingForm to update the apiMethod and apiEndpoint const newlyAssociatedMapping = newlyAssociatedMappings.find( ({externalName}) => externalName === mapping.externalName ); return newlyAssociatedMapping ?? mapping; }); return [...inlineMappings, ...mappings]; } renderMappingName(mapping: ExternalActorMappingOrSuggestion) { const { type, getBaseFormEndpoint, integration, dataEndpoint, sentryNamesMapper, onResults, defaultOptions, } = this.props; return ( { this.setState({ newlyAssociatedMappings: [ ...this.state.newlyAssociatedMappings.filter( map => map.externalName !== newMapping.externalName ), newMapping as ExternalActorMapping, ], }); }} isInline defaultOptions={defaultOptions} /> ); } renderMappingActions(mapping: ExternalActorMappingOrSuggestion) { const {type, onDelete, organization} = this.props; const canDelete = organization.access.includes('org:integrations'); return isExternalActorMapping(mapping) ? ( onDelete(mapping)} message={t('Are you sure you want to remove this external %s mapping?', type)} >