123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252 |
- import styled from '@emotion/styled';
- import AlertLink from 'sentry/components/alertLink';
- import AsyncComponent from 'sentry/components/asyncComponent';
- import ErrorBoundary from 'sentry/components/errorBoundary';
- import ExternalIssueActions from 'sentry/components/group/externalIssueActions';
- import PluginActions from 'sentry/components/group/pluginActions';
- import SentryAppExternalIssueActions from 'sentry/components/group/sentryAppExternalIssueActions';
- import IssueSyncListElement from 'sentry/components/issueSyncListElement';
- import Placeholder from 'sentry/components/placeholder';
- import * as SidebarSection from 'sentry/components/sidebarSection';
- import {t} from 'sentry/locale';
- import ExternalIssueStore from 'sentry/stores/externalIssueStore';
- import SentryAppInstallationStore from 'sentry/stores/sentryAppInstallationsStore';
- import space from 'sentry/styles/space';
- import {
- Group,
- GroupIntegration,
- Organization,
- PlatformExternalIssue,
- Project,
- SentryAppComponent,
- SentryAppInstallation,
- } from 'sentry/types';
- import {Event} from 'sentry/types/event';
- import withOrganization from 'sentry/utils/withOrganization';
- import withSentryAppComponents from 'sentry/utils/withSentryAppComponents';
- type Props = AsyncComponent['props'] & {
- components: SentryAppComponent[];
- event: Event;
- group: Group;
- organization: Organization;
- project: Project;
- };
- type State = AsyncComponent['state'] & {
- externalIssues: PlatformExternalIssue[];
- integrations: GroupIntegration[];
- sentryAppInstallations: SentryAppInstallation[];
- };
- class ExternalIssueList extends AsyncComponent<Props, State> {
- unsubscribables: any[] = [];
- getEndpoints(): ReturnType<AsyncComponent['getEndpoints']> {
- const {group} = this.props;
- return [['integrations', `/groups/${group.id}/integrations/`]];
- }
- constructor(props: Props) {
- super(props, {});
- this.state = Object.assign({}, this.state, {
- sentryAppInstallations: SentryAppInstallationStore.getInitialState(),
- externalIssues: ExternalIssueStore.getInitialState(),
- });
- }
- UNSAFE_componentWillMount() {
- super.UNSAFE_componentWillMount();
- this.unsubscribables = [
- SentryAppInstallationStore.listen(this.onSentryAppInstallationChange, this),
- ExternalIssueStore.listen(this.onExternalIssueChange, this),
- ];
- this.fetchSentryAppData();
- }
- componentWillUnmount() {
- super.componentWillUnmount();
- this.unsubscribables.forEach(unsubscribe => unsubscribe());
- }
- onSentryAppInstallationChange = (sentryAppInstallations: SentryAppInstallation[]) => {
- this.setState({sentryAppInstallations});
- };
- onExternalIssueChange = (externalIssues: PlatformExternalIssue[]) => {
- this.setState({externalIssues});
- };
- // We want to do this explicitly so that we can handle errors gracefully,
- // instead of the entire component not rendering.
- //
- // Part of the API request here is fetching data from the Sentry App, so
- // we need to be more conservative about error cases since we don't have
- // control over those services.
- //
- fetchSentryAppData() {
- const {group, project, organization} = this.props;
- if (project && project.id && organization) {
- this.api
- .requestPromise(`/groups/${group.id}/external-issues/`)
- .then(data => {
- ExternalIssueStore.load(data);
- this.setState({externalIssues: data});
- })
- .catch(_error => {});
- }
- }
- async updateIntegrations(onSuccess = () => {}, onError = () => {}) {
- try {
- const {group} = this.props;
- const integrations = await this.api.requestPromise(
- `/groups/${group.id}/integrations/`
- );
- this.setState({integrations}, () => onSuccess());
- } catch (error) {
- onError();
- }
- }
- renderIntegrationIssues(integrations: GroupIntegration[] = []) {
- const {group} = this.props;
- const activeIntegrations = integrations.filter(
- integration => integration.status === 'active'
- );
- const activeIntegrationsByProvider: Map<string, GroupIntegration[]> =
- activeIntegrations.reduce((acc, curr) => {
- const items = acc.get(curr.provider.key);
- if (items) {
- acc.set(curr.provider.key, [...items, curr]);
- } else {
- acc.set(curr.provider.key, [curr]);
- }
- return acc;
- }, new Map());
- return activeIntegrations.length
- ? [...activeIntegrationsByProvider.entries()].map(([provider, configurations]) => (
- <ExternalIssueActions
- key={provider}
- configurations={configurations}
- group={group}
- onChange={this.updateIntegrations.bind(this)}
- />
- ))
- : null;
- }
- renderSentryAppIssues() {
- const {externalIssues, sentryAppInstallations} = this.state;
- const {components, group} = this.props;
- if (components.length === 0) {
- return null;
- }
- return components.map(component => {
- const {sentryApp, error: disabled} = component;
- const installation = sentryAppInstallations.find(
- i => i.app.uuid === sentryApp.uuid
- );
- // should always find a match but TS complains if we don't handle this case
- if (!installation) {
- return null;
- }
- const issue = (externalIssues || []).find(i => i.serviceType === sentryApp.slug);
- return (
- <ErrorBoundary key={sentryApp.slug} mini>
- <SentryAppExternalIssueActions
- key={sentryApp.slug}
- group={group}
- event={this.props.event}
- sentryAppComponent={component}
- sentryAppInstallation={installation}
- externalIssue={issue}
- disabled={disabled}
- />
- </ErrorBoundary>
- );
- });
- }
- renderPluginIssues() {
- const {group, project} = this.props;
- return group.pluginIssues && group.pluginIssues.length
- ? group.pluginIssues.map((plugin, i) => (
- <PluginActions group={group} project={project} plugin={plugin} key={i} />
- ))
- : null;
- }
- renderPluginActions() {
- const {group} = this.props;
- return group.pluginActions && group.pluginActions.length
- ? group.pluginActions.map((plugin, i) => (
- <IssueSyncListElement externalIssueLink={plugin[1]} key={i}>
- {plugin[0]}
- </IssueSyncListElement>
- ))
- : null;
- }
- renderLoading() {
- return (
- <SidebarSection.Wrap data-test-id="linked-issues">
- <SidebarSection.Title>{t('Linked Issues')}</SidebarSection.Title>
- <SidebarSection.Content>
- <Placeholder height="120px" />
- </SidebarSection.Content>
- </SidebarSection.Wrap>
- );
- }
- renderBody() {
- const sentryAppIssues = this.renderSentryAppIssues();
- const integrationIssues = this.renderIntegrationIssues(this.state.integrations);
- const pluginIssues = this.renderPluginIssues();
- const pluginActions = this.renderPluginActions();
- const showSetup =
- !sentryAppIssues && !integrationIssues && !pluginIssues && !pluginActions;
- return (
- <SidebarSection.Wrap data-test-id="linked-issues">
- <SidebarSection.Title>{t('Issue Tracking')}</SidebarSection.Title>
- <SidebarSection.Content>
- {showSetup && (
- <AlertLink
- priority="muted"
- size="small"
- to={`/settings/${this.props.organization.slug}/integrations/?category=issue%20tracking`}
- >
- {t('Track this issue in Jira, GitHub, etc.')}
- </AlertLink>
- )}
- {sentryAppIssues && <Wrapper>{sentryAppIssues}</Wrapper>}
- {integrationIssues && <Wrapper>{integrationIssues}</Wrapper>}
- {pluginIssues && <Wrapper>{pluginIssues}</Wrapper>}
- {pluginActions && <Wrapper>{pluginActions}</Wrapper>}
- </SidebarSection.Content>
- </SidebarSection.Wrap>
- );
- }
- }
- const Wrapper = styled('div')`
- margin-bottom: ${space(2)};
- `;
- export default withSentryAppComponents(withOrganization(ExternalIssueList), {
- componentType: 'issue-link',
- });
|