diff --git a/src/sentry/api/bases/sentryapps.py b/src/sentry/api/bases/sentryapps.py index 85f395bfa547..3e5bcc4af5f2 100644 --- a/src/sentry/api/bases/sentryapps.py +++ b/src/sentry/api/bases/sentryapps.py @@ -131,9 +131,10 @@ def has_object_permission(self, request, view, sentry_app): if is_active_superuser(request): return True - # User must be in the Org who owns the app. - if sentry_app.owner not in request.user.get_orgs(): - raise Http404 + # if app is unpublished, user must be in the Org who owns the app. + if not sentry_app.is_published: + if sentry_app.owner not in request.user.get_orgs(): + raise Http404 return ensure_scoped_permission( request, diff --git a/src/sentry/models/integrationfeature.py b/src/sentry/models/integrationfeature.py index 0a3b9513f5ec..0a06fe2dc99a 100644 --- a/src/sentry/models/integrationfeature.py +++ b/src/sentry/models/integrationfeature.py @@ -33,15 +33,15 @@ def as_str(cls, feature): return 'integrations-event-hooks' @classmethod - def description(cls, feature): + def description(cls, feature, name): if feature == cls.API: - return "This integration can utilize the Sentry API (with the permissions granted) to pull data or update resources in Sentry!" + return "%s can **utilize the Sentry API** to pull data or update resources in Sentry (with permissions granted, of course)." % name elif feature == cls.ISSUE_LINK: - return "This integration can allow your organization to create or link Sentry issues to another service!" + return "Organizations can **create or link Sentry issues** to another service." elif feature == cls.STACKTRACE_LINK: - return "This integration allows your organization to open a line in Sentry's stack trace in another service!" + return "Organizations can **open a line to Sentry's stack trace** in another service." elif feature == cls.EVENT_HOOKS: - return "This integration allows your organization to forward events to another service!" + return "%s allows organizations to **forward events to another service**." % name class IntegrationFeature(Model): @@ -68,4 +68,4 @@ def description(self): if self.user_description: return self.user_description else: - return Feature.description(self.feature) + return Feature.description(self.feature, self.sentry_app.name) diff --git a/src/sentry/static/sentry/app/components/modals/sentryAppDetailsModal.jsx b/src/sentry/static/sentry/app/components/modals/sentryAppDetailsModal.jsx index fd3b39f61ac7..2956175cbd78 100644 --- a/src/sentry/static/sentry/app/components/modals/sentryAppDetailsModal.jsx +++ b/src/sentry/static/sentry/app/components/modals/sentryAppDetailsModal.jsx @@ -9,9 +9,31 @@ import PluginIcon from 'app/plugins/components/pluginIcon'; import SentryTypes from 'app/sentryTypes'; import space from 'app/styles/space'; import {t} from 'app/locale'; -import marked from 'app/utils/marked'; -export default class SentryAppDetailsModal extends React.Component { +import AsyncComponent from 'app/components/asyncComponent'; +import HookStore from 'app/stores/hookStore'; +import marked, {singleLineRenderer} from 'app/utils/marked'; +import InlineSvg from 'app/components/inlineSvg'; +import Tag from 'app/views/settings/components/tag'; + +const defaultFeatureGateComponents = { + IntegrationFeatures: p => + p.children({ + disabled: false, + disabledReason: null, + ungatedFeatures: p.features, + gatedFeatureGroups: [], + }), + FeatureList: p => ( + + ), +}; + +export default class SentryAppDetailsModal extends AsyncComponent { static propTypes = { sentryApp: SentryTypes.SentryApplication.isRequired, organization: SentryTypes.Organization.isRequired, @@ -20,10 +42,35 @@ export default class SentryAppDetailsModal extends React.Component { closeModal: PropTypes.func.isRequired, }; - render() { + getEndpoints() { + const {sentryApp} = this.props; + return [['featureData', `/sentry-apps/${sentryApp.slug}/features/`]]; + } + + featureTags(features) { + return features.map(feature => { + const feat = feature.featureGate.replace(/integrations/g, ''); + return {feat.replace(/-/g, ' ')}; + }); + } + + renderBody() { const {sentryApp, closeModal, onInstall, isInstalled, organization} = this.props; + const {featureData} = this.state; + // Prepare the features list + const features = (featureData || []).map(f => ({ + featureGate: f.featureGate, + description: ( + + ), + })); + + const defaultHook = () => defaultFeatureGateComponents; + const featureHook = HookStore.get('integrations:feature-gates')[0] || defaultHook; + const {FeatureList, IntegrationFeatures} = featureHook(); const overview = sentryApp.overview || ''; + const featureProps = {organization, features}; return ( @@ -32,36 +79,43 @@ export default class SentryAppDetailsModal extends React.Component { {sentryApp.name} + {features.length && this.featureTags(features)} + {t('By %s', sentryApp.author)} -
- - - - {({hasAccess}) => - hasAccess && ( - - ) - } - -
+ + {({disabled, disabledReason}) => ( +
+ {disabled && } + + + + {({hasAccess}) => + hasAccess && ( + + ) + } + +
+ )} +
); } @@ -95,3 +149,19 @@ const Metadata = styled(Flex)` const Author = styled(Box)` color: ${p => p.theme.gray2}; `; + +const DisabledNotice = styled(({reason, ...p}) => ( + + + {reason} + +))` + color: ${p => p.theme.red}; + font-size: 0.9em; +`; + +const StyledTag = styled(Tag)` + &:not(:first-child) { + margin-left: ${space(0.5)}; + } +`; diff --git a/src/sentry/static/sentry/app/views/settings/organizationDeveloperSettings/sentryApplicationRow.jsx b/src/sentry/static/sentry/app/views/settings/organizationDeveloperSettings/sentryApplicationRow.jsx index 249b5afbe8ca..2a2937f96df5 100644 --- a/src/sentry/static/sentry/app/views/settings/organizationDeveloperSettings/sentryApplicationRow.jsx +++ b/src/sentry/static/sentry/app/views/settings/organizationDeveloperSettings/sentryApplicationRow.jsx @@ -130,7 +130,7 @@ export default class SentryApplicationRow extends React.PureComponent { {!isInstalled ? (