integrationUtil.tsx 7.4 KB

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