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