Browse Source

feat(app-platform): Web Installation Flow (#10513)

App developers will have two options for installation paths. First, a
purely async path that relies on an installation webhook. This is useful
when you don't need any information about Users and just want to push
data into Sentry.

The second is when you do need information about the User or to
correlate data between Sentry and the App in some way.

This change adds the latter (just in Developer Settings for now). When
the User clicks the install button, it creates the record on our side
and redirects the User to the App. It includes data about the
installation and everything the App needs to do the token exchange.
Matte Noble 6 years ago
parent
commit
5fdcfdc550

+ 1 - 0
package.json

@@ -56,6 +56,7 @@
     "moment-timezone": "0.5.4",
     "node-libs-browser": "0.5.3",
     "papaparse": "^4.6.0",
+    "parseurl": "^1.3.2",
     "platformicons": "2.0.3",
     "po-catalog-loader": "2.0.0",
     "prop-types": "^15.6.0",

+ 1 - 1
src/sentry/api/endpoints/sentry_app_installations.py

@@ -52,7 +52,7 @@ class SentryAppInstallationsEndpoint(SentryAppInstallationsBaseEndpoint):
         if not serializer.is_valid():
             return Response(serializer.errors, status=400)
 
-        install, _ = Creator.run(
+        install, grant = Creator.run(
             organization=organization,
             slug=serializer.object.get('slug'),
             user=request.user,

+ 7 - 7
src/sentry/api/serializers/models/sentry_app_installation.py

@@ -7,19 +7,19 @@ from sentry.models import SentryAppInstallation
 
 @register(SentryAppInstallation)
 class SentryAppInstallationSerializer(Serializer):
-    def serialize(self, obj, attrs, user):
+    def serialize(self, install, attrs, user):
         data = {
             'app': {
-                'uuid': obj.sentry_app.uuid,
-                'slug': obj.sentry_app.slug,
+                'uuid': install.sentry_app.uuid,
+                'slug': install.sentry_app.slug,
             },
             'organization': {
-                'slug': obj.organization.slug,
+                'slug': install.organization.slug,
             },
-            'uuid': obj.uuid,
+            'uuid': install.uuid,
         }
 
-        if 'code' in attrs:
-            data['code'] = attrs['code']
+        if install.is_new:
+            data['code'] = install.api_grant.code
 
         return data

+ 1 - 0
src/sentry/mediators/sentry_app_installations/creator.py

@@ -20,6 +20,7 @@ class Creator(Mediator):
         self._create_api_grant()
         self._create_install()
         self._notify_service()
+        self.install.is_new = True
         return (self.install, self.api_grant)
 
     def _create_authorization(self):

+ 4 - 0
src/sentry/models/sentryappinstallation.py

@@ -49,3 +49,7 @@ class SentryAppInstallation(ParanoidModel):
     class Meta:
         app_label = 'sentry'
         db_table = 'sentry_sentryappinstallation'
+
+    # Used when first creating an Installation to tell the serializer that the
+    # grant code should be included in the serialization.
+    is_new = False

+ 99 - 12
src/sentry/static/sentry/app/views/settings/organizationDeveloperSettings/index.jsx

@@ -1,10 +1,14 @@
+import {groupBy} from 'lodash';
 import {Box, Flex} from 'grid-emotion';
 import React from 'react';
-import {Link} from 'react-router';
+import {Link, browserHistory} from 'react-router';
+import parseurl from 'parseurl';
+import qs from 'query-string';
 
+import {addErrorMessage, addSuccessMessage} from 'app/actionCreators/indicator';
 import AsyncView from 'app/views/asyncView';
 import Button from 'app/components/button';
-import CircleIndicator from 'app/components/circleIndicator';
+import {Client} from 'app/api';
 import EmptyMessage from 'app/views/settings/components/emptyMessage';
 import SentryAppAvatar from 'app/components/avatar/sentryAppAvatar';
 import PropTypes from 'prop-types';
@@ -15,14 +19,57 @@ import styled from 'react-emotion';
 import space from 'app/styles/space';
 import {withTheme} from 'emotion-theming';
 
+const api = new Client();
+
 class SentryApplicationRow extends React.PureComponent {
   static propTypes = {
     app: PropTypes.object.isRequired,
     orgId: PropTypes.string.isRequired,
+    installs: PropTypes.array,
+  };
+
+  redirectUrl = install => {
+    const {orgId, app} = this.props;
+    let redirectUrl = `/settings/${orgId}/integrations/`;
+
+    if (app.redirectUrl) {
+      const url = parseurl({url: app.redirectUrl});
+      // Order the query params alphabetically.
+      // Otherwise ``qs`` orders them randomly and it's impossible to test.
+      const installQuery = JSON.parse(
+        JSON.stringify({installationId: install.uuid, code: install.code})
+      );
+      const query = Object.assign(qs.parse(url.query), installQuery);
+      redirectUrl = `${url.protocol}//${url.host}${url.pathname}?${qs.stringify(query)}`;
+    }
+
+    return redirectUrl;
+  };
+
+  install = () => {
+    const {orgId, app} = this.props;
+
+    const success = install => {
+      addSuccessMessage(t(`${app.slug} successfully installed.`));
+      browserHistory.push(this.redirectUrl(install));
+    };
+
+    const error = err => {
+      addErrorMessage(err.responseJSON);
+    };
+
+    const opts = {
+      method: 'POST',
+      data: {slug: app.slug},
+      success,
+      error,
+    };
+
+    api.request(`/organizations/${orgId}/sentry-app-installations/`, opts);
   };
 
   render() {
-    let {app, orgId} = this.props;
+    let {app, orgId, installs} = this.props;
     let btnClassName = 'btn btn-default';
 
     return (
@@ -39,11 +86,27 @@ class SentryApplicationRow extends React.PureComponent {
           </SentryAppBox>
         </Flex>
 
-        <Flex>
+        <StyledButtonGroup>
           <Box>
-            <Button icon="icon-trash" onClick={() => {}} className={btnClassName} />
+            <StyledInstallButton
+              onClick={this.install}
+              size="small"
+              className="btn btn-default"
+              disabled={installs && installs.length > 0}
+            >
+              {t('Install')}
+            </StyledInstallButton>
           </Box>
-        </Flex>
+
+          <Box>
+            <Button
+              icon="icon-trash"
+              size="small"
+              onClick={() => {}}
+              className={btnClassName}
+            />
+          </Box>
+        </StyledButtonGroup>
       </SentryAppItem>
     );
   }
@@ -52,7 +115,15 @@ class SentryApplicationRow extends React.PureComponent {
 export default class OrganizationDeveloperSettings extends AsyncView {
   getEndpoints() {
     let {orgId} = this.props.params;
-    return [['applications', `/organizations/${orgId}/sentry-apps/`]];
+
+    return [
+      ['applications', `/organizations/${orgId}/sentry-apps/`],
+      ['installs', `/organizations/${orgId}/sentry-app-installations/`],
+    ];
+  }
+
+  get installsByApp() {
+    return groupBy(this.state.installs, install => install.app.slug);
   }
 
   renderBody() {
@@ -69,6 +140,7 @@ export default class OrganizationDeveloperSettings extends AsyncView {
     );
 
     let isEmpty = this.state.applications.length === 0;
+
     return (
       <div>
         <SettingsPageHeader title={t('Developer Settings')} action={action} />
@@ -77,7 +149,14 @@ export default class OrganizationDeveloperSettings extends AsyncView {
           <PanelBody>
             {!isEmpty ? (
               this.state.applications.map(app => {
-                return <SentryApplicationRow key={app.uuid} app={app} orgId={orgId} />;
+                return (
+                  <SentryApplicationRow
+                    key={app.uuid}
+                    app={app}
+                    orgId={orgId}
+                    installs={this.installsByApp[app.slug]}
+                  />
+                );
               })
             ) : (
               <EmptyMessage>{t('No applications have been created yet.')}</EmptyMessage>
@@ -89,6 +168,10 @@ export default class OrganizationDeveloperSettings extends AsyncView {
   }
 }
 
+const StyledButtonGroup = styled(Flex)`
+  align-items: center;
+`;
+
 const SentryAppItem = styled(PanelItem)`
   justify-content: space-between;
   padding: 15px;
@@ -103,6 +186,14 @@ const SentryAppName = styled('div')`
   margin-bottom: 3px;
 `;
 
+const StyledInstallButton = styled(
+  withTheme(({...props}) => {
+    return <Button {...props}>{t('Install')}</Button>;
+  })
+)`
+  margin-right: ${space(1)};
+`;
+
 const StyledLink = styled(Link)`
   font-size: 22px;
   color: ${props => props.theme.textColor};
@@ -112,10 +203,6 @@ const Status = styled(
   withTheme(({published, ...props}) => {
     return (
       <Flex align="center">
-        <CircleIndicator
-          size={4}
-          color={published ? props.theme.success : props.theme.gray2}
-        />
         <div {...props}>{published ? t('published') : t('unpublished')}</div>
       </Flex>
     );

+ 13 - 0
tests/js/fixtures/sentryAppInstallation.js

@@ -0,0 +1,13 @@
+export function SentryAppInstallation(params = {}) {
+  return {
+    uuid: 'd950595e-cba2-46f6-8a94-b79e42806f98',
+    app: {
+      slug: 'sample-app',
+      uuid: 'f4d972ba-1177-4974-943e-4800fe8c7d05',
+    },
+    organization: {
+      slug: 'the-best-org',
+    },
+    ...params,
+  };
+}

+ 804 - 44
tests/js/spec/views/settings/organizationDeveloperSettings/__snapshots__/index.spec.jsx.snap

@@ -1,6 +1,6 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`Organization Developer Settings renders developer settings it lists sentry apps for an organization 1`] = `
+exports[`Organization Developer Settings when Apps exist displays all Apps owned by the Org 1`] = `
 <OrganizationDeveloperSettings
   params={
     Object {
@@ -239,10 +239,10 @@ exports[`Organization Developer Settings renders developer settings it lists sen
                   >
                     <SentryAppItem>
                       <Base
-                        className="css-4rurrd-PanelItem-SentryAppItem ew9bjim0"
+                        className="css-4rurrd-PanelItem-SentryAppItem ew9bjim1"
                       >
                         <div
-                          className="css-4rurrd-PanelItem-SentryAppItem ew9bjim0"
+                          className="css-4rurrd-PanelItem-SentryAppItem ew9bjim1"
                           is={null}
                         >
                           <Flex>
@@ -350,27 +350,27 @@ exports[`Organization Developer Settings renders developer settings it lists sen
                                 </SentryAppAvatar>
                                 <SentryAppBox>
                                   <Base
-                                    className="css-1jd8uiq-SentryAppBox ew9bjim1"
+                                    className="css-1jd8uiq-SentryAppBox ew9bjim2"
                                   >
                                     <div
-                                      className="css-1jd8uiq-SentryAppBox ew9bjim1"
+                                      className="css-1jd8uiq-SentryAppBox ew9bjim2"
                                       is={null}
                                     >
                                       <SentryAppName>
                                         <div
-                                          className="css-677vhf-SentryAppName ew9bjim2"
+                                          className="css-677vhf-SentryAppName ew9bjim3"
                                         >
                                           <StyledLink
                                             to="/settings/org-slug/developer-settings/sample-app/"
                                           >
                                             <Link
-                                              className="css-k5tnsd-StyledLink ew9bjim3"
+                                              className="css-k5tnsd-StyledLink ew9bjim5"
                                               onlyActiveOnIndex={false}
                                               style={Object {}}
                                               to="/settings/org-slug/developer-settings/sample-app/"
                                             >
                                               <a
-                                                className="css-k5tnsd-StyledLink ew9bjim3"
+                                                className="css-k5tnsd-StyledLink ew9bjim5"
                                                 onClick={[Function]}
                                                 style={Object {}}
                                               >
@@ -384,11 +384,11 @@ exports[`Organization Developer Settings renders developer settings it lists sen
                                         published={false}
                                       >
                                         <WithTheme(Component)
-                                          className="css-1m055qq-Status ew9bjim4"
+                                          className="css-1m055qq-Status ew9bjim6"
                                           published={false}
                                         >
                                           <Component
-                                            className="css-1m055qq-Status ew9bjim4"
+                                            className="css-1m055qq-Status ew9bjim6"
                                             published={false}
                                             theme={
                                               Object {
@@ -632,25 +632,8 @@ exports[`Organization Developer Settings renders developer settings it lists sen
                                                   className="css-5ipae5"
                                                   is={null}
                                                 >
-                                                  <CircleIndicator
-                                                    color="#9585A3"
-                                                    enabled={true}
-                                                    size={4}
-                                                  >
-                                                    <Circle
-                                                      color="#9585A3"
-                                                      enabled={true}
-                                                      size={4}
-                                                    >
-                                                      <div
-                                                        className="css-1b6es9-Circle eljqieg0"
-                                                        color="#9585A3"
-                                                        size={4}
-                                                      />
-                                                    </Circle>
-                                                  </CircleIndicator>
                                                   <div
-                                                    className="css-1m055qq-Status ew9bjim4"
+                                                    className="css-1m055qq-Status ew9bjim6"
                                                     theme={
                                                       Object {
                                                         "alert": Object {
@@ -896,14 +879,781 @@ exports[`Organization Developer Settings renders developer settings it lists sen
                               </div>
                             </Base>
                           </Flex>
-                          <Flex>
+                          <StyledButtonGroup>
                             <Base
-                              className="css-sncxrr"
+                              className="css-b3hw0m-StyledButtonGroup ew9bjim0"
                             >
                               <div
-                                className="css-sncxrr"
+                                className="css-b3hw0m-StyledButtonGroup ew9bjim0"
                                 is={null}
                               >
+                                <Box>
+                                  <Base
+                                    className="css-roynbj"
+                                  >
+                                    <div
+                                      className="css-roynbj"
+                                      is={null}
+                                    >
+                                      <StyledInstallButton
+                                        className="btn btn-default"
+                                        onClick={[Function]}
+                                        size="small"
+                                      >
+                                        <WithTheme(Component)
+                                          className="btn btn-default css-1158k9d-StyledInstallButton ew9bjim4"
+                                          onClick={[Function]}
+                                          size="small"
+                                        >
+                                          <Component
+                                            className="btn btn-default css-1158k9d-StyledInstallButton ew9bjim4"
+                                            onClick={[Function]}
+                                            size="small"
+                                            theme={
+                                              Object {
+                                                "alert": Object {
+                                                  "attention": Object {
+                                                    "background": "#F09E71",
+                                                    "backgroundLight": "#ECBFA6",
+                                                    "border": "#D0816D",
+                                                  },
+                                                  "default": Object {
+                                                    "background": "#BDB4C7",
+                                                    "backgroundLight": "#FAF9FB",
+                                                    "border": "#E2DBE8",
+                                                  },
+                                                  "error": Object {
+                                                    "background": "#e03e2f",
+                                                    "backgroundLight": "#FDF6F5",
+                                                    "border": "#E7C0BC",
+                                                    "iconColor": "#C72516",
+                                                    "textLight": "#92635f",
+                                                  },
+                                                  "info": Object {
+                                                    "background": "#3B6ECC",
+                                                    "backgroundLight": "#F5FAFE",
+                                                    "border": "#B5D6ED",
+                                                    "iconColor": "#3B6ECC",
+                                                  },
+                                                  "success": Object {
+                                                    "background": "#57be8c",
+                                                    "backgroundLight": "#F8FCF7",
+                                                    "border": "#BBD6B3",
+                                                    "iconColor": "#3EA573",
+                                                  },
+                                                  "warn": Object {
+                                                    "background": "#ecc844",
+                                                    "backgroundLight": "#FFFDF7",
+                                                    "border": "#E1D697",
+                                                    "iconColor": "#e6bc23",
+                                                  },
+                                                  "warning": Object {
+                                                    "background": "#ecc844",
+                                                    "backgroundLight": "#FFFDF7",
+                                                    "border": "#E1D697",
+                                                    "iconColor": "#e6bc23",
+                                                  },
+                                                },
+                                                "background": "#fff",
+                                                "blue": "#3B6ECC",
+                                                "blueDark": "#2F58A3",
+                                                "blueLight": "#628BD6",
+                                                "borderDark": "#D1CAD8",
+                                                "borderLight": "#E2DBE8",
+                                                "borderLighter": "#f9f6fd",
+                                                "borderRadius": "4px",
+                                                "breakpoints": Array [
+                                                  "768px",
+                                                  "992px",
+                                                  "1200px",
+                                                ],
+                                                "button": Object {
+                                                  "borderRadius": "3px",
+                                                  "danger": Object {
+                                                    "background": "#e03e2f",
+                                                    "backgroundActive": "#bf2a1d",
+                                                    "border": "#bf2a1d",
+                                                    "borderActive": "#7d1c13",
+                                                    "color": "#FFFFFF",
+                                                  },
+                                                  "default": Object {
+                                                    "background": "#FFFFFF",
+                                                    "backgroundActive": "#FFFFFF",
+                                                    "border": "#d8d2de",
+                                                    "borderActive": "#c9c0d1",
+                                                    "color": "#2f2936",
+                                                    "colorActive": "#161319",
+                                                  },
+                                                  "disabled": Object {
+                                                    "background": "#FFFFFF",
+                                                    "backgroundActive": "#FFFFFF",
+                                                    "border": "#e3e5e6",
+                                                    "borderActive": "#e3e5e6",
+                                                    "color": "#ced3d6",
+                                                  },
+                                                  "link": Object {
+                                                    "background": "transparent",
+                                                    "backgroundActive": "transparent",
+                                                    "color": "#3B6ECC",
+                                                  },
+                                                  "primary": Object {
+                                                    "background": "#6C5FC7",
+                                                    "backgroundActive": "#4e3fb4",
+                                                    "border": "#3d328e",
+                                                    "borderActive": "#352b7b",
+                                                    "color": "#FFFFFF",
+                                                  },
+                                                  "success": Object {
+                                                    "background": "#3fa372",
+                                                    "backgroundActive": "#57be8c",
+                                                    "border": "#7ccca5",
+                                                    "borderActive": "#7ccca5",
+                                                    "color": "#FFFFFF",
+                                                  },
+                                                },
+                                                "charts": Object {
+                                                  "colors": Array [
+                                                    "#444674",
+                                                    "#524a7e",
+                                                    "#624d84",
+                                                    "#744f88",
+                                                    "#865189",
+                                                    "#985389",
+                                                    "#aa5488",
+                                                    "#bc5585",
+                                                    "#cd5681",
+                                                    "#df567c",
+                                                    "#e86070",
+                                                    "#ed6c64",
+                                                    "#f17959",
+                                                    "#f4854e",
+                                                    "#f59242",
+                                                    "#f59e35",
+                                                    "#f4aa27",
+                                                    "#f2b712",
+                                                  ],
+                                                  "getColorPalette": [Function],
+                                                  "previousPeriod": "#BDB4C7",
+                                                },
+                                                "diff": Object {
+                                                  "added": "#d8f0e4",
+                                                  "addedRow": "#f5fbf8",
+                                                  "removed": "#f7ceca",
+                                                  "removedRow": "#fcefee",
+                                                },
+                                                "disabled": "#ced3d6",
+                                                "dropShadowHeavy": "0 1px 4px 1px rgba(47,40,55,0.08), 0 4px 16px 0 rgba(47,40,55,0.12)",
+                                                "dropShadowLight": "0 2px 0 rgba(37, 11, 54, 0.04)",
+                                                "error": "#e03e2f",
+                                                "fontSizeExtraLarge": "18px",
+                                                "fontSizeLarge": "16px",
+                                                "fontSizeMedium": "14px",
+                                                "fontSizeSmall": "12px",
+                                                "gray1": "#BDB4C7",
+                                                "gray2": "#9585A3",
+                                                "gray3": "#645574",
+                                                "gray4": "#4A3E56",
+                                                "gray5": "#302839",
+                                                "gray6": "#AFA3BB",
+                                                "green": "#57be8c",
+                                                "greenDark": "#3EA573",
+                                                "greenLight": "#71D8A6",
+                                                "greenTransparent": "rgba(87, 190, 140, 0.5)",
+                                                "grid": 8,
+                                                "offWhite": "#FAF9FB",
+                                                "offWhite2": "#E7E1EC",
+                                                "orange": "#ec5e44",
+                                                "orangeDark": "#D3452B",
+                                                "orangeLight": "#FF785E",
+                                                "pink": "#F868BC",
+                                                "pinkDark": "#DF4FA3",
+                                                "pinkLight": "#FF82D6",
+                                                "purple": "#6C5FC7",
+                                                "purple2": "#6f617c",
+                                                "purpleDark": "#5346AE",
+                                                "purpleDarkest": "#392C94",
+                                                "purpleLight": "#8679E1",
+                                                "purpleLightest": "#9F92FA",
+                                                "red": "#e03e2f",
+                                                "redDark": "#C72516",
+                                                "redLight": "#FA5849",
+                                                "settings": Object {
+                                                  "containerWidth": "1140px",
+                                                  "headerHeight": "115px",
+                                                  "maxCrumbWidth": "240px",
+                                                  "sidebarWidth": "210px",
+                                                },
+                                                "sidebar": Object {
+                                                  "background": "#2f2936",
+                                                  "badgeSize": "22px",
+                                                  "collapsedWidth": "70px",
+                                                  "color": "#9586a5",
+                                                  "divider": "#493e54",
+                                                  "expandedWidth": "220px",
+                                                  "menuSpacing": "15px",
+                                                  "mobileHeight": "54px",
+                                                  "panel": Object {
+                                                    "headerHeight": "62px",
+                                                    "width": "320px",
+                                                  },
+                                                  "smallBadgeSize": "11px",
+                                                },
+                                                "similarity": Object {
+                                                  "colors": Array [
+                                                    "#ec5e44",
+                                                    "#f38259",
+                                                    "#f9a66d",
+                                                    "#98b480",
+                                                    "#57be8c",
+                                                  ],
+                                                  "empty": "#e2dee6",
+                                                },
+                                                "success": "#57be8c",
+                                                "text": Object {
+                                                  "family": "\\"Rubik\\", \\"Avenir Next\\", sans-serif",
+                                                  "familyMono": "Monaco, Consolas, \\"Courier New\\", monospace",
+                                                  "lineHeightBody": "1.4",
+                                                  "lineHeightHeading": "1.15",
+                                                },
+                                                "textColor": "#302839",
+                                                "white": "#FFFFFF",
+                                                "whiteDark": "#fbfbfc",
+                                                "yellow": "#ecc844",
+                                                "yellowDark": "#e6bc23",
+                                                "yellowLight": "#FFF15E",
+                                                "yellowLightest": "#FFFDF7",
+                                                "yellowOrange": "#f9a66d",
+                                                "yellowOrangeDark": "#E08D54",
+                                                "yellowOrangeLight": "#FFC087",
+                                                "zIndex": Object {
+                                                  "dropdown": 1001,
+                                                  "dropdownAutocomplete": Object {
+                                                    "actor": 1008,
+                                                    "menu": 1007,
+                                                  },
+                                                  "header": 1000,
+                                                  "modal": 10000,
+                                                  "orgAndUserMenu": 1003,
+                                                  "sidebar": 1002,
+                                                  "toast": 10001,
+                                                },
+                                              }
+                                            }
+                                          >
+                                            <Button
+                                              className="btn btn-default css-1158k9d-StyledInstallButton ew9bjim4"
+                                              disabled={false}
+                                              onClick={[Function]}
+                                              size="small"
+                                              theme={
+                                                Object {
+                                                  "alert": Object {
+                                                    "attention": Object {
+                                                      "background": "#F09E71",
+                                                      "backgroundLight": "#ECBFA6",
+                                                      "border": "#D0816D",
+                                                    },
+                                                    "default": Object {
+                                                      "background": "#BDB4C7",
+                                                      "backgroundLight": "#FAF9FB",
+                                                      "border": "#E2DBE8",
+                                                    },
+                                                    "error": Object {
+                                                      "background": "#e03e2f",
+                                                      "backgroundLight": "#FDF6F5",
+                                                      "border": "#E7C0BC",
+                                                      "iconColor": "#C72516",
+                                                      "textLight": "#92635f",
+                                                    },
+                                                    "info": Object {
+                                                      "background": "#3B6ECC",
+                                                      "backgroundLight": "#F5FAFE",
+                                                      "border": "#B5D6ED",
+                                                      "iconColor": "#3B6ECC",
+                                                    },
+                                                    "success": Object {
+                                                      "background": "#57be8c",
+                                                      "backgroundLight": "#F8FCF7",
+                                                      "border": "#BBD6B3",
+                                                      "iconColor": "#3EA573",
+                                                    },
+                                                    "warn": Object {
+                                                      "background": "#ecc844",
+                                                      "backgroundLight": "#FFFDF7",
+                                                      "border": "#E1D697",
+                                                      "iconColor": "#e6bc23",
+                                                    },
+                                                    "warning": Object {
+                                                      "background": "#ecc844",
+                                                      "backgroundLight": "#FFFDF7",
+                                                      "border": "#E1D697",
+                                                      "iconColor": "#e6bc23",
+                                                    },
+                                                  },
+                                                  "background": "#fff",
+                                                  "blue": "#3B6ECC",
+                                                  "blueDark": "#2F58A3",
+                                                  "blueLight": "#628BD6",
+                                                  "borderDark": "#D1CAD8",
+                                                  "borderLight": "#E2DBE8",
+                                                  "borderLighter": "#f9f6fd",
+                                                  "borderRadius": "4px",
+                                                  "breakpoints": Array [
+                                                    "768px",
+                                                    "992px",
+                                                    "1200px",
+                                                  ],
+                                                  "button": Object {
+                                                    "borderRadius": "3px",
+                                                    "danger": Object {
+                                                      "background": "#e03e2f",
+                                                      "backgroundActive": "#bf2a1d",
+                                                      "border": "#bf2a1d",
+                                                      "borderActive": "#7d1c13",
+                                                      "color": "#FFFFFF",
+                                                    },
+                                                    "default": Object {
+                                                      "background": "#FFFFFF",
+                                                      "backgroundActive": "#FFFFFF",
+                                                      "border": "#d8d2de",
+                                                      "borderActive": "#c9c0d1",
+                                                      "color": "#2f2936",
+                                                      "colorActive": "#161319",
+                                                    },
+                                                    "disabled": Object {
+                                                      "background": "#FFFFFF",
+                                                      "backgroundActive": "#FFFFFF",
+                                                      "border": "#e3e5e6",
+                                                      "borderActive": "#e3e5e6",
+                                                      "color": "#ced3d6",
+                                                    },
+                                                    "link": Object {
+                                                      "background": "transparent",
+                                                      "backgroundActive": "transparent",
+                                                      "color": "#3B6ECC",
+                                                    },
+                                                    "primary": Object {
+                                                      "background": "#6C5FC7",
+                                                      "backgroundActive": "#4e3fb4",
+                                                      "border": "#3d328e",
+                                                      "borderActive": "#352b7b",
+                                                      "color": "#FFFFFF",
+                                                    },
+                                                    "success": Object {
+                                                      "background": "#3fa372",
+                                                      "backgroundActive": "#57be8c",
+                                                      "border": "#7ccca5",
+                                                      "borderActive": "#7ccca5",
+                                                      "color": "#FFFFFF",
+                                                    },
+                                                  },
+                                                  "charts": Object {
+                                                    "colors": Array [
+                                                      "#444674",
+                                                      "#524a7e",
+                                                      "#624d84",
+                                                      "#744f88",
+                                                      "#865189",
+                                                      "#985389",
+                                                      "#aa5488",
+                                                      "#bc5585",
+                                                      "#cd5681",
+                                                      "#df567c",
+                                                      "#e86070",
+                                                      "#ed6c64",
+                                                      "#f17959",
+                                                      "#f4854e",
+                                                      "#f59242",
+                                                      "#f59e35",
+                                                      "#f4aa27",
+                                                      "#f2b712",
+                                                    ],
+                                                    "getColorPalette": [Function],
+                                                    "previousPeriod": "#BDB4C7",
+                                                  },
+                                                  "diff": Object {
+                                                    "added": "#d8f0e4",
+                                                    "addedRow": "#f5fbf8",
+                                                    "removed": "#f7ceca",
+                                                    "removedRow": "#fcefee",
+                                                  },
+                                                  "disabled": "#ced3d6",
+                                                  "dropShadowHeavy": "0 1px 4px 1px rgba(47,40,55,0.08), 0 4px 16px 0 rgba(47,40,55,0.12)",
+                                                  "dropShadowLight": "0 2px 0 rgba(37, 11, 54, 0.04)",
+                                                  "error": "#e03e2f",
+                                                  "fontSizeExtraLarge": "18px",
+                                                  "fontSizeLarge": "16px",
+                                                  "fontSizeMedium": "14px",
+                                                  "fontSizeSmall": "12px",
+                                                  "gray1": "#BDB4C7",
+                                                  "gray2": "#9585A3",
+                                                  "gray3": "#645574",
+                                                  "gray4": "#4A3E56",
+                                                  "gray5": "#302839",
+                                                  "gray6": "#AFA3BB",
+                                                  "green": "#57be8c",
+                                                  "greenDark": "#3EA573",
+                                                  "greenLight": "#71D8A6",
+                                                  "greenTransparent": "rgba(87, 190, 140, 0.5)",
+                                                  "grid": 8,
+                                                  "offWhite": "#FAF9FB",
+                                                  "offWhite2": "#E7E1EC",
+                                                  "orange": "#ec5e44",
+                                                  "orangeDark": "#D3452B",
+                                                  "orangeLight": "#FF785E",
+                                                  "pink": "#F868BC",
+                                                  "pinkDark": "#DF4FA3",
+                                                  "pinkLight": "#FF82D6",
+                                                  "purple": "#6C5FC7",
+                                                  "purple2": "#6f617c",
+                                                  "purpleDark": "#5346AE",
+                                                  "purpleDarkest": "#392C94",
+                                                  "purpleLight": "#8679E1",
+                                                  "purpleLightest": "#9F92FA",
+                                                  "red": "#e03e2f",
+                                                  "redDark": "#C72516",
+                                                  "redLight": "#FA5849",
+                                                  "settings": Object {
+                                                    "containerWidth": "1140px",
+                                                    "headerHeight": "115px",
+                                                    "maxCrumbWidth": "240px",
+                                                    "sidebarWidth": "210px",
+                                                  },
+                                                  "sidebar": Object {
+                                                    "background": "#2f2936",
+                                                    "badgeSize": "22px",
+                                                    "collapsedWidth": "70px",
+                                                    "color": "#9586a5",
+                                                    "divider": "#493e54",
+                                                    "expandedWidth": "220px",
+                                                    "menuSpacing": "15px",
+                                                    "mobileHeight": "54px",
+                                                    "panel": Object {
+                                                      "headerHeight": "62px",
+                                                      "width": "320px",
+                                                    },
+                                                    "smallBadgeSize": "11px",
+                                                  },
+                                                  "similarity": Object {
+                                                    "colors": Array [
+                                                      "#ec5e44",
+                                                      "#f38259",
+                                                      "#f9a66d",
+                                                      "#98b480",
+                                                      "#57be8c",
+                                                    ],
+                                                    "empty": "#e2dee6",
+                                                  },
+                                                  "success": "#57be8c",
+                                                  "text": Object {
+                                                    "family": "\\"Rubik\\", \\"Avenir Next\\", sans-serif",
+                                                    "familyMono": "Monaco, Consolas, \\"Courier New\\", monospace",
+                                                    "lineHeightBody": "1.4",
+                                                    "lineHeightHeading": "1.15",
+                                                  },
+                                                  "textColor": "#302839",
+                                                  "white": "#FFFFFF",
+                                                  "whiteDark": "#fbfbfc",
+                                                  "yellow": "#ecc844",
+                                                  "yellowDark": "#e6bc23",
+                                                  "yellowLight": "#FFF15E",
+                                                  "yellowLightest": "#FFFDF7",
+                                                  "yellowOrange": "#f9a66d",
+                                                  "yellowOrangeDark": "#E08D54",
+                                                  "yellowOrangeLight": "#FFC087",
+                                                  "zIndex": Object {
+                                                    "dropdown": 1001,
+                                                    "dropdownAutocomplete": Object {
+                                                      "actor": 1008,
+                                                      "menu": 1007,
+                                                    },
+                                                    "header": 1000,
+                                                    "modal": 10000,
+                                                    "orgAndUserMenu": 1003,
+                                                    "sidebar": 1002,
+                                                    "toast": 10001,
+                                                  },
+                                                }
+                                              }
+                                            >
+                                              <StyledButton
+                                                aria-label="Install"
+                                                className="btn btn-default css-1158k9d-StyledInstallButton ew9bjim4"
+                                                disabled={false}
+                                                onClick={[Function]}
+                                                role="button"
+                                                size="small"
+                                                theme={
+                                                  Object {
+                                                    "alert": Object {
+                                                      "attention": Object {
+                                                        "background": "#F09E71",
+                                                        "backgroundLight": "#ECBFA6",
+                                                        "border": "#D0816D",
+                                                      },
+                                                      "default": Object {
+                                                        "background": "#BDB4C7",
+                                                        "backgroundLight": "#FAF9FB",
+                                                        "border": "#E2DBE8",
+                                                      },
+                                                      "error": Object {
+                                                        "background": "#e03e2f",
+                                                        "backgroundLight": "#FDF6F5",
+                                                        "border": "#E7C0BC",
+                                                        "iconColor": "#C72516",
+                                                        "textLight": "#92635f",
+                                                      },
+                                                      "info": Object {
+                                                        "background": "#3B6ECC",
+                                                        "backgroundLight": "#F5FAFE",
+                                                        "border": "#B5D6ED",
+                                                        "iconColor": "#3B6ECC",
+                                                      },
+                                                      "success": Object {
+                                                        "background": "#57be8c",
+                                                        "backgroundLight": "#F8FCF7",
+                                                        "border": "#BBD6B3",
+                                                        "iconColor": "#3EA573",
+                                                      },
+                                                      "warn": Object {
+                                                        "background": "#ecc844",
+                                                        "backgroundLight": "#FFFDF7",
+                                                        "border": "#E1D697",
+                                                        "iconColor": "#e6bc23",
+                                                      },
+                                                      "warning": Object {
+                                                        "background": "#ecc844",
+                                                        "backgroundLight": "#FFFDF7",
+                                                        "border": "#E1D697",
+                                                        "iconColor": "#e6bc23",
+                                                      },
+                                                    },
+                                                    "background": "#fff",
+                                                    "blue": "#3B6ECC",
+                                                    "blueDark": "#2F58A3",
+                                                    "blueLight": "#628BD6",
+                                                    "borderDark": "#D1CAD8",
+                                                    "borderLight": "#E2DBE8",
+                                                    "borderLighter": "#f9f6fd",
+                                                    "borderRadius": "4px",
+                                                    "breakpoints": Array [
+                                                      "768px",
+                                                      "992px",
+                                                      "1200px",
+                                                    ],
+                                                    "button": Object {
+                                                      "borderRadius": "3px",
+                                                      "danger": Object {
+                                                        "background": "#e03e2f",
+                                                        "backgroundActive": "#bf2a1d",
+                                                        "border": "#bf2a1d",
+                                                        "borderActive": "#7d1c13",
+                                                        "color": "#FFFFFF",
+                                                      },
+                                                      "default": Object {
+                                                        "background": "#FFFFFF",
+                                                        "backgroundActive": "#FFFFFF",
+                                                        "border": "#d8d2de",
+                                                        "borderActive": "#c9c0d1",
+                                                        "color": "#2f2936",
+                                                        "colorActive": "#161319",
+                                                      },
+                                                      "disabled": Object {
+                                                        "background": "#FFFFFF",
+                                                        "backgroundActive": "#FFFFFF",
+                                                        "border": "#e3e5e6",
+                                                        "borderActive": "#e3e5e6",
+                                                        "color": "#ced3d6",
+                                                      },
+                                                      "link": Object {
+                                                        "background": "transparent",
+                                                        "backgroundActive": "transparent",
+                                                        "color": "#3B6ECC",
+                                                      },
+                                                      "primary": Object {
+                                                        "background": "#6C5FC7",
+                                                        "backgroundActive": "#4e3fb4",
+                                                        "border": "#3d328e",
+                                                        "borderActive": "#352b7b",
+                                                        "color": "#FFFFFF",
+                                                      },
+                                                      "success": Object {
+                                                        "background": "#3fa372",
+                                                        "backgroundActive": "#57be8c",
+                                                        "border": "#7ccca5",
+                                                        "borderActive": "#7ccca5",
+                                                        "color": "#FFFFFF",
+                                                      },
+                                                    },
+                                                    "charts": Object {
+                                                      "colors": Array [
+                                                        "#444674",
+                                                        "#524a7e",
+                                                        "#624d84",
+                                                        "#744f88",
+                                                        "#865189",
+                                                        "#985389",
+                                                        "#aa5488",
+                                                        "#bc5585",
+                                                        "#cd5681",
+                                                        "#df567c",
+                                                        "#e86070",
+                                                        "#ed6c64",
+                                                        "#f17959",
+                                                        "#f4854e",
+                                                        "#f59242",
+                                                        "#f59e35",
+                                                        "#f4aa27",
+                                                        "#f2b712",
+                                                      ],
+                                                      "getColorPalette": [Function],
+                                                      "previousPeriod": "#BDB4C7",
+                                                    },
+                                                    "diff": Object {
+                                                      "added": "#d8f0e4",
+                                                      "addedRow": "#f5fbf8",
+                                                      "removed": "#f7ceca",
+                                                      "removedRow": "#fcefee",
+                                                    },
+                                                    "disabled": "#ced3d6",
+                                                    "dropShadowHeavy": "0 1px 4px 1px rgba(47,40,55,0.08), 0 4px 16px 0 rgba(47,40,55,0.12)",
+                                                    "dropShadowLight": "0 2px 0 rgba(37, 11, 54, 0.04)",
+                                                    "error": "#e03e2f",
+                                                    "fontSizeExtraLarge": "18px",
+                                                    "fontSizeLarge": "16px",
+                                                    "fontSizeMedium": "14px",
+                                                    "fontSizeSmall": "12px",
+                                                    "gray1": "#BDB4C7",
+                                                    "gray2": "#9585A3",
+                                                    "gray3": "#645574",
+                                                    "gray4": "#4A3E56",
+                                                    "gray5": "#302839",
+                                                    "gray6": "#AFA3BB",
+                                                    "green": "#57be8c",
+                                                    "greenDark": "#3EA573",
+                                                    "greenLight": "#71D8A6",
+                                                    "greenTransparent": "rgba(87, 190, 140, 0.5)",
+                                                    "grid": 8,
+                                                    "offWhite": "#FAF9FB",
+                                                    "offWhite2": "#E7E1EC",
+                                                    "orange": "#ec5e44",
+                                                    "orangeDark": "#D3452B",
+                                                    "orangeLight": "#FF785E",
+                                                    "pink": "#F868BC",
+                                                    "pinkDark": "#DF4FA3",
+                                                    "pinkLight": "#FF82D6",
+                                                    "purple": "#6C5FC7",
+                                                    "purple2": "#6f617c",
+                                                    "purpleDark": "#5346AE",
+                                                    "purpleDarkest": "#392C94",
+                                                    "purpleLight": "#8679E1",
+                                                    "purpleLightest": "#9F92FA",
+                                                    "red": "#e03e2f",
+                                                    "redDark": "#C72516",
+                                                    "redLight": "#FA5849",
+                                                    "settings": Object {
+                                                      "containerWidth": "1140px",
+                                                      "headerHeight": "115px",
+                                                      "maxCrumbWidth": "240px",
+                                                      "sidebarWidth": "210px",
+                                                    },
+                                                    "sidebar": Object {
+                                                      "background": "#2f2936",
+                                                      "badgeSize": "22px",
+                                                      "collapsedWidth": "70px",
+                                                      "color": "#9586a5",
+                                                      "divider": "#493e54",
+                                                      "expandedWidth": "220px",
+                                                      "menuSpacing": "15px",
+                                                      "mobileHeight": "54px",
+                                                      "panel": Object {
+                                                        "headerHeight": "62px",
+                                                        "width": "320px",
+                                                      },
+                                                      "smallBadgeSize": "11px",
+                                                    },
+                                                    "similarity": Object {
+                                                      "colors": Array [
+                                                        "#ec5e44",
+                                                        "#f38259",
+                                                        "#f9a66d",
+                                                        "#98b480",
+                                                        "#57be8c",
+                                                      ],
+                                                      "empty": "#e2dee6",
+                                                    },
+                                                    "success": "#57be8c",
+                                                    "text": Object {
+                                                      "family": "\\"Rubik\\", \\"Avenir Next\\", sans-serif",
+                                                      "familyMono": "Monaco, Consolas, \\"Courier New\\", monospace",
+                                                      "lineHeightBody": "1.4",
+                                                      "lineHeightHeading": "1.15",
+                                                    },
+                                                    "textColor": "#302839",
+                                                    "white": "#FFFFFF",
+                                                    "whiteDark": "#fbfbfc",
+                                                    "yellow": "#ecc844",
+                                                    "yellowDark": "#e6bc23",
+                                                    "yellowLight": "#FFF15E",
+                                                    "yellowLightest": "#FFFDF7",
+                                                    "yellowOrange": "#f9a66d",
+                                                    "yellowOrangeDark": "#E08D54",
+                                                    "yellowOrangeLight": "#FFC087",
+                                                    "zIndex": Object {
+                                                      "dropdown": 1001,
+                                                      "dropdownAutocomplete": Object {
+                                                        "actor": 1008,
+                                                        "menu": 1007,
+                                                      },
+                                                      "header": 1000,
+                                                      "modal": 10000,
+                                                      "orgAndUserMenu": 1003,
+                                                      "sidebar": 1002,
+                                                      "toast": 10001,
+                                                    },
+                                                  }
+                                                }
+                                              >
+                                                <Component
+                                                  aria-label="Install"
+                                                  className="btn btn-default ew9bjim4 css-bu1b4v-StyledButton-getColors-StyledInstallButton eqrebog0"
+                                                  disabled={false}
+                                                  onClick={[Function]}
+                                                  role="button"
+                                                  size="small"
+                                                >
+                                                  <button
+                                                    aria-label="Install"
+                                                    className="btn btn-default ew9bjim4 css-bu1b4v-StyledButton-getColors-StyledInstallButton eqrebog0"
+                                                    disabled={false}
+                                                    onClick={[Function]}
+                                                    role="button"
+                                                    size="small"
+                                                  >
+                                                    <ButtonLabel
+                                                      size="small"
+                                                    >
+                                                      <Component
+                                                        className="css-7ui8bl-ButtonLabel eqrebog1"
+                                                        size="small"
+                                                      >
+                                                        <span
+                                                          className="css-7ui8bl-ButtonLabel eqrebog1"
+                                                        >
+                                                          Install
+                                                        </span>
+                                                      </Component>
+                                                    </ButtonLabel>
+                                                  </button>
+                                                </Component>
+                                              </StyledButton>
+                                            </Button>
+                                          </Component>
+                                        </WithTheme(Component)>
+                                      </StyledInstallButton>
+                                    </div>
+                                  </Base>
+                                </Box>
                                 <Box>
                                   <Base
                                     className="css-roynbj"
@@ -917,62 +1667,72 @@ exports[`Organization Developer Settings renders developer settings it lists sen
                                         disabled={false}
                                         icon="icon-trash"
                                         onClick={[Function]}
+                                        size="small"
                                       >
                                         <StyledButton
                                           className="btn btn-default"
                                           disabled={false}
                                           onClick={[Function]}
                                           role="button"
+                                          size="small"
                                         >
                                           <Component
-                                            className="btn btn-default css-cmo08-StyledButton-getColors eqrebog0"
+                                            className="btn btn-default css-dkprmi-StyledButton-getColors eqrebog0"
                                             disabled={false}
                                             onClick={[Function]}
                                             role="button"
+                                            size="small"
                                           >
                                             <button
-                                              className="btn btn-default css-cmo08-StyledButton-getColors eqrebog0"
+                                              className="btn btn-default css-dkprmi-StyledButton-getColors eqrebog0"
                                               disabled={false}
                                               onClick={[Function]}
                                               role="button"
+                                              size="small"
                                             >
-                                              <ButtonLabel>
+                                              <ButtonLabel
+                                                size="small"
+                                              >
                                                 <Component
-                                                  className="css-ga4b18-ButtonLabel eqrebog1"
+                                                  className="css-7ui8bl-ButtonLabel eqrebog1"
+                                                  size="small"
                                                 >
                                                   <span
-                                                    className="css-ga4b18-ButtonLabel eqrebog1"
+                                                    className="css-7ui8bl-ButtonLabel eqrebog1"
                                                   >
                                                     <Icon
                                                       hasChildren={false}
+                                                      size="small"
                                                     >
                                                       <Component
                                                         className="css-ljhpxy-Icon eqrebog2"
                                                         hasChildren={false}
+                                                        size="small"
                                                       >
                                                         <span
                                                           className="css-ljhpxy-Icon eqrebog2"
+                                                          size="small"
                                                         >
                                                           <StyledInlineSvg
-                                                            size="16px"
+                                                            size="12px"
                                                             src="icon-trash"
                                                           >
                                                             <InlineSvg
                                                               className="css-1ov3rcq-StyledInlineSvg eqrebog3"
-                                                              size="16px"
+                                                              size="12px"
                                                               src="icon-trash"
                                                             >
                                                               <StyledSvg
                                                                 className="css-1ov3rcq-StyledInlineSvg eqrebog3"
-                                                                height="16px"
+                                                                height="12px"
                                                                 viewBox={Object {}}
-                                                                width="16px"
+                                                                width="12px"
                                                               >
                                                                 <svg
                                                                   className="eqrebog3 css-1jjmnki-StyledSvg-StyledInlineSvg e2idor0"
-                                                                  height="16px"
+                                                                  height="12px"
                                                                   viewBox={Object {}}
-                                                                  width="16px"
+                                                                  width="12px"
                                                                 >
                                                                   <use
                                                                     href="#test"
@@ -997,7 +1757,7 @@ exports[`Organization Developer Settings renders developer settings it lists sen
                                 </Box>
                               </div>
                             </Base>
-                          </Flex>
+                          </StyledButtonGroup>
                         </div>
                       </Base>
                     </SentryAppItem>
@@ -1013,7 +1773,7 @@ exports[`Organization Developer Settings renders developer settings it lists sen
 </OrganizationDeveloperSettings>
 `;
 
-exports[`Organization Developer Settings renders developer settings it shows empty state 1`] = `
+exports[`Organization Developer Settings when no Apps exist displays empty state 1`] = `
 <OrganizationDeveloperSettings
   params={
     Object {

+ 151 - 23
tests/js/spec/views/settings/organizationDeveloperSettings/index.spec.jsx

@@ -1,43 +1,171 @@
 /*global global*/
 import React from 'react';
+import {browserHistory} from 'react-router';
 
 import {Client} from 'app/api';
 import {mount} from 'enzyme';
 import OrganizationDeveloperSettings from 'app/views/settings/organizationDeveloperSettings/index';
 
 describe('Organization Developer Settings', function() {
-  beforeEach(function() {
+  let org = TestStubs.Organization();
+  let sentryApp = TestStubs.SentryApp();
+  let install = TestStubs.SentryAppInstallation({
+    organization: {slug: org.slug},
+    app: {slug: sentryApp.slug, uuid: 'f4d972ba-1177-4974-943e-4800fe8c7d05'},
+    code: '50624ecb-7aac-49d6-934a-83e53677560f',
+  });
+
+  let routerContext = TestStubs.routerContext();
+
+  beforeEach(() => {
     Client.clearMockResponses();
   });
 
-  describe('renders developer settings', () => {
-    const org = TestStubs.Organization();
-    const sentryApp = TestStubs.SentryApp();
-    const routerContext = TestStubs.routerContext();
-    it('it shows empty state', function() {
-      Client.addMockResponse({
-        url: `/organizations/${org.slug}/sentry-apps/`,
-        body: [],
-      });
-      const wrapper = mount(
-        <OrganizationDeveloperSettings params={{orgId: org.slug}} />,
-        routerContext
-      );
+  describe('when no Apps exist', () => {
+    Client.addMockResponse({
+      url: `/organizations/${org.slug}/sentry-apps/`,
+      body: [],
+    });
+
+    Client.addMockResponse({
+      url: `/organizations/${org.slug}/sentry-app-installations/`,
+      body: [],
+    });
+
+    const wrapper = mount(
+      <OrganizationDeveloperSettings params={{orgId: org.slug}} />,
+      routerContext
+    );
+
+    it('displays empty state', () => {
       expect(wrapper).toMatchSnapshot();
       expect(wrapper.exists('EmptyMessage')).toBe(true);
     });
+  });
 
-    it('it lists sentry apps for an organization', function() {
-      Client.addMockResponse({
-        url: `/organizations/${org.slug}/sentry-apps/`,
-        body: [sentryApp],
-      });
-      const wrapper = mount(
-        <OrganizationDeveloperSettings params={{orgId: org.slug}} />,
-        routerContext
-      );
+  describe('when Apps exist', () => {
+    Client.addMockResponse({
+      url: `/organizations/${org.slug}/sentry-apps/`,
+      body: [sentryApp],
+    });
+
+    Client.addMockResponse({
+      url: `/organizations/${org.slug}/sentry-app-installations/`,
+      body: [],
+    });
+
+    let wrapper = mount(
+      <OrganizationDeveloperSettings params={{orgId: org.slug}} />,
+      routerContext
+    );
+
+    it('displays all Apps owned by the Org', () => {
       expect(wrapper).toMatchSnapshot();
       expect(wrapper.find('SentryApplicationRow').prop('app').name).toBe('Sample App');
     });
+
+    describe('when installing', () => {
+      beforeEach(() => {
+        Client.addMockResponse({
+          url: `/organizations/${org.slug}/sentry-app-installations/`,
+          method: 'POST',
+          body: install,
+        });
+      });
+
+      it('disallows installation when already installed', () => {
+        Client.addMockResponse({
+          url: `/organizations/${org.slug}/sentry-apps/`,
+          method: 'GET',
+          body: [sentryApp],
+        });
+
+        Client.addMockResponse({
+          url: `/organizations/${org.slug}/sentry-app-installations/`,
+          body: [install],
+          method: 'GET',
+        });
+
+        wrapper = mount(
+          <OrganizationDeveloperSettings params={{orgId: org.slug}} />,
+          routerContext
+        );
+
+        expect(wrapper.find('StyledInstallButton').prop('disabled')).toBe(true);
+      });
+
+      it('redirects the user to the Integrations page when a redirectUrl is not set', () => {
+        Client.addMockResponse({
+          url: `/organizations/${org.slug}/sentry-apps/`,
+          body: [TestStubs.SentryApp({redirectUrl: null})],
+        });
+
+        Client.addMockResponse({
+          url: `/organizations/${org.slug}/sentry-app-installations/`,
+          body: [],
+        });
+
+        wrapper = mount(
+          <OrganizationDeveloperSettings params={{orgId: org.slug}} />,
+          routerContext
+        );
+
+        wrapper.find('StyledInstallButton').simulate('click');
+
+        expect(browserHistory.push).toHaveBeenCalledWith(
+          `/settings/${org.slug}/integrations/`
+        );
+      });
+
+      it('redirects the user to the App when a redirectUrl is set', () => {
+        Client.addMockResponse({
+          url: `/organizations/${org.slug}/sentry-apps/`,
+          body: [sentryApp],
+        });
+
+        Client.addMockResponse({
+          url: `/organizations/${org.slug}/sentry-app-installations/`,
+          body: [],
+        });
+
+        wrapper = mount(
+          <OrganizationDeveloperSettings params={{orgId: org.slug}} />,
+          routerContext
+        );
+
+        wrapper.find('StyledInstallButton').simulate('click');
+
+        expect(browserHistory.push).toHaveBeenCalledWith(
+          `${sentryApp.redirectUrl}?code=${install.code}&installationId=${install.uuid}`
+        );
+      });
+
+      it('handles a redirectUrl with pre-existing query params', () => {
+        const sentryAppWithQuery = TestStubs.SentryApp({
+          redirectUrl: 'https://example.com/setup?hello=1',
+        });
+
+        Client.addMockResponse({
+          url: `/organizations/${org.slug}/sentry-apps/`,
+          body: [sentryAppWithQuery],
+        });
+
+        Client.addMockResponse({
+          url: `/organizations/${org.slug}/sentry-app-installations/`,
+          body: [],
+        });
+
+        wrapper = mount(
+          <OrganizationDeveloperSettings params={{orgId: org.slug}} />,
+          routerContext
+        );
+
+        wrapper.find('StyledInstallButton').simulate('click');
+
+        expect(browserHistory.push).toHaveBeenCalledWith(
+          `https://example.com/setup?code=${install.code}&hello=1&installationId=${install.uuid}`
+        );
+      });
+    });
   });
 });

+ 1 - 1
yarn.lock

@@ -8864,7 +8864,7 @@ parse5@^3.0.1, parse5@^3.0.2:
   dependencies:
     "@types/node" "*"
 
-parseurl@~1.3.2:
+parseurl@^1.3.2, parseurl@~1.3.2:
   version "1.3.2"
   resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.2.tgz#fc289d4ed8993119460c156253262cdc8de65bf3"
   integrity sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=