You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

urls.ts 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2024 SonarSource SA
  4. * mailto:info AT sonarsource DOT com
  5. *
  6. * This program is free software; you can redistribute it and/or
  7. * modify it under the terms of the GNU Lesser General Public
  8. * License as published by the Free Software Foundation; either
  9. * version 3 of the License, or (at your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  14. * Lesser General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU Lesser General Public License
  17. * along with this program; if not, write to the Free Software Foundation,
  18. * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  19. */
  20. import { Path, To } from 'react-router-dom';
  21. import { getBranchLikeQuery, isBranch, isMainBranch } from '~sonar-aligned/helpers/branch-like';
  22. import { queryToSearchString } from '~sonar-aligned/helpers/urls';
  23. import { BranchParameters } from '~sonar-aligned/types/branch-like';
  24. import { ComponentQualifier } from '~sonar-aligned/types/component';
  25. import { getProfilePath } from '../apps/quality-profiles/utils';
  26. import { DEFAULT_ISSUES_QUERY } from '../components/shared/utils';
  27. import { isPortfolioLike } from '../sonar-aligned/helpers/component';
  28. import { BranchLike } from '../types/branch-like';
  29. import { isApplication } from '../types/component';
  30. import { MeasurePageView } from '../types/measures';
  31. import { GraphType } from '../types/project-activity';
  32. import { Dict } from '../types/types';
  33. import { HomePage } from '../types/users';
  34. import { isPullRequest } from './branch-like';
  35. import { serializeOptionalBoolean } from './query';
  36. import { getBaseUrl } from './system';
  37. export interface Location {
  38. pathname: string;
  39. query?: Dict<string | undefined | number>;
  40. }
  41. export enum CodeScope {
  42. Overall = 'overall',
  43. New = 'new',
  44. }
  45. type CodeScopeType = CodeScope.Overall | CodeScope.New;
  46. export type Query = Location['query'];
  47. const PROJECT_BASE_URL = '/dashboard';
  48. export function getComponentOverviewUrl(
  49. componentKey: string,
  50. componentQualifier: ComponentQualifier | string,
  51. branchParameters?: BranchParameters,
  52. codeScope?: CodeScopeType,
  53. ) {
  54. return isPortfolioLike(componentQualifier)
  55. ? getPortfolioUrl(componentKey)
  56. : getProjectQueryUrl(componentKey, branchParameters, codeScope);
  57. }
  58. export function getComponentAdminUrl(
  59. componentKey: string,
  60. componentQualifier: ComponentQualifier | string,
  61. ) {
  62. if (isPortfolioLike(componentQualifier)) {
  63. return getPortfolioAdminUrl(componentKey);
  64. } else if (isApplication(componentQualifier)) {
  65. return getApplicationAdminUrl(componentKey);
  66. }
  67. return getProjectUrl(componentKey);
  68. }
  69. export function getProjectUrl(
  70. project: string,
  71. branch?: string,
  72. codeScope?: CodeScopeType,
  73. ): Partial<Path> {
  74. return {
  75. pathname: PROJECT_BASE_URL,
  76. search: queryToSearchString({
  77. id: project,
  78. branch,
  79. ...(codeScope && { code_scope: codeScope }),
  80. }),
  81. };
  82. }
  83. export function getProjectSecurityHotspots(project: string): To {
  84. return {
  85. pathname: '/security_hotspots',
  86. search: queryToSearchString({ id: project }),
  87. };
  88. }
  89. export function getProjectQueryUrl(
  90. project: string,
  91. branchParameters?: BranchParameters,
  92. codeScope?: CodeScopeType,
  93. ): To {
  94. return {
  95. pathname: PROJECT_BASE_URL,
  96. search: queryToSearchString({
  97. id: project,
  98. ...branchParameters,
  99. ...(codeScope && { code_scope: codeScope }),
  100. }),
  101. };
  102. }
  103. export function getPortfolioUrl(key: string): To {
  104. return { pathname: '/portfolio', search: queryToSearchString({ id: key }) };
  105. }
  106. export function getPortfolioAdminUrl(key: string): To {
  107. return {
  108. pathname: '/project/admin/extension/governance/console',
  109. search: queryToSearchString({ id: key, qualifier: ComponentQualifier.Portfolio }),
  110. };
  111. }
  112. export function getApplicationAdminUrl(key: string): To {
  113. return {
  114. pathname: '/project/admin/extension/developer-server/application-console',
  115. search: queryToSearchString({ id: key }),
  116. };
  117. }
  118. export function getComponentBackgroundTaskUrl(
  119. componentKey: string,
  120. status?: string,
  121. taskType?: string,
  122. ): Partial<Path> {
  123. return {
  124. pathname: '/project/background_tasks',
  125. search: queryToSearchString({ id: componentKey, status, taskType }),
  126. hash: '',
  127. };
  128. }
  129. export function getBranchLikeUrl(project: string, branchLike?: BranchLike): Partial<Path> {
  130. if (isPullRequest(branchLike)) {
  131. return getPullRequestUrl(project, branchLike.key);
  132. } else if (isBranch(branchLike) && !isMainBranch(branchLike)) {
  133. return getBranchUrl(project, branchLike.name);
  134. }
  135. return getProjectUrl(project);
  136. }
  137. export function getBranchUrl(project: string, branch: string): Partial<Path> {
  138. return { pathname: PROJECT_BASE_URL, search: queryToSearchString({ branch, id: project }) };
  139. }
  140. export function getPullRequestUrl(project: string, pullRequest: string): Partial<Path> {
  141. return { pathname: PROJECT_BASE_URL, search: queryToSearchString({ id: project, pullRequest }) };
  142. }
  143. /**
  144. * Generate URL for a global issues page
  145. */
  146. export function getIssuesUrl(query: Query): To {
  147. const pathname = '/issues';
  148. return { pathname, search: queryToSearchString(query) };
  149. }
  150. /**
  151. * Generate URL for a component's drilldown page
  152. */
  153. export function getComponentDrilldownUrl(options: {
  154. componentKey: string;
  155. metric: string;
  156. branchLike?: BranchLike;
  157. selectionKey?: string;
  158. treemapView?: boolean;
  159. listView?: boolean;
  160. asc?: boolean;
  161. }): To {
  162. const { componentKey, metric, branchLike, selectionKey, treemapView, listView, asc } = options;
  163. const query: Query = { id: componentKey, metric, ...getBranchLikeQuery(branchLike) };
  164. if (treemapView) {
  165. query.view = 'treemap';
  166. }
  167. if (listView) {
  168. query.view = 'list';
  169. query.asc = serializeOptionalBoolean(asc);
  170. }
  171. if (selectionKey) {
  172. query.selected = selectionKey;
  173. }
  174. return { pathname: '/component_measures', search: queryToSearchString(query) };
  175. }
  176. export function getComponentDrilldownUrlWithSelection(
  177. componentKey: string,
  178. selectionKey: string,
  179. metric: string,
  180. branchLike?: BranchLike,
  181. view?: MeasurePageView,
  182. ): To {
  183. return getComponentDrilldownUrl({
  184. componentKey,
  185. selectionKey,
  186. metric,
  187. branchLike,
  188. treemapView: view === MeasurePageView.treemap,
  189. listView: view === MeasurePageView.list,
  190. });
  191. }
  192. export function getMeasureTreemapUrl(componentKey: string, metric: string) {
  193. return getComponentDrilldownUrl({ componentKey, metric, treemapView: true });
  194. }
  195. export function getActivityUrl(component: string, branchLike?: BranchLike, graph?: GraphType) {
  196. return {
  197. pathname: '/project/activity',
  198. search: queryToSearchString({ id: component, graph, ...getBranchLikeQuery(branchLike) }),
  199. };
  200. }
  201. /**
  202. * Generate URL for a component's measure history
  203. */
  204. export function getMeasureHistoryUrl(component: string, metric: string, branchLike?: BranchLike) {
  205. return {
  206. pathname: '/project/activity',
  207. search: queryToSearchString({
  208. id: component,
  209. graph: 'custom',
  210. custom_metrics: metric,
  211. ...getBranchLikeQuery(branchLike),
  212. }),
  213. };
  214. }
  215. /**
  216. * Generate URL for a component's permissions page
  217. */
  218. export function getComponentPermissionsUrl(componentKey: string): To {
  219. return { pathname: '/project_roles', search: queryToSearchString({ id: componentKey }) };
  220. }
  221. /**
  222. * Generate URL for a quality profile
  223. */
  224. export function getQualityProfileUrl(name: string, language: string): To {
  225. return getProfilePath(name, language);
  226. }
  227. export function getQualityGateUrl(name: string): To {
  228. // This is a workaround for the react router bug: https://github.com/remix-run/react-router/issues/10814
  229. const qualityGateName = name.replace(/%/g, '%25');
  230. return {
  231. pathname: '/quality_gates/show/' + encodeURIComponent(qualityGateName),
  232. };
  233. }
  234. /**
  235. * Generate URL for the project tutorial page
  236. */
  237. export function getProjectTutorialLocation(
  238. project: string,
  239. selectedTutorial?: string,
  240. ): Partial<Path> {
  241. return {
  242. pathname: '/tutorials',
  243. search: queryToSearchString({ id: project, selectedTutorial }),
  244. };
  245. }
  246. /**
  247. * Generate URL for the project creation page
  248. */
  249. export function getCreateProjectModeLocation(mode?: string): Partial<Path> {
  250. return {
  251. search: queryToSearchString({ mode }),
  252. };
  253. }
  254. export function getQualityGatesUrl(): To {
  255. return {
  256. pathname: '/quality_gates',
  257. };
  258. }
  259. export function getGlobalSettingsUrl(
  260. category?: string,
  261. query?: Dict<string | undefined | number>,
  262. ): Partial<Path> {
  263. return {
  264. pathname: '/admin/settings',
  265. search: queryToSearchString({ category, ...query }),
  266. };
  267. }
  268. export function getProjectSettingsUrl(id: string, category?: string): Partial<Path> {
  269. return {
  270. pathname: '/project/settings',
  271. search: queryToSearchString({ id, category }),
  272. };
  273. }
  274. /**
  275. * Generate URL for the rules page
  276. */
  277. export function getRulesUrl(query: Query): Partial<Path> {
  278. return { pathname: '/coding_rules', search: queryToSearchString(query) };
  279. }
  280. /**
  281. * Generate URL for the rules page filtering only active deprecated rules
  282. */
  283. export function getDeprecatedActiveRulesUrl(query: Query = {}): To {
  284. const baseQuery = { activation: 'true', statuses: 'DEPRECATED' };
  285. return getRulesUrl({ ...query, ...baseQuery });
  286. }
  287. export function getRuleUrl(rule: string) {
  288. return getRulesUrl({ open: rule, rule_key: rule });
  289. }
  290. export function getFormattingHelpUrl(): string {
  291. return '/formatting/help';
  292. }
  293. export function getCodeUrl(
  294. project: string,
  295. branchLike?: BranchLike,
  296. selected?: string,
  297. line?: number,
  298. ): Partial<Path> {
  299. return {
  300. pathname: '/code',
  301. search: queryToSearchString({
  302. id: project,
  303. ...getBranchLikeQuery(branchLike),
  304. selected,
  305. line: line?.toFixed(),
  306. }),
  307. };
  308. }
  309. export function getHomePageUrl(homepage: HomePage) {
  310. switch (homepage.type) {
  311. case 'APPLICATION':
  312. return homepage.branch
  313. ? getProjectUrl(homepage.component, homepage.branch)
  314. : getProjectUrl(homepage.component);
  315. case 'PROJECT':
  316. return homepage.branch
  317. ? getBranchUrl(homepage.component, homepage.branch)
  318. : getProjectUrl(homepage.component);
  319. case 'PORTFOLIO':
  320. return getPortfolioUrl(homepage.component);
  321. case 'PORTFOLIOS':
  322. return '/portfolios';
  323. case 'MY_PROJECTS':
  324. return '/projects';
  325. case 'ISSUES':
  326. case 'MY_ISSUES':
  327. return { pathname: '/issues', query: DEFAULT_ISSUES_QUERY };
  328. }
  329. // should never happen, but just in case...
  330. return '/projects';
  331. }
  332. export function convertGithubApiUrlToLink(url: string) {
  333. return url
  334. .replace(/^https?:\/\/api\.github\.com/, 'https://github.com') // GH.com
  335. .replace(/\/api\/v\d+\/?$/, ''); // GH Enterprise
  336. }
  337. export function stripTrailingSlash(url: string) {
  338. return url.replace(/\/$/, '');
  339. }
  340. export function getHostUrl(): string {
  341. return window.location.origin + getBaseUrl();
  342. }
  343. export function getPathUrlAsString(path: Partial<Path>, internal = true): string {
  344. return `${internal ? getBaseUrl() : getHostUrl()}${path.pathname ?? '/'}${path.search ?? ''}`;
  345. }
  346. export function getReturnUrl(location: { hash?: string; query?: { return_to?: string } }) {
  347. const returnTo = location.query && location.query['return_to'];
  348. if (isRelativeUrl(returnTo)) {
  349. return returnTo + (location.hash ? location.hash : '');
  350. }
  351. return `${getBaseUrl()}/`;
  352. }
  353. export function isRelativeUrl(url?: string): boolean {
  354. const regex = new RegExp(/^\/[^/\\]/);
  355. return Boolean(url && regex.test(url));
  356. }
  357. export function convertToTo(link: string | Location) {
  358. if (linkIsLocation(link)) {
  359. return { pathname: link.pathname, search: queryToSearchString(link.query) } as Partial<Path>;
  360. }
  361. return link;
  362. }
  363. function linkIsLocation(link: string | Location): link is Location {
  364. return (link as Location).query !== undefined;
  365. }