import {RouteComponentProps} from 'react-router'; import styled from '@emotion/styled'; import { addErrorMessage, addLoadingMessage, addSuccessMessage, } from 'sentry/actionCreators/indicator'; import {disablePlugin, enablePlugin} from 'sentry/actionCreators/plugins'; import {Button} from 'sentry/components/button'; import ExternalLink from 'sentry/components/links/externalLink'; import PluginConfig from 'sentry/components/pluginConfig'; import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import {Organization, Plugin, Project} from 'sentry/types'; import getDynamicText from 'sentry/utils/getDynamicText'; import {trackIntegrationAnalytics} from 'sentry/utils/integrationUtil'; import withPlugins from 'sentry/utils/withPlugins'; import DeprecatedAsyncView from 'sentry/views/deprecatedAsyncView'; import SettingsPageHeader from 'sentry/views/settings/components/settingsPageHeader'; type Props = { organization: Organization; plugins: { plugins: Plugin[]; }; project: Project; } & RouteComponentProps<{pluginId: string; projectId: string}, {}>; type State = { pluginDetails?: Plugin; } & DeprecatedAsyncView['state']; /** * There are currently two sources of truths for plugin details: * * 1) PluginsStore has a list of plugins, and this is where ENABLED state lives * 2) We fetch "plugin details" via API and save it to local state as `pluginDetails`. * This is because "details" call contains form `config` and the "list" endpoint does not. * The more correct way would be to pass `config` to PluginConfig and use plugin from * PluginsStore */ class ProjectPluginDetails extends DeprecatedAsyncView { componentDidUpdate(prevProps: Props, prevState: State) { super.componentDidUpdate(prevProps, prevState); if (prevProps.params.pluginId !== this.props.params.pluginId) { this.recordDetailsViewed(); } } componentDidMount() { super.componentDidMount(); this.recordDetailsViewed(); } recordDetailsViewed() { const {pluginId} = this.props.params; trackIntegrationAnalytics('integrations.details_viewed', { integration: pluginId, integration_type: 'plugin', view: 'plugin_details', organization: this.props.organization, }); } getTitle() { const {plugin} = this.state; if (plugin && plugin.name) { return plugin.name; } return 'Sentry'; } getEndpoints(): ReturnType { const {organization} = this.props; const {projectId, pluginId} = this.props.params; return [ [ 'pluginDetails', `/projects/${organization.slug}/${projectId}/plugins/${pluginId}/`, ], ]; } trimSchema(value) { return value.split('//')[1]; } handleReset = () => { const {organization} = this.props; const {projectId, pluginId} = this.props.params; addLoadingMessage(t('Saving changes\u2026')); trackIntegrationAnalytics('integrations.uninstall_clicked', { integration: pluginId, integration_type: 'plugin', view: 'plugin_details', organization: this.props.organization, }); this.api.request(`/projects/${organization.slug}/${projectId}/plugins/${pluginId}/`, { method: 'POST', data: {reset: true}, success: pluginDetails => { this.setState({pluginDetails}); addSuccessMessage(t('Plugin was reset')); trackIntegrationAnalytics('integrations.uninstall_completed', { integration: pluginId, integration_type: 'plugin', view: 'plugin_details', organization: this.props.organization, }); }, error: () => { addErrorMessage(t('An error occurred')); }, }); }; handleEnable = () => { const {organization, params} = this.props; enablePlugin({...params, orgId: organization.slug}); this.analyticsChangeEnableStatus(true); }; handleDisable = () => { const {organization, params} = this.props; disablePlugin({...params, orgId: organization.slug}); this.analyticsChangeEnableStatus(false); }; analyticsChangeEnableStatus = (enabled: boolean) => { const {pluginId} = this.props.params; const eventKey = enabled ? 'integrations.enabled' : 'integrations.disabled'; trackIntegrationAnalytics(eventKey, { integration: pluginId, integration_type: 'plugin', view: 'plugin_details', organization: this.props.organization, }); }; // Enabled state is handled via PluginsStore and not via plugins detail getEnabled() { const {pluginDetails} = this.state; const {plugins} = this.props; const plugin = plugins && plugins.plugins && plugins.plugins.find(({slug}) => slug === this.props.params.pluginId); return plugin ? plugin.enabled : pluginDetails && pluginDetails.enabled; } renderActions() { const {pluginDetails} = this.state; if (!pluginDetails) { return null; } const enabled = this.getEnabled(); const enable = ( {t('Enable Plugin')} ); const disable = ( {t('Disable Plugin')} ); const toggleEnable = enabled ? disable : enable; return (
{pluginDetails.canDisable && toggleEnable}
); } renderBody() { const {organization, project} = this.props; const {pluginDetails} = this.state; if (!pluginDetails) { return null; } return (

{t('Plugin Information')}

{t('Name')}
{pluginDetails.name}
{t('Author')}
{pluginDetails.author?.name}
{pluginDetails.author?.url && (
{t('URL')}
{this.trimSchema(pluginDetails.author.url)}
)}
{t('Version')}
{getDynamicText({ value: pluginDetails.version, fixed: '1.0.0', })}
{pluginDetails.description && (

{t('Description')}

{pluginDetails.description}

)} {pluginDetails.resourceLinks && (

{t('Resources')}

{pluginDetails.resourceLinks.map(({title, url}) => (
{title}
))}
)}
); } } export {ProjectPluginDetails}; export default withPlugins(ProjectPluginDetails); const StyledButton = styled(Button)` margin-right: ${space(0.75)}; `;