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';
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'>;
.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)
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', () => {
})
}));
-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
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' }
});
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
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 {
<Link
className="link-no-underline text-ellipsis"
title={breadcrumbElement.name}
- to={getProjectUrl(breadcrumbElement.key)}>
+ to={getComponentOverviewUrl(breadcrumbElement.key, breadcrumbElement.qualifier)}>
{breadcrumbElement.name}
</Link>
) : (
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 = [
isPortfolio = () => {
const { qualifier } = this.props.component;
- return (
- qualifier === ComponentQualifier.Portfolio || qualifier === ComponentQualifier.SubPortfolio
- );
+ return isPortfolioLike(qualifier);
};
isApplication = () => {
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>
title="parent-portfolio"
to={
Object {
- "pathname": "/dashboard",
+ "pathname": "/portfolio",
"query": Object {
- "branch": undefined,
"id": "parent-portfolio",
},
}
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';
};
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();
}
};
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 {
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
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', () => {
open: true,
results: {
TRK: [component('foo'), component('bar')],
- BRC: [component('qwe', 'BRC')]
+ BRC: [component('qwe', ComponentQualifier.SubProject)]
},
selected: 'foo'
});
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', () => {
);
}
-function component(key: string, qualifier = 'TRK') {
+function component(key: string, qualifier = ComponentQualifier.Project) {
return { key, name: key, qualifier };
}
Object {
"pathname": "/dashboard",
"query": Object {
- "branch": undefined,
"id": "foo",
},
}
Object {
"pathname": "/dashboard",
"query": Object {
- "branch": undefined,
"id": "foo",
},
}
Object {
"pathname": "/dashboard",
"query": Object {
- "branch": undefined,
"id": "foo",
},
}
Object {
"pathname": "/dashboard",
"query": Object {
- "branch": undefined,
"id": "foo",
},
}
Object {
"pathname": "/dashboard",
"query": Object {
- "branch": undefined,
"id": "qwe",
},
}
Object {
"pathname": "/dashboard",
"query": Object {
- "branch": undefined,
"id": "foo",
},
}
Object {
"pathname": "/dashboard",
"query": Object {
- "branch": undefined,
"id": "foo",
},
}
Object {
"pathname": "/dashboard",
"query": Object {
- "branch": undefined,
"id": "foo",
},
}
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;
</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 && (
getProjectUrl,
getPullRequestUrl
} from '../../../helpers/urls';
-import { ComponentQualifier } from '../../../types/component';
+import { isPortfolioLike } from '../../../types/component';
import TaskType from './TaskType';
interface 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);
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) {
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>
);
Object {
"pathname": "/dashboard",
"query": Object {
+ "branch": undefined,
"id": "src/main/ts/app",
},
}
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'));
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() {
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 {
<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')}
style={Object {}}
to={
Object {
- "pathname": "/dashboard",
+ "pathname": "/portfolio",
"query": Object {
- "branch": undefined,
"id": "foo",
},
}
Object {
"pathname": "/dashboard",
"query": Object {
- "branch": undefined,
"id": "barbar",
},
}
Object {
"pathname": "/dashboard",
"query": Object {
- "branch": undefined,
"id": "bazbaz",
},
}
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';
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;
</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">
Object {
"pathname": "/dashboard",
"query": Object {
- "branch": undefined,
"id": "project",
},
}
Object {
"pathname": "/dashboard",
"query": Object {
- "branch": undefined,
"id": "project",
},
}
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';
<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>
Object {
"pathname": "/dashboard",
"query": Object {
+ "branch": undefined,
"id": "org.sonarsource.xml:xml",
},
}
* 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
});
});
+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(
*/
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 } };
}
* 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',
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'