import {Fragment, useState} from 'react'; import styled from '@emotion/styled'; import {Button} from 'sentry/components/button'; import Confirm from 'sentry/components/confirm'; import LoadingError from 'sentry/components/loadingError'; import LoadingIndicator from 'sentry/components/loadingIndicator'; 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, } from 'sentry/types/integrations'; import {isExternalActorMapping} from 'sentry/utils/integrationUtil'; import {useApiQuery} from 'sentry/utils/queryClient'; import {capitalize} from 'sentry/utils/string/capitalize'; import {useLocation} from 'sentry/utils/useLocation'; import useOrganization from 'sentry/utils/useOrganization'; import IntegrationExternalMappingForm from './integrationExternalMappingForm'; type CodeOwnersAssociationMappings = { [projectSlug: string]: { associations: { [externalName: string]: string; }; errors: { [errorKey: string]: string; }; }; }; type Props = Pick< IntegrationExternalMappingForm['props'], | 'dataEndpoint' | 'getBaseFormEndpoint' | 'sentryNamesMapper' | 'onResults' | 'defaultOptions' > & { integration: Integration; mappings: ExternalActorMapping[]; onCreate: (mapping?: ExternalActorMappingOrSuggestion) => void; onDelete: (mapping: ExternalActorMapping) => void; type: 'team' | 'user'; pageLinks?: string; }; type LocationQuery = { cursor?: string; }; function IntegrationExternalMappings(props: Props) { const { integration, type, mappings, pageLinks, dataEndpoint, defaultOptions, onCreate, onResults, onDelete, getBaseFormEndpoint, sentryNamesMapper, } = props; const [newlyAssociatedMappings, setNewlyAssociatedMappings] = useState< ExternalActorMapping[] >([]); const organization = useOrganization(); const location = useLocation(); const {cursor} = location.query; const isFirstPage = cursor ? cursor.split(':')[1] === '0' : true; const { data: associationMappings, isPending, isError, refetch, } = useApiQuery( [ `/organizations/${organization.slug}/codeowners-associations/`, {query: {provider: integration.provider.key}}, ], {staleTime: 0} ); if (isPending) { return ; } if (isError) { return ; } const unassociatedMappings = (): ExternalActorSuggestion[] => { const errorKey = `missing_external_${type}s`; const unassociatedMappingsSet = Object.values(associationMappings).reduce( (map, {errors}) => { return new Set([...map, ...errors[errorKey]!]); }, new Set() ); return Array.from(unassociatedMappingsSet).map(externalName => ({externalName})); }; const allMappings = (): ExternalActorMappingOrSuggestion[] => { if (!isFirstPage) { return mappings; } const inlineMappings = 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]; }; const renderMappingName = (mapping: ExternalActorMappingOrSuggestion) => { return ( { setNewlyAssociatedMappings([ ...newlyAssociatedMappings.filter( map => map.externalName !== newMapping.externalName ), newMapping, ]); }} isInline defaultOptions={defaultOptions} /> ); }; const renderMappingActions = (mapping: ExternalActorMappingOrSuggestion) => { 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)} >