Procházet zdrojové kódy

SONAR-20510 Always show project name in project facet

tags/10.5.0.89998
Viktor Vorona před 3 měsíci
rodič
revize
afa1e65cfb

+ 31
- 0
server/sonar-web/src/main/js/api/mocks/ComponentsServiceMock.ts Zobrazit soubor

getDuplications, getDuplications,
getSources, getSources,
getTree, getTree,
searchProjects,
setApplicationTags, setApplicationTags,
setProjectTags, setProjectTags,
} from '../components'; } from '../components';
} from './data/components'; } from './data/components';
import { mockIssuesList } from './data/issues'; import { mockIssuesList } from './data/issues';
import { MeasureRecords, mockFullMeasureData } from './data/measures'; import { MeasureRecords, mockFullMeasureData } from './data/measures';
import { mockProjects } from './data/projects';
import { listAllComponent, listChildComponent, listLeavesComponent } from './data/utils'; import { listAllComponent, listChildComponent, listLeavesComponent } from './data/utils';


jest.mock('../components'); jest.mock('../components');
export default class ComponentsServiceMock { export default class ComponentsServiceMock {
failLoadingComponentStatus: HttpStatus | undefined = undefined; failLoadingComponentStatus: HttpStatus | undefined = undefined;
defaultComponents: ComponentTree[]; defaultComponents: ComponentTree[];
defaultProjects: ComponentRaw[];
components: ComponentTree[]; components: ComponentTree[];
defaultSourceFiles: SourceFile[]; defaultSourceFiles: SourceFile[];
sourceFiles: SourceFile[]; sourceFiles: SourceFile[];
defaultMeasures: MeasureRecords; defaultMeasures: MeasureRecords;
measures: MeasureRecords; measures: MeasureRecords;
projects: ComponentRaw[];


constructor(components?: ComponentTree[], sourceFiles?: SourceFile[], measures?: MeasureRecords) { constructor(components?: ComponentTree[], sourceFiles?: SourceFile[], measures?: MeasureRecords) {
this.defaultComponents = components || [mockFullComponentTree()]; this.defaultComponents = components || [mockFullComponentTree()];
(acc, tree) => ({ ...acc, ...mockFullMeasureData(tree, issueList) }), (acc, tree) => ({ ...acc, ...mockFullMeasureData(tree, issueList) }),
{}, {},
); );
this.defaultProjects = mockProjects();


this.components = cloneDeep(this.defaultComponents); this.components = cloneDeep(this.defaultComponents);
this.sourceFiles = cloneDeep(this.defaultSourceFiles); this.sourceFiles = cloneDeep(this.defaultSourceFiles);
this.measures = cloneDeep(this.defaultMeasures); this.measures = cloneDeep(this.defaultMeasures);
this.projects = cloneDeep(this.defaultProjects);


jest.mocked(getComponentTree).mockImplementation(this.handleGetComponentTree); jest.mocked(getComponentTree).mockImplementation(this.handleGetComponentTree);
jest.mocked(getChildren).mockImplementation(this.handleGetChildren); jest.mocked(getChildren).mockImplementation(this.handleGetChildren);
jest.mocked(getBreadcrumbs).mockImplementation(this.handleGetBreadcrumbs); jest.mocked(getBreadcrumbs).mockImplementation(this.handleGetBreadcrumbs);
jest.mocked(setProjectTags).mockImplementation(this.handleSetProjectTags); jest.mocked(setProjectTags).mockImplementation(this.handleSetProjectTags);
jest.mocked(setApplicationTags).mockImplementation(this.handleSetApplicationTags); 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) => { findComponentTree = (key: string, from?: ComponentTree) => {
let tree: ComponentTree | undefined; let tree: ComponentTree | undefined;
const recurse = (node: ComponentTree): boolean => { const recurse = (node: ComponentTree): boolean => {

+ 1
- 1
server/sonar-web/src/main/js/api/mocks/IssuesServiceMock.ts Zobrazit soubor

issueStatuses: ISSUE_STATUSES, issueStatuses: ISSUE_STATUSES,
types: ISSUE_TYPES, types: ISSUE_TYPES,
scopes: SOURCE_SCOPES.map(({ scope }) => scope), scopes: SOURCE_SCOPES.map(({ scope }) => scope),
projects: ['org.project1', 'org.project2'],
projects: ['org.project1', 'org.sonarsource.javascript:javascript'],
impactSoftwareQualities: Object.values(SoftwareQuality), impactSoftwareQualities: Object.values(SoftwareQuality),
impactSeverities: Object.values(SoftwareImpactSeverity), impactSeverities: Object.values(SoftwareImpactSeverity),
cleanCodeAttributeCategories: cleanCodeCategories, cleanCodeAttributeCategories: cleanCodeCategories,

+ 1
- 1
server/sonar-web/src/main/js/api/mocks/data/issues.ts Zobrazit soubor

quickFixAvailable: true, quickFixAvailable: true,
tags: ['unused'], tags: ['unused'],
codeVariants: ['variant 1', 'variant 2'], codeVariants: ['variant 1', 'variant 2'],
project: 'org.project2',
project: 'org.sonarsource.javascript:javascript',
assignee: 'email1@sonarsource.com', assignee: 'email1@sonarsource.com',
author: 'email3@sonarsource.com', author: 'email3@sonarsource.com',
issueStatus: IssueStatus.Confirmed, issueStatus: IssueStatus.Confirmed,

+ 6
- 1
server/sonar-web/src/main/js/apps/issues/__tests__/IssuesApp-Filtering-it.tsx Zobrazit soubor



// Project // Project
await user.click(ui.projectFacet.get()); 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 // Assignee
await user.click(ui.assigneeFacet.get()); await user.click(ui.assigneeFacet.get());

+ 74
- 40
server/sonar-web/src/main/js/apps/issues/sidebar/ProjectFacet.tsx Zobrazit soubor

* along with this program; if not, write to the Free Software Foundation, * along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * 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 { omit } from 'lodash';
import * as React from 'react'; import * as React from 'react';
import { getTree, searchProjects } from '../../../api/components'; import { getTree, searchProjects } from '../../../api/components';
import { translate } from '../../../helpers/l10n'; import { translate } from '../../../helpers/l10n';
import { highlightTerm } from '../../../helpers/search'; import { highlightTerm } from '../../../helpers/search';
import { useProjectQuery } from '../../../queries/projects';
import { ComponentQualifier } from '../../../types/component'; import { ComponentQualifier } from '../../../types/component';
import { Facet, ReferencedComponent } from '../../../types/issues'; import { Facet, ReferencedComponent } from '../../../types/issues';
import { MetricKey } from '../../../types/metrics'; import { MetricKey } from '../../../types/metrics';
name: string; 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, query: string,
page = 1, page = 1,
): Promise<{ results: SearchedProject[]; paging: Paging }> => { ): Promise<{ results: SearchedProject[]; paging: Paging }> => {
const { component } = this.props;

if ( if (
component && component &&
[ [
})); }));
}; };


getProjectName = (project: string) => {
const { referencedComponents } = this.props;

const getProjectName = (project: string) => {
return referencedComponents[project] ? referencedComponents[project].name : project; 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), projects: projects.map((project) => project.key),
}); });
}; };


renderFacetItem = (projectKey: string) => {
const renderFacetItem = (projectKey: string) => {
const projectName = getProjectName(projectKey);
return ( 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" /> <ProjectIcon className="sw-mr-1" />


</> </>
); );


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>
);
} }

+ 35
- 0
server/sonar-web/src/main/js/queries/projects.ts Zobrazit soubor

/*
* 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,
});
}

Načítá se…
Zrušit
Uložit