aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web
diff options
context:
space:
mode:
authorViktor Vorona <viktor.vorona@sonarsource.com>2024-02-15 11:19:17 +0100
committersonartech <sonartech@sonarsource.com>2024-02-15 20:02:34 +0000
commitafa1e65cfbe2f17f39ba73e1fe2dd2a543c5bf9a (patch)
tree357d7a02fff76e99f75170e70722c38950dee09b /server/sonar-web
parent9a1d70adca457f84a6f7da5561328c8d85b1a7bb (diff)
downloadsonarqube-afa1e65cfbe2f17f39ba73e1fe2dd2a543c5bf9a.tar.gz
sonarqube-afa1e65cfbe2f17f39ba73e1fe2dd2a543c5bf9a.zip
SONAR-20510 Always show project name in project facet
Diffstat (limited to 'server/sonar-web')
-rw-r--r--server/sonar-web/src/main/js/api/mocks/ComponentsServiceMock.ts31
-rw-r--r--server/sonar-web/src/main/js/api/mocks/IssuesServiceMock.ts2
-rw-r--r--server/sonar-web/src/main/js/api/mocks/data/issues.ts2
-rw-r--r--server/sonar-web/src/main/js/apps/issues/__tests__/IssuesApp-Filtering-it.tsx7
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/ProjectFacet.tsx114
-rw-r--r--server/sonar-web/src/main/js/queries/projects.ts35
6 files changed, 148 insertions, 43 deletions
diff --git a/server/sonar-web/src/main/js/api/mocks/ComponentsServiceMock.ts b/server/sonar-web/src/main/js/api/mocks/ComponentsServiceMock.ts
index 6fb1144fcdd..8f0404dda49 100644
--- a/server/sonar-web/src/main/js/api/mocks/ComponentsServiceMock.ts
+++ b/server/sonar-web/src/main/js/api/mocks/ComponentsServiceMock.ts
@@ -46,6 +46,7 @@ import {
getDuplications,
getSources,
getTree,
+ searchProjects,
setApplicationTags,
setProjectTags,
} from '../components';
@@ -57,6 +58,7 @@ import {
} from './data/components';
import { mockIssuesList } from './data/issues';
import { MeasureRecords, mockFullMeasureData } from './data/measures';
+import { mockProjects } from './data/projects';
import { listAllComponent, listChildComponent, listLeavesComponent } from './data/utils';
jest.mock('../components');
@@ -64,11 +66,13 @@ jest.mock('../components');
export default class ComponentsServiceMock {
failLoadingComponentStatus: HttpStatus | undefined = undefined;
defaultComponents: ComponentTree[];
+ defaultProjects: ComponentRaw[];
components: ComponentTree[];
defaultSourceFiles: SourceFile[];
sourceFiles: SourceFile[];
defaultMeasures: MeasureRecords;
measures: MeasureRecords;
+ projects: ComponentRaw[];
constructor(components?: ComponentTree[], sourceFiles?: SourceFile[], measures?: MeasureRecords) {
this.defaultComponents = components || [mockFullComponentTree()];
@@ -80,10 +84,12 @@ export default class ComponentsServiceMock {
(acc, tree) => ({ ...acc, ...mockFullMeasureData(tree, issueList) }),
{},
);
+ this.defaultProjects = mockProjects();
this.components = cloneDeep(this.defaultComponents);
this.sourceFiles = cloneDeep(this.defaultSourceFiles);
this.measures = cloneDeep(this.defaultMeasures);
+ this.projects = cloneDeep(this.defaultProjects);
jest.mocked(getComponentTree).mockImplementation(this.handleGetComponentTree);
jest.mocked(getChildren).mockImplementation(this.handleGetChildren);
@@ -99,8 +105,33 @@ export default class ComponentsServiceMock {
jest.mocked(getBreadcrumbs).mockImplementation(this.handleGetBreadcrumbs);
jest.mocked(setProjectTags).mockImplementation(this.handleSetProjectTags);
jest.mocked(setApplicationTags).mockImplementation(this.handleSetApplicationTags);
+ jest.mocked(searchProjects).mockImplementation(this.handleSearchProjects);
}
+ handleSearchProjects: typeof searchProjects = (data) => {
+ const pageIndex = data.p ?? 1;
+ const pageSize = data.ps ?? 100;
+
+ const components = this.projects
+ .filter((c) => {
+ if (data.filter && data.filter.startsWith('query')) {
+ const query = data.filter.split('query=')[1];
+ return c.key.includes(query) || c.name.includes(query);
+ }
+ })
+ .map((c) => c);
+
+ return this.reply({
+ components: components.slice((pageIndex - 1) * pageSize, pageIndex * pageSize),
+ facets: [],
+ paging: {
+ pageSize,
+ pageIndex,
+ total: components.length,
+ },
+ });
+ };
+
findComponentTree = (key: string, from?: ComponentTree) => {
let tree: ComponentTree | undefined;
const recurse = (node: ComponentTree): boolean => {
diff --git a/server/sonar-web/src/main/js/api/mocks/IssuesServiceMock.ts b/server/sonar-web/src/main/js/api/mocks/IssuesServiceMock.ts
index 833a508712d..11e3de98d6c 100644
--- a/server/sonar-web/src/main/js/api/mocks/IssuesServiceMock.ts
+++ b/server/sonar-web/src/main/js/api/mocks/IssuesServiceMock.ts
@@ -325,7 +325,7 @@ export default class IssuesServiceMock {
issueStatuses: ISSUE_STATUSES,
types: ISSUE_TYPES,
scopes: SOURCE_SCOPES.map(({ scope }) => scope),
- projects: ['org.project1', 'org.project2'],
+ projects: ['org.project1', 'org.sonarsource.javascript:javascript'],
impactSoftwareQualities: Object.values(SoftwareQuality),
impactSeverities: Object.values(SoftwareImpactSeverity),
cleanCodeAttributeCategories: cleanCodeCategories,
diff --git a/server/sonar-web/src/main/js/api/mocks/data/issues.ts b/server/sonar-web/src/main/js/api/mocks/data/issues.ts
index 3efa63ae18d..9bdcd439460 100644
--- a/server/sonar-web/src/main/js/api/mocks/data/issues.ts
+++ b/server/sonar-web/src/main/js/api/mocks/data/issues.ts
@@ -385,7 +385,7 @@ export function mockIssuesList(baseComponentKey = PARENT_COMPONENT_KEY): IssueDa
quickFixAvailable: true,
tags: ['unused'],
codeVariants: ['variant 1', 'variant 2'],
- project: 'org.project2',
+ project: 'org.sonarsource.javascript:javascript',
assignee: 'email1@sonarsource.com',
author: 'email3@sonarsource.com',
issueStatus: IssueStatus.Confirmed,
diff --git a/server/sonar-web/src/main/js/apps/issues/__tests__/IssuesApp-Filtering-it.tsx b/server/sonar-web/src/main/js/apps/issues/__tests__/IssuesApp-Filtering-it.tsx
index 35501cdd50e..a00e7fe5450 100644
--- a/server/sonar-web/src/main/js/apps/issues/__tests__/IssuesApp-Filtering-it.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/__tests__/IssuesApp-Filtering-it.tsx
@@ -115,7 +115,12 @@ describe('issues app filtering', () => {
// Project
await user.click(ui.projectFacet.get());
- await user.click(screen.getByRole('checkbox', { name: 'org.project2' }));
+ expect(
+ screen.getByRole('checkbox', { name: 'org.sonarsource.javascript:javascript' }),
+ ).toHaveTextContent('SonarJS');
+ await user.click(
+ screen.getByRole('checkbox', { name: 'org.sonarsource.javascript:javascript' }),
+ );
// Assignee
await user.click(ui.assigneeFacet.get());
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/ProjectFacet.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/ProjectFacet.tsx
index 0c47345603a..19371524f0d 100644
--- a/server/sonar-web/src/main/js/apps/issues/sidebar/ProjectFacet.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/ProjectFacet.tsx
@@ -17,12 +17,13 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { ProjectIcon } from 'design-system';
+import { ProjectIcon, Spinner } from 'design-system';
import { omit } from 'lodash';
import * as React from 'react';
import { getTree, searchProjects } from '../../../api/components';
import { translate } from '../../../helpers/l10n';
import { highlightTerm } from '../../../helpers/search';
+import { useProjectQuery } from '../../../queries/projects';
import { ComponentQualifier } from '../../../types/component';
import { Facet, ReferencedComponent } from '../../../types/issues';
import { MetricKey } from '../../../types/metrics';
@@ -48,13 +49,23 @@ interface SearchedProject {
name: string;
}
-export class ProjectFacet extends React.PureComponent<Props> {
- handleSearch = (
+export function ProjectFacet(props: Readonly<Props>) {
+ const {
+ component,
+ fetching,
+ onChange,
+ onToggle,
+ open,
+ projects,
+ query,
+ referencedComponents,
+ stats,
+ } = props;
+
+ const handleSearch = (
query: string,
page = 1,
): Promise<{ results: SearchedProject[]; paging: Paging }> => {
- const { component } = this.props;
-
if (
component &&
[
@@ -91,29 +102,27 @@ export class ProjectFacet extends React.PureComponent<Props> {
}));
};
- getProjectName = (project: string) => {
- const { referencedComponents } = this.props;
-
+ const getProjectName = (project: string) => {
return referencedComponents[project] ? referencedComponents[project].name : project;
};
- loadSearchResultCount = (projects: SearchedProject[]) => {
- return this.props.loadSearchResultCount(MetricKey.projects, {
+ const loadSearchResultCount = (projects: SearchedProject[]) => {
+ return props.loadSearchResultCount(MetricKey.projects, {
projects: projects.map((project) => project.key),
});
};
- renderFacetItem = (projectKey: string) => {
+ const renderFacetItem = (projectKey: string) => {
+ const projectName = getProjectName(projectKey);
return (
- <span>
- <ProjectIcon className="sw-mr-1" />
-
- {this.getProjectName(projectKey)}
- </span>
+ <ProjectItem
+ projectKey={projectKey}
+ projectName={projectName === projectKey ? undefined : projectName}
+ />
);
};
- renderSearchResult = (project: Pick<SearchedProject, 'name'>, term: string) => (
+ const renderSearchResult = (project: Pick<SearchedProject, 'name'>, term: string) => (
<>
<ProjectIcon className="sw-mr-1" />
@@ -121,27 +130,52 @@ export class ProjectFacet extends React.PureComponent<Props> {
</>
);
- render() {
- return (
- <ListStyleFacet<SearchedProject>
- facetHeader={translate('issues.facet.projects')}
- fetching={this.props.fetching}
- getFacetItemText={this.getProjectName}
- getSearchResultKey={(project) => project.key}
- getSearchResultText={(project) => project.name}
- loadSearchResultCount={this.loadSearchResultCount}
- onChange={this.props.onChange}
- onSearch={this.handleSearch}
- onToggle={this.props.onToggle}
- open={this.props.open}
- property={MetricKey.projects}
- query={omit(this.props.query, MetricKey.projects)}
- renderFacetItem={this.renderFacetItem}
- renderSearchResult={this.renderSearchResult}
- searchPlaceholder={translate('search.search_for_projects')}
- stats={this.props.stats}
- values={this.props.projects}
- />
- );
- }
+ return (
+ <ListStyleFacet<SearchedProject>
+ facetHeader={translate('issues.facet.projects')}
+ fetching={fetching}
+ getFacetItemText={getProjectName}
+ getSearchResultKey={(project) => project.key}
+ getSearchResultText={(project) => project.name}
+ loadSearchResultCount={loadSearchResultCount}
+ onChange={onChange}
+ onSearch={handleSearch}
+ onToggle={onToggle}
+ open={open}
+ property={MetricKey.projects}
+ query={omit(query, MetricKey.projects)}
+ renderFacetItem={renderFacetItem}
+ renderSearchResult={renderSearchResult}
+ searchPlaceholder={translate('search.search_for_projects')}
+ stats={stats}
+ values={projects}
+ />
+ );
+}
+
+function ProjectItem({
+ projectKey,
+ projectName,
+}: Readonly<{
+ projectKey: string;
+ projectName?: string;
+}>) {
+ const { data, isLoading } = useProjectQuery(projectKey, {
+ enabled: projectName === undefined,
+ select: (data) => data.components.find((el) => el.key === projectKey),
+ });
+
+ const label = projectName ?? (isLoading ? '' : data?.name ?? projectKey);
+
+ return (
+ <div className="sw-flex sw-items-center">
+ <ProjectIcon className="sw-mr-1" />
+
+ <Spinner loading={projectName === undefined && isLoading} />
+
+ <span className="sw-min-w-0 sw-truncate" title={label}>
+ {label}
+ </span>
+ </div>
+ );
}
diff --git a/server/sonar-web/src/main/js/queries/projects.ts b/server/sonar-web/src/main/js/queries/projects.ts
new file mode 100644
index 00000000000..9fe51a5ccd1
--- /dev/null
+++ b/server/sonar-web/src/main/js/queries/projects.ts
@@ -0,0 +1,35 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { UseQueryOptions, useQuery } from '@tanstack/react-query';
+import { searchProjects } from '../api/components';
+
+export function useProjectQuery<T = Awaited<ReturnType<typeof searchProjects>>>(
+ key: string,
+ options?: Omit<
+ UseQueryOptions<Awaited<ReturnType<typeof searchProjects>>, Error, T>,
+ 'queryKey' | 'queryFn'
+ >,
+) {
+ return useQuery({
+ queryKey: ['project', key],
+ queryFn: ({ queryKey: [, key] }) => searchProjects({ filter: `query=${key}` }),
+ ...options,
+ });
+}