Browse Source

feat(sentry apps): Show disabled integration link (#37454)

* feat(sentry apps): Show disabled integration link
Colleen O'Rourke 2 years ago
parent
commit
653234db88

+ 2 - 1
static/app/components/group/externalIssuesList.tsx

@@ -160,7 +160,7 @@ class ExternalIssueList extends AsyncComponent<Props, State> {
     }
 
     return components.map(component => {
-      const {sentryApp} = component;
+      const {sentryApp, error: disabled} = component;
       const installation = sentryAppInstallations.find(
         i => i.app.uuid === sentryApp.uuid
       );
@@ -180,6 +180,7 @@ class ExternalIssueList extends AsyncComponent<Props, State> {
             sentryAppComponent={component}
             sentryAppInstallation={installation}
             externalIssue={issue}
+            disabled={disabled}
           />
         </ErrorBoundary>
       );

+ 27 - 7
static/app/components/group/sentryAppExternalIssueActions.tsx

@@ -7,6 +7,7 @@ import {deleteExternalIssue} from 'sentry/actionCreators/platformExternalIssues'
 import {Client} from 'sentry/api';
 import {IntegrationLink} from 'sentry/components/issueSyncListElement';
 import SentryAppComponentIcon from 'sentry/components/sentryAppComponentIcon';
+import Tooltip from 'sentry/components/tooltip';
 import {IconAdd, IconClose} from 'sentry/icons';
 import {t, tct} from 'sentry/locale';
 import space from 'sentry/styles/space';
@@ -28,6 +29,7 @@ type Props = {
   group: Group;
   sentryAppComponent: SentryAppComponent;
   sentryAppInstallation: SentryAppInstallation;
+  disabled?: boolean;
   externalIssue?: PlatformExternalIssue;
 };
 
@@ -111,7 +113,7 @@ class SentryAppExternalIssueActions extends Component<Props, State> {
   };
 
   render() {
-    const {sentryAppComponent} = this.props;
+    const {sentryAppComponent, disabled} = this.props;
     const {externalIssue} = this.state;
     const name = sentryAppComponent.sentryApp.name;
 
@@ -127,11 +129,25 @@ class SentryAppExternalIssueActions extends Component<Props, State> {
       <IssueLinkContainer>
         <IssueLink>
           <StyledSentryAppComponentIcon sentryAppComponent={sentryAppComponent} />
-          <IntegrationLink onClick={this.doOpenModal} href={url}>
-            {displayName}
-          </IntegrationLink>
+          <Tooltip
+            title={tct('Unable to connect to [provider].', {
+              provider: sentryAppComponent.sentryApp.name,
+            })}
+            disabled={!disabled}
+          >
+            <StyledIntegrationLink
+              onClick={e => (disabled ? e.preventDefault() : this.doOpenModal())}
+              href={url}
+              disabled={disabled}
+            >
+              {displayName}
+            </StyledIntegrationLink>
+          </Tooltip>
         </IssueLink>
-        <StyledIcon onClick={this.onAddRemoveClick}>
+        <StyledIcon
+          disabled={disabled}
+          onClick={() => !disabled && this.onAddRemoveClick()}
+        >
           {!!externalIssue ? <IconClose /> : <IconAdd />}
         </StyledIcon>
       </IssueLinkContainer>
@@ -153,6 +169,10 @@ const IssueLink = styled('div')`
   min-width: 0;
 `;
 
+const StyledIntegrationLink = styled(IntegrationLink)<{disabled?: boolean}>`
+  color: ${({disabled, theme}) => (disabled ? theme.disabled : theme.textColor)};
+`;
+
 const IssueLinkContainer = styled('div')`
   line-height: 0;
   display: flex;
@@ -161,8 +181,8 @@ const IssueLinkContainer = styled('div')`
   margin-bottom: 16px;
 `;
 
-const StyledIcon = styled('span')`
-  color: ${p => p.theme.textColor};
+const StyledIcon = styled('span')<{disabled?: boolean}>`
+  color: ${({disabled, theme}) => (disabled ? theme.disabled : theme.textColor)};
   cursor: pointer;
 `;
 

+ 8 - 2
static/app/components/issueSyncListElement.tsx

@@ -11,6 +11,7 @@ import {callIfFunction} from 'sentry/utils/callIfFunction';
 import {getIntegrationIcon} from 'sentry/utils/integrationUtil';
 
 type Props = {
+  disabled?: boolean;
   externalIssueDisplayName?: string | null;
   externalIssueId?: string | null;
   externalIssueKey?: string | null;
@@ -67,6 +68,7 @@ class IssueSyncListElement extends Component<Props> {
       <IntegrationLink
         href={this.props.externalIssueLink || undefined}
         onClick={!this.isLinked() ? this.props.onOpen : undefined}
+        disabled={this.props.disabled}
       >
         {this.getText()}
       </IntegrationLink>
@@ -137,7 +139,7 @@ export const IssueSyncListElementContainer = styled('div')`
   }
 `;
 
-export const IntegrationLink = styled('a')`
+export const IntegrationLink = styled('a')<{disabled?: boolean}>`
   text-decoration: none;
   padding-bottom: ${space(0.25)};
   margin-left: ${space(1)};
@@ -151,7 +153,11 @@ export const IntegrationLink = styled('a')`
 
   &,
   &:hover {
-    border-bottom: 1px solid ${p => p.theme.blue300};
+    border-bottom: 1px solid
+      ${({disabled, theme}) => (disabled ? theme.disabled : theme.blue300)};
+  }
+  &:hover {
+    color: ${({disabled, theme}) => (disabled ? theme.disabled : theme.blue300)};
   }
 `;
 

+ 18 - 5
static/app/components/sentryAppComponentIcon.tsx

@@ -12,23 +12,36 @@ type Props = {
  * Icon Renderer for SentryAppComponents with UI
  * (e.g. Issue Linking, Stacktrace Linking)
  */
-const SentryAppComponentIcon = ({sentryAppComponent: {sentryApp}}: Props) => {
-  const selectedAvatar = sentryApp?.avatars?.find(({color}) => color === false);
+const SentryAppComponentIcon = ({sentryAppComponent}: Props) => {
+  const selectedAvatar = sentryAppComponent.sentryApp?.avatars?.find(
+    ({color}) => color === false
+  );
   const isDefault = selectedAvatar?.avatarType !== 'upload';
+  const isDisabled = sentryAppComponent.error;
   return (
     <SentryAppAvatarWrapper
       isDark={ConfigStore.get('theme') === 'dark'}
       isDefault={isDefault}
+      isDisabled={isDisabled}
     >
-      <SentryAppAvatar sentryApp={sentryApp} size={20} isColor={false} />
+      <SentryAppAvatar
+        sentryApp={sentryAppComponent.sentryApp}
+        size={20}
+        isColor={false}
+      />
     </SentryAppAvatarWrapper>
   );
 };
 
 export default SentryAppComponentIcon;
 
-const SentryAppAvatarWrapper = styled('span')<{isDark: boolean; isDefault: boolean}>`
-  color: ${({isDark}) => (isDark ? 'white' : 'black')};
+const SentryAppAvatarWrapper = styled('span')<{
+  isDark: boolean;
+  isDefault: boolean;
+  isDisabled?: boolean;
+}>`
+  color: ${({isDark, isDisabled, theme}) =>
+    isDisabled ? theme.disabled : isDark ? 'white' : 'black'};
   filter: ${p => (p.isDark && !p.isDefault ? 'invert(1)' : 'invert(0)')};
   line-height: 0;
   flex-shrink: 0;

+ 1 - 0
static/app/types/integrations.tsx

@@ -208,6 +208,7 @@ export type SentryAppComponent = {
   };
   type: 'issue-link' | 'alert-rule-action' | 'issue-media' | 'stacktrace-link';
   uuid: string;
+  error?: boolean;
 };
 
 export type SentryAppWebhookRequest = {

+ 10 - 8
tests/js/spec/components/group/sentryAppExternalIssueActions.spec.jsx

@@ -54,7 +54,7 @@ describe('SentryAppExternalIssueActions', () => {
     });
 
     it('renders a link to open the modal', () => {
-      expect(wrapper.find('IntegrationLink a').text()).toEqual(
+      expect(wrapper.find('StyledIntegrationLink a').text()).toEqual(
         `Link ${component.sentryApp.name} Issue`
       );
     });
@@ -64,7 +64,7 @@ describe('SentryAppExternalIssueActions', () => {
     });
 
     it('opens the modal', async () => {
-      wrapper.find('IntegrationLink a').simulate('click');
+      wrapper.find('StyledIntegrationLink a').simulate('click');
 
       await tick();
       wrapper.update();
@@ -73,7 +73,7 @@ describe('SentryAppExternalIssueActions', () => {
     });
 
     it('renders the Create Issue form fields, based on schema', async () => {
-      wrapper.find('IntegrationLink a').simulate('click');
+      wrapper.find('StyledIntegrationLink a').simulate('click');
       await tick();
       wrapper.update();
 
@@ -89,7 +89,7 @@ describe('SentryAppExternalIssueActions', () => {
     });
 
     it('renders the Link Issue form fields, based on schema', async () => {
-      wrapper.find('IntegrationLink a').simulate('click');
+      wrapper.find('StyledIntegrationLink a').simulate('click');
       await tick();
       wrapper.update();
 
@@ -111,7 +111,7 @@ describe('SentryAppExternalIssueActions', () => {
         body: externalIssue,
       });
 
-      wrapper.find('IntegrationLink a').simulate('click');
+      wrapper.find('StyledIntegrationLink a').simulate('click');
 
       await tick();
       wrapper.update();
@@ -141,7 +141,7 @@ describe('SentryAppExternalIssueActions', () => {
         body: externalIssue,
       });
 
-      wrapper.find('IntegrationLink a').simulate('click');
+      wrapper.find('StyledIntegrationLink a').simulate('click');
       await tick();
       wrapper.update();
 
@@ -179,11 +179,13 @@ describe('SentryAppExternalIssueActions', () => {
     });
 
     it('renders a link to the external issue', () => {
-      expect(wrapper.find('IntegrationLink a').text()).toEqual(externalIssue.displayName);
+      expect(wrapper.find('StyledIntegrationLink a').text()).toEqual(
+        externalIssue.displayName
+      );
     });
 
     it('links to the issue', () => {
-      expect(wrapper.find('IntegrationLink').first().prop('href')).toEqual(
+      expect(wrapper.find('StyledIntegrationLink').first().prop('href')).toEqual(
         externalIssue.webUrl
       );
     });