import {Fragment} from 'react'; import type {WithRouterProps} from 'react-router'; import uniqBy from 'lodash/uniqBy'; import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator'; import {openModal} from 'sentry/actionCreators/modal'; import DeprecatedAsyncComponent from 'sentry/components/deprecatedAsyncComponent'; import {t} from 'sentry/locale'; import type { ExternalActorMapping, ExternalActorMappingOrSuggestion, Integration, Organization, Team, } from 'sentry/types'; import {sentryNameToOption} from 'sentry/utils/integrationUtil'; import withOrganization from 'sentry/utils/withOrganization'; // eslint-disable-next-line no-restricted-imports import withSentryRouter from 'sentry/utils/withSentryRouter'; import IntegrationExternalMappingForm from './integrationExternalMappingForm'; import IntegrationExternalMappings from './integrationExternalMappings'; type Props = DeprecatedAsyncComponent['props'] & WithRouterProps & { integration: Integration; organization: Organization; }; type State = DeprecatedAsyncComponent['state'] & { initialResults: Team[]; queryResults: { // For inline forms, the mappingKey will be the external name (since multiple will be rendered at one time) // For the modal form, the mappingKey will be this.modalMappingKey (since only one modal form is rendered at any time) [mappingKey: string]: Team[]; }; teams: Team[]; }; class IntegrationExternalTeamMappings extends DeprecatedAsyncComponent { getDefaultState() { return { ...super.getDefaultState(), teams: [], initialResults: [], queryResults: {}, }; } getEndpoints(): ReturnType { const {organization, location} = this.props; return [ // We paginate on this query, since we're filtering by hasExternalTeams:true [ 'teams', `/organizations/${organization.slug}/teams/`, {query: {...location?.query, query: 'hasExternalTeams:true'}}, ], // We use this query as defaultOptions to reduce identical API calls ['initialResults', `/organizations/${organization.slug}/teams/`], ]; } handleDelete = async (mapping: ExternalActorMapping) => { try { const {organization} = this.props; const {teams} = this.state; const team = teams.find(item => item.id === mapping.teamId); if (!team) { throw new Error('Cannot find correct team slug.'); } const endpoint = `/teams/${organization.slug}/${team.slug}/external-teams/${mapping.id}/`; await this.api.requestPromise(endpoint, { method: 'DELETE', }); // remove config and update state addSuccessMessage(t('Deletion successful')); this.fetchData(); } catch { // no 4xx errors should happen on delete addErrorMessage(t('An error occurred')); } }; handleSubmitSuccess = () => { this.fetchData(); }; get mappings() { const {integration} = this.props; const {teams} = this.state; const externalTeamMappings = teams.reduce((acc, team) => { const {externalTeams} = team; acc.push( ...externalTeams .filter(externalTeam => externalTeam.provider === integration.provider.key) .map(externalTeam => ({...externalTeam, sentryName: team.slug})) ); return acc; }, []); return externalTeamMappings.sort((a, b) => parseInt(a.id, 10) - parseInt(b.id, 10)); } modalMappingKey = '__MODAL_RESULTS__'; get dataEndpoint() { const {organization} = this.props; return `/organizations/${organization.slug}/teams/`; } get defaultTeamOptions() { const {initialResults} = this.state; return this.sentryNamesMapper(initialResults).map(sentryNameToOption); } getBaseFormEndpoint(mapping?: ExternalActorMappingOrSuggestion) { if (!mapping) { return ''; } const {organization} = this.props; const {queryResults, initialResults} = this.state; const fieldResults = queryResults[mapping.externalName] ?? queryResults[this.modalMappingKey]; const team = // First, search for the team in the query results... fieldResults?.find(item => item.id === mapping.teamId) ?? // Then in the initial results, if nothing was found. initialResults?.find(item => item.id === mapping.teamId); return `/teams/${organization.slug}/${team?.slug ?? ''}/external-teams/`; } sentryNamesMapper(teams: Team[]) { return teams.map(({id, slug}) => ({id, name: slug})); } /** * This method combines the results from searches made on a form dropping repeated entries * that have identical 'id's. This is because we need the result of the the search query when * the user submits to get the team slug, but it won't always be the last query they've made. * * If they search (but not select) after making a selection, and we didn't keep a running collection of results, * we wouldn't have the team to generate the endpoint from. */ combineResultsById = (resultList1, resultList2) => { return uniqBy([...resultList1, ...resultList2], 'id'); }; handleResults = (results, mappingKey?: string) => { if (mappingKey) { const {queryResults} = this.state; this.setState({ queryResults: { ...queryResults, // Ensure we always have a team to pull the slug from [mappingKey]: this.combineResultsById(results, queryResults[mappingKey] ?? []), }, }); } }; openModal = (mapping?: ExternalActorMappingOrSuggestion) => { const {integration} = this.props; openModal(({Body, Header, closeModal}) => (
{t('Configure External Team Mapping')}
this.getBaseFormEndpoint(map)} defaultOptions={this.defaultTeamOptions} mapping={mapping} mappingKey={this.modalMappingKey} sentryNamesMapper={this.sentryNamesMapper} onCancel={closeModal} onResults={this.handleResults} onSubmitSuccess={() => { this.handleSubmitSuccess(); closeModal(); }} />
)); }; renderBody() { const {integration, organization} = this.props; const {teamsPageLinks} = this.state; return ( this.getBaseFormEndpoint(mapping)} defaultOptions={this.defaultTeamOptions} sentryNamesMapper={this.sentryNamesMapper} onCreate={this.openModal} onDelete={this.handleDelete} pageLinks={teamsPageLinks} onResults={this.handleResults} /> ); } } export default withSentryRouter(withOrganization(IntegrationExternalTeamMappings));