details.tsx 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  1. import {RouteComponentProps} from 'react-router';
  2. import styled from '@emotion/styled';
  3. import {
  4. addErrorMessage,
  5. addLoadingMessage,
  6. addSuccessMessage,
  7. } from 'sentry/actionCreators/indicator';
  8. import {disablePlugin, enablePlugin} from 'sentry/actionCreators/plugins';
  9. import Button from 'sentry/components/button';
  10. import ExternalLink from 'sentry/components/links/externalLink';
  11. import PluginConfig from 'sentry/components/pluginConfig';
  12. import {t} from 'sentry/locale';
  13. import space from 'sentry/styles/space';
  14. import {Organization, Plugin, Project} from 'sentry/types';
  15. import {trackIntegrationAnalytics} from 'sentry/utils/integrationUtil';
  16. import withPlugins from 'sentry/utils/withPlugins';
  17. import AsyncView from 'sentry/views/asyncView';
  18. import SettingsPageHeader from 'sentry/views/settings/components/settingsPageHeader';
  19. type Props = {
  20. organization: Organization;
  21. plugins: {
  22. plugins: Plugin[];
  23. };
  24. project: Project;
  25. } & RouteComponentProps<{orgId: string; pluginId: string; projectId: string}, {}>;
  26. type State = {
  27. pluginDetails?: Plugin;
  28. } & AsyncView['state'];
  29. /**
  30. * There are currently two sources of truths for plugin details:
  31. *
  32. * 1) PluginsStore has a list of plugins, and this is where ENABLED state lives
  33. * 2) We fetch "plugin details" via API and save it to local state as `pluginDetails`.
  34. * This is because "details" call contains form `config` and the "list" endpoint does not.
  35. * The more correct way would be to pass `config` to PluginConfig and use plugin from
  36. * PluginsStore
  37. */
  38. class ProjectPluginDetails extends AsyncView<Props, State> {
  39. componentDidUpdate(prevProps: Props, prevState: State) {
  40. super.componentDidUpdate(prevProps, prevState);
  41. if (prevProps.params.pluginId !== this.props.params.pluginId) {
  42. this.recordDetailsViewed();
  43. }
  44. }
  45. componentDidMount() {
  46. this.recordDetailsViewed();
  47. }
  48. recordDetailsViewed() {
  49. const {pluginId} = this.props.params;
  50. trackIntegrationAnalytics('integrations.details_viewed', {
  51. integration: pluginId,
  52. integration_type: 'plugin',
  53. view: 'plugin_details',
  54. organization: this.props.organization,
  55. });
  56. }
  57. getTitle() {
  58. const {plugin} = this.state;
  59. if (plugin && plugin.name) {
  60. return plugin.name;
  61. }
  62. return 'Sentry';
  63. }
  64. getEndpoints(): ReturnType<AsyncView['getEndpoints']> {
  65. const {projectId, orgId, pluginId} = this.props.params;
  66. return [['pluginDetails', `/projects/${orgId}/${projectId}/plugins/${pluginId}/`]];
  67. }
  68. trimSchema(value) {
  69. return value.split('//')[1];
  70. }
  71. handleReset = () => {
  72. const {projectId, orgId, pluginId} = this.props.params;
  73. addLoadingMessage(t('Saving changes\u2026'));
  74. trackIntegrationAnalytics('integrations.uninstall_clicked', {
  75. integration: pluginId,
  76. integration_type: 'plugin',
  77. view: 'plugin_details',
  78. organization: this.props.organization,
  79. });
  80. this.api.request(`/projects/${orgId}/${projectId}/plugins/${pluginId}/`, {
  81. method: 'POST',
  82. data: {reset: true},
  83. success: pluginDetails => {
  84. this.setState({pluginDetails});
  85. addSuccessMessage(t('Plugin was reset'));
  86. trackIntegrationAnalytics('integrations.uninstall_completed', {
  87. integration: pluginId,
  88. integration_type: 'plugin',
  89. view: 'plugin_details',
  90. organization: this.props.organization,
  91. });
  92. },
  93. error: () => {
  94. addErrorMessage(t('An error occurred'));
  95. },
  96. });
  97. };
  98. handleEnable = () => {
  99. enablePlugin(this.props.params);
  100. this.analyticsChangeEnableStatus(true);
  101. };
  102. handleDisable = () => {
  103. disablePlugin(this.props.params);
  104. this.analyticsChangeEnableStatus(false);
  105. };
  106. analyticsChangeEnableStatus = (enabled: boolean) => {
  107. const {pluginId} = this.props.params;
  108. const eventKey = enabled ? 'integrations.enabled' : 'integrations.disabled';
  109. trackIntegrationAnalytics(eventKey, {
  110. integration: pluginId,
  111. integration_type: 'plugin',
  112. view: 'plugin_details',
  113. organization: this.props.organization,
  114. });
  115. };
  116. // Enabled state is handled via PluginsStore and not via plugins detail
  117. getEnabled() {
  118. const {pluginDetails} = this.state;
  119. const {plugins} = this.props;
  120. const plugin =
  121. plugins &&
  122. plugins.plugins &&
  123. plugins.plugins.find(({slug}) => slug === this.props.params.pluginId);
  124. return plugin ? plugin.enabled : pluginDetails && pluginDetails.enabled;
  125. }
  126. renderActions() {
  127. const {pluginDetails} = this.state;
  128. if (!pluginDetails) {
  129. return null;
  130. }
  131. const enabled = this.getEnabled();
  132. const enable = (
  133. <StyledButton size="sm" onClick={this.handleEnable}>
  134. {t('Enable Plugin')}
  135. </StyledButton>
  136. );
  137. const disable = (
  138. <StyledButton size="sm" priority="danger" onClick={this.handleDisable}>
  139. {t('Disable Plugin')}
  140. </StyledButton>
  141. );
  142. const toggleEnable = enabled ? disable : enable;
  143. return (
  144. <div className="pull-right">
  145. {pluginDetails.canDisable && toggleEnable}
  146. <Button size="sm" onClick={this.handleReset}>
  147. {t('Reset Configuration')}
  148. </Button>
  149. </div>
  150. );
  151. }
  152. renderBody() {
  153. const {organization, project} = this.props;
  154. const {pluginDetails} = this.state;
  155. if (!pluginDetails) {
  156. return null;
  157. }
  158. return (
  159. <div>
  160. <SettingsPageHeader title={pluginDetails.name} action={this.renderActions()} />
  161. <div className="row">
  162. <div className="col-md-7">
  163. <PluginConfig
  164. organization={organization}
  165. project={project}
  166. data={pluginDetails}
  167. enabled={this.getEnabled()}
  168. onDisablePlugin={this.handleDisable}
  169. />
  170. </div>
  171. <div className="col-md-4 col-md-offset-1">
  172. <div className="pluginDetails-meta">
  173. <h4>{t('Plugin Information')}</h4>
  174. <dl className="flat">
  175. <dt>{t('Name')}</dt>
  176. <dd>{pluginDetails.name}</dd>
  177. <dt>{t('Author')}</dt>
  178. <dd>{pluginDetails.author?.name}</dd>
  179. {pluginDetails.author?.url && (
  180. <div>
  181. <dt>{t('URL')}</dt>
  182. <dd>
  183. <ExternalLink href={pluginDetails.author.url}>
  184. {this.trimSchema(pluginDetails.author.url)}
  185. </ExternalLink>
  186. </dd>
  187. </div>
  188. )}
  189. <dt>{t('Version')}</dt>
  190. <dd>{pluginDetails.version}</dd>
  191. </dl>
  192. {pluginDetails.description && (
  193. <div>
  194. <h4>{t('Description')}</h4>
  195. <p className="description">{pluginDetails.description}</p>
  196. </div>
  197. )}
  198. {pluginDetails.resourceLinks && (
  199. <div>
  200. <h4>{t('Resources')}</h4>
  201. <dl className="flat">
  202. {pluginDetails.resourceLinks.map(({title, url}) => (
  203. <dd key={url}>
  204. <ExternalLink href={url}>{title}</ExternalLink>
  205. </dd>
  206. ))}
  207. </dl>
  208. </div>
  209. )}
  210. </div>
  211. </div>
  212. </div>
  213. </div>
  214. );
  215. }
  216. }
  217. export {ProjectPluginDetails};
  218. export default withPlugins(ProjectPluginDetails);
  219. const StyledButton = styled(Button)`
  220. margin-right: ${space(0.75)};
  221. `;