integrationUtil.tsx 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  1. import * as qs from 'query-string';
  2. import type {Result} from 'sentry/components/forms/controls/selectAsyncControl';
  3. import {
  4. IconAsana,
  5. IconBitbucket,
  6. IconCodecov,
  7. IconGeneric,
  8. IconGithub,
  9. IconGitlab,
  10. IconJira,
  11. IconSentry,
  12. IconVsts,
  13. } from 'sentry/icons';
  14. import {t} from 'sentry/locale';
  15. import HookStore from 'sentry/stores/hookStore';
  16. import type {
  17. AppOrProviderOrPlugin,
  18. CodeOwner,
  19. DocIntegration,
  20. ExternalActorMapping,
  21. ExternalActorMappingOrSuggestion,
  22. Integration,
  23. IntegrationFeature,
  24. IntegrationInstallationStatus,
  25. IntegrationType,
  26. PluginWithProjectList,
  27. SentryApp,
  28. SentryAppInstallation,
  29. } from 'sentry/types';
  30. import type {Hooks} from 'sentry/types/hooks';
  31. import {trackAnalytics} from 'sentry/utils/analytics';
  32. import {capitalize} from 'sentry/utils/string/capitalize';
  33. import type {IconSize} from './theme';
  34. /**
  35. * TODO: remove alias once all usages are updated
  36. * @deprecated Use trackAnalytics instead
  37. */
  38. export const trackIntegrationAnalytics = trackAnalytics;
  39. /**
  40. * In sentry.io the features list supports rendering plan details. If the hook
  41. * is not registered for rendering the features list like this simply show the
  42. * features as a normal list.
  43. */
  44. const generateFeaturesList = p => (
  45. <ul>
  46. {p.features.map((f, i) => (
  47. <li key={i}>{f.description}</li>
  48. ))}
  49. </ul>
  50. );
  51. const generateIntegrationFeatures = p =>
  52. p.children({
  53. disabled: false,
  54. disabledReason: null,
  55. ungatedFeatures: p.features,
  56. gatedFeatureGroups: [],
  57. });
  58. const defaultFeatureGateComponents: ReturnType<Hooks['integrations:feature-gates']> = {
  59. IntegrationFeatures: generateIntegrationFeatures,
  60. FeatureList: generateFeaturesList,
  61. };
  62. export const getIntegrationFeatureGate = () => {
  63. const defaultHook = () => defaultFeatureGateComponents;
  64. const featureHook = HookStore.get('integrations:feature-gates')[0] || defaultHook;
  65. return featureHook();
  66. };
  67. export const getSentryAppInstallStatus = (install: SentryAppInstallation | undefined) => {
  68. if (install) {
  69. return capitalize(install.status) as IntegrationInstallationStatus;
  70. }
  71. return 'Not Installed';
  72. };
  73. export const getCategories = (features: IntegrationFeature[]): string[] => {
  74. const transform = features.map(({featureGate}) => {
  75. const feature = featureGate
  76. .replace(/integrations/g, '')
  77. .replace(/-/g, ' ')
  78. .trim();
  79. switch (feature) {
  80. case 'actionable notification':
  81. return 'notification action';
  82. case 'issue basic':
  83. case 'issue link':
  84. case 'issue sync':
  85. case 'project management':
  86. return 'issue tracking';
  87. case 'commits':
  88. return 'source code management';
  89. case 'chat unfurl':
  90. return 'chat';
  91. default:
  92. return feature;
  93. }
  94. });
  95. return [...new Set(transform)];
  96. };
  97. export const getCategoriesForIntegration = (
  98. integration: AppOrProviderOrPlugin
  99. ): string[] => {
  100. if (isSentryApp(integration)) {
  101. return ['internal', 'unpublished'].includes(integration.status)
  102. ? [integration.status]
  103. : getCategories(integration.featureData);
  104. }
  105. if (isPlugin(integration)) {
  106. return getCategories(integration.featureDescriptions);
  107. }
  108. if (isDocIntegration(integration)) {
  109. return getCategories(integration.features ?? []);
  110. }
  111. return getCategories(integration.metadata.features);
  112. };
  113. export function isSentryApp(
  114. integration: AppOrProviderOrPlugin
  115. ): integration is SentryApp {
  116. return !!(integration as SentryApp).uuid;
  117. }
  118. export function isPlugin(
  119. integration: AppOrProviderOrPlugin
  120. ): integration is PluginWithProjectList {
  121. return integration.hasOwnProperty('shortName');
  122. }
  123. export function isDocIntegration(
  124. integration: AppOrProviderOrPlugin
  125. ): integration is DocIntegration {
  126. return integration.hasOwnProperty('isDraft');
  127. }
  128. export function isExternalActorMapping(
  129. mapping: ExternalActorMappingOrSuggestion
  130. ): mapping is ExternalActorMapping {
  131. return mapping.hasOwnProperty('id');
  132. }
  133. export const getIntegrationType = (
  134. integration: AppOrProviderOrPlugin
  135. ): IntegrationType => {
  136. if (isSentryApp(integration)) {
  137. return 'sentry_app';
  138. }
  139. if (isPlugin(integration)) {
  140. return 'plugin';
  141. }
  142. if (isDocIntegration(integration)) {
  143. return 'document';
  144. }
  145. return 'first_party';
  146. };
  147. export const convertIntegrationTypeToSnakeCase = (
  148. type: 'plugin' | 'firstParty' | 'sentryApp' | 'docIntegration'
  149. ) => {
  150. switch (type) {
  151. case 'firstParty':
  152. return 'first_party';
  153. case 'sentryApp':
  154. return 'sentry_app';
  155. case 'docIntegration':
  156. return 'document';
  157. default:
  158. return type;
  159. }
  160. };
  161. export const safeGetQsParam = (param: string) => {
  162. try {
  163. const query = qs.parse(window.location.search) || {};
  164. return query[param];
  165. } catch {
  166. return undefined;
  167. }
  168. };
  169. export const getIntegrationIcon = (
  170. integrationType?: string,
  171. iconSize: IconSize = 'md'
  172. ) => {
  173. switch (integrationType) {
  174. case 'asana':
  175. return <IconAsana size={iconSize} />;
  176. case 'bitbucket':
  177. return <IconBitbucket size={iconSize} />;
  178. case 'gitlab':
  179. return <IconGitlab size={iconSize} />;
  180. case 'github':
  181. case 'github_enterprise':
  182. return <IconGithub size={iconSize} />;
  183. case 'jira':
  184. case 'jira_server':
  185. return <IconJira size={iconSize} />;
  186. case 'vsts':
  187. return <IconVsts size={iconSize} />;
  188. case 'codecov':
  189. return <IconCodecov size={iconSize} />;
  190. default:
  191. return <IconGeneric size={iconSize} />;
  192. }
  193. };
  194. export const getIntegrationDisplayName = (integrationType?: string) => {
  195. switch (integrationType) {
  196. case 'asana':
  197. return 'Asana';
  198. case 'bitbucket':
  199. return 'Bitbucket';
  200. case 'gitlab':
  201. return 'GitLab';
  202. case 'github':
  203. case 'github_enterprise':
  204. return 'GitHub';
  205. case 'jira':
  206. case 'jira_server':
  207. return 'Jira';
  208. case 'vsts':
  209. return 'VSTS';
  210. case 'codecov':
  211. return 'Codeov';
  212. default:
  213. return '';
  214. }
  215. };
  216. export const getIntegrationSourceUrl = (
  217. integrationType: string,
  218. sourceUrl: string,
  219. lineNo: number | null
  220. ) => {
  221. switch (integrationType) {
  222. case 'bitbucket':
  223. case 'bitbucket_server':
  224. return `${sourceUrl}#lines-${lineNo}`;
  225. case 'vsts':
  226. const url = new URL(sourceUrl);
  227. if (lineNo) {
  228. url.searchParams.set('line', lineNo.toString());
  229. url.searchParams.set('lineEnd', (lineNo + 1).toString());
  230. url.searchParams.set('lineStartColumn', '1');
  231. url.searchParams.set('lineEndColumn', '1');
  232. url.searchParams.set('lineStyle', 'plain');
  233. url.searchParams.set('_a', 'contents');
  234. }
  235. return url.toString();
  236. case 'github':
  237. case 'github_enterprise':
  238. default:
  239. if (lineNo === null) {
  240. return sourceUrl;
  241. }
  242. return `${sourceUrl}#L${lineNo}`;
  243. }
  244. };
  245. export function getCodeOwnerIcon(
  246. provider: CodeOwner['provider'],
  247. iconSize: IconSize = 'md'
  248. ) {
  249. switch (provider ?? '') {
  250. case 'github':
  251. return <IconGithub size={iconSize} />;
  252. case 'gitlab':
  253. return <IconGitlab size={iconSize} />;
  254. default:
  255. return <IconSentry size={iconSize} />;
  256. }
  257. }
  258. // used for project creation and onboarding
  259. // determines what integration maps to what project platform
  260. export const platformToIntegrationMap = {
  261. 'node-awslambda': 'aws_lambda',
  262. 'python-awslambda': 'aws_lambda',
  263. };
  264. export const isSlackIntegrationUpToDate = (integrations: Integration[]): boolean => {
  265. return integrations.every(
  266. integration =>
  267. integration.provider.key !== 'slack' || integration.scopes?.includes('commands')
  268. );
  269. };
  270. export const getAlertText = (integrations?: Integration[]): string | undefined => {
  271. return isSlackIntegrationUpToDate(integrations || [])
  272. ? undefined
  273. : t(
  274. 'Update to the latest version of our Slack app to get access to personal and team notifications.'
  275. );
  276. };
  277. /**
  278. * Uses the mapping and baseEndpoint to derive the details for the mappings request.
  279. * @param baseEndpoint Must have a trailing slash, since the id is appended for PUT requests!
  280. * @param mapping The mapping or suggestion being sent to the endpoint
  281. * @returns An object containing the request method (apiMethod), and final endpoint (apiEndpoint)
  282. */
  283. export const getExternalActorEndpointDetails = (
  284. baseEndpoint: string,
  285. mapping?: ExternalActorMappingOrSuggestion
  286. ): {apiEndpoint: string; apiMethod: 'POST' | 'PUT'} => {
  287. const isValidMapping = mapping && isExternalActorMapping(mapping);
  288. return {
  289. apiMethod: isValidMapping ? 'PUT' : 'POST',
  290. apiEndpoint: isValidMapping ? `${baseEndpoint}${mapping.id}/` : baseEndpoint,
  291. };
  292. };
  293. export const sentryNameToOption = ({id, name}): Result => ({
  294. value: id,
  295. label: name,
  296. });
  297. export function getIntegrationStatus(integration: Integration) {
  298. // there are multiple status fields for an integration we consider
  299. const statusList = [integration.organizationIntegrationStatus, integration.status];
  300. const firstNotActive = statusList.find(s => s !== 'active');
  301. // Active if everything is active, otherwise the first inactive status
  302. return firstNotActive ?? 'active';
  303. }