import {Fragment} from 'react'; import {RouteComponentProps} from 'react-router'; import styled from '@emotion/styled'; import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator'; import {Client} from 'sentry/api'; import Access from 'sentry/components/acl/access'; import Alert from 'sentry/components/alert'; import Button from 'sentry/components/button'; import Confirm from 'sentry/components/confirm'; import Form from 'sentry/components/forms/form'; import JsonForm from 'sentry/components/forms/jsonForm'; import List from 'sentry/components/list'; import ListItem from 'sentry/components/list/listItem'; import NavTabs from 'sentry/components/navTabs'; import {IconAdd, IconArrow} from 'sentry/icons'; import {t} from 'sentry/locale'; import { IntegrationProvider, IntegrationWithConfig, Organization, PluginWithProjectList, } from 'sentry/types'; import {trackIntegrationAnalytics} from 'sentry/utils/integrationUtil'; import {singleLineRenderer} from 'sentry/utils/marked'; import withApi from 'sentry/utils/withApi'; import withOrganization from 'sentry/utils/withOrganization'; import AsyncView from 'sentry/views/asyncView'; import AddIntegration from 'sentry/views/organizationIntegrations/addIntegration'; import IntegrationAlertRules from 'sentry/views/organizationIntegrations/integrationAlertRules'; import IntegrationCodeMappings from 'sentry/views/organizationIntegrations/integrationCodeMappings'; import IntegrationExternalTeamMappings from 'sentry/views/organizationIntegrations/integrationExternalTeamMappings'; import IntegrationExternalUserMappings from 'sentry/views/organizationIntegrations/integrationExternalUserMappings'; import IntegrationItem from 'sentry/views/organizationIntegrations/integrationItem'; import IntegrationMainSettings from 'sentry/views/organizationIntegrations/integrationMainSettings'; import IntegrationRepos from 'sentry/views/organizationIntegrations/integrationRepos'; import IntegrationServerlessFunctions from 'sentry/views/organizationIntegrations/integrationServerlessFunctions'; import BreadcrumbTitle from 'sentry/views/settings/components/settingsBreadcrumb/breadcrumbTitle'; import SettingsPageHeader from 'sentry/views/settings/components/settingsPageHeader'; type RouteParams = { integrationId: string; orgId: string; providerKey: string; }; type Props = RouteComponentProps & { api: Client; organization: Organization; }; type Tab = 'repos' | 'codeMappings' | 'userMappings' | 'teamMappings' | 'settings'; type State = AsyncView['state'] & { config: {providers: IntegrationProvider[]}; integration: IntegrationWithConfig; plugins: PluginWithProjectList[] | null; tab?: Tab; }; class ConfigureIntegration extends AsyncView { getEndpoints(): ReturnType { const {orgId, integrationId} = this.props.params; return [ ['config', `/organizations/${orgId}/config/integrations/`], ['integration', `/organizations/${orgId}/integrations/${integrationId}/`], ['plugins', `/organizations/${orgId}/plugins/configs/`], ]; } componentDidMount() { const { location, router, organization, params: {orgId, providerKey}, } = this.props; // This page should not be accessible by members (unless its github or gitlab) const allowMemberConfiguration = ['github', 'gitlab'].includes(providerKey); if (!allowMemberConfiguration && !organization.access.includes('org:integrations')) { router.push({ pathname: `/settings/${orgId}/integrations/${providerKey}/`, }); } const value = (['codeMappings', 'userMappings', 'teamMappings'] as const).find( tab => tab === location.query.tab ) || 'repos'; // eslint-disable-next-line react/no-did-mount-set-state this.setState({tab: value}); } onRequestSuccess({stateKey, data}) { if (stateKey !== 'integration') { return; } trackIntegrationAnalytics('integrations.details_viewed', { integration: data.provider.key, integration_type: 'first_party', organization: this.props.organization, }); } getTitle() { return this.state.integration ? this.state.integration.provider.name : 'Configure Integration'; } hasStacktraceLinking(provider: IntegrationProvider) { // CodeOwners will only work if the provider has StackTrace Linking return ( provider.features.includes('stacktrace-link') && this.props.organization.features.includes('integrations-stacktrace-link') ); } hasCodeOwners() { return this.props.organization.features.includes('integrations-codeowners'); } isCustomIntegration() { const {integration} = this.state; const {organization} = this.props; return ( organization.features.includes('integrations-custom-scm') && integration.provider.key === 'custom_scm' ); } onTabChange = (value: Tab) => { this.setState({tab: value}); }; get tab() { return this.state.tab || 'repos'; } onUpdateIntegration = () => { this.setState(this.getDefaultState(), this.fetchData); }; handleJiraMigration = async () => { try { const { params: {orgId, integrationId}, } = this.props; await this.api.requestPromise( `/organizations/${orgId}/integrations/${integrationId}/issues/`, { method: 'PUT', data: {}, } ); this.setState( { plugins: (this.state.plugins || []).filter(({id}) => id === 'jira'), }, () => addSuccessMessage(t('Migration in progress.')) ); } catch (error) { addErrorMessage(t('Something went wrong! Please try again.')); } }; getAction = (provider: IntegrationProvider | undefined) => { const {integration, plugins} = this.state; const shouldMigrateJiraPlugin = provider && ['jira', 'jira_server'].includes(provider.key) && (plugins || []).find(({id}) => id === 'jira'); const action = provider && provider.key === 'pagerduty' ? ( {onClick => ( )} ) : shouldMigrateJiraPlugin ? ( {({hasAccess}) => ( (

{t( 'This will automatically associate all the Linked Issues of your Jira Plugins to this integration.' )}

{t( 'If the Jira Plugins had the option checked to automatically create a Jira ticket for every new Sentry issue checked, you will need to create alert rules to recreate this behavior. Jira Server does not have this feature.' )}

{t( 'Once the migration is complete, your Jira Plugins will be disabled.' )}

)} onConfirm={() => { this.handleJiraMigration(); }} >
)}
) : null; return action; }; // TODO(Steve): Refactor components into separate tabs and use more generic tab logic renderMainTab(provider: IntegrationProvider) { const {orgId} = this.props.params; const {integration} = this.state; const instructions = integration.dynamicDisplayInformation?.configure_integration?.instructions; return ( {integration.configOrganization.length > 0 && (
)} {instructions && instructions.length > 0 && ( {instructions?.length === 1 ? ( ) : ( }> {instructions?.map((instruction, i) => ( )) ?? []} )} )} {provider.features.includes('alert-rule') && } {provider.features.includes('commits') && ( )} {provider.features.includes('serverless') && ( )}
); } renderBody() { const {integration} = this.state; const provider = this.state.config.providers.find( p => p.key === integration.provider.key ); if (!provider) { return null; } const title = ; const header = ( ); return ( {header} {this.renderMainContent(provider)} ); } // renders everything below header renderMainContent(provider: IntegrationProvider) { // if no code mappings, render the single tab if (!this.hasStacktraceLinking(provider)) { return this.renderMainTab(provider); } // otherwise render the tab view const tabs = [ ['repos', t('Repositories')], ['codeMappings', t('Code Mappings')], ...(this.hasCodeOwners() ? [['userMappings', t('User Mappings')]] : []), ...(this.hasCodeOwners() ? [['teamMappings', t('Team Mappings')]] : []), ] as [id: Tab, label: string][]; if (this.isCustomIntegration()) { tabs.unshift(['settings', t('Settings')]); } return ( {tabs.map(tabTuple => (
  • this.onTabChange(tabTuple[0])} > {tabTuple[1]}
  • ))}
    {this.renderTabContent(this.tab, provider)}
    ); } renderTabContent(tab: Tab, provider: IntegrationProvider) { const {integration} = this.state; const {organization} = this.props; switch (tab) { case 'codeMappings': return ; case 'repos': return this.renderMainTab(provider); case 'userMappings': return ; case 'teamMappings': return ; case 'settings': return ( ); default: return this.renderMainTab(provider); } } } export default withOrganization(withApi(ConfigureIntegration)); const CapitalizedLink = styled('a')` text-transform: capitalize; `;