integrationUtil.tsx 7.4 KB

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