Files
2024-02-15 17:01:50 +00:00

212 lines
7.1 KiB
TypeScript

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<Props, State> {
getDefaultState() {
return {
...super.getDefaultState(),
teams: [],
initialResults: [],
queryResults: {},
};
}
getEndpoints(): ReturnType<DeprecatedAsyncComponent['getEndpoints']> {
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<ExternalActorMapping[]>((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}) => (
<Fragment>
<Header closeButton>{t('Configure External Team Mapping')}</Header>
<Body>
<IntegrationExternalMappingForm
type="team"
integration={integration}
dataEndpoint={this.dataEndpoint}
getBaseFormEndpoint={map => this.getBaseFormEndpoint(map)}
defaultOptions={this.defaultTeamOptions}
mapping={mapping}
mappingKey={this.modalMappingKey}
sentryNamesMapper={this.sentryNamesMapper}
onCancel={closeModal}
onResults={this.handleResults}
onSubmitSuccess={() => {
this.handleSubmitSuccess();
closeModal();
}}
/>
</Body>
</Fragment>
));
};
renderBody() {
const {integration, organization} = this.props;
const {teamsPageLinks} = this.state;
return (
<IntegrationExternalMappings
type="team"
integration={integration}
organization={organization}
mappings={this.mappings}
dataEndpoint={this.dataEndpoint}
getBaseFormEndpoint={mapping => 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));