123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268 |
- import type {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 type {Plugin} from 'sentry/types/integrations';
- import type {Organization} from 'sentry/types/organization';
- import type {Project} from 'sentry/types/project';
- 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<Props, State> {
- 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?.name) {
- return plugin.name;
- }
- return 'Sentry';
- }
- getEndpoints(): ReturnType<DeprecatedAsyncView['getEndpoints']> {
- 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?.find(
- ({slug}) => slug === this.props.params.pluginId
- );
- return plugin ? plugin.enabled : pluginDetails?.enabled;
- }
- renderActions() {
- const {pluginDetails} = this.state;
- if (!pluginDetails) {
- return null;
- }
- const enabled = this.getEnabled();
- const enable = (
- <StyledButton size="sm" onClick={this.handleEnable}>
- {t('Enable Plugin')}
- </StyledButton>
- );
- const disable = (
- <StyledButton size="sm" priority="danger" onClick={this.handleDisable}>
- {t('Disable Plugin')}
- </StyledButton>
- );
- const toggleEnable = enabled ? disable : enable;
- return (
- <div className="pull-right">
- {pluginDetails.canDisable && toggleEnable}
- <Button size="sm" onClick={this.handleReset}>
- {t('Reset Configuration')}
- </Button>
- </div>
- );
- }
- renderBody() {
- const {project} = this.props;
- const {pluginDetails} = this.state;
- if (!pluginDetails) {
- return null;
- }
- return (
- <div>
- <SettingsPageHeader title={pluginDetails.name} action={this.renderActions()} />
- <div className="row">
- <div className="col-md-7">
- <PluginConfig
- project={project}
- plugin={pluginDetails}
- enabled={this.getEnabled()}
- onDisablePlugin={this.handleDisable}
- />
- </div>
- <div className="col-md-4 col-md-offset-1">
- <div className="pluginDetails-meta">
- <h4>{t('Plugin Information')}</h4>
- <dl className="flat">
- <dt>{t('Name')}</dt>
- <dd>{pluginDetails.name}</dd>
- <dt>{t('Author')}</dt>
- <dd>{pluginDetails.author?.name}</dd>
- {pluginDetails.author?.url && (
- <div>
- <dt>{t('URL')}</dt>
- <dd>
- <ExternalLink href={pluginDetails.author.url}>
- {this.trimSchema(pluginDetails.author.url)}
- </ExternalLink>
- </dd>
- </div>
- )}
- <dt>{t('Version')}</dt>
- <dd>
- {getDynamicText({
- value: pluginDetails.version,
- fixed: '1.0.0',
- })}
- </dd>
- </dl>
- {pluginDetails.description && (
- <div>
- <h4>{t('Description')}</h4>
- <p className="description">{pluginDetails.description}</p>
- </div>
- )}
- {pluginDetails.resourceLinks && (
- <div>
- <h4>{t('Resources')}</h4>
- <dl className="flat">
- {pluginDetails.resourceLinks.map(({title, url}) => (
- <dd key={url}>
- <ExternalLink href={url}>{title}</ExternalLink>
- </dd>
- ))}
- </dl>
- </div>
- )}
- </div>
- </div>
- </div>
- </div>
- );
- }
- }
- export {ProjectPluginDetails};
- export default withPlugins(ProjectPluginDetails);
- const StyledButton = styled(Button)`
- margin-right: ${space(0.75)};
- `;
|