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.

AzureProjectsList.tsx 4.7KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  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 { FlagMessage, Link } from 'design-system';
  21. import { uniqBy } from 'lodash';
  22. import * as React from 'react';
  23. import { FormattedMessage } from 'react-intl';
  24. import { queryToSearchString } from '~sonar-aligned/helpers/urls';
  25. import ListFooter from '../../../../components/controls/ListFooter';
  26. import { translate, translateWithParameters } from '../../../../helpers/l10n';
  27. import { AzureProject, AzureRepository } from '../../../../types/alm-integration';
  28. import { Dict } from '../../../../types/types';
  29. import { CreateProjectModes } from '../types';
  30. import AzureProjectAccordion from './AzureProjectAccordion';
  31. export interface AzureProjectsListProps {
  32. loadingRepositories: Dict<boolean>;
  33. onOpenProject: (key: string) => void;
  34. onImportRepository: (repository: AzureRepository) => void;
  35. projects?: AzureProject[];
  36. repositories: Dict<AzureRepository[]>;
  37. searchResults?: AzureRepository[];
  38. searchQuery?: string;
  39. }
  40. const PAGE_SIZE = 10;
  41. export default function AzureProjectsList(props: AzureProjectsListProps) {
  42. const { loadingRepositories, projects = [], repositories, searchResults, searchQuery } = props;
  43. const [page, setPage] = React.useState(1);
  44. if (searchResults && searchResults.length === 0) {
  45. return (
  46. <FlagMessage className="sw-mt-2" variant="warning">
  47. {translate('onboarding.create_project.azure.no_results')}
  48. </FlagMessage>
  49. );
  50. }
  51. if (projects.length === 0) {
  52. return (
  53. <FlagMessage className="sw-mt-2" variant="warning">
  54. <span>
  55. <FormattedMessage
  56. defaultMessage={translate('onboarding.create_project.azure.no_projects')}
  57. id="onboarding.create_project.azure.no_projects"
  58. values={{
  59. link: (
  60. <Link
  61. to={{
  62. pathname: '/projects/create',
  63. search: queryToSearchString({
  64. mode: CreateProjectModes.AzureDevOps,
  65. resetPat: 1,
  66. }),
  67. }}
  68. >
  69. {translate('onboarding.create_project.update_your_token')}
  70. </Link>
  71. ),
  72. }}
  73. />
  74. </span>
  75. </FlagMessage>
  76. );
  77. }
  78. let filteredProjects: AzureProject[];
  79. if (searchResults !== undefined) {
  80. filteredProjects = uniqBy(
  81. searchResults.map((r) => {
  82. return (
  83. projects.find((p) => p.name === r.projectName) || {
  84. name: r.projectName,
  85. description: translateWithParameters(
  86. 'onboarding.create_project.azure.search_results_for_project_X',
  87. r.projectName,
  88. ),
  89. }
  90. );
  91. }),
  92. 'name',
  93. );
  94. } else {
  95. filteredProjects = projects;
  96. }
  97. const displayedProjects = filteredProjects.slice(0, page * PAGE_SIZE);
  98. // Add a suffix to the key to force react to not reuse AzureProjectAccordions between
  99. // search results and project exploration
  100. const keySuffix = searchResults ? ' - result' : '';
  101. return (
  102. <div>
  103. <div className="sw-flex sw-flex-col sw-gap-6">
  104. {displayedProjects.map((p, i) => (
  105. <AzureProjectAccordion
  106. key={`${p.name}${keySuffix}`}
  107. loading={Boolean(loadingRepositories[p.name])}
  108. onOpen={props.onOpenProject}
  109. onImportRepository={props.onImportRepository}
  110. project={p}
  111. repositories={
  112. searchResults
  113. ? searchResults.filter((s) => s.projectName === p.name)
  114. : repositories[p.name]
  115. }
  116. searchQuery={searchQuery}
  117. startsOpen={searchResults !== undefined || i === 0}
  118. />
  119. ))}
  120. </div>
  121. <ListFooter
  122. className="sw-mb-12"
  123. count={displayedProjects.length}
  124. loadMore={() => setPage((p) => p + 1)}
  125. total={filteredProjects.length}
  126. />
  127. </div>
  128. );
  129. }