Skip to content

feat(app-platform): Integration "Learn More" modal #12638

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 15, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/sentry/static/sentry/app/actionCreators/modal.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -151,3 +151,13 @@ export function openSentryAppPermissionModal(options = {}) {
});
});
}

export function openSentryAppDetailsModal(options = {}) {
import(/* webpackChunkName: "SentryAppDetailsModal" */ 'app/components/modals/sentryAppDetailsModal')
.then(mod => mod.default)
.then(Modal => {
openModal(deps => <Modal {...deps} {...options} />, {
modalClassName: 'sentry-app-details',
});
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import {Box, Flex} from 'grid-emotion';
import PropTypes from 'prop-types';
import React from 'react';
import styled from 'react-emotion';

import Access from 'app/components/acl/access';
import Button from 'app/components/button';
import PluginIcon from 'app/plugins/components/pluginIcon';
import SentryTypes from 'app/sentryTypes';
import space from 'app/styles/space';
import {t} from 'app/locale';

export default class SentryAppDetailsModal extends React.Component {
static propTypes = {
sentryApp: SentryTypes.SentryApplication.isRequired,
organization: SentryTypes.Organization.isRequired,
onInstall: PropTypes.func.isRequired,
isInstalled: PropTypes.bool.isRequired,
closeModal: PropTypes.func.isRequired,
};

render() {
const {sentryApp, closeModal, onInstall, isInstalled, organization} = this.props;

return (
<React.Fragment>
<Flex align="center" mb={2}>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm assuming this is copy-pasta from the other modals, but it seems weird that we have some inline styling and then some using the styled components. I feel like they should all be styled components

<PluginIcon pluginId={sentryApp.slug} size={50} />

<Flex pl={1} align="flex-start" direction="column" justify="center">
<Name>{sentryApp.name}</Name>
</Flex>
</Flex>

<Description>{sentryApp.overview}</Description>

<Metadata>
<Author flex={1}>{t('By %s', sentryApp.author)}</Author>
</Metadata>

<div className="modal-footer">
<Button size="small" onClick={closeModal}>
{t('Cancel')}
</Button>

<Access organization={organization} access={['org:integrations']}>
{({hasAccess}) =>
hasAccess && (
<Button
size="small"
priority="primary"
disabled={isInstalled}
onClick={onInstall}
style={{marginLeft: space(1)}}
>
{t('Install')}
</Button>
)
}
</Access>
</div>
</React.Fragment>
);
}
}

const Name = styled(Box)`
font-weight: bold;
font-size: 1.4em;
margin-bottom: ${space(1)};
`;

const Description = styled.div`
font-size: 1.5rem;
line-height: 2.1rem;
margin-bottom: ${space(2)};

li {
margin-bottom: 6px;
}
`;

const Metadata = styled(Flex)`
font-size: 0.9em;
margin-bottom: ${space(2)};

a {
margin-left: ${space(1)};
}
`;

const Author = styled(Box)`
color: ${p => p.theme.gray2};
`;
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import PermissionAlert from 'app/views/settings/organization/permissionAlert';
import ProviderRow from 'app/views/organizationIntegrations/providerRow';
import SettingsPageHeader from 'app/views/settings/components/settingsPageHeader';
import SentryAppInstallations from 'app/views/organizationIntegrations/sentryAppInstallations';
import SentryTypes from 'app/sentryTypes';
import withOrganization from 'app/utils/withOrganization';

class OrganizationIntegrations extends AsyncComponent {
Expand All @@ -24,6 +25,10 @@ class OrganizationIntegrations extends AsyncComponent {
reloadOnVisible = true;
shouldReloadOnVisible = true;

static propTypes = {
organization: SentryTypes.Organization,
};

componentDidMount() {
analytics('integrations.index_viewed', {
org_id: parseInt(this.props.organization.id, 10),
Expand Down Expand Up @@ -151,13 +156,14 @@ class OrganizationIntegrations extends AsyncComponent {
}

renderSentryApps(apps, key) {
const {organization} = this.props;
const {appInstalls} = this.state;

return (
<IntegrationRow key={`row-${key}`}>
<SentryAppInstallations
key={key}
orgId={this.props.params.orgId}
organization={organization}
installs={appInstalls}
applications={apps}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import PropTypes from 'prop-types';
import React from 'react';
import {groupBy} from 'lodash';

import {addErrorMessage, addSuccessMessage} from 'app/actionCreators/indicator';
import SentryApplicationRow from 'app/views/settings/organizationDeveloperSettings/sentryApplicationRow';
import SentryTypes from 'app/sentryTypes';
import {t} from 'app/locale';
import {
installSentryApp,
Expand All @@ -15,7 +17,7 @@ import withApi from 'app/utils/withApi';
class SentryAppInstallations extends React.Component {
static propTypes = {
api: PropTypes.object,
orgId: PropTypes.string.isRequired,
organization: SentryTypes.Organization.isRequired,
installs: PropTypes.array.isRequired,
applications: PropTypes.array.isRequired,
};
Expand All @@ -31,7 +33,7 @@ class SentryAppInstallations extends React.Component {
redirectUser = data => {
const {install, app} = data;
const {installs} = this.state;
const {orgId} = this.props;
const {organization} = this.props;

if (!app.redirectUrl) {
addSuccessMessage(t(`${app.slug} successfully installed.`));
Expand All @@ -40,16 +42,16 @@ class SentryAppInstallations extends React.Component {
const queryParams = {
installationId: install.uuid,
code: install.code,
orgSlug: orgId,
orgSlug: organization.slug,
};
const redirectUrl = addQueryParamsToExistingUrl(app.redirectUrl, queryParams);
window.location.assign(redirectUrl);
}
};

install = app => {
const {orgId, api} = this.props;
installSentryApp(api, orgId, app).then(
const {organization, api} = this.props;
installSentryApp(api, organization.slug, app).then(
data => {
this.redirectUser({install: {...data}, app: {...app}});
},
Expand All @@ -72,17 +74,17 @@ class SentryAppInstallations extends React.Component {
};

openModal = app => {
const {orgId} = this.props;
const {organization} = this.props;
const onInstall = () => this.install(app);
openSentryAppPermissionModal({app, orgId, onInstall});
openSentryAppPermissionModal({app, onInstall, orgId: organization.slug});
};

get installsByApp() {
return groupBy(this.state.installs, install => install.app.slug);
}

render() {
const {orgId} = this.props;
const {organization} = this.props;
const isEmpty = this.state.applications.length === 0;

return (
Expand All @@ -93,7 +95,7 @@ class SentryAppInstallations extends React.Component {
<SentryApplicationRow
key={app.uuid}
app={app}
orgId={orgId}
organization={organization}
onInstall={() => this.openModal(app)}
onUninstall={this.uninstall}
installs={this.installsByApp[app.slug]}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
import React from 'react';

import AsyncView from 'app/views/asyncView';
import Button from 'app/components/button';
import EmptyMessage from 'app/views/settings/components/emptyMessage';
import {Panel, PanelBody, PanelHeader} from 'app/components/panels';
import {removeSentryApp} from 'app/actionCreators/sentryApps';
import SentryTypes from 'app/sentryTypes';
import SettingsPageHeader from 'app/views/settings/components/settingsPageHeader';
import SentryApplicationRow from 'app/views/settings/organizationDeveloperSettings/sentryApplicationRow';
import withOrganization from 'app/utils/withOrganization';
import {t} from 'app/locale';

export default class OrganizationDeveloperSettings extends AsyncView {
class OrganizationDeveloperSettings extends AsyncView {
static propTypes = {
organization: SentryTypes.Organization.isRequired,
};

getEndpoints() {
const {orgId} = this.props.params;

Expand All @@ -26,6 +33,7 @@ export default class OrganizationDeveloperSettings extends AsyncView {
};

renderBody() {
const {organization} = this.props;
const {orgId} = this.props.params;
const action = (
<Button
Expand All @@ -52,7 +60,7 @@ export default class OrganizationDeveloperSettings extends AsyncView {
<SentryApplicationRow
key={app.uuid}
app={app}
orgId={orgId}
organization={organization}
onRemoveApp={this.removeApp}
showPublishStatus={true}
/>
Expand All @@ -67,3 +75,5 @@ export default class OrganizationDeveloperSettings extends AsyncView {
);
}
}

export default withOrganization(OrganizationDeveloperSettings);
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react';
import {Box, Flex} from 'grid-emotion';
import {Link} from 'react-router';

import Access from 'app/components/acl/access';
import Button from 'app/components/button';
import Confirm from 'app/components/confirm';
Expand All @@ -15,11 +16,12 @@ import space from 'app/styles/space';
import {withTheme} from 'emotion-theming';
import CircleIndicator from 'app/components/circleIndicator';
import PluginIcon from 'app/plugins/components/pluginIcon';
import {openSentryAppDetailsModal} from 'app/actionCreators/modal';

export default class SentryApplicationRow extends React.PureComponent {
static propTypes = {
app: SentryTypes.SentryApplication,
orgId: PropTypes.string.isRequired,
organization: SentryTypes.Organization.isRequired,
installs: PropTypes.array,
onInstall: PropTypes.func,
onUninstall: PropTypes.func,
Expand Down Expand Up @@ -65,9 +67,25 @@ export default class SentryApplicationRow extends React.PureComponent {
);
}

get isInstalled() {
return this.props.installs && this.props.installs.length > 0;
}

openLearnMore = () => {
const {app, onInstall, organization} = this.props;
const isInstalled = !!this.isInstalled;

openSentryAppDetailsModal({
sentryApp: app,
isInstalled,
onInstall,
organization,
});
};

render() {
const {app, orgId, installs, showPublishStatus} = this.props;
const isInstalled = installs && installs.length > 0;
const {app, organization, installs, showPublishStatus} = this.props;
const isInstalled = this.isInstalled;

return (
<SentryAppItem>
Expand All @@ -76,7 +94,9 @@ export default class SentryApplicationRow extends React.PureComponent {
<SentryAppBox>
<SentryAppName>
{showPublishStatus ? (
<SentryAppLink to={`/settings/${orgId}/developer-settings/${app.slug}/`}>
<SentryAppLink
to={`/settings/${organization.slug}/developer-settings/${app.slug}/`}
>
{app.name}
</SentryAppLink>
) : (
Expand All @@ -89,11 +109,12 @@ export default class SentryApplicationRow extends React.PureComponent {
) : (
<React.Fragment>
<Status enabled={isInstalled} />
<StyledLink onClick={() => {}}>{t('Learn More')}</StyledLink>
<StyledLink onClick={this.openLearnMore}>{t('Learn More')}</StyledLink>
</React.Fragment>
)}
</SentryAppDetails>
</SentryAppBox>

{!showPublishStatus ? (
<Box>
{!isInstalled ? (
Expand Down
Loading