@@ -32,13 +32,14 @@ import { | |||
isMainBranch, | |||
isPullRequest | |||
} from '../../helpers/branch-like'; | |||
import { isSonarCloud } from '../../helpers/system'; | |||
import { getPortfolioUrl } from '../../helpers/urls'; | |||
import { | |||
fetchOrganization, | |||
registerBranchStatus, | |||
requireAuthorization | |||
} from '../../store/rootActions'; | |||
import { BranchLike } from '../../types/branch-like'; | |||
import { isPortfolioLike } from '../../types/component'; | |||
import ComponentContainerNotFound from './ComponentContainerNotFound'; | |||
import { ComponentContext } from './ComponentContext'; | |||
import ComponentNav from './nav/component/ComponentNav'; | |||
@@ -46,7 +47,7 @@ import ComponentNav from './nav/component/ComponentNav'; | |||
interface Props { | |||
children: React.ReactElement; | |||
fetchOrganization: (organization: string) => void; | |||
location: Pick<Location, 'query'>; | |||
location: Pick<Location, 'query' | 'pathname'>; | |||
registerBranchStatus: (branchLike: BranchLike, component: string, status: T.Status) => void; | |||
requireAuthorization: (router: Pick<Router, 'replace'>) => void; | |||
router: Pick<Router, 'replace'>; | |||
@@ -116,9 +117,18 @@ export class ComponentContainer extends React.PureComponent<Props, State> { | |||
.then(([nav, { component }]) => { | |||
const componentWithQualifier = this.addQualifier({ ...nav, ...component }); | |||
if (isSonarCloud()) { | |||
this.props.fetchOrganization(componentWithQualifier.organization); | |||
/* | |||
* There used to be a redirect from /dashboard to /portfolio which caused issues. | |||
* Links should be fixed to not rely on this redirect, but: | |||
* This is a fail-safe in case there are still some faulty links remaining. | |||
*/ | |||
if ( | |||
this.props.location.pathname.match('dashboard') && | |||
isPortfolioLike(componentWithQualifier.qualifier) | |||
) { | |||
this.props.router.replace(getPortfolioUrl(component.key)); | |||
} | |||
return componentWithQualifier; | |||
}, onError) | |||
.then(this.fetchBranches) |
@@ -26,8 +26,8 @@ import { getComponentData } from '../../../api/components'; | |||
import { getComponentNavigation } from '../../../api/nav'; | |||
import { STATUSES } from '../../../apps/background-tasks/constants'; | |||
import { mockBranch, mockMainBranch, mockPullRequest } from '../../../helpers/mocks/branch-like'; | |||
import { isSonarCloud } from '../../../helpers/system'; | |||
import { mockComponent, mockLocation, mockRouter } from '../../../helpers/testMocks'; | |||
import { ComponentQualifier } from '../../../types/component'; | |||
import { ComponentContainer } from '../ComponentContainer'; | |||
jest.mock('../../../api/branches', () => { | |||
@@ -63,10 +63,6 @@ jest.mock('../../../api/nav', () => ({ | |||
}) | |||
})); | |||
jest.mock('../../../helpers/system', () => ({ | |||
isSonarCloud: jest.fn().mockReturnValue(false) | |||
})); | |||
// mock this, because some of its children are using redux store | |||
jest.mock('../nav/component/ComponentNav', () => ({ | |||
default: () => null | |||
@@ -123,18 +119,6 @@ it('updates branches on change', async () => { | |||
expect(registerBranchStatus).toBeCalledTimes(2); | |||
}); | |||
it('loads organization', async () => { | |||
(isSonarCloud as jest.Mock).mockReturnValue(true); | |||
(getComponentData as jest.Mock<any>).mockResolvedValueOnce({ | |||
component: { organization: 'org' } | |||
}); | |||
const fetchOrganization = jest.fn(); | |||
shallowRender({ fetchOrganization }); | |||
await new Promise(setImmediate); | |||
expect(fetchOrganization).toBeCalledWith('org'); | |||
}); | |||
it('fetches status', async () => { | |||
(getComponentData as jest.Mock<any>).mockResolvedValueOnce({ | |||
component: { organization: 'org' } | |||
@@ -196,20 +180,36 @@ it('reload component after task progress finished', async () => { | |||
}); | |||
it('should show component not found if it does not exist', async () => { | |||
(getComponentNavigation as jest.Mock).mockRejectedValue({ status: 404 }); | |||
(getComponentNavigation as jest.Mock).mockRejectedValueOnce({ status: 404 }); | |||
const wrapper = shallowRender(); | |||
await waitAndUpdate(wrapper); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); | |||
it('should redirect if the user has no access', async () => { | |||
(getComponentNavigation as jest.Mock).mockRejectedValue({ status: 403 }); | |||
(getComponentNavigation as jest.Mock).mockRejectedValueOnce({ status: 403 }); | |||
const requireAuthorization = jest.fn(); | |||
const wrapper = shallowRender({ requireAuthorization }); | |||
await waitAndUpdate(wrapper); | |||
expect(requireAuthorization).toBeCalled(); | |||
}); | |||
it('should redirect if the component is a portfolio', async () => { | |||
const componentKey = 'comp-key'; | |||
(getComponentData as jest.Mock<any>).mockResolvedValueOnce({ | |||
component: { key: componentKey, breadcrumbs: [{ qualifier: ComponentQualifier.Portfolio }] } | |||
}); | |||
const replace = jest.fn(); | |||
const wrapper = shallowRender({ | |||
location: mockLocation({ pathname: '/dashboard' }), | |||
router: mockRouter({ replace }) | |||
}); | |||
await waitAndUpdate(wrapper); | |||
expect(replace).toBeCalledWith({ pathname: '/portfolio', query: { id: componentKey } }); | |||
}); | |||
function shallowRender(props: Partial<ComponentContainer['props']> = {}) { | |||
return shallow<ComponentContainer>( | |||
<ComponentContainer |
@@ -22,7 +22,7 @@ import * as React from 'react'; | |||
import { Link } from 'react-router'; | |||
import QualifierIcon from 'sonar-ui-common/components/icons/QualifierIcon'; | |||
import { isMainBranch } from '../../../../helpers/branch-like'; | |||
import { getProjectUrl } from '../../../../helpers/urls'; | |||
import { getComponentOverviewUrl } from '../../../../helpers/urls'; | |||
import { BranchLike } from '../../../../types/branch-like'; | |||
interface Props { | |||
@@ -53,7 +53,7 @@ export function Breadcrumb(props: Props) { | |||
<Link | |||
className="link-no-underline text-ellipsis" | |||
title={breadcrumbElement.name} | |||
to={getProjectUrl(breadcrumbElement.key)}> | |||
to={getComponentOverviewUrl(breadcrumbElement.key, breadcrumbElement.qualifier)}> | |||
{breadcrumbElement.name} | |||
</Link> | |||
) : ( |
@@ -29,8 +29,9 @@ import { hasMessage, translate } from 'sonar-ui-common/helpers/l10n'; | |||
import { withAppState } from '../../../../components/hoc/withAppState'; | |||
import { getBranchLikeQuery, isMainBranch, isPullRequest } from '../../../../helpers/branch-like'; | |||
import { isSonarCloud } from '../../../../helpers/system'; | |||
import { getPortfolioUrl, getProjectQueryUrl } from '../../../../helpers/urls'; | |||
import { BranchLike, BranchParameters } from '../../../../types/branch-like'; | |||
import { ComponentQualifier } from '../../../../types/component'; | |||
import { ComponentQualifier, isPortfolioLike } from '../../../../types/component'; | |||
import './Menu.css'; | |||
const SETTINGS_URLS = [ | |||
@@ -95,9 +96,7 @@ export class Menu extends React.PureComponent<Props> { | |||
isPortfolio = () => { | |||
const { qualifier } = this.props.component; | |||
return ( | |||
qualifier === ComponentQualifier.Portfolio || qualifier === ComponentQualifier.SubPortfolio | |||
); | |||
return isPortfolioLike(qualifier); | |||
}; | |||
isApplication = () => { | |||
@@ -112,11 +111,12 @@ export class Menu extends React.PureComponent<Props> { | |||
return { id: this.props.component.key, ...getBranchLikeQuery(this.props.branchLike) }; | |||
}; | |||
renderDashboardLink = (query: Query, isPortfolio: boolean) => { | |||
const pathname = isPortfolio ? '/portfolio' : '/dashboard'; | |||
renderDashboardLink = ({ id, ...branchLike }: Query, isPortfolio: boolean) => { | |||
return ( | |||
<li> | |||
<Link activeClassName="active" to={{ pathname, query }}> | |||
<Link | |||
activeClassName="active" | |||
to={isPortfolio ? getPortfolioUrl(id) : getProjectQueryUrl(id, branchLike)}> | |||
{translate('overview.page')} | |||
</Link> | |||
</li> |
@@ -19,9 +19,8 @@ exports[`should render correctly 1`] = ` | |||
title="parent-portfolio" | |||
to={ | |||
Object { | |||
"pathname": "/dashboard", | |||
"pathname": "/portfolio", | |||
"query": Object { | |||
"branch": undefined, | |||
"id": "parent-portfolio", | |||
}, | |||
} |
@@ -31,7 +31,8 @@ import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner'; | |||
import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n'; | |||
import { scrollToElement } from 'sonar-ui-common/helpers/scrolling'; | |||
import { getSuggestions } from '../../../api/components'; | |||
import { getCodeUrl, getProjectUrl } from '../../../helpers/urls'; | |||
import { getCodeUrl, getComponentOverviewUrl } from '../../../helpers/urls'; | |||
import { ComponentQualifier } from '../../../types/component'; | |||
import RecentHistory from '../RecentHistory'; | |||
import './Search.css'; | |||
import { ComponentResult, More, Results, sortQualifiers } from './utils'; | |||
@@ -275,20 +276,30 @@ export class Search extends React.PureComponent<Props, State> { | |||
}; | |||
openSelected = () => { | |||
const { selected } = this.state; | |||
const { results, selected } = this.state; | |||
if (selected) { | |||
if (selected.startsWith('qualifier###')) { | |||
this.searchMore(selected.substr(12)); | |||
if (!selected) { | |||
return; | |||
} | |||
if (selected.startsWith('qualifier###')) { | |||
this.searchMore(selected.substr(12)); | |||
} else { | |||
const file = this.findFile(selected); | |||
if (file) { | |||
this.props.router.push(getCodeUrl(file.project!, undefined, file.key)); | |||
} else { | |||
const file = this.findFile(selected); | |||
if (file) { | |||
this.props.router.push(getCodeUrl(file.project!, undefined, file.key)); | |||
} else { | |||
this.props.router.push(getProjectUrl(selected)); | |||
let qualifier = ComponentQualifier.Project; | |||
if ((results[ComponentQualifier.Portfolio] ?? []).find(r => r.key === selected)) { | |||
qualifier = ComponentQualifier.Portfolio; | |||
} else if ((results[ComponentQualifier.SubPortfolio] ?? []).find(r => r.key === selected)) { | |||
qualifier = ComponentQualifier.SubPortfolio; | |||
} | |||
this.closeSearch(); | |||
this.props.router.push(getComponentOverviewUrl(selected, qualifier)); | |||
} | |||
this.closeSearch(); | |||
} | |||
}; | |||
@@ -23,7 +23,7 @@ import Tooltip from 'sonar-ui-common/components/controls/Tooltip'; | |||
import ClockIcon from 'sonar-ui-common/components/icons/ClockIcon'; | |||
import FavoriteIcon from 'sonar-ui-common/components/icons/FavoriteIcon'; | |||
import QualifierIcon from 'sonar-ui-common/components/icons/QualifierIcon'; | |||
import { getCodeUrl, getProjectUrl } from '../../../helpers/urls'; | |||
import { getCodeUrl, getComponentOverviewUrl } from '../../../helpers/urls'; | |||
import { ComponentResult } from './utils'; | |||
interface Props { | |||
@@ -114,7 +114,7 @@ export default class SearchResult extends React.PureComponent<Props, State> { | |||
const isFile = component.qualifier === 'FIL' || component.qualifier === 'UTS'; | |||
const to = isFile | |||
? getCodeUrl(component.project!, undefined, component.key) | |||
: getProjectUrl(component.key); | |||
: getComponentOverviewUrl(component.key, component.qualifier); | |||
return ( | |||
<li |
@@ -20,6 +20,8 @@ | |||
import { shallow, ShallowWrapper } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { elementKeydown } from 'sonar-ui-common/helpers/testUtils'; | |||
import { mockRouter } from '../../../../helpers/testMocks'; | |||
import { ComponentQualifier } from '../../../../types/component'; | |||
import { Search } from '../Search'; | |||
it('selects results', () => { | |||
@@ -29,7 +31,7 @@ it('selects results', () => { | |||
open: true, | |||
results: { | |||
TRK: [component('foo'), component('bar')], | |||
BRC: [component('qwe', 'BRC')] | |||
BRC: [component('qwe', ComponentQualifier.SubProject)] | |||
}, | |||
selected: 'foo' | |||
}); | |||
@@ -44,17 +46,50 @@ it('selects results', () => { | |||
prev(form, 'foo'); | |||
}); | |||
it('opens selected on enter', () => { | |||
const form = shallowRender(); | |||
it('opens selected project on enter', () => { | |||
const router = mockRouter(); | |||
const form = shallowRender({ router }); | |||
const selectedKey = 'project'; | |||
form.setState({ | |||
open: true, | |||
results: { TRK: [component('foo')] }, | |||
selected: 'foo' | |||
results: { [ComponentQualifier.Project]: [component(selectedKey)] }, | |||
selected: selectedKey | |||
}); | |||
elementKeydown(form.find('SearchBox'), 13); | |||
expect(router.push).toBeCalledWith({ pathname: '/dashboard', query: { id: selectedKey } }); | |||
}); | |||
it('opens selected portfolio on enter', () => { | |||
const router = mockRouter(); | |||
const form = shallowRender({ router }); | |||
const selectedKey = 'portfolio'; | |||
form.setState({ | |||
open: true, | |||
results: { | |||
[ComponentQualifier.Portfolio]: [component(selectedKey, ComponentQualifier.Portfolio)] | |||
}, | |||
selected: selectedKey | |||
}); | |||
const openSelected = jest.fn(); | |||
(form.instance() as Search).openSelected = openSelected; | |||
elementKeydown(form.find('SearchBox'), 13); | |||
expect(router.push).toBeCalledWith({ pathname: '/portfolio', query: { id: selectedKey } }); | |||
}); | |||
it('opens selected subportfolio on enter', () => { | |||
const router = mockRouter(); | |||
const form = shallowRender({ router }); | |||
const selectedKey = 'sbprtfl'; | |||
form.setState({ | |||
open: true, | |||
results: { | |||
[ComponentQualifier.SubPortfolio]: [component(selectedKey, ComponentQualifier.SubPortfolio)] | |||
}, | |||
selected: selectedKey | |||
}); | |||
elementKeydown(form.find('SearchBox'), 13); | |||
expect(openSelected).toBeCalled(); | |||
expect(router.push).toBeCalledWith({ pathname: '/portfolio', query: { id: selectedKey } }); | |||
}); | |||
it('shows warning about short input', () => { | |||
@@ -76,7 +111,7 @@ function shallowRender(props: Partial<Search['props']> = {}) { | |||
); | |||
} | |||
function component(key: string, qualifier = 'TRK') { | |||
function component(key: string, qualifier = ComponentQualifier.Project) { | |||
return { key, name: key, qualifier }; | |||
} | |||
@@ -19,7 +19,6 @@ exports[`renders favorite 1`] = ` | |||
Object { | |||
"pathname": "/dashboard", | |||
"query": Object { | |||
"branch": undefined, | |||
"id": "foo", | |||
}, | |||
} | |||
@@ -71,7 +70,6 @@ exports[`renders match 1`] = ` | |||
Object { | |||
"pathname": "/dashboard", | |||
"query": Object { | |||
"branch": undefined, | |||
"id": "foo", | |||
}, | |||
} | |||
@@ -122,7 +120,6 @@ exports[`renders organizations 1`] = ` | |||
Object { | |||
"pathname": "/dashboard", | |||
"query": Object { | |||
"branch": undefined, | |||
"id": "foo", | |||
}, | |||
} | |||
@@ -178,7 +175,6 @@ exports[`renders organizations 2`] = ` | |||
Object { | |||
"pathname": "/dashboard", | |||
"query": Object { | |||
"branch": undefined, | |||
"id": "foo", | |||
}, | |||
} | |||
@@ -229,7 +225,6 @@ exports[`renders projects 1`] = ` | |||
Object { | |||
"pathname": "/dashboard", | |||
"query": Object { | |||
"branch": undefined, | |||
"id": "qwe", | |||
}, | |||
} | |||
@@ -285,7 +280,6 @@ exports[`renders recently browsed 1`] = ` | |||
Object { | |||
"pathname": "/dashboard", | |||
"query": Object { | |||
"branch": undefined, | |||
"id": "foo", | |||
}, | |||
} | |||
@@ -336,7 +330,6 @@ exports[`renders selected 1`] = ` | |||
Object { | |||
"pathname": "/dashboard", | |||
"query": Object { | |||
"branch": undefined, | |||
"id": "foo", | |||
}, | |||
} | |||
@@ -385,7 +378,6 @@ exports[`renders selected 2`] = ` | |||
Object { | |||
"pathname": "/dashboard", | |||
"query": Object { | |||
"branch": undefined, | |||
"id": "foo", | |||
}, | |||
} |
@@ -27,6 +27,7 @@ import Level from 'sonar-ui-common/components/ui/Level'; | |||
import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n'; | |||
import MetaLink from '../../../app/components/nav/component/projectInformation/meta/MetaLink'; | |||
import { orderLinks } from '../../../helpers/projectLinks'; | |||
import { getProjectUrl } from '../../../helpers/urls'; | |||
interface Props { | |||
project: T.MyProject; | |||
@@ -82,7 +83,7 @@ export default function ProjectCard({ project }: Props) { | |||
</aside> | |||
<h3 className="account-project-name"> | |||
<Link to={{ pathname: '/dashboard', query: { id: project.key } }}>{project.name}</Link> | |||
<Link to={getProjectUrl(project.key)}>{project.name}</Link> | |||
</h3> | |||
{orderedLinks.length > 0 && ( |
@@ -29,7 +29,7 @@ import { | |||
getProjectUrl, | |||
getPullRequestUrl | |||
} from '../../../helpers/urls'; | |||
import { ComponentQualifier } from '../../../types/component'; | |||
import { isPortfolioLike } from '../../../types/component'; | |||
import TaskType from './TaskType'; | |||
interface Props { | |||
@@ -85,7 +85,7 @@ export default function TaskComponent({ task }: Props) { | |||
} | |||
function getTaskComponentUrl(componentKey: string, task: T.Task) { | |||
if (task.componentQualifier === ComponentQualifier.Portfolio) { | |||
if (isPortfolioLike(task.componentQualifier)) { | |||
return getPortfolioUrl(componentKey); | |||
} else if (task.branch) { | |||
return getBranchUrl(componentKey, task.branch); |
@@ -24,6 +24,7 @@ import QualifierIcon from 'sonar-ui-common/components/icons/QualifierIcon'; | |||
import { translate } from 'sonar-ui-common/helpers/l10n'; | |||
import { colors } from '../../../app/theme'; | |||
import { getBranchLikeQuery } from '../../../helpers/branch-like'; | |||
import { getProjectUrl } from '../../../helpers/urls'; | |||
import { BranchLike } from '../../../types/branch-like'; | |||
export function getTooltip(component: T.ComponentMeasure) { | |||
@@ -82,11 +83,9 @@ export default function ComponentName({ | |||
let inner = null; | |||
if (component.refKey && component.qualifier !== 'SVW') { | |||
const branch = rootComponent.qualifier === 'APP' ? { branch: component.branch } : {}; | |||
const branch = rootComponent.qualifier === 'APP' ? component.branch : undefined; | |||
inner = ( | |||
<Link | |||
className="link-with-icon" | |||
to={{ pathname: '/dashboard', query: { id: component.refKey, ...branch } }}> | |||
<Link className="link-with-icon" to={getProjectUrl(component.refKey, branch)}> | |||
<QualifierIcon qualifier={component.qualifier} /> <span>{name}</span> | |||
</Link> | |||
); |
@@ -187,6 +187,7 @@ foo" | |||
Object { | |||
"pathname": "/dashboard", | |||
"query": Object { | |||
"branch": undefined, | |||
"id": "src/main/ts/app", | |||
}, | |||
} |
@@ -23,7 +23,7 @@ import Suggestions from '../../../app/components/embed-docs-modal/Suggestions'; | |||
import { Router, withRouter } from '../../../components/hoc/withRouter'; | |||
import { isPullRequest } from '../../../helpers/branch-like'; | |||
import { BranchLike } from '../../../types/branch-like'; | |||
import { ComponentQualifier } from '../../../types/component'; | |||
import { isPortfolioLike } from '../../../types/component'; | |||
import BranchOverview from '../branches/BranchOverview'; | |||
const EmptyOverview = lazyLoadComponent(() => import('./EmptyOverview')); | |||
@@ -40,9 +40,7 @@ interface Props { | |||
export class App extends React.PureComponent<Props> { | |||
isPortfolio = () => { | |||
return ([ComponentQualifier.Portfolio, ComponentQualifier.SubPortfolio] as string[]).includes( | |||
this.props.component.qualifier | |||
); | |||
return isPortfolioLike(this.props.component.qualifier); | |||
}; | |||
render() { |
@@ -25,7 +25,8 @@ import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n | |||
import { formatMeasure } from 'sonar-ui-common/helpers/measures'; | |||
import { colors } from '../../../app/theme'; | |||
import Measure from '../../../components/measure/Measure'; | |||
import { getProjectUrl } from '../../../helpers/urls'; | |||
import { getComponentOverviewUrl } from '../../../helpers/urls'; | |||
import { ComponentQualifier } from '../../../types/component'; | |||
import { SubComponent } from '../types'; | |||
interface Props { | |||
@@ -79,11 +80,14 @@ export default function WorstProjects({ component, subComponents, total }: Props | |||
<td> | |||
<Link | |||
className="link-with-icon" | |||
to={getProjectUrl(component.refKey || component.key)}> | |||
to={getComponentOverviewUrl( | |||
component.refKey || component.key, | |||
component.qualifier | |||
)}> | |||
<QualifierIcon qualifier={component.qualifier} /> {component.name} | |||
</Link> | |||
</td> | |||
{component.qualifier === 'TRK' | |||
{component.qualifier === ComponentQualifier.Project | |||
? renderCell(component.measures, 'alert_status', 'LEVEL') | |||
: renderCell(component.measures, 'releasability_rating', 'RATING')} | |||
{renderCell(component.measures, 'reliability_rating', 'RATING')} |
@@ -56,9 +56,8 @@ exports[`renders 1`] = ` | |||
style={Object {}} | |||
to={ | |||
Object { | |||
"pathname": "/dashboard", | |||
"pathname": "/portfolio", | |||
"query": Object { | |||
"branch": undefined, | |||
"id": "foo", | |||
}, | |||
} | |||
@@ -155,7 +154,6 @@ exports[`renders 1`] = ` | |||
Object { | |||
"pathname": "/dashboard", | |||
"query": Object { | |||
"branch": undefined, | |||
"id": "barbar", | |||
}, | |||
} | |||
@@ -252,7 +250,6 @@ exports[`renders 1`] = ` | |||
Object { | |||
"pathname": "/dashboard", | |||
"query": Object { | |||
"branch": undefined, | |||
"id": "bazbaz", | |||
}, | |||
} |
@@ -25,8 +25,7 @@ import QualifierIcon from 'sonar-ui-common/components/icons/QualifierIcon'; | |||
import DateTooltipFormatter from 'sonar-ui-common/components/intl/DateTooltipFormatter'; | |||
import { Project } from '../../api/components'; | |||
import PrivacyBadgeContainer from '../../components/common/PrivacyBadgeContainer'; | |||
import { getPortfolioUrl, getProjectUrl } from '../../helpers/urls'; | |||
import { ComponentQualifier } from '../../types/component'; | |||
import { getComponentOverviewUrl } from '../../helpers/urls'; | |||
import './ProjectRow.css'; | |||
import ProjectRowActions from './ProjectRowActions'; | |||
@@ -43,12 +42,6 @@ export default class ProjectRow extends React.PureComponent<Props> { | |||
this.props.onProjectCheck(this.props.project, checked); | |||
}; | |||
getComponentUrl(project: Project) { | |||
return project.qualifier === ComponentQualifier.Portfolio | |||
? getPortfolioUrl(project.key) | |||
: getProjectUrl(project.key); | |||
} | |||
render() { | |||
const { organization, project, selected } = this.props; | |||
@@ -59,7 +52,9 @@ export default class ProjectRow extends React.PureComponent<Props> { | |||
</td> | |||
<td className="nowrap hide-overflow project-row-text-cell"> | |||
<Link className="link-with-icon" to={this.getComponentUrl(project)}> | |||
<Link | |||
className="link-with-icon" | |||
to={getComponentOverviewUrl(project.key, project.qualifier)}> | |||
<QualifierIcon className="little-spacer-right" qualifier={project.qualifier} /> | |||
<Tooltip overlay={project.name} placement="left"> |
@@ -24,7 +24,6 @@ exports[`renders 1`] = ` | |||
Object { | |||
"pathname": "/dashboard", | |||
"query": Object { | |||
"branch": undefined, | |||
"id": "project", | |||
}, | |||
} | |||
@@ -217,7 +216,6 @@ exports[`renders: with lastAnalysisDate 1`] = ` | |||
Object { | |||
"pathname": "/dashboard", | |||
"query": Object { | |||
"branch": undefined, | |||
"id": "project", | |||
}, | |||
} |
@@ -24,6 +24,7 @@ import ListFooter from 'sonar-ui-common/components/controls/ListFooter'; | |||
import QualifierIcon from 'sonar-ui-common/components/icons/QualifierIcon'; | |||
import { translate } from 'sonar-ui-common/helpers/l10n'; | |||
import { getProfileProjects } from '../../../api/quality-profiles'; | |||
import { getProjectUrl } from '../../../helpers/urls'; | |||
import { Profile } from '../types'; | |||
import ChangeProjectsForm from './ChangeProjectsForm'; | |||
@@ -141,9 +142,7 @@ export default class ProfileProjects extends React.PureComponent<Props, State> { | |||
<ul> | |||
{projects.map(project => ( | |||
<li className="spacer-top js-profile-project" data-key={project.key} key={project.key}> | |||
<Link | |||
className="link-with-icon" | |||
to={{ pathname: '/dashboard', query: { id: project.key } }}> | |||
<Link className="link-with-icon" to={getProjectUrl(project.key)}> | |||
<QualifierIcon qualifier="TRK" /> <span>{project.name}</span> | |||
</Link> | |||
</li> |
@@ -69,6 +69,7 @@ exports[`should render correctly 2`] = ` | |||
Object { | |||
"pathname": "/dashboard", | |||
"query": Object { | |||
"branch": undefined, | |||
"id": "org.sonarsource.xml:xml", | |||
}, | |||
} |
@@ -17,9 +17,11 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { ComponentQualifier } from '../../types/component'; | |||
import { | |||
getComponentDrilldownUrl, | |||
getComponentIssuesUrl, | |||
getComponentOverviewUrl, | |||
getComponentSecurityHotspotsUrl, | |||
getQualityGatesUrl, | |||
getQualityGateUrl | |||
@@ -67,6 +69,33 @@ describe('getComponentSecurityHotspotsUrl', () => { | |||
}); | |||
}); | |||
describe('getComponentOverviewUrl', () => { | |||
it('should return a portfolio url for a portfolio', () => { | |||
expect(getComponentOverviewUrl(SIMPLE_COMPONENT_KEY, ComponentQualifier.Portfolio)).toEqual({ | |||
pathname: '/portfolio', | |||
query: { id: SIMPLE_COMPONENT_KEY } | |||
}); | |||
}); | |||
it('should return a portfolio url for a subportfolio', () => { | |||
expect(getComponentOverviewUrl(SIMPLE_COMPONENT_KEY, ComponentQualifier.SubPortfolio)).toEqual({ | |||
pathname: '/portfolio', | |||
query: { id: SIMPLE_COMPONENT_KEY } | |||
}); | |||
}); | |||
it('should return a dashboard url for a project', () => { | |||
expect(getComponentOverviewUrl(SIMPLE_COMPONENT_KEY, ComponentQualifier.Project)).toEqual({ | |||
pathname: '/dashboard', | |||
query: { id: SIMPLE_COMPONENT_KEY } | |||
}); | |||
}); | |||
it('should return a dashboard url for an app', () => { | |||
expect(getComponentOverviewUrl(SIMPLE_COMPONENT_KEY, ComponentQualifier.Application)).toEqual({ | |||
pathname: '/dashboard', | |||
query: { id: SIMPLE_COMPONENT_KEY } | |||
}); | |||
}); | |||
}); | |||
describe('#getComponentDrilldownUrl', () => { | |||
it('should return component drilldown url', () => { | |||
expect( |
@@ -19,16 +19,31 @@ | |||
*/ | |||
import { getBaseUrl, Location } from 'sonar-ui-common/helpers/urls'; | |||
import { getProfilePath } from '../apps/quality-profiles/utils'; | |||
import { BranchLike } from '../types/branch-like'; | |||
import { BranchLike, BranchParameters } from '../types/branch-like'; | |||
import { ComponentQualifier, isPortfolioLike } from '../types/component'; | |||
import { GraphType } from '../types/project-activity'; | |||
import { getBranchLikeQuery, isBranch, isMainBranch, isPullRequest } from './branch-like'; | |||
type Query = Location['query']; | |||
export function getComponentOverviewUrl( | |||
componentKey: string, | |||
componentQualifier: ComponentQualifier | string, | |||
branchParameters?: BranchParameters | |||
) { | |||
return isPortfolioLike(componentQualifier) | |||
? getPortfolioUrl(componentKey) | |||
: getProjectQueryUrl(componentKey, branchParameters); | |||
} | |||
export function getProjectUrl(project: string, branch?: string): Location { | |||
return { pathname: '/dashboard', query: { id: project, branch } }; | |||
} | |||
export function getProjectQueryUrl(project: string, branchParameters?: BranchParameters): Location { | |||
return { pathname: '/dashboard', query: { id: project, ...branchParameters } }; | |||
} | |||
export function getPortfolioUrl(key: string): Location { | |||
return { pathname: '/portfolio', query: { id: key } }; | |||
} |
@@ -17,6 +17,7 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
export enum ComponentQualifier { | |||
Application = 'APP', | |||
Directory = 'DIR', | |||
@@ -29,6 +30,16 @@ export enum ComponentQualifier { | |||
TestFile = 'UTS' | |||
} | |||
export function isPortfolioLike(componentQualifier?: string | ComponentQualifier) { | |||
return Boolean( | |||
componentQualifier && | |||
[ | |||
ComponentQualifier.Portfolio.toString(), | |||
ComponentQualifier.SubPortfolio.toString() | |||
].includes(componentQualifier) | |||
); | |||
} | |||
export enum Visibility { | |||
Public = 'public', | |||
Private = 'private' |