details.tsx 7.6 KB

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