integrationUtil.tsx 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  1. import capitalize from 'lodash/capitalize';
  2. import * as qs from 'query-string';
  3. import {Result} from 'sentry/components/forms/selectAsyncControl';
  4. import {
  5. IconBitbucket,
  6. IconGeneric,
  7. IconGithub,
  8. IconGitlab,
  9. IconJira,
  10. IconVsts,
  11. } from 'sentry/icons';
  12. import {t} from 'sentry/locale';
  13. import HookStore from 'sentry/stores/hookStore';
  14. import {
  15. AppOrProviderOrPlugin,
  16. DocIntegration,
  17. ExternalActorMapping,
  18. ExternalActorMappingOrSuggestion,
  19. Integration,
  20. IntegrationFeature,
  21. IntegrationInstallationStatus,
  22. IntegrationType,
  23. Organization,
  24. PluginWithProjectList,
  25. SentryApp,
  26. SentryAppInstallation,
  27. } from 'sentry/types';
  28. import {Hooks} from 'sentry/types/hooks';
  29. import {
  30. integrationEventMap,
  31. IntegrationEventParameters,
  32. } from 'sentry/utils/analytics/integrationAnalyticsEvents';
  33. import makeAnalyticsFunction from 'sentry/utils/analytics/makeAnalyticsFunction';
  34. const mapIntegrationParams = analyticsParams => {
  35. // Reload expects integration_status even though it's not relevant for non-sentry apps
  36. // Passing in a dummy value of published in those cases
  37. const fullParams = {...analyticsParams};
  38. if (analyticsParams.integration && analyticsParams.integration_type !== 'sentry_app') {
  39. fullParams.integration_status = 'published';
  40. }
  41. return fullParams;
  42. };
  43. export const trackIntegrationAnalytics = makeAnalyticsFunction<
  44. IntegrationEventParameters,
  45. {organization: Organization} // org is required
  46. >(integrationEventMap, {
  47. mapValuesFn: mapIntegrationParams,
  48. });
  49. /**
  50. * In sentry.io the features list supports rendering plan details. If the hook
  51. * is not registered for rendering the features list like this simply show the
  52. * features as a normal list.
  53. */
  54. const generateFeaturesList = p => (
  55. <ul>
  56. {p.features.map((f, i) => (
  57. <li key={i}>{f.description}</li>
  58. ))}
  59. </ul>
  60. );
  61. const generateIntegrationFeatures = p =>
  62. p.children({
  63. disabled: false,
  64. disabledReason: null,
  65. ungatedFeatures: p.features,
  66. gatedFeatureGroups: [],
  67. });
  68. const defaultFeatureGateComponents = {
  69. IntegrationFeatures: generateIntegrationFeatures,
  70. IntegrationDirectoryFeatures: generateIntegrationFeatures,
  71. FeatureList: generateFeaturesList,
  72. IntegrationDirectoryFeatureList: generateFeaturesList,
  73. } as ReturnType<Hooks['integrations:feature-gates']>;
  74. export const getIntegrationFeatureGate = () => {
  75. const defaultHook = () => defaultFeatureGateComponents;
  76. const featureHook = HookStore.get('integrations:feature-gates')[0] || defaultHook;
  77. return featureHook();
  78. };
  79. export const getSentryAppInstallStatus = (install: SentryAppInstallation | undefined) => {
  80. if (install) {
  81. return capitalize(install.status) as IntegrationInstallationStatus;
  82. }
  83. return 'Not Installed';
  84. };
  85. export const getCategories = (features: IntegrationFeature[]): string[] => {
  86. const transform = features.map(({featureGate}) => {
  87. const feature = featureGate
  88. .replace(/integrations/g, '')
  89. .replace(/-/g, ' ')
  90. .trim();
  91. switch (feature) {
  92. case 'actionable notification':
  93. return 'notification action';
  94. case 'issue basic':
  95. case 'issue link':
  96. case 'issue sync':
  97. return 'project management';
  98. case 'commits':
  99. return 'source code management';
  100. case 'chat unfurl':
  101. return 'chat';
  102. default:
  103. return feature;
  104. }
  105. });
  106. return [...new Set(transform)];
  107. };
  108. export const getCategoriesForIntegration = (
  109. integration: AppOrProviderOrPlugin
  110. ): string[] => {
  111. if (isSentryApp(integration)) {
  112. return ['internal', 'unpublished'].includes(integration.status)
  113. ? [integration.status]
  114. : getCategories(integration.featureData);
  115. }
  116. if (isPlugin(integration)) {
  117. return getCategories(integration.featureDescriptions);
  118. }
  119. if (isDocIntegration(integration)) {
  120. return getCategories(integration.features ?? []);
  121. }
  122. return getCategories(integration.metadata.features);
  123. };
  124. export function isSentryApp(
  125. integration: AppOrProviderOrPlugin
  126. ): integration is SentryApp {
  127. return !!(integration as SentryApp).uuid;
  128. }
  129. export function isPlugin(
  130. integration: AppOrProviderOrPlugin
  131. ): integration is PluginWithProjectList {
  132. return integration.hasOwnProperty('shortName');
  133. }
  134. export function isDocIntegration(
  135. integration: AppOrProviderOrPlugin
  136. ): integration is DocIntegration {
  137. return integration.hasOwnProperty('isDraft');
  138. }
  139. export function isExternalActorMapping(
  140. mapping: ExternalActorMappingOrSuggestion
  141. ): mapping is ExternalActorMapping {
  142. return mapping.hasOwnProperty('id');
  143. }
  144. export const getIntegrationType = (
  145. integration: AppOrProviderOrPlugin
  146. ): IntegrationType => {
  147. if (isSentryApp(integration)) {
  148. return 'sentry_app';
  149. }
  150. if (isPlugin(integration)) {
  151. return 'plugin';
  152. }
  153. if (isDocIntegration(integration)) {
  154. return 'document';
  155. }
  156. return 'first_party';
  157. };
  158. export const convertIntegrationTypeToSnakeCase = (
  159. type: 'plugin' | 'firstParty' | 'sentryApp' | 'docIntegration'
  160. ) => {
  161. switch (type) {
  162. case 'firstParty':
  163. return 'first_party';
  164. case 'sentryApp':
  165. return 'sentry_app';
  166. case 'docIntegration':
  167. return 'document';
  168. default:
  169. return type;
  170. }
  171. };
  172. export const safeGetQsParam = (param: string) => {
  173. try {
  174. const query = qs.parse(window.location.search) || {};
  175. return query[param];
  176. } catch {
  177. return undefined;
  178. }
  179. };
  180. export const getIntegrationIcon = (integrationType?: string, size?: string) => {
  181. const iconSize = size || 'md';
  182. switch (integrationType) {
  183. case 'bitbucket':
  184. return <IconBitbucket size={iconSize} />;
  185. case 'gitlab':
  186. return <IconGitlab size={iconSize} />;
  187. case 'github':
  188. case 'github_enterprise':
  189. return <IconGithub size={iconSize} />;
  190. case 'jira':
  191. case 'jira_server':
  192. return <IconJira size={iconSize} />;
  193. case 'vsts':
  194. return <IconVsts size={iconSize} />;
  195. default:
  196. return <IconGeneric size={iconSize} />;
  197. }
  198. };
  199. // used for project creation and onboarding
  200. // determines what integration maps to what project platform
  201. export const platformToIntegrationMap = {
  202. 'node-awslambda': 'aws_lambda',
  203. 'python-awslambda': 'aws_lambda',
  204. };
  205. export const isSlackIntegrationUpToDate = (integrations: Integration[]): boolean => {
  206. return integrations.every(
  207. integration =>
  208. integration.provider.key !== 'slack' || integration.scopes?.includes('commands')
  209. );
  210. };
  211. export const getAlertText = (integrations?: Integration[]): string | undefined => {
  212. return isSlackIntegrationUpToDate(integrations || [])
  213. ? undefined
  214. : t(
  215. 'Update to the latest version of our Slack app to get access to personal and team notifications.'
  216. );
  217. };
  218. /**
  219. * Uses the mapping and baseEndpoint to derive the details for the mappings request.
  220. * @param baseEndpoint Must have a trailing slash, since the id is appended for PUT requests!
  221. * @param mapping The mapping or suggestion being sent to the endpoint
  222. * @returns An object containing the request method (apiMethod), and final endpoint (apiEndpoint)
  223. */
  224. export const getExternalActorEndpointDetails = (
  225. baseEndpoint: string,
  226. mapping?: ExternalActorMappingOrSuggestion
  227. ): {apiEndpoint: string; apiMethod: 'POST' | 'PUT'} => {
  228. const isValidMapping = mapping && isExternalActorMapping(mapping);
  229. return {
  230. apiMethod: isValidMapping ? 'PUT' : 'POST',
  231. apiEndpoint: isValidMapping ? `${baseEndpoint}${mapping.id}/` : baseEndpoint,
  232. };
  233. };
  234. export const sentryNameToOption = ({id, name}): Result => ({
  235. value: id,
  236. label: name,
  237. });