integrationUtil.tsx 7.8 KB

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