Browse Source

fix(notifications): Update alert copy (#28470)

Marcos Gaeta 3 years ago
parent
commit
39e1874bf9

+ 3 - 1
static/app/utils/integrationUtil.tsx

@@ -224,5 +224,7 @@ export const isSlackIntegrationUpToDate = (integrations: Integration[]): boolean
 export const getAlertText = (integrations?: Integration[]): string | undefined => {
   return isSlackIntegrationUpToDate(integrations || [])
     ? undefined
-    : t('Your Slack installation is out of date. Please re-install.');
+    : t(
+        'Update to the latest version of our Slack app to get access to personal and team notifications.'
+      );
 };

+ 20 - 14
static/app/views/organizationIntegrations/addIntegration.tsx

@@ -63,24 +63,26 @@ export default class AddIntegration extends React.Component<Props> {
   }
 
   openDialog = (urlParams?: {[key: string]: string}) => {
+    const {account, analyticsParams, modalParams, organization, provider} = this.props;
+
     trackIntegrationAnalytics('integrations.installation_start', {
-      integration: this.props.provider.key,
+      integration: provider.key,
       integration_type: 'first_party',
-      organization: this.props.organization,
-      ...this.props.analyticsParams,
+      organization,
+      ...analyticsParams,
     });
     const name = 'sentryAddIntegration';
-    const {url, width, height} = this.props.provider.setupDialog;
+    const {url, width, height} = provider.setupDialog;
     const {left, top} = this.computeCenteredWindow(width, height);
 
     let query: {[key: string]: string} = {...urlParams};
 
-    if (this.props.account) {
-      query.account = this.props.account;
+    if (account) {
+      query.account = account;
     }
 
-    if (this.props.modalParams) {
-      query = {...query, ...this.props.modalParams};
+    if (modalParams) {
+      query = {...query, ...modalParams};
     }
 
     const installUrl = `${url}?${queryString.stringify(query)}`;
@@ -91,6 +93,8 @@ export default class AddIntegration extends React.Component<Props> {
   };
 
   didReceiveMessage = (message: MessageEvent) => {
+    const {analyticsParams, onInstall, organization, provider} = this.props;
+
     if (message.origin !== document.location.origin) {
       return;
     }
@@ -111,16 +115,18 @@ export default class AddIntegration extends React.Component<Props> {
       return;
     }
     trackIntegrationAnalytics('integrations.installation_complete', {
-      integration: this.props.provider.key,
+      integration: provider.key,
       integration_type: 'first_party',
-      organization: this.props.organization,
-      ...this.props.analyticsParams,
+      organization,
+      ...analyticsParams,
     });
-    addSuccessMessage(t('%s added', this.props.provider.name));
-    this.props.onInstall(data);
+    addSuccessMessage(t('%s added', provider.name));
+    onInstall(data);
   };
 
   render() {
-    return this.props.children(this.openDialog);
+    const {children} = this.props;
+
+    return children(this.openDialog);
   }
 }

+ 22 - 11
static/app/views/organizationIntegrations/installedIntegration.tsx

@@ -8,14 +8,14 @@ import Button from 'app/components/button';
 import CircleIndicator from 'app/components/circleIndicator';
 import Confirm from 'app/components/confirm';
 import Tooltip from 'app/components/tooltip';
-import {IconDelete, IconFlag, IconSettings} from 'app/icons';
+import {IconDelete, IconFlag, IconSettings, IconWarning} from 'app/icons';
 import {t} from 'app/locale';
 import space from 'app/styles/space';
 import {Integration, IntegrationProvider, ObjectStatus, Organization} from 'app/types';
 import {IntegrationAnalyticsKey} from 'app/utils/analytics/integrationAnalyticsEvents';
-import {getAlertText} from 'app/utils/integrationUtil';
 import {Theme} from 'app/utils/theme';
 
+import AddIntegrationButton from './addIntegrationButton';
 import IntegrationItem from './integrationItem';
 
 export type Props = {
@@ -26,6 +26,7 @@ export type Props = {
   onDisable: (integration: Integration) => void;
   trackIntegrationAnalytics: (eventKey: IntegrationAnalyticsKey) => void; // analytics callback
   className?: string;
+  requiresUpgrade?: boolean;
 };
 
 export default class InstalledIntegration extends React.Component<Props> {
@@ -93,15 +94,13 @@ export default class InstalledIntegration extends React.Component<Props> {
   }
 
   render() {
-    const {className, integration, provider, organization} = this.props;
+    const {className, integration, organization, provider, requiresUpgrade} = this.props;
 
     const removeConfirmProps =
       integration.status === 'active' && integration.provider.canDisable
         ? this.disableConfirmProps
         : this.removeConfirmProps;
 
-    const alertText = getAlertText([integration]);
-
     return (
       <Access access={['org:integrations']}>
         {({hasAccess}) => (
@@ -109,11 +108,6 @@ export default class InstalledIntegration extends React.Component<Props> {
             <IntegrationItemBox>
               <IntegrationItem integration={integration} />
             </IntegrationItemBox>
-            {alertText && (
-              <Alert type="warning" icon={<IconFlag size="sm" />}>
-                {alertText}
-              </Alert>
-            )}
             <div>
               <Tooltip
                 disabled={hasAccess}
@@ -122,10 +116,27 @@ export default class InstalledIntegration extends React.Component<Props> {
                   'You must be an organization owner, manager or admin to configure'
                 )}
               >
+                {requiresUpgrade && (
+                  <AddIntegrationButton
+                    analyticsParams={{
+                      view: 'integrations_directory_integration_detail',
+                      already_installed: true,
+                    }}
+                    buttonText={t('Update Now')}
+                    data-test-id="integration-upgrade-button"
+                    disabled={!(hasAccess && integration.status === 'active')}
+                    icon={<IconWarning />}
+                    onAddIntegration={() => {}}
+                    organization={organization}
+                    provider={provider}
+                    priority="primary"
+                    size="small"
+                  />
+                )}
                 <StyledButton
                   borderless
                   icon={<IconSettings />}
-                  disabled={!hasAccess || integration.status !== 'active'}
+                  disabled={!(hasAccess && integration.status === 'active')}
                   to={`/settings/${organization.slug}/integrations/${provider.key}/${integration.id}/`}
                   data-test-id="integration-configure-button"
                 >

+ 22 - 12
static/app/views/organizationIntegrations/integrationDetailedView.tsx

@@ -3,11 +3,14 @@ import styled from '@emotion/styled';
 
 import {addErrorMessage} from 'app/actionCreators/indicator';
 import {RequestOptions} from 'app/api';
+import Alert from 'app/components/alert';
+import AsyncComponent from 'app/components/asyncComponent';
 import Button from 'app/components/button';
 import {IconFlag, IconOpen, IconWarning} from 'app/icons';
 import {t} from 'app/locale';
 import space from 'app/styles/space';
 import {Integration, IntegrationProvider} from 'app/types';
+import {getAlertText} from 'app/utils/integrationUtil';
 import withOrganization from 'app/utils/withOrganization';
 
 import AbstractIntegrationDetailedView from './abstractIntegrationDetailedView';
@@ -23,9 +26,9 @@ class IntegrationDetailedView extends AbstractIntegrationDetailedView<
   AbstractIntegrationDetailedView['props'],
   State & AbstractIntegrationDetailedView['state']
 > {
-  getEndpoints(): ([string, string, any] | [string, string])[] {
+  getEndpoints(): ReturnType<AsyncComponent['getEndpoints']> {
     const {orgId, integrationSlug} = this.props.params;
-    const baseEndpoints: ([string, string, any] | [string, string])[] = [
+    return [
       [
         'information',
         `/organizations/${orgId}/config/integrations/?provider_key=${integrationSlug}`,
@@ -35,8 +38,6 @@ class IntegrationDetailedView extends AbstractIntegrationDetailedView<
         `/organizations/${orgId}/integrations/?provider_key=${integrationSlug}&includeConfig=0`,
       ],
     ];
-
-    return baseEndpoints;
   }
 
   get integrationType() {
@@ -210,9 +211,19 @@ class IntegrationDetailedView extends AbstractIntegrationDetailedView<
     const {organization} = this.props;
     const provider = this.provider;
 
-    if (configurations.length) {
-      return configurations.map(integration => {
-        return (
+    if (!configurations.length) {
+      return this.renderEmptyConfigurations();
+    }
+
+    const alertText = getAlertText(configurations);
+    return (
+      <Fragment>
+        {alertText && (
+          <Alert type="warning" icon={<IconFlag size="sm" />}>
+            {alertText}
+          </Alert>
+        )}
+        {configurations.map(integration => (
           <InstallWrapper key={integration.id}>
             <InstalledIntegration
               organization={organization}
@@ -222,13 +233,12 @@ class IntegrationDetailedView extends AbstractIntegrationDetailedView<
               onDisable={this.onDisable}
               data-test-id={integration.id}
               trackIntegrationAnalytics={this.trackIntegrationAnalytics}
+              requiresUpgrade={!!alertText}
             />
           </InstallWrapper>
-        );
-      });
-    }
-
-    return this.renderEmptyConfigurations();
+        ))}
+      </Fragment>
+    );
   }
 }
 

+ 1 - 0
static/app/views/organizationIntegrations/integrationListDirectory.tsx

@@ -377,6 +377,7 @@ export class IntegrationListDirectory extends AsyncComponent<
         configurations={integrations.length}
         categories={getCategoriesForIntegration(provider)}
         alertText={getAlertText(integrations)}
+        resolveText={t('Update Now')}
       />
     );
   };

+ 8 - 1
static/app/views/organizationIntegrations/integrationRow.tsx

@@ -32,7 +32,13 @@ type Props = {
   publishStatus: 'unpublished' | 'published' | 'internal';
   configurations: number;
   categories: string[];
+
+  /** If provided, render an alert message with this text. */
   alertText?: string;
+
+  /** If `alertText` was provided, this text overrides the "Resolve now" message in the alert. */
+  resolveText?: string;
+
   plugin?: PluginWithProjectList;
 };
 
@@ -54,6 +60,7 @@ const IntegrationRow = (props: Props) => {
     configurations,
     categories,
     alertText,
+    resolveText,
     plugin,
   } = props;
 
@@ -118,7 +125,7 @@ const IntegrationRow = (props: Props) => {
                 })
               }
             >
-              {t('Resolve Now')}
+              {resolveText || t('Resolve Now')}
             </ResolveNowButton>
           </Alert>
         </AlertContainer>

+ 2 - 1
static/app/views/organizationIntegrations/pluginDetailedView.tsx

@@ -2,6 +2,7 @@ import {Fragment} from 'react';
 import styled from '@emotion/styled';
 
 import * as modal from 'app/actionCreators/modal';
+import AsyncComponent from 'app/components/asyncComponent';
 import Button from 'app/components/button';
 import ContextPickerModal from 'app/components/contextPickerModal';
 import {t} from 'app/locale';
@@ -23,7 +24,7 @@ class PluginDetailedView extends AbstractIntegrationDetailedView<
   AbstractIntegrationDetailedView['props'],
   State & AbstractIntegrationDetailedView['state']
 > {
-  getEndpoints(): ([string, string, any] | [string, string])[] {
+  getEndpoints(): ReturnType<AsyncComponent['getEndpoints']> {
     const {orgId, integrationSlug} = this.props.params;
     return [
       ['plugins', `/organizations/${orgId}/plugins/configs/?plugins=${integrationSlug}`],

+ 3 - 4
static/app/views/organizationIntegrations/sentryAppDetailedView.tsx

@@ -6,6 +6,7 @@ import {
   installSentryApp,
   uninstallSentryApp,
 } from 'app/actionCreators/sentryAppInstallations';
+import AsyncComponent from 'app/components/asyncComponent';
 import Button from 'app/components/button';
 import CircleIndicator from 'app/components/circleIndicator';
 import Confirm from 'app/components/confirm';
@@ -35,18 +36,16 @@ class SentryAppDetailedView extends AbstractIntegrationDetailedView<
   State & AbstractIntegrationDetailedView['state']
 > {
   tabs: Tab[] = ['overview'];
-  getEndpoints(): ([string, string, any] | [string, string])[] {
+  getEndpoints(): ReturnType<AsyncComponent['getEndpoints']> {
     const {
       organization,
       params: {integrationSlug},
     } = this.props;
-    const baseEndpoints: ([string, string, any] | [string, string])[] = [
+    return [
       ['sentryApp', `/sentry-apps/${integrationSlug}/`],
       ['featureData', `/sentry-apps/${integrationSlug}/features/`],
       ['appInstalls', `/organizations/${organization.slug}/sentry-app-installations/`],
     ];
-
-    return baseEndpoints;
   }
 
   onLoadAllEndpointsSuccess() {