Browse Source

SONAR-12633 Move project info into sidedrawer

tags/8.2.0.32929
Jeremy Davis 4 years ago
parent
commit
f2a2df85dd
75 changed files with 3179 additions and 3021 deletions
  1. 1
    0
      server/sonar-web/src/main/js/app/components/ComponentContainer.tsx
  2. 63
    60
      server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx
  3. 27
    0
      server/sonar-web/src/main/js/app/components/nav/component/Menu.css
  4. 32
    1
      server/sonar-web/src/main/js/app/components/nav/component/Menu.tsx
  5. 3
    1
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNav-test.tsx
  6. 33
    37
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/Menu-test.tsx
  7. 48
    2
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNav-test.tsx.snap
  8. 0
    1417
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.tsx.snap
  9. 160
    1
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/Menu-test.tsx.snap
  10. 18
    12
      server/sonar-web/src/main/js/app/components/nav/component/projectInformation/DrawerLink.tsx
  11. 70
    0
      server/sonar-web/src/main/js/app/components/nav/component/projectInformation/InfoDrawer.css
  12. 53
    0
      server/sonar-web/src/main/js/app/components/nav/component/projectInformation/InfoDrawer.tsx
  13. 49
    0
      server/sonar-web/src/main/js/app/components/nav/component/projectInformation/InfoDrawerPage.tsx
  14. 47
    0
      server/sonar-web/src/main/js/app/components/nav/component/projectInformation/ProjectInformation.css
  15. 137
    0
      server/sonar-web/src/main/js/app/components/nav/component/projectInformation/ProjectInformation.tsx
  16. 24
    0
      server/sonar-web/src/main/js/app/components/nav/component/projectInformation/ProjectInformationPages.ts
  17. 123
    0
      server/sonar-web/src/main/js/app/components/nav/component/projectInformation/ProjectInformationRenderer.tsx
  18. 40
    0
      server/sonar-web/src/main/js/app/components/nav/component/projectInformation/__tests__/DrawerLink-test.tsx
  19. 44
    0
      server/sonar-web/src/main/js/app/components/nav/component/projectInformation/__tests__/InfoDrawer-test.tsx
  20. 44
    0
      server/sonar-web/src/main/js/app/components/nav/component/projectInformation/__tests__/InfoDrawerPage-test.tsx
  21. 73
    0
      server/sonar-web/src/main/js/app/components/nav/component/projectInformation/__tests__/ProjectInformation-test.tsx
  22. 65
    0
      server/sonar-web/src/main/js/app/components/nav/component/projectInformation/__tests__/ProjectInformationRenderer-test.tsx
  23. 13
    0
      server/sonar-web/src/main/js/app/components/nav/component/projectInformation/__tests__/__snapshots__/DrawerLink-test.tsx.snap
  24. 54
    0
      server/sonar-web/src/main/js/app/components/nav/component/projectInformation/__tests__/__snapshots__/InfoDrawer-test.tsx.snap
  25. 40
    0
      server/sonar-web/src/main/js/app/components/nav/component/projectInformation/__tests__/__snapshots__/InfoDrawerPage-test.tsx.snap
  26. 241
    0
      server/sonar-web/src/main/js/app/components/nav/component/projectInformation/__tests__/__snapshots__/ProjectInformation-test.tsx.snap
  27. 996
    0
      server/sonar-web/src/main/js/app/components/nav/component/projectInformation/__tests__/__snapshots__/ProjectInformationRenderer-test.tsx.snap
  28. 0
    0
      server/sonar-web/src/main/js/app/components/nav/component/projectInformation/badges/BadgeButton.tsx
  29. 6
    5
      server/sonar-web/src/main/js/app/components/nav/component/projectInformation/badges/BadgeParams.tsx
  30. 95
    0
      server/sonar-web/src/main/js/app/components/nav/component/projectInformation/badges/ProjectBadges.tsx
  31. 0
    0
      server/sonar-web/src/main/js/app/components/nav/component/projectInformation/badges/__tests__/BadgeButton-test.tsx
  32. 1
    1
      server/sonar-web/src/main/js/app/components/nav/component/projectInformation/badges/__tests__/BadgeParams-test.tsx
  33. 19
    24
      server/sonar-web/src/main/js/app/components/nav/component/projectInformation/badges/__tests__/ProjectBadges-test.tsx
  34. 0
    0
      server/sonar-web/src/main/js/app/components/nav/component/projectInformation/badges/__tests__/__snapshots__/BadgeButton-test.tsx.snap
  35. 7
    2
      server/sonar-web/src/main/js/app/components/nav/component/projectInformation/badges/__tests__/__snapshots__/BadgeParams-test.tsx.snap
  36. 69
    0
      server/sonar-web/src/main/js/app/components/nav/component/projectInformation/badges/__tests__/__snapshots__/ProjectBadges-test.tsx.snap
  37. 0
    0
      server/sonar-web/src/main/js/app/components/nav/component/projectInformation/badges/__tests__/utils-test.ts
  38. 5
    5
      server/sonar-web/src/main/js/app/components/nav/component/projectInformation/badges/styles.css
  39. 1
    1
      server/sonar-web/src/main/js/app/components/nav/component/projectInformation/badges/utils.ts
  40. 1
    1
      server/sonar-web/src/main/js/app/components/nav/component/projectInformation/meta/MetaKey.tsx
  41. 2
    2
      server/sonar-web/src/main/js/app/components/nav/component/projectInformation/meta/MetaLink.tsx
  42. 12
    10
      server/sonar-web/src/main/js/app/components/nav/component/projectInformation/meta/MetaLinks.tsx
  43. 6
    7
      server/sonar-web/src/main/js/app/components/nav/component/projectInformation/meta/MetaQualityGate.tsx
  44. 10
    16
      server/sonar-web/src/main/js/app/components/nav/component/projectInformation/meta/MetaQualityProfiles.tsx
  45. 75
    0
      server/sonar-web/src/main/js/app/components/nav/component/projectInformation/meta/MetaSize.tsx
  46. 4
    4
      server/sonar-web/src/main/js/app/components/nav/component/projectInformation/meta/MetaTags.tsx
  47. 2
    2
      server/sonar-web/src/main/js/app/components/nav/component/projectInformation/meta/MetaTagsSelector.tsx
  48. 0
    0
      server/sonar-web/src/main/js/app/components/nav/component/projectInformation/meta/__tests__/MetaLink-test.tsx
  49. 5
    5
      server/sonar-web/src/main/js/app/components/nav/component/projectInformation/meta/__tests__/MetaQualityProfiles-test.tsx
  50. 46
    0
      server/sonar-web/src/main/js/app/components/nav/component/projectInformation/meta/__tests__/MetaSize-test.tsx
  51. 1
    1
      server/sonar-web/src/main/js/app/components/nav/component/projectInformation/meta/__tests__/MetaTags-test.tsx
  52. 2
    2
      server/sonar-web/src/main/js/app/components/nav/component/projectInformation/meta/__tests__/MetaTagsSelector-test.tsx
  53. 0
    0
      server/sonar-web/src/main/js/app/components/nav/component/projectInformation/meta/__tests__/__snapshots__/MetaLink-test.tsx.snap
  54. 21
    17
      server/sonar-web/src/main/js/app/components/nav/component/projectInformation/meta/__tests__/__snapshots__/MetaQualityProfiles-test.tsx.snap
  55. 72
    0
      server/sonar-web/src/main/js/app/components/nav/component/projectInformation/meta/__tests__/__snapshots__/MetaSize-test.tsx.snap
  56. 2
    2
      server/sonar-web/src/main/js/app/components/nav/component/projectInformation/meta/__tests__/__snapshots__/MetaTags-test.tsx.snap
  57. 92
    0
      server/sonar-web/src/main/js/app/components/nav/component/projectInformation/notifications/ProjectNotifications.tsx
  58. 3
    10
      server/sonar-web/src/main/js/app/components/nav/component/projectInformation/notifications/__tests__/ProjectNotifications-test.tsx
  59. 75
    0
      server/sonar-web/src/main/js/app/components/nav/component/projectInformation/notifications/__tests__/__snapshots__/ProjectNotifications-test.tsx.snap
  60. 13
    5
      server/sonar-web/src/main/js/app/styles/init/misc.css
  61. 2
    2
      server/sonar-web/src/main/js/apps/account/projects/ProjectCard.tsx
  62. 0
    127
      server/sonar-web/src/main/js/apps/overview/badges/ProjectBadges.tsx
  63. 0
    180
      server/sonar-web/src/main/js/apps/overview/badges/__tests__/__snapshots__/ProjectBadges-test.tsx.snap
  64. 0
    171
      server/sonar-web/src/main/js/apps/overview/meta/MetaContainer.tsx
  65. 0
    109
      server/sonar-web/src/main/js/apps/overview/meta/MetaSize.tsx
  66. 0
    69
      server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaContainer-test.tsx
  67. 0
    398
      server/sonar-web/src/main/js/apps/overview/meta/__tests__/__snapshots__/MetaContainer-test.tsx.snap
  68. 0
    116
      server/sonar-web/src/main/js/apps/overview/notifications/ProjectNotifications.tsx
  69. 0
    108
      server/sonar-web/src/main/js/apps/overview/notifications/__tests__/__snapshots__/ProjectNotifications.tsx.snap
  70. 0
    67
      server/sonar-web/src/main/js/apps/overview/styles.css
  71. 1
    1
      server/sonar-web/src/main/js/apps/projectLinks/LinkRow.tsx
  72. 1
    1
      server/sonar-web/src/main/js/apps/projectLinks/Table.tsx
  73. 8
    8
      server/sonar-web/src/main/js/helpers/__tests__/projectLinks-test.ts
  74. 0
    0
      server/sonar-web/src/main/js/helpers/projectLinks.ts
  75. 22
    11
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 1
- 0
server/sonar-web/src/main/js/app/components/ComponentContainer.tsx View File

@@ -326,6 +326,7 @@ export class ComponentContainer extends React.PureComponent<Props, State> {
currentTaskOnSameBranch={currentTask && this.isSameBranch(currentTask, branchLike)}
isInProgress={isInProgress}
isPending={isPending}
onComponentChange={this.handleComponentChange}
warnings={this.state.warnings}
/>
)}

+ 63
- 60
server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx View File

@@ -29,6 +29,8 @@ import ComponentNavBgTaskNotif from './ComponentNavBgTaskNotif';
import Header from './Header';
import HeaderMeta from './HeaderMeta';
import Menu from './Menu';
import InfoDrawer from './projectInformation/InfoDrawer';
import ProjectInformation from './projectInformation/ProjectInformation';

interface Props {
branchLikes: BranchLike[];
@@ -38,24 +40,27 @@ interface Props {
currentTaskOnSameBranch?: boolean;
isInProgress?: boolean;
isPending?: boolean;
onComponentChange: (changes: Partial<T.Component>) => void;
warnings: string[];
}

export default class ComponentNav extends React.PureComponent<Props> {
mounted = false;
export default function ComponentNav(props: Props) {
const {
branchLikes,
component,
currentBranchLike,
currentTask,
currentTaskOnSameBranch,
isInProgress,
isPending,
warnings
} = props;
const { contextNavHeightRaw, globalNavHeightRaw } = rawSizes;

componentDidMount() {
this.populateRecentHistory();
}

componentDidUpdate(prevProps: Props) {
if (this.props.component.key !== prevProps.component.key) {
this.populateRecentHistory();
}
}
const [displayProjectInfo, setDisplayProjectInfo] = React.useState(false);

populateRecentHistory = () => {
const { breadcrumbs } = this.props.component;
React.useEffect(() => {
const { breadcrumbs, key, name, organization } = component;
const { qualifier } = breadcrumbs[breadcrumbs.length - 1];
if (
[
@@ -65,55 +70,53 @@ export default class ComponentNav extends React.PureComponent<Props> {
ComponentQualifier.Developper
].includes(qualifier as ComponentQualifier)
) {
RecentHistory.add(
this.props.component.key,
this.props.component.name,
qualifier.toLowerCase(),
this.props.component.organization
);
RecentHistory.add(key, name, qualifier.toLowerCase(), organization);
}
};
}, [component, component.key]);

render() {
const { component, currentBranchLike, currentTask, isInProgress, isPending } = this.props;
const contextNavHeight = rawSizes.contextNavHeightRaw;
let notifComponent;
if (isInProgress || isPending || (currentTask && currentTask.status === STATUSES.FAILED)) {
notifComponent = (
<ComponentNavBgTaskNotif
component={component}
currentTask={currentTask}
currentTaskOnSameBranch={this.props.currentTaskOnSameBranch}
isInProgress={isInProgress}
isPending={isPending}
/>
);
}
return (
<ContextNavBar
height={notifComponent ? contextNavHeight + 30 : contextNavHeight}
id="context-navigation"
notif={notifComponent}>
<div
className={classNames(
'display-flex-center display-flex-space-between little-padded-top',
{
'padded-bottom': this.props.warnings.length === 0
}
)}>
<Header
branchLikes={this.props.branchLikes}
component={component}
currentBranchLike={currentBranchLike}
/>
<HeaderMeta
branchLike={currentBranchLike}
component={component}
warnings={this.props.warnings}
/>
</div>
<Menu branchLike={currentBranchLike} component={component} />
</ContextNavBar>
let notifComponent;
if (isInProgress || isPending || (currentTask && currentTask.status === STATUSES.FAILED)) {
notifComponent = (
<ComponentNavBgTaskNotif
component={component}
currentTask={currentTask}
currentTaskOnSameBranch={currentTaskOnSameBranch}
isInProgress={isInProgress}
isPending={isPending}
/>
);
}

const contextNavHeight = notifComponent ? contextNavHeightRaw + 30 : contextNavHeightRaw;

return (
<ContextNavBar height={contextNavHeight} id="context-navigation" notif={notifComponent}>
<div
className={classNames('display-flex-center display-flex-space-between little-padded-top', {
'padded-bottom': warnings.length === 0
})}>
<Header
branchLikes={branchLikes}
component={component}
currentBranchLike={currentBranchLike}
/>
<HeaderMeta branchLike={currentBranchLike} component={component} warnings={warnings} />
</div>
<Menu
branchLike={currentBranchLike}
component={component}
onToggleProjectInfo={() => setDisplayProjectInfo(!displayProjectInfo)}
/>
<InfoDrawer
displayed={displayProjectInfo}
onClose={() => setDisplayProjectInfo(false)}
top={globalNavHeightRaw + contextNavHeightRaw}>
<ProjectInformation
branchLike={currentBranchLike}
component={component}
onComponentChange={props.onComponentChange}
/>
</InfoDrawer>
</ContextNavBar>
);
}

+ 27
- 0
server/sonar-web/src/main/js/app/components/nav/component/Menu.css View File

@@ -0,0 +1,27 @@
/*
* SonarQube
* Copyright (C) 2009-2020 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.
*/
.navbar-tabs > li > a.menu-button {
color: var(--darkBlue);
}

.navbar-tabs > li > a.menu-button:hover {
color: var(--blue);
border-bottom-color: transparent;
}

+ 32
- 1
server/sonar-web/src/main/js/app/components/nav/component/Menu.tsx View File

@@ -21,6 +21,7 @@ import * as classNames from 'classnames';
import * as React from 'react';
import { Link } from 'react-router';
import Dropdown from 'sonar-ui-common/components/controls/Dropdown';
import BulletListIcon from 'sonar-ui-common/components/icons/BulletListIcon';
import DropdownIcon from 'sonar-ui-common/components/icons/DropdownIcon';
import NavBarTabs from 'sonar-ui-common/components/ui/NavBarTabs';
import { hasMessage, translate } from 'sonar-ui-common/helpers/l10n';
@@ -29,6 +30,7 @@ import { getBranchLikeQuery, isMainBranch, isPullRequest } from '../../../../hel
import { isSonarCloud } from '../../../../helpers/system';
import { BranchLike } from '../../../../types/branch-like';
import { ComponentQualifier } from '../../../../types/component';
import './Menu.css';

const SETTINGS_URLS = [
'/project/admin',
@@ -51,6 +53,7 @@ interface Props {
appState: Pick<T.AppState, 'branchesEnabled'>;
branchLike: BranchLike | undefined;
component: T.Component;
onToggleProjectInfo: () => void;
}

export class Menu extends React.PureComponent<Props> {
@@ -249,6 +252,31 @@ export class Menu extends React.PureComponent<Props> {
];
}

renderProjectInformationButton() {
if (isPullRequest(this.props.branchLike)) {
return null;
}

return (
(this.isProject() || this.isApplication()) && (
<li>
<a
className="menu-button"
onClick={(e: React.SyntheticEvent<HTMLAnchorElement>) => {
e.preventDefault();
e.currentTarget.blur();
this.props.onToggleProjectInfo();
}}
role="button"
tabIndex={0}>
<BulletListIcon className="little-spacer-right" />
{translate(this.isProject() ? 'project' : 'application', 'info.title')}
</a>
</li>
)
);
}

renderSettingsLink() {
if (!this.getConfiguration().showSettings || this.isApplication() || this.isPortfolio()) {
return null;
@@ -511,7 +539,10 @@ export class Menu extends React.PureComponent<Props> {
{this.renderActivityLink()}
{this.renderExtensions()}
</NavBarTabs>
<NavBarTabs>{this.renderAdministration()}</NavBarTabs>
<NavBarTabs>
{this.renderAdministration()}
{this.renderProjectInformationButton()}
</NavBarTabs>
</div>
);
}

+ 3
- 1
server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNav-test.tsx View File

@@ -35,9 +35,11 @@ it('renders', () => {
branchLikes={[]}
component={component}
currentBranchLike={undefined}
isInProgress={true}
isPending={true}
onComponentChange={jest.fn()}
warnings={[]}
/>
);
wrapper.setState({ isInProgress: true, isPending: true });
expect(wrapper).toMatchSnapshot();
});

+ 33
- 37
server/sonar-web/src/main/js/app/components/nav/component/__tests__/Menu-test.tsx View File

@@ -44,9 +44,7 @@ it('should work with extensions', () => {
configuration: { showSettings: true, extensions: [{ key: 'foo', name: 'Foo' }] },
extensions: [{ key: 'component-foo', name: 'ComponentFoo' }]
};
const wrapper = shallow(
<Menu appState={{ branchesEnabled: true }} branchLike={mainBranch} component={component} />
);
const wrapper = shallowRender({ component });
expect(wrapper.find('Dropdown[data-test="extensions"]')).toMatchSnapshot();
expect(wrapper.find('Dropdown[data-test="administration"]')).toMatchSnapshot();
});
@@ -66,9 +64,7 @@ it('should work with multiple extensions', () => {
{ key: 'component-bar', name: 'ComponentBar' }
]
};
const wrapper = shallow(
<Menu appState={{ branchesEnabled: true }} branchLike={mainBranch} component={component} />
);
const wrapper = shallowRender({ component });
expect(wrapper.find('Dropdown[data-test="extensions"]')).toMatchSnapshot();
expect(wrapper.find('Dropdown[data-test="administration"]')).toMatchSnapshot();
});
@@ -88,30 +84,25 @@ it('should render correctly for security extensions', () => {
{ key: 'component-bar', name: 'ComponentBar' }
]
};
const wrapper = shallow(
<Menu appState={{ branchesEnabled: true }} branchLike={mainBranch} component={component} />
);
const wrapper = shallowRender({ component });
expect(wrapper.find('Dropdown[data-test="extensions"]')).toMatchSnapshot();
expect(wrapper.find('Dropdown[data-test="security"]')).toMatchSnapshot();
});

it('should work for a branch', () => {
const branch = mockBranch({
const branchLike = mockBranch({
name: 'release'
});
[true, false].forEach(showSettings =>
expect(
shallow(
<Menu
appState={{ branchesEnabled: true }}
branchLike={branch}
component={{
...baseComponent,
configuration: { showSettings },
extensions: [{ key: 'component-foo', name: 'ComponentFoo' }]
}}
/>
)
shallowRender({
branchLike,
component: {
...baseComponent,
configuration: { showSettings },
extensions: [{ key: 'component-foo', name: 'ComponentFoo' }]
}
})
).toMatchSnapshot()
);
});
@@ -119,17 +110,14 @@ it('should work for a branch', () => {
it('should work for pull requests', () => {
[true, false].forEach(showSettings =>
expect(
shallow(
<Menu
appState={{ branchesEnabled: true }}
branchLike={mockPullRequest()}
component={{
...baseComponent,
configuration: { showSettings },
extensions: [{ key: 'component-foo', name: 'ComponentFoo' }]
}}
/>
)
shallowRender({
branchLike: mockPullRequest(),
component: {
...baseComponent,
configuration: { showSettings },
extensions: [{ key: 'component-foo', name: 'ComponentFoo' }]
}
})
).toMatchSnapshot()
);
});
@@ -145,10 +133,18 @@ it('should work for all qualifiers', () => {

function checkWithQualifier(qualifier: string) {
const component = { ...baseComponent, configuration: { showSettings: true }, qualifier };
expect(
shallow(
<Menu appState={{ branchesEnabled: true }} branchLike={mainBranch} component={component} />
)
).toMatchSnapshot();
expect(shallowRender({ component })).toMatchSnapshot();
}
});

function shallowRender(props: Partial<Menu['props']>) {
return shallow<Menu>(
<Menu
appState={{ branchesEnabled: true }}
branchLike={mainBranch}
component={baseComponent}
onToggleProjectInfo={jest.fn()}
{...props}
/>
);
}

+ 48
- 2
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNav-test.tsx.snap View File

@@ -2,11 +2,32 @@

exports[`renders 1`] = `
<ContextNavBar
height={72}
height={102}
id="context-navigation"
notif={
<ComponentNavBgTaskNotif
component={
Object {
"breadcrumbs": Array [
Object {
"key": "component",
"name": "component",
"qualifier": "TRK",
},
],
"key": "component",
"name": "component",
"organization": "org",
"qualifier": "TRK",
}
}
isInProgress={true}
isPending={true}
/>
}
>
<div
className="display-flex-center display-flex-space-between little-padder-top padder-bottom"
className="display-flex-center display-flex-space-between little-padded-top padded-bottom"
>
<Connect(Component)
branchLikes={Array []}
@@ -61,6 +82,31 @@ exports[`renders 1`] = `
"qualifier": "TRK",
}
}
onToggleProjectInfo={[Function]}
/>
<InfoDrawer
displayed={false}
onClose={[Function]}
top={120}
>
<Connect(ProjectInformation)
component={
Object {
"breadcrumbs": Array [
Object {
"key": "component",
"name": "component",
"qualifier": "TRK",
},
],
"key": "component",
"name": "component",
"organization": "org",
"qualifier": "TRK",
}
}
onComponentChange={[MockFunction]}
/>
</InfoDrawer>
</ContextNavBar>
`;

+ 0
- 1417
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.tsx.snap
File diff suppressed because it is too large
View File


+ 160
- 1
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/Menu-test.tsx.snap View File

@@ -77,6 +77,24 @@ exports[`should work for a branch 1`] = `
issues.page
</Link>
</li>
<li>
<Link
activeClassName="active"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/security_hotspots",
"query": Object {
"branch": "release",
"id": "foo",
},
}
}
>
layout.security_hotspots
</Link>
</li>
<li>
<Link
activeClassName="active"
@@ -235,6 +253,19 @@ exports[`should work for a branch 1`] = `
>
<Component />
</Dropdown>
<li>
<a
className="menu-button"
onClick={[Function]}
role="button"
tabIndex={0}
>
<BulletListIcon
className="little-spacer-right"
/>
project.info.title
</a>
</li>
</NavBarTabs>
</div>
`;
@@ -281,6 +312,24 @@ exports[`should work for a branch 2`] = `
issues.page
</Link>
</li>
<li>
<Link
activeClassName="active"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/security_hotspots",
"query": Object {
"branch": "release",
"id": "foo",
},
}
}
>
layout.security_hotspots
</Link>
</li>
<li>
<Link
activeClassName="active"
@@ -336,7 +385,21 @@ exports[`should work for a branch 2`] = `
</Link>
</li>
</NavBarTabs>
<NavBarTabs />
<NavBarTabs>
<li>
<a
className="menu-button"
onClick={[Function]}
role="button"
tabIndex={0}
>
<BulletListIcon
className="little-spacer-right"
/>
project.info.title
</a>
</li>
</NavBarTabs>
</div>
`;

@@ -380,6 +443,23 @@ exports[`should work for all qualifiers 1`] = `
issues.page
</Link>
</li>
<li>
<Link
activeClassName="active"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/security_hotspots",
"query": Object {
"id": "foo",
},
}
}
>
layout.security_hotspots
</Link>
</li>
<li>
<Link
activeClassName="active"
@@ -530,6 +610,19 @@ exports[`should work for all qualifiers 1`] = `
>
<Component />
</Dropdown>
<li>
<a
className="menu-button"
onClick={[Function]}
role="button"
tabIndex={0}
>
<BulletListIcon
className="little-spacer-right"
/>
project.info.title
</a>
</li>
</NavBarTabs>
</div>
`;
@@ -796,6 +889,23 @@ exports[`should work for all qualifiers 4`] = `
issues.page
</Link>
</li>
<li>
<Link
activeClassName="active"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/security_hotspots",
"query": Object {
"id": "foo",
},
}
}
>
layout.security_hotspots
</Link>
</li>
<li>
<Link
activeClassName="active"
@@ -878,6 +988,19 @@ exports[`should work for all qualifiers 4`] = `
>
<Component />
</Dropdown>
<li>
<a
className="menu-button"
onClick={[Function]}
role="button"
tabIndex={0}
>
<BulletListIcon
className="little-spacer-right"
/>
application.info.title
</a>
</li>
</NavBarTabs>
</div>
`;
@@ -924,6 +1047,24 @@ exports[`should work for pull requests 1`] = `
issues.page
</Link>
</li>
<li>
<Link
activeClassName="active"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/security_hotspots",
"query": Object {
"id": "foo",
"pullRequest": "1001",
},
}
}
>
layout.security_hotspots
</Link>
</li>
<li>
<Link
activeClassName="active"
@@ -1007,6 +1148,24 @@ exports[`should work for pull requests 2`] = `
issues.page
</Link>
</li>
<li>
<Link
activeClassName="active"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/security_hotspots",
"query": Object {
"id": "foo",
"pullRequest": "1001",
},
}
}
>
layout.security_hotspots
</Link>
</li>
<li>
<Link
activeClassName="active"

server/sonar-web/src/main/js/apps/overview/meta/MetaOrganizationKey.tsx → server/sonar-web/src/main/js/app/components/nav/component/projectInformation/DrawerLink.tsx View File

@@ -18,21 +18,27 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import { ClipboardButton } from 'sonar-ui-common/components/controls/clipboard';
import { translate } from 'sonar-ui-common/helpers/l10n';
import ChevronRightIcon from 'sonar-ui-common/components/icons/ChevronRightIcon';

interface Props {
organization: string;
export interface DrawerLinkProps<P> {
label: string;
onPageChange: (page: P) => void;
to: P;
}

export default function MetaOrganizationKey({ organization }: Props) {
export function DrawerLink<P>(props: DrawerLinkProps<P>) {
const { label, to } = props;

return (
<>
<h4 className="overview-meta-header big-spacer-top">{translate('organization_key')}</h4>
<div className="display-flex-center">
<input className="overview-key" readOnly={true} type="text" value={organization} />
<ClipboardButton className="little-spacer-left" copyValue={organization} />
</div>
</>
<a
className="display-flex-space-between bordered-bottom big-padded"
onClick={() => props.onPageChange(to)}
role="link"
tabIndex={0}>
{label}
<ChevronRightIcon />
</a>
);
}

export default React.memo(DrawerLink);

+ 70
- 0
server/sonar-web/src/main/js/app/components/nav/component/projectInformation/InfoDrawer.css View File

@@ -0,0 +1,70 @@
/*
* SonarQube
* Copyright (C) 2009-2020 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.
*/
:root {
--drawer-width: 380px;
}

/* TODO: should we move this? Or handle it differently? */
.navbar-inner-with-notif .info-drawer {
border-top: 1px solid var(--barBorderColor);
}

.info-drawer-pane {
background-color: white;
right: calc(-1 * var(--drawer-width));
width: var(--drawer-width);
transition: right 0.3s ease-in-out;
border-left: 1px solid var(--barBorderColor);
box-sizing: border-box;
}

.info-drawer-pane.open {
right: 0;
}

.info-drawer {
position: fixed;
/* top is defined programmatically by ComponentNav */
bottom: 0;
z-index: var(--pageSideZIndex);
}

.info-drawer .close-button {
position: absolute;
top: 0;
right: 0;
background: white;
padding: calc(2 * var(--gridSize));
z-index: var(--normalZIndex);
}

.info-drawer .back-button {
cursor: pointer;
}

.info-drawer .back-button:hover {
color: var(--blue);
}

.info-drawer-page {
position: absolute;
top: 0;
bottom: 0;
}

+ 53
- 0
server/sonar-web/src/main/js/app/components/nav/component/projectInformation/InfoDrawer.tsx View File

@@ -0,0 +1,53 @@
/*
* SonarQube
* Copyright (C) 2009-2020 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 * as classNames from 'classnames';
import * as React from 'react';
import { ClearButton } from 'sonar-ui-common/components/controls/buttons';
import EscKeydownHandler from 'sonar-ui-common/components/controls/EscKeydownHandler';
import OutsideClickHandler from 'sonar-ui-common/components/controls/OutsideClickHandler';
import './InfoDrawer.css';

export interface InfoDrawerProps {
children: React.ReactNode;
displayed: boolean;
onClose: () => void;
top: number;
}

export default function InfoDrawer(props: InfoDrawerProps) {
const { children, displayed, onClose, top } = props;

return (
<div
className={classNames('info-drawer info-drawer-pane', { open: displayed })}
style={{ top }}>
<div className="close-button">
<ClearButton onClick={onClose} />
</div>
{displayed && (
<EscKeydownHandler onKeydown={onClose}>
<OutsideClickHandler onClickOutside={onClose}>
<div className="display-flex-column max-height-100">{children}</div>
</OutsideClickHandler>
</EscKeydownHandler>
)}
</div>
);
}

+ 49
- 0
server/sonar-web/src/main/js/app/components/nav/component/projectInformation/InfoDrawerPage.tsx View File

@@ -0,0 +1,49 @@
/*
* SonarQube
* Copyright (C) 2009-2020 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 * as classNames from 'classnames';
import * as React from 'react';
import BackIcon from 'sonar-ui-common/components/icons/BackIcon';
import { translate } from 'sonar-ui-common/helpers/l10n';

export interface InfoDrawerPageProps {
children: React.ReactNode;
displayed: boolean;
onPageChange: () => void;
}

export default function InfoDrawerPage(props: InfoDrawerPageProps) {
const { children, displayed, onPageChange } = props;
return (
<div
className={classNames(
'info-drawer-page info-drawer-pane display-flex-column overflow-hidden',
{
open: displayed
}
)}>
<h2 className="back-button big-padded bordered-bottom" onClick={() => onPageChange()}>
<BackIcon className="little-spacer-right" />
{translate('back')}
</h2>

{displayed && <div className="overflow-y-auto big-padded">{children}</div>}
</div>
);
}

+ 47
- 0
server/sonar-web/src/main/js/app/components/nav/component/projectInformation/ProjectInformation.css View File

@@ -0,0 +1,47 @@
/*
* SonarQube
* Copyright (C) 2009-2020 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.
*/

.project-info-list > li {
/* 1px to not cut icons on the left */
padding-left: 1px;
padding-bottom: 4px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

.project-info-tags {
position: relative;
}

.project-info-deleted-profile,
.project-info-deprecated-rules {
margin: 4px -6px 4px;
padding: 3px 6px !important;
border: 1px solid var(--alertBorderError);
border-radius: 3px;
background-color: var(--alertBackgroundError);
}

.project-info-deleted-profile a,
.project-info-deprecated-rules a {
color: var(--veryDarkBlue);
border-color: darken(var(--lightBlue));
}

+ 137
- 0
server/sonar-web/src/main/js/app/components/nav/component/projectInformation/ProjectInformation.tsx View File

@@ -0,0 +1,137 @@
/*
* SonarQube
* Copyright (C) 2009-2020 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 * as React from 'react';
import { connect } from 'react-redux';
import { getMeasures } from '../../../../../api/measures';
import { isLoggedIn } from '../../../../../helpers/users';
import { fetchMetrics } from '../../../../../store/rootActions';
import { getCurrentUser, getMetrics, Store } from '../../../../../store/rootReducer';
import { BranchLike } from '../../../../../types/branch-like';
import { ComponentQualifier } from '../../../../../types/component';
import { MetricKey } from '../../../../../types/metrics';
import ProjectBadges from './badges/ProjectBadges';
import InfoDrawerPage from './InfoDrawerPage';
import ProjectNotifications from './notifications/ProjectNotifications';
import './ProjectInformation.css';
import { ProjectInformationPages } from './ProjectInformationPages';
import ProjectInformationRenderer from './ProjectInformationRenderer';

interface Props {
branchLike?: BranchLike;
component: T.Component;
currentUser: T.CurrentUser;
fetchMetrics: () => void;
onComponentChange: (changes: {}) => void;
metrics: T.Dict<T.Metric>;
}

interface State {
measures?: T.Measure[];
page: ProjectInformationPages;
}

export class ProjectInformation extends React.PureComponent<Props, State> {
mounted = false;
state: State = {
page: ProjectInformationPages.main
};

componentDidMount() {
this.mounted = true;
this.props.fetchMetrics();
this.loadMeasures();
}

componentWillUnmount() {
this.mounted = false;
}

setPage = (page: ProjectInformationPages = ProjectInformationPages.main) => {
this.setState({ page });
};

loadMeasures = () => {
const {
component: { key }
} = this.props;

return getMeasures({
component: key,
metricKeys: [MetricKey.ncloc, MetricKey.projects].join()
}).then(measures => {
if (this.mounted) {
this.setState({ measures });
}
});
};

render() {
const { branchLike, component, currentUser, metrics } = this.props;
const { measures, page } = this.state;

const canConfigureNotifications = isLoggedIn(currentUser);
const canUseBadges =
metrics !== undefined &&
component.visibility !== 'private' &&
(component.qualifier === ComponentQualifier.Application ||
component.qualifier === ComponentQualifier.Project);

return (
<>
<ProjectInformationRenderer
canConfigureNotifications={canConfigureNotifications}
canUseBadges={canUseBadges}
component={component}
measures={measures}
onComponentChange={this.props.onComponentChange}
onPageChange={this.setPage}
/>
{canUseBadges && (
<InfoDrawerPage
displayed={page === ProjectInformationPages.badges}
onPageChange={this.setPage}>
<ProjectBadges
branchLike={branchLike}
metrics={metrics}
project={component.key}
qualifier={component.qualifier}
/>
</InfoDrawerPage>
)}
{canConfigureNotifications && (
<InfoDrawerPage
displayed={page === ProjectInformationPages.notifications}
onPageChange={this.setPage}>
<ProjectNotifications component={component} />
</InfoDrawerPage>
)}
</>
);
}
}

const mapDispatchToProps = { fetchMetrics };

const mapStateToProps = (state: Store) => ({
currentUser: getCurrentUser(state),
metrics: getMetrics(state)
});

export default connect(mapStateToProps, mapDispatchToProps)(ProjectInformation);

+ 24
- 0
server/sonar-web/src/main/js/app/components/nav/component/projectInformation/ProjectInformationPages.ts View File

@@ -0,0 +1,24 @@
/*
* SonarQube
* Copyright (C) 2009-2020 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.
*/
export enum ProjectInformationPages {
main,
badges,
notifications
}

+ 123
- 0
server/sonar-web/src/main/js/app/components/nav/component/projectInformation/ProjectInformationRenderer.tsx View File

@@ -0,0 +1,123 @@
/*
* SonarQube
* Copyright (C) 2009-2020 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 * as React from 'react';
import { translate } from 'sonar-ui-common/helpers/l10n';
import PrivacyBadgeContainer from '../../../../../components/common/PrivacyBadgeContainer';
import { ComponentQualifier } from '../../../../../types/component';
import DrawerLink from './DrawerLink';
import MetaKey from './meta/MetaKey';
import MetaLinks from './meta/MetaLinks';
import MetaQualityGate from './meta/MetaQualityGate';
import MetaQualityProfiles from './meta/MetaQualityProfiles';
import MetaSize from './meta/MetaSize';
import MetaTags from './meta/MetaTags';
import { ProjectInformationPages } from './ProjectInformationPages';

export interface ProjectInformationRendererProps {
canConfigureNotifications: boolean;
canUseBadges: boolean;
component: T.Component;
measures?: T.Measure[];
onComponentChange: (changes: {}) => void;
onPageChange: (page: ProjectInformationPages) => void;
}

export function ProjectInformationRenderer(props: ProjectInformationRendererProps) {
const { canConfigureNotifications, canUseBadges, component, measures = [] } = props;

const isApp = component.qualifier === ComponentQualifier.Application;

return (
<>
<div>
<h2 className="big-padded bordered-bottom">
{translate(isApp ? 'application' : 'project', 'info.title')}
</h2>
</div>

<div className="overflow-y-auto">
{(component.description || !isApp) && (
<div className="big-padded bordered-bottom">
<div className="display-flex-center">
<h3 className="spacer-right">{translate('project.info.description')}</h3>
{component.visibility && (
<PrivacyBadgeContainer
organization={undefined}
qualifier={component.qualifier}
tooltipProps={{ projectKey: component.key }}
visibility={component.visibility}
/>
)}
</div>

{component.description && <p className="spacer-bottom">{component.description}</p>}

{!isApp && (
<MetaTags component={component} onComponentChange={props.onComponentChange} />
)}
</div>
)}

<div className="big-padded bordered-bottom it__project-loc-value">
<MetaSize component={component} measures={measures} />
</div>

{(component.qualityGate ||
(component.qualityProfiles && component.qualityProfiles.length > 0)) && (
<>
<div className="big-padded bordered-bottom">
{component.qualityGate && <MetaQualityGate qualityGate={component.qualityGate} />}

{component.qualityProfiles && component.qualityProfiles.length > 0 && (
<MetaQualityProfiles
headerClassName={component.qualityGate ? 'big-spacer-top' : undefined}
profiles={component.qualityProfiles}
/>
)}
</div>
</>
)}

{!isApp && <MetaLinks component={component} />}

<div className="big-padded bordered-bottom">
<MetaKey componentKey={component.key} qualifier={component.qualifier} />
</div>

{canUseBadges && (
<DrawerLink
label={translate('overview.badges.get_badge', component.qualifier)}
onPageChange={props.onPageChange}
to={ProjectInformationPages.badges}
/>
)}
{canConfigureNotifications && (
<DrawerLink
label={translate('project.info.to_notifications')}
onPageChange={props.onPageChange}
to={ProjectInformationPages.notifications}
/>
)}
</div>
</>
);
}

export default React.memo(ProjectInformationRenderer);

+ 40
- 0
server/sonar-web/src/main/js/app/components/nav/component/projectInformation/__tests__/DrawerLink-test.tsx View File

@@ -0,0 +1,40 @@
/*
* SonarQube
* Copyright (C) 2009-2020 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 { shallow } from 'enzyme';
import * as React from 'react';
import { DrawerLink, DrawerLinkProps } from '../DrawerLink';

it('should render correctly', () => {
expect(shallowRender()).toMatchSnapshot();
});

it('should call onPageChange when clicked', () => {
const onPageChange = jest.fn();
const to = 'target';
const wrapper = shallowRender({ onPageChange, to });

wrapper.simulate('click');

expect(onPageChange).toBeCalledWith(to);
});

function shallowRender(props: Partial<DrawerLinkProps<string>> = {}) {
return shallow(<DrawerLink label="switch page" onPageChange={jest.fn()} to="id" {...props} />);
}

+ 44
- 0
server/sonar-web/src/main/js/app/components/nav/component/projectInformation/__tests__/InfoDrawer-test.tsx View File

@@ -0,0 +1,44 @@
/*
* SonarQube
* Copyright (C) 2009-2020 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 { shallow } from 'enzyme';
import * as React from 'react';
import InfoDrawer, { InfoDrawerProps } from '../InfoDrawer';

it('should render correctly', () => {
expect(shallowRender()).toMatchSnapshot('default');
expect(shallowRender({ displayed: true })).toMatchSnapshot('displayed');
});

it('should call onClose when button is clicked', () => {
const onClose = jest.fn();
const wrapper = shallowRender({ onClose });

wrapper.find('ClearButton').simulate('click');

expect(onClose).toBeCalled();
});

function shallowRender(props: Partial<InfoDrawerProps> = {}) {
return shallow(
<InfoDrawer displayed={false} onClose={jest.fn()} top={120} {...props}>
<span>content</span>
</InfoDrawer>
);
}

+ 44
- 0
server/sonar-web/src/main/js/app/components/nav/component/projectInformation/__tests__/InfoDrawerPage-test.tsx View File

@@ -0,0 +1,44 @@
/*
* SonarQube
* Copyright (C) 2009-2020 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 { shallow } from 'enzyme';
import * as React from 'react';
import InfoDrawerPage, { InfoDrawerPageProps } from '../InfoDrawerPage';

it('should render correctly', () => {
expect(shallowRender()).toMatchSnapshot();
expect(shallowRender({ displayed: true })).toMatchSnapshot();
});

it('should call onPageChange when clicked', () => {
const onPageChange = jest.fn();
const wrapper = shallowRender({ onPageChange });

wrapper.find('.back-button').simulate('click');

expect(onPageChange).toBeCalledTimes(1);
});

function shallowRender(props: Partial<InfoDrawerPageProps> = {}) {
return shallow(
<InfoDrawerPage displayed={false} onPageChange={jest.fn()} {...props}>
<div>content</div>
</InfoDrawerPage>
);
}

+ 73
- 0
server/sonar-web/src/main/js/app/components/nav/component/projectInformation/__tests__/ProjectInformation-test.tsx View File

@@ -0,0 +1,73 @@
/*
* SonarQube
* Copyright (C) 2009-2020 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 { shallow } from 'enzyme';
import * as React from 'react';
import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
import {
mockComponent,
mockCurrentUser,
mockLoggedInUser,
mockMetric
} from '../../../../../../helpers/testMocks';
import { ProjectInformation } from '../ProjectInformation';
import { ProjectInformationPages } from '../ProjectInformationPages';

jest.mock('../../../../../../api/measures', () => {
const { mockMeasure } = jest.requireActual('../../../../../../helpers/testMocks');
return {
getMeasures: jest.fn().mockResolvedValue([mockMeasure()])
};
});

it('should render correctly', async () => {
expect(shallowRender()).toMatchSnapshot('default');
expect(shallowRender({ currentUser: mockLoggedInUser() })).toMatchSnapshot('logged in user');
expect(shallowRender({ component: mockComponent({ visibility: 'private' }) })).toMatchSnapshot(
'private'
);
const wrapper = shallowRender();
await waitAndUpdate(wrapper);
expect(wrapper).toMatchSnapshot('measures loaded');
});

it('should handle page change', async () => {
const wrapper = shallowRender();

wrapper.instance().setPage(ProjectInformationPages.badges);

await waitAndUpdate(wrapper);

expect(wrapper.state().page).toBe(ProjectInformationPages.badges);
});

function shallowRender(props: Partial<ProjectInformation['props']> = {}) {
return shallow<ProjectInformation>(
<ProjectInformation
component={mockComponent()}
currentUser={mockCurrentUser()}
fetchMetrics={jest.fn()}
metrics={{
coverage: mockMetric()
}}
onComponentChange={jest.fn()}
{...props}
/>
);
}

+ 65
- 0
server/sonar-web/src/main/js/app/components/nav/component/projectInformation/__tests__/ProjectInformationRenderer-test.tsx View File

@@ -0,0 +1,65 @@
/*
* SonarQube
* Copyright (C) 2009-2020 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 { shallow } from 'enzyme';
import * as React from 'react';
import { mockComponent } from '../../../../../../helpers/testMocks';
import {
ProjectInformationRenderer,
ProjectInformationRendererProps
} from '../ProjectInformationRenderer';

it('should render correctly', () => {
expect(shallowRender()).toMatchSnapshot('default');
expect(shallowRender({ canConfigureNotifications: false })).toMatchSnapshot('with notifications');
expect(shallowRender({ canUseBadges: false })).toMatchSnapshot('no badges');
expect(shallowRender({ canConfigureNotifications: false, canUseBadges: false })).toMatchSnapshot(
'no badges, no notifications'
);
});

it('should render a private project correctly', () => {
expect(shallowRender({ component: mockComponent({ visibility: 'private' }) })).toMatchSnapshot();
});

it('should render an app correctly', () => {
const component = mockComponent({ qualifier: 'APP' });
expect(shallowRender({ component })).toMatchSnapshot('default');
});

it('should handle missing quality profiles and quality gates', () => {
expect(
shallowRender({
component: mockComponent({ qualityGate: undefined, qualityProfiles: undefined })
})
).toMatchSnapshot();
});

function shallowRender(props: Partial<ProjectInformationRendererProps> = {}) {
return shallow(
<ProjectInformationRenderer
canConfigureNotifications={true}
canUseBadges={true}
component={mockComponent({ qualifier: 'TRK', visibility: 'public' })}
onComponentChange={jest.fn()}
onPageChange={jest.fn()}
{...props}
/>
);
}

+ 13
- 0
server/sonar-web/src/main/js/app/components/nav/component/projectInformation/__tests__/__snapshots__/DrawerLink-test.tsx.snap View File

@@ -0,0 +1,13 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should render correctly 1`] = `
<a
className="display-flex-space-between bordered-bottom big-padded"
onClick={[Function]}
role="link"
tabIndex={0}
>
switch page
<ChevronRightIcon />
</a>
`;

+ 54
- 0
server/sonar-web/src/main/js/app/components/nav/component/projectInformation/__tests__/__snapshots__/InfoDrawer-test.tsx.snap View File

@@ -0,0 +1,54 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should render correctly: default 1`] = `
<div
className="info-drawer info-drawer-pane"
style={
Object {
"top": 120,
}
}
>
<div
className="close-button"
>
<ClearButton
onClick={[MockFunction]}
/>
</div>
</div>
`;

exports[`should render correctly: displayed 1`] = `
<div
className="info-drawer info-drawer-pane open"
style={
Object {
"top": 120,
}
}
>
<div
className="close-button"
>
<ClearButton
onClick={[MockFunction]}
/>
</div>
<EscKeydownHandler
onKeydown={[MockFunction]}
>
<OutsideClickHandler
onClickOutside={[MockFunction]}
>
<div
className="display-flex-column max-height-100"
>
<span>
content
</span>
</div>
</OutsideClickHandler>
</EscKeydownHandler>
</div>
`;

+ 40
- 0
server/sonar-web/src/main/js/app/components/nav/component/projectInformation/__tests__/__snapshots__/InfoDrawerPage-test.tsx.snap View File

@@ -0,0 +1,40 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should render correctly 1`] = `
<div
className="info-drawer-page info-drawer-pane display-flex-column overflow-hidden"
>
<h2
className="back-button big-padded bordered-bottom"
onClick={[Function]}
>
<BackIcon
className="little-spacer-right"
/>
back
</h2>
</div>
`;

exports[`should render correctly 2`] = `
<div
className="info-drawer-page info-drawer-pane display-flex-column overflow-hidden open"
>
<h2
className="back-button big-padded bordered-bottom"
onClick={[Function]}
>
<BackIcon
className="little-spacer-right"
/>
back
</h2>
<div
className="overflow-y-auto big-padded"
>
<div>
content
</div>
</div>
</div>
`;

+ 241
- 0
server/sonar-web/src/main/js/app/components/nav/component/projectInformation/__tests__/__snapshots__/ProjectInformation-test.tsx.snap View File

@@ -0,0 +1,241 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should render correctly: default 1`] = `
<Fragment>
<Memo(ProjectInformationRenderer)
canConfigureNotifications={false}
canUseBadges={true}
component={
Object {
"breadcrumbs": Array [],
"key": "my-project",
"name": "MyProject",
"organization": "foo",
"qualifier": "TRK",
"qualityGate": Object {
"isDefault": true,
"key": "30",
"name": "Sonar way",
},
"qualityProfiles": Array [
Object {
"deleted": false,
"key": "my-qp",
"language": "ts",
"name": "Sonar way",
},
],
"tags": Array [],
}
}
onComponentChange={[MockFunction]}
onPageChange={[Function]}
/>
<InfoDrawerPage
displayed={false}
onPageChange={[Function]}
>
<ProjectBadges
metrics={
Object {
"coverage": Object {
"id": "coverage",
"key": "coverage",
"name": "Coverage",
"type": "PERCENT",
},
}
}
project="my-project"
qualifier="TRK"
/>
</InfoDrawerPage>
</Fragment>
`;

exports[`should render correctly: logged in user 1`] = `
<Fragment>
<Memo(ProjectInformationRenderer)
canConfigureNotifications={true}
canUseBadges={true}
component={
Object {
"breadcrumbs": Array [],
"key": "my-project",
"name": "MyProject",
"organization": "foo",
"qualifier": "TRK",
"qualityGate": Object {
"isDefault": true,
"key": "30",
"name": "Sonar way",
},
"qualityProfiles": Array [
Object {
"deleted": false,
"key": "my-qp",
"language": "ts",
"name": "Sonar way",
},
],
"tags": Array [],
}
}
onComponentChange={[MockFunction]}
onPageChange={[Function]}
/>
<InfoDrawerPage
displayed={false}
onPageChange={[Function]}
>
<ProjectBadges
metrics={
Object {
"coverage": Object {
"id": "coverage",
"key": "coverage",
"name": "Coverage",
"type": "PERCENT",
},
}
}
project="my-project"
qualifier="TRK"
/>
</InfoDrawerPage>
<InfoDrawerPage
displayed={false}
onPageChange={[Function]}
>
<withNotifications(ProjectNotifications)
component={
Object {
"breadcrumbs": Array [],
"key": "my-project",
"name": "MyProject",
"organization": "foo",
"qualifier": "TRK",
"qualityGate": Object {
"isDefault": true,
"key": "30",
"name": "Sonar way",
},
"qualityProfiles": Array [
Object {
"deleted": false,
"key": "my-qp",
"language": "ts",
"name": "Sonar way",
},
],
"tags": Array [],
}
}
/>
</InfoDrawerPage>
</Fragment>
`;

exports[`should render correctly: measures loaded 1`] = `
<Fragment>
<Memo(ProjectInformationRenderer)
canConfigureNotifications={false}
canUseBadges={true}
component={
Object {
"breadcrumbs": Array [],
"key": "my-project",
"name": "MyProject",
"organization": "foo",
"qualifier": "TRK",
"qualityGate": Object {
"isDefault": true,
"key": "30",
"name": "Sonar way",
},
"qualityProfiles": Array [
Object {
"deleted": false,
"key": "my-qp",
"language": "ts",
"name": "Sonar way",
},
],
"tags": Array [],
}
}
measures={
Array [
Object {
"bestValue": true,
"metric": "bugs",
"periods": Array [
Object {
"bestValue": true,
"index": 1,
"value": "1.0",
},
],
"value": "1.0",
},
]
}
onComponentChange={[MockFunction]}
onPageChange={[Function]}
/>
<InfoDrawerPage
displayed={false}
onPageChange={[Function]}
>
<ProjectBadges
metrics={
Object {
"coverage": Object {
"id": "coverage",
"key": "coverage",
"name": "Coverage",
"type": "PERCENT",
},
}
}
project="my-project"
qualifier="TRK"
/>
</InfoDrawerPage>
</Fragment>
`;

exports[`should render correctly: private 1`] = `
<Fragment>
<Memo(ProjectInformationRenderer)
canConfigureNotifications={false}
canUseBadges={false}
component={
Object {
"breadcrumbs": Array [],
"key": "my-project",
"name": "MyProject",
"organization": "foo",
"qualifier": "TRK",
"qualityGate": Object {
"isDefault": true,
"key": "30",
"name": "Sonar way",
},
"qualityProfiles": Array [
Object {
"deleted": false,
"key": "my-qp",
"language": "ts",
"name": "Sonar way",
},
],
"tags": Array [],
"visibility": "private",
}
}
onComponentChange={[MockFunction]}
onPageChange={[Function]}
/>
</Fragment>
`;

+ 996
- 0
server/sonar-web/src/main/js/app/components/nav/component/projectInformation/__tests__/__snapshots__/ProjectInformationRenderer-test.tsx.snap View File

@@ -0,0 +1,996 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should handle missing quality profiles and quality gates 1`] = `
<Fragment>
<div>
<h2
className="big-padded bordered-bottom"
>
project.info.title
</h2>
</div>
<div
className="overflow-y-auto"
>
<div
className="big-padded bordered-bottom"
>
<div
className="display-flex-center"
>
<h3
className="spacer-right"
>
project.info.description
</h3>
</div>
<MetaTags
component={
Object {
"breadcrumbs": Array [],
"key": "my-project",
"name": "MyProject",
"organization": "foo",
"qualifier": "TRK",
"qualityGate": undefined,
"qualityProfiles": undefined,
"tags": Array [],
}
}
onComponentChange={[MockFunction]}
/>
</div>
<div
className="big-padded bordered-bottom it__project-loc-value"
>
<MetaSize
component={
Object {
"breadcrumbs": Array [],
"key": "my-project",
"name": "MyProject",
"organization": "foo",
"qualifier": "TRK",
"qualityGate": undefined,
"qualityProfiles": undefined,
"tags": Array [],
}
}
measures={Array []}
/>
</div>
<MetaLinks
component={
Object {
"breadcrumbs": Array [],
"key": "my-project",
"name": "MyProject",
"organization": "foo",
"qualifier": "TRK",
"qualityGate": undefined,
"qualityProfiles": undefined,
"tags": Array [],
}
}
/>
<div
className="big-padded bordered-bottom"
>
<MetaKey
componentKey="my-project"
qualifier="TRK"
/>
</div>
<Memo(DrawerLink)
label="overview.badges.get_badge.TRK"
onPageChange={[MockFunction]}
to={1}
/>
<Memo(DrawerLink)
label="project.info.to_notifications"
onPageChange={[MockFunction]}
to={2}
/>
</div>
</Fragment>
`;

exports[`should render a private project correctly 1`] = `
<Fragment>
<div>
<h2
className="big-padded bordered-bottom"
>
project.info.title
</h2>
</div>
<div
className="overflow-y-auto"
>
<div
className="big-padded bordered-bottom"
>
<div
className="display-flex-center"
>
<h3
className="spacer-right"
>
project.info.description
</h3>
<Connect(PrivacyBadge)
qualifier="TRK"
tooltipProps={
Object {
"projectKey": "my-project",
}
}
visibility="private"
/>
</div>
<MetaTags
component={
Object {
"breadcrumbs": Array [],
"key": "my-project",
"name": "MyProject",
"organization": "foo",
"qualifier": "TRK",
"qualityGate": Object {
"isDefault": true,
"key": "30",
"name": "Sonar way",
},
"qualityProfiles": Array [
Object {
"deleted": false,
"key": "my-qp",
"language": "ts",
"name": "Sonar way",
},
],
"tags": Array [],
"visibility": "private",
}
}
onComponentChange={[MockFunction]}
/>
</div>
<div
className="big-padded bordered-bottom it__project-loc-value"
>
<MetaSize
component={
Object {
"breadcrumbs": Array [],
"key": "my-project",
"name": "MyProject",
"organization": "foo",
"qualifier": "TRK",
"qualityGate": Object {
"isDefault": true,
"key": "30",
"name": "Sonar way",
},
"qualityProfiles": Array [
Object {
"deleted": false,
"key": "my-qp",
"language": "ts",
"name": "Sonar way",
},
],
"tags": Array [],
"visibility": "private",
}
}
measures={Array []}
/>
</div>
<div
className="big-padded bordered-bottom"
>
<MetaQualityGate
qualityGate={
Object {
"isDefault": true,
"key": "30",
"name": "Sonar way",
}
}
/>
<Connect(MetaQualityProfiles)
headerClassName="big-spacer-top"
profiles={
Array [
Object {
"deleted": false,
"key": "my-qp",
"language": "ts",
"name": "Sonar way",
},
]
}
/>
</div>
<MetaLinks
component={
Object {
"breadcrumbs": Array [],
"key": "my-project",
"name": "MyProject",
"organization": "foo",
"qualifier": "TRK",
"qualityGate": Object {
"isDefault": true,
"key": "30",
"name": "Sonar way",
},
"qualityProfiles": Array [
Object {
"deleted": false,
"key": "my-qp",
"language": "ts",
"name": "Sonar way",
},
],
"tags": Array [],
"visibility": "private",
}
}
/>
<div
className="big-padded bordered-bottom"
>
<MetaKey
componentKey="my-project"
qualifier="TRK"
/>
</div>
<Memo(DrawerLink)
label="overview.badges.get_badge.TRK"
onPageChange={[MockFunction]}
to={1}
/>
<Memo(DrawerLink)
label="project.info.to_notifications"
onPageChange={[MockFunction]}
to={2}
/>
</div>
</Fragment>
`;

exports[`should render an app correctly: default 1`] = `
<Fragment>
<div>
<h2
className="big-padded bordered-bottom"
>
application.info.title
</h2>
</div>
<div
className="overflow-y-auto"
>
<div
className="big-padded bordered-bottom it__project-loc-value"
>
<MetaSize
component={
Object {
"breadcrumbs": Array [],
"key": "my-project",
"name": "MyProject",
"organization": "foo",
"qualifier": "APP",
"qualityGate": Object {
"isDefault": true,
"key": "30",
"name": "Sonar way",
},
"qualityProfiles": Array [
Object {
"deleted": false,
"key": "my-qp",
"language": "ts",
"name": "Sonar way",
},
],
"tags": Array [],
}
}
measures={Array []}
/>
</div>
<div
className="big-padded bordered-bottom"
>
<MetaQualityGate
qualityGate={
Object {
"isDefault": true,
"key": "30",
"name": "Sonar way",
}
}
/>
<Connect(MetaQualityProfiles)
headerClassName="big-spacer-top"
profiles={
Array [
Object {
"deleted": false,
"key": "my-qp",
"language": "ts",
"name": "Sonar way",
},
]
}
/>
</div>
<div
className="big-padded bordered-bottom"
>
<MetaKey
componentKey="my-project"
qualifier="APP"
/>
</div>
<Memo(DrawerLink)
label="overview.badges.get_badge.APP"
onPageChange={[MockFunction]}
to={1}
/>
<Memo(DrawerLink)
label="project.info.to_notifications"
onPageChange={[MockFunction]}
to={2}
/>
</div>
</Fragment>
`;

exports[`should render correctly: default 1`] = `
<Fragment>
<div>
<h2
className="big-padded bordered-bottom"
>
project.info.title
</h2>
</div>
<div
className="overflow-y-auto"
>
<div
className="big-padded bordered-bottom"
>
<div
className="display-flex-center"
>
<h3
className="spacer-right"
>
project.info.description
</h3>
<Connect(PrivacyBadge)
qualifier="TRK"
tooltipProps={
Object {
"projectKey": "my-project",
}
}
visibility="public"
/>
</div>
<MetaTags
component={
Object {
"breadcrumbs": Array [],
"key": "my-project",
"name": "MyProject",
"organization": "foo",
"qualifier": "TRK",
"qualityGate": Object {
"isDefault": true,
"key": "30",
"name": "Sonar way",
},
"qualityProfiles": Array [
Object {
"deleted": false,
"key": "my-qp",
"language": "ts",
"name": "Sonar way",
},
],
"tags": Array [],
"visibility": "public",
}
}
onComponentChange={[MockFunction]}
/>
</div>
<div
className="big-padded bordered-bottom it__project-loc-value"
>
<MetaSize
component={
Object {
"breadcrumbs": Array [],
"key": "my-project",
"name": "MyProject",
"organization": "foo",
"qualifier": "TRK",
"qualityGate": Object {
"isDefault": true,
"key": "30",
"name": "Sonar way",
},
"qualityProfiles": Array [
Object {
"deleted": false,
"key": "my-qp",
"language": "ts",
"name": "Sonar way",
},
],
"tags": Array [],
"visibility": "public",
}
}
measures={Array []}
/>
</div>
<div
className="big-padded bordered-bottom"
>
<MetaQualityGate
qualityGate={
Object {
"isDefault": true,
"key": "30",
"name": "Sonar way",
}
}
/>
<Connect(MetaQualityProfiles)
headerClassName="big-spacer-top"
profiles={
Array [
Object {
"deleted": false,
"key": "my-qp",
"language": "ts",
"name": "Sonar way",
},
]
}
/>
</div>
<MetaLinks
component={
Object {
"breadcrumbs": Array [],
"key": "my-project",
"name": "MyProject",
"organization": "foo",
"qualifier": "TRK",
"qualityGate": Object {
"isDefault": true,
"key": "30",
"name": "Sonar way",
},
"qualityProfiles": Array [
Object {
"deleted": false,
"key": "my-qp",
"language": "ts",
"name": "Sonar way",
},
],
"tags": Array [],
"visibility": "public",
}
}
/>
<div
className="big-padded bordered-bottom"
>
<MetaKey
componentKey="my-project"
qualifier="TRK"
/>
</div>
<Memo(DrawerLink)
label="overview.badges.get_badge.TRK"
onPageChange={[MockFunction]}
to={1}
/>
<Memo(DrawerLink)
label="project.info.to_notifications"
onPageChange={[MockFunction]}
to={2}
/>
</div>
</Fragment>
`;

exports[`should render correctly: no badges 1`] = `
<Fragment>
<div>
<h2
className="big-padded bordered-bottom"
>
project.info.title
</h2>
</div>
<div
className="overflow-y-auto"
>
<div
className="big-padded bordered-bottom"
>
<div
className="display-flex-center"
>
<h3
className="spacer-right"
>
project.info.description
</h3>
<Connect(PrivacyBadge)
qualifier="TRK"
tooltipProps={
Object {
"projectKey": "my-project",
}
}
visibility="public"
/>
</div>
<MetaTags
component={
Object {
"breadcrumbs": Array [],
"key": "my-project",
"name": "MyProject",
"organization": "foo",
"qualifier": "TRK",
"qualityGate": Object {
"isDefault": true,
"key": "30",
"name": "Sonar way",
},
"qualityProfiles": Array [
Object {
"deleted": false,
"key": "my-qp",
"language": "ts",
"name": "Sonar way",
},
],
"tags": Array [],
"visibility": "public",
}
}
onComponentChange={[MockFunction]}
/>
</div>
<div
className="big-padded bordered-bottom it__project-loc-value"
>
<MetaSize
component={
Object {
"breadcrumbs": Array [],
"key": "my-project",
"name": "MyProject",
"organization": "foo",
"qualifier": "TRK",
"qualityGate": Object {
"isDefault": true,
"key": "30",
"name": "Sonar way",
},
"qualityProfiles": Array [
Object {
"deleted": false,
"key": "my-qp",
"language": "ts",
"name": "Sonar way",
},
],
"tags": Array [],
"visibility": "public",
}
}
measures={Array []}
/>
</div>
<div
className="big-padded bordered-bottom"
>
<MetaQualityGate
qualityGate={
Object {
"isDefault": true,
"key": "30",
"name": "Sonar way",
}
}
/>
<Connect(MetaQualityProfiles)
headerClassName="big-spacer-top"
profiles={
Array [
Object {
"deleted": false,
"key": "my-qp",
"language": "ts",
"name": "Sonar way",
},
]
}
/>
</div>
<MetaLinks
component={
Object {
"breadcrumbs": Array [],
"key": "my-project",
"name": "MyProject",
"organization": "foo",
"qualifier": "TRK",
"qualityGate": Object {
"isDefault": true,
"key": "30",
"name": "Sonar way",
},
"qualityProfiles": Array [
Object {
"deleted": false,
"key": "my-qp",
"language": "ts",
"name": "Sonar way",
},
],
"tags": Array [],
"visibility": "public",
}
}
/>
<div
className="big-padded bordered-bottom"
>
<MetaKey
componentKey="my-project"
qualifier="TRK"
/>
</div>
<Memo(DrawerLink)
label="project.info.to_notifications"
onPageChange={[MockFunction]}
to={2}
/>
</div>
</Fragment>
`;

exports[`should render correctly: no badges, no notifications 1`] = `
<Fragment>
<div>
<h2
className="big-padded bordered-bottom"
>
project.info.title
</h2>
</div>
<div
className="overflow-y-auto"
>
<div
className="big-padded bordered-bottom"
>
<div
className="display-flex-center"
>
<h3
className="spacer-right"
>
project.info.description
</h3>
<Connect(PrivacyBadge)
qualifier="TRK"
tooltipProps={
Object {
"projectKey": "my-project",
}
}
visibility="public"
/>
</div>
<MetaTags
component={
Object {
"breadcrumbs": Array [],
"key": "my-project",
"name": "MyProject",
"organization": "foo",
"qualifier": "TRK",
"qualityGate": Object {
"isDefault": true,
"key": "30",
"name": "Sonar way",
},
"qualityProfiles": Array [
Object {
"deleted": false,
"key": "my-qp",
"language": "ts",
"name": "Sonar way",
},
],
"tags": Array [],
"visibility": "public",
}
}
onComponentChange={[MockFunction]}
/>
</div>
<div
className="big-padded bordered-bottom it__project-loc-value"
>
<MetaSize
component={
Object {
"breadcrumbs": Array [],
"key": "my-project",
"name": "MyProject",
"organization": "foo",
"qualifier": "TRK",
"qualityGate": Object {
"isDefault": true,
"key": "30",
"name": "Sonar way",
},
"qualityProfiles": Array [
Object {
"deleted": false,
"key": "my-qp",
"language": "ts",
"name": "Sonar way",
},
],
"tags": Array [],
"visibility": "public",
}
}
measures={Array []}
/>
</div>
<div
className="big-padded bordered-bottom"
>
<MetaQualityGate
qualityGate={
Object {
"isDefault": true,
"key": "30",
"name": "Sonar way",
}
}
/>
<Connect(MetaQualityProfiles)
headerClassName="big-spacer-top"
profiles={
Array [
Object {
"deleted": false,
"key": "my-qp",
"language": "ts",
"name": "Sonar way",
},
]
}
/>
</div>
<MetaLinks
component={
Object {
"breadcrumbs": Array [],
"key": "my-project",
"name": "MyProject",
"organization": "foo",
"qualifier": "TRK",
"qualityGate": Object {
"isDefault": true,
"key": "30",
"name": "Sonar way",
},
"qualityProfiles": Array [
Object {
"deleted": false,
"key": "my-qp",
"language": "ts",
"name": "Sonar way",
},
],
"tags": Array [],
"visibility": "public",
}
}
/>
<div
className="big-padded bordered-bottom"
>
<MetaKey
componentKey="my-project"
qualifier="TRK"
/>
</div>
</div>
</Fragment>
`;

exports[`should render correctly: with notifications 1`] = `
<Fragment>
<div>
<h2
className="big-padded bordered-bottom"
>
project.info.title
</h2>
</div>
<div
className="overflow-y-auto"
>
<div
className="big-padded bordered-bottom"
>
<div
className="display-flex-center"
>
<h3
className="spacer-right"
>
project.info.description
</h3>
<Connect(PrivacyBadge)
qualifier="TRK"
tooltipProps={
Object {
"projectKey": "my-project",
}
}
visibility="public"
/>
</div>
<MetaTags
component={
Object {
"breadcrumbs": Array [],
"key": "my-project",
"name": "MyProject",
"organization": "foo",
"qualifier": "TRK",
"qualityGate": Object {
"isDefault": true,
"key": "30",
"name": "Sonar way",
},
"qualityProfiles": Array [
Object {
"deleted": false,
"key": "my-qp",
"language": "ts",
"name": "Sonar way",
},
],
"tags": Array [],
"visibility": "public",
}
}
onComponentChange={[MockFunction]}
/>
</div>
<div
className="big-padded bordered-bottom it__project-loc-value"
>
<MetaSize
component={
Object {
"breadcrumbs": Array [],
"key": "my-project",
"name": "MyProject",
"organization": "foo",
"qualifier": "TRK",
"qualityGate": Object {
"isDefault": true,
"key": "30",
"name": "Sonar way",
},
"qualityProfiles": Array [
Object {
"deleted": false,
"key": "my-qp",
"language": "ts",
"name": "Sonar way",
},
],
"tags": Array [],
"visibility": "public",
}
}
measures={Array []}
/>
</div>
<div
className="big-padded bordered-bottom"
>
<MetaQualityGate
qualityGate={
Object {
"isDefault": true,
"key": "30",
"name": "Sonar way",
}
}
/>
<Connect(MetaQualityProfiles)
headerClassName="big-spacer-top"
profiles={
Array [
Object {
"deleted": false,
"key": "my-qp",
"language": "ts",
"name": "Sonar way",
},
]
}
/>
</div>
<MetaLinks
component={
Object {
"breadcrumbs": Array [],
"key": "my-project",
"name": "MyProject",
"organization": "foo",
"qualifier": "TRK",
"qualityGate": Object {
"isDefault": true,
"key": "30",
"name": "Sonar way",
},
"qualityProfiles": Array [
Object {
"deleted": false,
"key": "my-qp",
"language": "ts",
"name": "Sonar way",
},
],
"tags": Array [],
"visibility": "public",
}
}
/>
<div
className="big-padded bordered-bottom"
>
<MetaKey
componentKey="my-project"
qualifier="TRK"
/>
</div>
<Memo(DrawerLink)
label="overview.badges.get_badge.TRK"
onPageChange={[MockFunction]}
to={1}
/>
</div>
</Fragment>
`;

server/sonar-web/src/main/js/apps/overview/badges/BadgeButton.tsx → server/sonar-web/src/main/js/app/components/nav/component/projectInformation/badges/BadgeButton.tsx View File


server/sonar-web/src/main/js/apps/overview/badges/BadgeParams.tsx → server/sonar-web/src/main/js/app/components/nav/component/projectInformation/badges/BadgeParams.tsx View File

@@ -21,7 +21,7 @@ import * as classNames from 'classnames';
import * as React from 'react';
import Select from 'sonar-ui-common/components/controls/Select';
import { getLocalizedMetricName, translate } from 'sonar-ui-common/helpers/l10n';
import { fetchWebApi } from '../../../api/web-api';
import { fetchWebApi } from '../../../../../../api/web-api';
import { BadgeColors, BadgeFormats, BadgeOptions, BadgeType } from './utils';

interface Props {
@@ -54,9 +54,9 @@ export default class BadgeParams extends React.PureComponent<Props> {
fetchWebApi(false).then(
webservices => {
if (this.mounted) {
const domain = webservices.find(domain => domain.path === 'api/project_badges');
const ws = domain && domain.actions.find(ws => ws.key === 'measure');
const param = ws && ws.params && ws.params.find(param => param.key === 'metric');
const domain = webservices.find(d => d.path === 'api/project_badges');
const ws = domain && domain.actions.find(w => w.key === 'measure');
const param = ws && ws.params && ws.params.find(p => p.key === 'metric');
if (param && param.possibleValues) {
this.setState({ badgeMetrics: param.possibleValues });
}
@@ -129,6 +129,7 @@ export default class BadgeParams extends React.PureComponent<Props> {
<Select
className="input-medium"
clearable={false}
menuStyle={{ maxHeight: 100 }}
name="badge-metric"
onChange={this.handleMetricChange}
options={this.getMetricOptions()}
@@ -150,7 +151,7 @@ export default class BadgeParams extends React.PureComponent<Props> {

<label
className={classNames('spacer-right', {
'big-spacer-left': type !== BadgeType.qualityGate
'spacer-top': type !== BadgeType.qualityGate
})}
htmlFor="badge-format">
{translate('format')}:

+ 95
- 0
server/sonar-web/src/main/js/app/components/nav/component/projectInformation/badges/ProjectBadges.tsx View File

@@ -0,0 +1,95 @@
/*
* SonarQube
* Copyright (C) 2009-2020 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 * as React from 'react';
import { translate } from 'sonar-ui-common/helpers/l10n';
import CodeSnippet from '../../../../../../components/common/CodeSnippet';
import { getBranchLikeQuery } from '../../../../../../helpers/branch-like';
import { BranchLike } from '../../../../../../types/branch-like';
import { MetricKey } from '../../../../../../types/metrics';
import BadgeButton from './BadgeButton';
import BadgeParams from './BadgeParams';
import './styles.css';
import { BadgeOptions, BadgeType, getBadgeSnippet, getBadgeUrl } from './utils';

interface Props {
branchLike?: BranchLike;
metrics: T.Dict<T.Metric>;
project: string;
qualifier: string;
}

interface State {
selectedType: BadgeType;
badgeOptions: BadgeOptions;
}

export default class ProjectBadges extends React.PureComponent<Props, State> {
state: State = {
selectedType: BadgeType.measure,
badgeOptions: { color: 'white', metric: MetricKey.alert_status }
};

handleSelectBadge = (selectedType: BadgeType) => {
this.setState({ selectedType });
};

handleUpdateOptions = (options: Partial<BadgeOptions>) => {
this.setState(state => ({ badgeOptions: { ...state.badgeOptions, ...options } }));
};

render() {
const { branchLike, project, qualifier } = this.props;
const { selectedType, badgeOptions } = this.state;
const fullBadgeOptions = { project, ...badgeOptions, ...getBranchLikeQuery(branchLike) };

return (
<div className="display-flex-column">
<h3>{translate('overview.badges.get_badge', qualifier)}</h3>
<p className="big-spacer-bottom">{translate('overview.badges.description', qualifier)}</p>
<BadgeButton
onClick={this.handleSelectBadge}
selected={BadgeType.measure === selectedType}
type={BadgeType.measure}
url={getBadgeUrl(BadgeType.measure, fullBadgeOptions)}
/>
<p className="huge-spacer-bottom spacer-top">
{translate('overview.badges', BadgeType.measure, 'description', qualifier)}
</p>
<BadgeButton
onClick={this.handleSelectBadge}
selected={BadgeType.qualityGate === selectedType}
type={BadgeType.qualityGate}
url={getBadgeUrl(BadgeType.qualityGate, fullBadgeOptions)}
/>
<p className="huge-spacer-bottom spacer-top">
{translate('overview.badges', BadgeType.qualityGate, 'description', qualifier)}
</p>
<BadgeParams
className="big-spacer-bottom display-flex-column"
metrics={this.props.metrics}
options={badgeOptions}
type={selectedType}
updateOptions={this.handleUpdateOptions}
/>
<CodeSnippet isOneLine={true} snippet={getBadgeSnippet(selectedType, fullBadgeOptions)} />
</div>
);
}
}

server/sonar-web/src/main/js/apps/overview/badges/__tests__/BadgeButton-test.tsx → server/sonar-web/src/main/js/app/components/nav/component/projectInformation/badges/__tests__/BadgeButton-test.tsx View File


server/sonar-web/src/main/js/apps/overview/badges/__tests__/BadgeParams-test.tsx → server/sonar-web/src/main/js/app/components/nav/component/projectInformation/badges/__tests__/BadgeParams-test.tsx View File

@@ -22,7 +22,7 @@ import * as React from 'react';
import BadgeParams from '../BadgeParams';
import { BadgeType } from '../utils';

jest.mock('../../../../api/web-api', () => ({
jest.mock('../../../../../../../api/web-api', () => ({
fetchWebApi: () =>
Promise.resolve([
{

server/sonar-web/src/main/js/apps/overview/badges/__tests__/ProjectBadges-test.tsx → server/sonar-web/src/main/js/app/components/nav/component/projectInformation/badges/__tests__/ProjectBadges-test.tsx View File

@@ -19,10 +19,10 @@
*/
import { shallow } from 'enzyme';
import * as React from 'react';
import { click } from 'sonar-ui-common/helpers/testUtils';
import { Location } from 'sonar-ui-common/helpers/urls';
import { mockBranch } from '../../../../helpers/mocks/branch-like';
import { isSonarCloud } from '../../../../helpers/system';
import { mockBranch } from '../../../../../../../helpers/mocks/branch-like';
import { mockMetric } from '../../../../../../../helpers/testMocks';
import { MetricKey } from '../../../../../../../types/metrics';
import ProjectBadges from '../ProjectBadges';

jest.mock('sonar-ui-common/helpers/urls', () => ({
@@ -30,30 +30,25 @@ jest.mock('sonar-ui-common/helpers/urls', () => ({
getPathUrlAsString: (l: Location) => l.pathname
}));

jest.mock('../../../../helpers/urls', () => ({
jest.mock('../../../../../../../helpers/urls', () => ({
getProjectUrl: () => ({ pathname: '/dashboard' } as Location)
}));

jest.mock('../../../../helpers/system', () => ({ isSonarCloud: jest.fn() }));

const shortBranch = mockBranch({ name: 'branch-6.6' });

it('should display the modal after click on sonarcloud', () => {
(isSonarCloud as jest.Mock).mockImplementation(() => true);
const wrapper = shallow(
<ProjectBadges branchLike={shortBranch} metrics={{}} project="foo" qualifier="TRK" />
);
expect(wrapper).toMatchSnapshot();
click(wrapper.find('Button'));
expect(wrapper.find('Modal')).toMatchSnapshot();
it('should display correctly', () => {
expect(shallowRender()).toMatchSnapshot();
});

it('should display the modal after click on sonarqube', () => {
(isSonarCloud as jest.Mock).mockImplementation(() => false);
const wrapper = shallow(
<ProjectBadges branchLike={shortBranch} metrics={{}} project="foo" qualifier="TRK" />
function shallowRender(overrides = {}) {
return shallow(
<ProjectBadges
branchLike={mockBranch()}
metrics={{
[MetricKey.coverage]: mockMetric({ key: MetricKey.coverage }),
[MetricKey.new_code_smells]: mockMetric({ key: MetricKey.new_code_smells })
}}
project="foo"
qualifier="TRK"
{...overrides}
/>
);
expect(wrapper).toMatchSnapshot();
click(wrapper.find('Button'));
expect(wrapper.find('Modal')).toMatchSnapshot();
});
}

server/sonar-web/src/main/js/apps/overview/badges/__tests__/__snapshots__/BadgeButton-test.tsx.snap → server/sonar-web/src/main/js/app/components/nav/component/projectInformation/badges/__tests__/__snapshots__/BadgeButton-test.tsx.snap View File


server/sonar-web/src/main/js/apps/overview/badges/__tests__/__snapshots__/BadgeParams-test.tsx.snap → server/sonar-web/src/main/js/app/components/nav/component/projectInformation/badges/__tests__/__snapshots__/BadgeParams-test.tsx.snap View File

@@ -34,7 +34,7 @@ exports[`should display marketing badge params 1`] = `
value="white"
/>
<label
className="spacer-right big-spacer-left"
className="spacer-right spacer-top"
htmlFor="badge-format"
>
format
@@ -75,6 +75,11 @@ exports[`should display measure badge params 1`] = `
<Select
className="input-medium"
clearable={false}
menuStyle={
Object {
"maxHeight": 100,
}
}
name="badge-metric"
onChange={[Function]}
options={Array []}
@@ -82,7 +87,7 @@ exports[`should display measure badge params 1`] = `
value="alert_status"
/>
<label
className="spacer-right big-spacer-left"
className="spacer-right spacer-top"
htmlFor="badge-format"
>
format

+ 69
- 0
server/sonar-web/src/main/js/app/components/nav/component/projectInformation/badges/__tests__/__snapshots__/ProjectBadges-test.tsx.snap View File

@@ -0,0 +1,69 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should display correctly 1`] = `
<div
className="display-flex-column"
>
<h3>
overview.badges.get_badge.TRK
</h3>
<p
className="big-spacer-bottom"
>
overview.badges.description.TRK
</p>
<BadgeButton
onClick={[Function]}
selected={true}
type="measure"
url="host/api/project_badges/measure?branch=branch-6.7&project=foo&metric=alert_status"
/>
<p
className="huge-spacer-bottom spacer-top"
>
overview.badges.measure.description.TRK
</p>
<BadgeButton
onClick={[Function]}
selected={false}
type="quality_gate"
url="host/api/project_badges/quality_gate?branch=branch-6.7&project=foo"
/>
<p
className="huge-spacer-bottom spacer-top"
>
overview.badges.quality_gate.description.TRK
</p>
<BadgeParams
className="big-spacer-bottom display-flex-column"
metrics={
Object {
"coverage": Object {
"id": "coverage",
"key": "coverage",
"name": "Coverage",
"type": "PERCENT",
},
"new_code_smells": Object {
"id": "new_code_smells",
"key": "new_code_smells",
"name": "New_code_smells",
"type": "PERCENT",
},
}
}
options={
Object {
"color": "white",
"metric": "alert_status",
}
}
type="measure"
updateOptions={[Function]}
/>
<CodeSnippet
isOneLine={true}
snippet="[![alert_status](host/api/project_badges/measure?branch=branch-6.7&project=foo&metric=alert_status)](/dashboard)"
/>
</div>
`;

server/sonar-web/src/main/js/apps/overview/badges/__tests__/utils-test.ts → server/sonar-web/src/main/js/app/components/nav/component/projectInformation/badges/__tests__/utils-test.ts View File


server/sonar-web/src/main/js/apps/overview/badges/styles.css → server/sonar-web/src/main/js/app/components/nav/component/projectInformation/badges/styles.css View File

@@ -24,7 +24,7 @@
flex-wrap: nowrap;
}

.badge-button {
.button.badge-button {
display: flex;
justify-content: center;
padding: var(--gridSize);
@@ -36,15 +36,15 @@
transition: all 0.3s ease;
}

.badge-button:hover,
.badge-button:focus,
.badge-button:active {
.button.badge-button:hover,
.button.badge-button:focus,
.button.badge-button:active {
background-color: var(--barBackgroundColor);
border-color: var(--blue);
box-shadow: none;
}

.badge-button.selected {
.button.badge-button.selected {
background-color: var(--lightBlue);
border-color: var(--darkBlue);
}

server/sonar-web/src/main/js/apps/overview/badges/utils.ts → server/sonar-web/src/main/js/app/components/nav/component/projectInformation/badges/utils.ts View File

@@ -21,7 +21,7 @@ import { stringify } from 'querystring';
import { getLocalizedMetricName } from 'sonar-ui-common/helpers/l10n';
import { omitNil } from 'sonar-ui-common/helpers/request';
import { getHostUrl, getPathUrlAsString } from 'sonar-ui-common/helpers/urls';
import { getProjectUrl } from '../../../helpers/urls';
import { getProjectUrl } from '../../../../../../helpers/urls';

export type BadgeColors = 'white' | 'black' | 'orange';
export type BadgeFormats = 'md' | 'url';

server/sonar-web/src/main/js/apps/overview/meta/MetaKey.tsx → server/sonar-web/src/main/js/app/components/nav/component/projectInformation/meta/MetaKey.tsx View File

@@ -29,7 +29,7 @@ interface Props {
export default function MetaKey({ componentKey, qualifier }: Props) {
return (
<>
<h4 className="overview-meta-header">{translate('overview.project_key', qualifier)}</h4>
<h3>{translate('overview.project_key', qualifier)}</h3>
<div className="display-flex-center">
<input className="overview-key" readOnly={true} type="text" value={componentKey} />
<ClipboardButton className="little-spacer-left" copyValue={componentKey} />

server/sonar-web/src/main/js/apps/overview/meta/MetaLink.tsx → server/sonar-web/src/main/js/app/components/nav/component/projectInformation/meta/MetaLink.tsx View File

@@ -20,8 +20,8 @@
import * as React from 'react';
import { ClearButton } from 'sonar-ui-common/components/controls/buttons';
import ProjectLinkIcon from 'sonar-ui-common/components/icons/ProjectLinkIcon';
import isValidUri from '../../../app/utils/isValidUri';
import { getLinkName } from '../../projectLinks/utils';
import { getLinkName } from '../../../../../../helpers/projectLinks';
import isValidUri from '../../../../../utils/isValidUri';

interface Props {
iconOnly?: boolean;

server/sonar-web/src/main/js/apps/overview/meta/MetaLinks.tsx → server/sonar-web/src/main/js/app/components/nav/component/projectInformation/meta/MetaLinks.tsx View File

@@ -19,8 +19,8 @@
*/
import * as React from 'react';
import { translate } from 'sonar-ui-common/helpers/l10n';
import { getProjectLinks } from '../../../api/projectLinks';
import { orderLinks } from '../../projectLinks/utils';
import { getProjectLinks } from '../../../../../../api/projectLinks';
import { orderLinks } from '../../../../../../helpers/projectLinks';
import MetaLink from './MetaLink';

interface Props {
@@ -70,14 +70,16 @@ export default class MetaLinks extends React.PureComponent<Props, State> {
const orderedLinks = orderLinks(links);

return (
<div className="overview-meta-card">
<h4 className="overview-meta-header">{translate('overview.external_links')}</h4>
<ul className="overview-meta-list">
{orderedLinks.map(link => (
<MetaLink key={link.id} link={link} />
))}
</ul>
</div>
<>
<div className="big-padded bordered-bottom">
<h3>{translate('overview.external_links')}</h3>
<ul className="project-info-list">
{orderedLinks.map(link => (
<MetaLink key={link.id} link={link} />
))}
</ul>
</div>
</>
);
}
}

server/sonar-web/src/main/js/apps/overview/meta/MetaQualityGate.tsx → server/sonar-web/src/main/js/app/components/nav/component/projectInformation/meta/MetaQualityGate.tsx View File

@@ -20,24 +20,23 @@
import * as React from 'react';
import { Link } from 'react-router';
import { translate } from 'sonar-ui-common/helpers/l10n';
import { getQualityGateUrl } from '../../../helpers/urls';
import { getQualityGateUrl } from '../../../../../../helpers/urls';

interface Props {
organization?: string;
qualityGate: { isDefault?: boolean; key: string; name: string };
}

export default function MetaQualityGate({ qualityGate, organization }: Props) {
export default function MetaQualityGate({ qualityGate }: Props) {
return (
<>
<h4 className="overview-meta-header">{translate('overview.quality_gate')}</h4>
<h3>{translate('project.info.quality_gate')}</h3>

<ul className="overview-meta-list">
<ul className="project-info-list">
<li>
{qualityGate.isDefault && (
<span className="note spacer-right">{'(' + translate('default') + ')'}</span>
<span className="note spacer-right">({translate('default')})</span>
)}
<Link to={getQualityGateUrl(qualityGate.key, organization)}>{qualityGate.name}</Link>
<Link to={getQualityGateUrl(qualityGate.key)}>{qualityGate.name}</Link>
</li>
</ul>
</>

server/sonar-web/src/main/js/apps/overview/meta/MetaQualityProfiles.tsx → server/sonar-web/src/main/js/app/components/nav/component/projectInformation/meta/MetaQualityProfiles.tsx View File

@@ -17,15 +17,14 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as classNames from 'classnames';
import * as React from 'react';
import { connect } from 'react-redux';
import { Link } from 'react-router';
import Tooltip from 'sonar-ui-common/components/controls/Tooltip';
import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n';
import { searchRules } from '../../../api/rules';
import { getQualityProfileUrl } from '../../../helpers/urls';
import { getLanguages, Store } from '../../../store/rootReducer';
import { searchRules } from '../../../../../../api/rules';
import { getQualityProfileUrl } from '../../../../../../helpers/urls';
import { getLanguages, Store } from '../../../../../../store/rootReducer';

interface StateProps {
languages: T.Languages;
@@ -33,7 +32,6 @@ interface StateProps {

interface OwnProps {
headerClassName?: string;
organization?: string;
profiles: T.ComponentQualityProfile[];
}

@@ -77,7 +75,7 @@ export class MetaQualityProfiles extends React.PureComponent<StateProps & OwnPro
loadDeprecatedRulesForProfile(profileKey: string) {
const data = {
activation: 'true',
organization: this.props.organization,
organization: undefined,
ps: 1,
qprofile: profileKey,
statuses: 'DEPRECATED'
@@ -96,13 +94,11 @@ export class MetaQualityProfiles extends React.PureComponent<StateProps & OwnPro

const inner = (
<div className="text-ellipsis">
<span className="note spacer-right">{'(' + languageName + ')'}</span>
<span className="note spacer-right">({languageName})</span>
{profile.deleted ? (
profile.name
) : (
<Link to={getQualityProfileUrl(profile.name, profile.language, this.props.organization)}>
{profile.name}
</Link>
<Link to={getQualityProfileUrl(profile.name, profile.language)}>{profile.name}</Link>
)}
</div>
);
@@ -111,7 +107,7 @@ export class MetaQualityProfiles extends React.PureComponent<StateProps & OwnPro
const tooltip = translateWithParameters('overview.deleted_profile', profile.name);
return (
<Tooltip key={profile.key} overlay={tooltip}>
<li className="overview-deleted-profile">{inner}</li>
<li className="project-info-deleted-profile">{inner}</li>
</Tooltip>
);
}
@@ -122,7 +118,7 @@ export class MetaQualityProfiles extends React.PureComponent<StateProps & OwnPro
const tooltip = translateWithParameters('overview.deprecated_profile', count);
return (
<Tooltip key={profile.key} overlay={tooltip}>
<li className="overview-deprecated-rules">{inner}</li>
<li className="project-info-deprecated-rules">{inner}</li>
</Tooltip>
);
}
@@ -135,11 +131,9 @@ export class MetaQualityProfiles extends React.PureComponent<StateProps & OwnPro

return (
<>
<h4 className={classNames('overview-meta-header', headerClassName)}>
{translate('overview.quality_profiles')}
</h4>
<h3 className={headerClassName}>{translate('overview.quality_profiles')}</h3>

<ul className="overview-meta-list">
<ul className="project-info-list">
{profiles.map(profile => this.renderProfile(profile))}
</ul>
</>

+ 75
- 0
server/sonar-web/src/main/js/app/components/nav/component/projectInformation/meta/MetaSize.tsx View File

@@ -0,0 +1,75 @@
/*
* SonarQube
* Copyright (C) 2009-2020 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 * as React from 'react';
import SizeRating from 'sonar-ui-common/components/ui/SizeRating';
import { translate } from 'sonar-ui-common/helpers/l10n';
import { formatMeasure, localizeMetric } from 'sonar-ui-common/helpers/measures';
import DrilldownLink from '../../../../../../components/shared/DrilldownLink';
import { ComponentQualifier } from '../../../../../../types/component';
import { MetricKey } from '../../../../../../types/metrics';

export interface MetaSizeProps {
component: T.Component;
measures: T.Measure[];
}

export default function MetaSize({ component, measures }: MetaSizeProps) {
const isApp = component.qualifier === ComponentQualifier.Application;
const ncloc = measures.find(measure => measure.metric === MetricKey.ncloc);
const projects = isApp
? measures.find(measure => measure.metric === MetricKey.projects)
: undefined;

return (
<>
<h3>{localizeMetric(MetricKey.ncloc)}</h3>
<div className="display-flex-center">
{ncloc ? (
<>
<DrilldownLink className="huge" component={component.key} metric={MetricKey.ncloc}>
{formatMeasure(ncloc.value, 'SHORT_INT')}
</DrilldownLink>

<span className="spacer-left">
<SizeRating value={Number(ncloc.value)} />
</span>
</>
) : (
<span>0</span>
)}

{isApp && (
<span className="huge-spacer-left display-inline-flex-center">
{projects ? (
<DrilldownLink component={component.key} metric={MetricKey.projects}>
<span className="big">{formatMeasure(projects.value, 'SHORT_INT')}</span>
</DrilldownLink>
) : (
<span className="big">0</span>
)}
<span className="little-spacer-left text-muted">
{translate('metric.projects.name')}
</span>
</span>
)}
</div>
</>
);
}

server/sonar-web/src/main/js/apps/overview/meta/MetaTags.tsx → server/sonar-web/src/main/js/app/components/nav/component/projectInformation/meta/MetaTags.tsx View File

@@ -22,8 +22,8 @@ import { ButtonLink } from 'sonar-ui-common/components/controls/buttons';
import Dropdown from 'sonar-ui-common/components/controls/Dropdown';
import { PopupPlacement } from 'sonar-ui-common/components/ui/popups';
import { translate } from 'sonar-ui-common/helpers/l10n';
import { setProjectTags } from '../../../api/components';
import TagsList from '../../../components/tags/TagsList';
import { setProjectTags } from '../../../../../../api/components';
import TagsList from '../../../../../../components/tags/TagsList';
import MetaTagsSelector from './MetaTagsSelector';

interface Props {
@@ -62,7 +62,7 @@ export default class MetaTags extends React.PureComponent<Props> {

if (this.canUpdateTags()) {
return (
<div className="big-spacer-top overview-meta-tags" ref={card => (this.card = card)}>
<div className="project-info-tags" ref={card => (this.card = card)}>
<Dropdown
closeOnClick={false}
closeOnClickOutside={true}
@@ -82,7 +82,7 @@ export default class MetaTags extends React.PureComponent<Props> {
);
} else {
return (
<div className="big-spacer-top overview-meta-tags">
<div className="big-spacer-top project-info-tags">
<TagsList
allowUpdate={false}
className="note"

server/sonar-web/src/main/js/apps/overview/meta/MetaTagsSelector.tsx → server/sonar-web/src/main/js/app/components/nav/component/projectInformation/meta/MetaTagsSelector.tsx View File

@@ -19,8 +19,8 @@
*/
import { difference, without } from 'lodash';
import * as React from 'react';
import { searchProjectTags } from '../../../api/components';
import TagsSelector from '../../../components/tags/TagsSelector';
import { searchProjectTags } from '../../../../../../api/components';
import TagsSelector from '../../../../../../components/tags/TagsSelector';

interface Props {
project: string;

server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaLink-test.tsx → server/sonar-web/src/main/js/app/components/nav/component/projectInformation/meta/__tests__/MetaLink-test.tsx View File


server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaQualityProfiles-test.tsx → server/sonar-web/src/main/js/app/components/nav/component/projectInformation/meta/__tests__/MetaQualityProfiles-test.tsx View File

@@ -20,11 +20,11 @@
import { shallow } from 'enzyme';
import * as React from 'react';
import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
import { searchRules } from '../../../../api/rules';
import { mockLanguage, mockQualityProfile } from '../../../../helpers/testMocks';
import { searchRules } from '../../../../../../../api/rules';
import { mockLanguage, mockQualityProfile } from '../../../../../../../helpers/testMocks';
import { MetaQualityProfiles } from '../MetaQualityProfiles';

jest.mock('../../../../api/rules', () => {
jest.mock('../../../../../../../api/rules', () => {
return {
searchRules: jest.fn().mockResolvedValue({
total: 10
@@ -38,8 +38,8 @@ it('should render correctly', async () => {

await waitAndUpdate(wrapper);
expect(wrapper).toMatchSnapshot();
expect(wrapper.find('.overview-deprecated-rules').exists()).toBe(true);
expect(wrapper.find('.overview-deleted-profile').exists()).toBe(true);
expect(wrapper.find('.project-info-deprecated-rules').exists()).toBe(true);
expect(wrapper.find('.project-info-deleted-profile').exists()).toBe(true);
expect(searchRules).toBeCalled();
});


+ 46
- 0
server/sonar-web/src/main/js/app/components/nav/component/projectInformation/meta/__tests__/MetaSize-test.tsx View File

@@ -0,0 +1,46 @@
/*
* SonarQube
* Copyright (C) 2009-2020 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 { shallow } from 'enzyme';
import * as React from 'react';
import { mockComponent, mockMeasure } from '../../../../../../../helpers/testMocks';
import { ComponentQualifier } from '../../../../../../../types/component';
import { MetricKey } from '../../../../../../../types/metrics';
import MetaSize, { MetaSizeProps } from '../MetaSize';

it('should render correctly', () => {
expect(shallowRender()).toMatchSnapshot('project');
expect(
shallowRender({ component: mockComponent({ qualifier: ComponentQualifier.Application }) })
).toMatchSnapshot('application');
});

function shallowRender(props: Partial<MetaSizeProps> = {}) {
return shallow<MetaSizeProps>(
<MetaSize
component={mockComponent()}
measures={[
mockMeasure({ metric: MetricKey.ncloc }),
mockMeasure({ metric: MetricKey.projects })
]}
{...props}
/>
);
}

server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaTags-test.tsx → server/sonar-web/src/main/js/app/components/nav/component/projectInformation/meta/__tests__/MetaTags-test.tsx View File

@@ -19,7 +19,7 @@
*/
import { shallow } from 'enzyme';
import * as React from 'react';
import { mockComponent } from '../../../../helpers/testMocks';
import { mockComponent } from '../../../../../../../helpers/testMocks';
import MetaTags from '../MetaTags';

const component = mockComponent({

server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaTagsSelector-test.tsx → server/sonar-web/src/main/js/app/components/nav/component/projectInformation/meta/__tests__/MetaTagsSelector-test.tsx View File

@@ -20,10 +20,10 @@
/* eslint-disable import/first */
import { mount, shallow } from 'enzyme';
import * as React from 'react';
import { searchProjectTags } from '../../../../api/components';
import { searchProjectTags } from '../../../../../../../api/components';
import MetaTagsSelector from '../MetaTagsSelector';

jest.mock('../../../../api/components', () => ({
jest.mock('../../../../../../../api/components', () => ({
searchProjectTags: jest.fn()
}));


server/sonar-web/src/main/js/apps/overview/meta/__tests__/__snapshots__/MetaLink-test.tsx.snap → server/sonar-web/src/main/js/app/components/nav/component/projectInformation/meta/__tests__/__snapshots__/MetaLink-test.tsx.snap View File


server/sonar-web/src/main/js/apps/overview/meta/__tests__/__snapshots__/MetaQualityProfiles-test.tsx.snap → server/sonar-web/src/main/js/app/components/nav/component/projectInformation/meta/__tests__/__snapshots__/MetaQualityProfiles-test.tsx.snap View File

@@ -2,20 +2,18 @@

exports[`should render correctly 1`] = `
<Fragment>
<h4
className="overview-meta-header"
>
<h3>
overview.quality_profiles
</h4>
</h3>
<ul
className="overview-meta-list"
className="project-info-list"
>
<Tooltip
key="js"
overlay="overview.deleted_profile.name"
>
<li
className="overview-deleted-profile"
className="project-info-deleted-profile"
>
<div
className="text-ellipsis"
@@ -23,7 +21,9 @@ exports[`should render correctly 1`] = `
<span
className="note spacer-right"
>
(js)
(
js
)
</span>
name
</div>
@@ -38,7 +38,9 @@ exports[`should render correctly 1`] = `
<span
className="note spacer-right"
>
(CSS)
(
CSS
)
</span>
<Link
onlyActiveOnIndex={false}
@@ -63,20 +65,18 @@ exports[`should render correctly 1`] = `

exports[`should render correctly 2`] = `
<Fragment>
<h4
className="overview-meta-header"
>
<h3>
overview.quality_profiles
</h4>
</h3>
<ul
className="overview-meta-list"
className="project-info-list"
>
<Tooltip
key="js"
overlay="overview.deleted_profile.name"
>
<li
className="overview-deleted-profile"
className="project-info-deleted-profile"
>
<div
className="text-ellipsis"
@@ -84,7 +84,9 @@ exports[`should render correctly 2`] = `
<span
className="note spacer-right"
>
(js)
(
js
)
</span>
name
</div>
@@ -95,7 +97,7 @@ exports[`should render correctly 2`] = `
overlay="overview.deprecated_profile.10"
>
<li
className="overview-deprecated-rules"
className="project-info-deprecated-rules"
>
<div
className="text-ellipsis"
@@ -103,7 +105,9 @@ exports[`should render correctly 2`] = `
<span
className="note spacer-right"
>
(CSS)
(
CSS
)
</span>
<Link
onlyActiveOnIndex={false}

+ 72
- 0
server/sonar-web/src/main/js/app/components/nav/component/projectInformation/meta/__tests__/__snapshots__/MetaSize-test.tsx.snap View File

@@ -0,0 +1,72 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should render correctly: application 1`] = `
<Fragment>
<h3>
metric.ncloc.name
</h3>
<div
className="display-flex-center"
>
<DrilldownLink
className="huge"
component="my-project"
metric="ncloc"
>
1
</DrilldownLink>
<span
className="spacer-left"
>
<SizeRating
value={1}
/>
</span>
<span
className="huge-spacer-left display-inline-flex-center"
>
<DrilldownLink
component="my-project"
metric="projects"
>
<span
className="big"
>
1
</span>
</DrilldownLink>
<span
className="little-spacer-left text-muted"
>
metric.projects.name
</span>
</span>
</div>
</Fragment>
`;

exports[`should render correctly: project 1`] = `
<Fragment>
<h3>
metric.ncloc.name
</h3>
<div
className="display-flex-center"
>
<DrilldownLink
className="huge"
component="my-project"
metric="ncloc"
>
1
</DrilldownLink>
<span
className="spacer-left"
>
<SizeRating
value={1}
/>
</span>
</div>
</Fragment>
`;

server/sonar-web/src/main/js/apps/overview/meta/__tests__/__snapshots__/MetaTags-test.tsx.snap → server/sonar-web/src/main/js/app/components/nav/component/projectInformation/meta/__tests__/__snapshots__/MetaTags-test.tsx.snap View File

@@ -2,7 +2,7 @@

exports[`should render with tags and admin rights 1`] = `
<div
className="big-spacer-top overview-meta-tags"
className="project-info-tags"
>
<Dropdown
closeOnClick={false}
@@ -41,7 +41,7 @@ exports[`should render with tags and admin rights 1`] = `

exports[`should render without tags and admin rights 1`] = `
<div
className="big-spacer-top overview-meta-tags"
className="big-spacer-top project-info-tags"
>
<TagsList
allowUpdate={false}

+ 92
- 0
server/sonar-web/src/main/js/app/components/nav/component/projectInformation/notifications/ProjectNotifications.tsx View File

@@ -0,0 +1,92 @@
/*
* SonarQube
* Copyright (C) 2009-2020 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 * as React from 'react';
import { Alert } from 'sonar-ui-common/components/ui/Alert';
import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner';
import { translate } from 'sonar-ui-common/helpers/l10n';
import NotificationsList from '../../../../../../apps/account/notifications/NotificationsList';
import {
withNotifications,
WithNotificationsProps
} from '../../../../../../components/hoc/withNotifications';

interface Props {
className?: string;
component: T.Component;
}

export function ProjectNotifications(props: WithNotificationsProps & Props) {
const { channels, component, loading, notifications, perProjectTypes } = props;

const handleAddNotification = ({ channel, type }: { channel: string; type: string }) => {
props.addNotification({ project: component.key, channel, type });
};

const handleRemoveNotification = ({ channel, type }: { channel: string; type: string }) => {
props.removeNotification({
project: component.key,
channel,
type
});
};

const getCheckboxId = (type: string, channel: string) => {
return `project-notification-${component.key}-${type}-${channel}`;
};

const projectNotifications = notifications.filter(n => n.project && n.project === component.key);

return (
<>
<h3>{translate('project.info.notifications')}</h3>

<Alert className="spacer-top" variant="info">
{translate('notification.dispatcher.information')}
</Alert>

<DeferredSpinner loading={loading}>
<table className="data zebra notifications-table">
<thead>
<tr>
<th aria-label={translate('project')} />
{channels.map(channel => (
<th className="text-center" key={channel}>
<h4>{translate('notification.channel', channel)}</h4>
</th>
))}
</tr>
</thead>

<NotificationsList
channels={channels}
checkboxId={getCheckboxId}
notifications={projectNotifications}
onAdd={handleAddNotification}
onRemove={handleRemoveNotification}
project={true}
types={perProjectTypes}
/>
</table>
</DeferredSpinner>
</>
);
}

export default withNotifications(ProjectNotifications);

server/sonar-web/src/main/js/apps/overview/notifications/__tests__/ProjectNotifications.tsx → server/sonar-web/src/main/js/app/components/nav/component/projectInformation/notifications/__tests__/ProjectNotifications-test.tsx View File

@@ -17,9 +17,10 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
/* eslint-disable sonarjs/no-duplicate-string */
import { shallow } from 'enzyme';
import * as React from 'react';
import { mockComponent } from '../../../../helpers/testMocks';
import { mockComponent } from '../../../../../../../helpers/testMocks';
import { ProjectNotifications } from '../ProjectNotifications';

it('should render correctly', () => {
@@ -43,7 +44,7 @@ it('should add and remove a notification for the project', () => {
});

function shallowRender(props = {}) {
const wrapper = shallow(
return shallow(
<ProjectNotifications
addNotification={jest.fn()}
channels={['channel1', 'channel2']}
@@ -78,12 +79,4 @@ function shallowRender(props = {}) {
{...props}
/>
);

// Get the modal element. We need to trigger the ModalButton's `modal` prop,
// which is a function. It will return our Modal component.
return shallow(
wrapper.find('ModalButton').prop<Function>('modal')({
onClose: jest.fn()
})
);
}

+ 75
- 0
server/sonar-web/src/main/js/app/components/nav/component/projectInformation/notifications/__tests__/__snapshots__/ProjectNotifications-test.tsx.snap View File

@@ -0,0 +1,75 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should render correctly 1`] = `
<Fragment>
<h3>
project.info.notifications
</h3>
<Alert
className="spacer-top"
variant="info"
>
notification.dispatcher.information
</Alert>
<DeferredSpinner
loading={false}
timeout={100}
>
<table
className="data zebra notifications-table"
>
<thead>
<tr>
<th
aria-label="project"
/>
<th
className="text-center"
key="channel1"
>
<h4>
notification.channel.channel1
</h4>
</th>
<th
className="text-center"
key="channel2"
>
<h4>
notification.channel.channel2
</h4>
</th>
</tr>
</thead>
<NotificationsList
channels={
Array [
"channel1",
"channel2",
]
}
checkboxId={[Function]}
notifications={
Array [
Object {
"channel": "channel1",
"organization": "org",
"project": "foo",
"projectName": "Foo",
"type": "type-global",
},
]
}
onAdd={[Function]}
onRemove={[Function]}
project={true}
types={
Array [
"type-common",
]
}
/>
</table>
</DeferredSpinner>
</Fragment>
`;

+ 13
- 5
server/sonar-web/src/main/js/app/styles/init/misc.css View File

@@ -137,7 +137,7 @@ th.hide-overflow {
}

.big-padded {
padding: calc(2 * var(--gridSize));
padding: calc(2 * var(--gridSize)) !important;
}

.padded-top {
@@ -152,10 +152,6 @@ th.hide-overflow {
padding-top: calc(var(--gridSize) / 2) !important;
}

.little-padded-bottom {
padding-bottom: calc(var(--gridSize) / 2) !important;
}

td.little-spacer-left {
padding-left: 4px !important;
}
@@ -210,6 +206,10 @@ th.huge-spacer-right {
float: right !important;
}

.borderless {
border: none !important;
}

.bordered {
border: 1px solid var(--barBorderColor);
}
@@ -234,6 +234,10 @@ th.huge-spacer-right {
overflow: hidden !important;
}

.overflow-y-auto {
overflow-y: auto !important;
}

.max-width-100 {
max-width: 100% !important;
}
@@ -301,6 +305,10 @@ th.huge-spacer-right {
width: 600px !important;
}

.max-height-100 {
max-height: 100% !important;
}

.justify {
margin-bottom: -1em;
text-align: justify;

+ 2
- 2
server/sonar-web/src/main/js/apps/account/projects/ProjectCard.tsx View File

@@ -25,8 +25,8 @@ import Level from 'sonar-ui-common/components/ui/Level';
import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n';
import DateFromNow from '../../../components/intl/DateFromNow';
import DateTimeFormatter from '../../../components/intl/DateTimeFormatter';
import MetaLink from '../../overview/meta/MetaLink';
import { orderLinks } from '../../projectLinks/utils';
import MetaLink from '../../../app/components/nav/component/projectInformation/meta/MetaLink';
import { orderLinks } from '../../../helpers/projectLinks';

interface Props {
project: T.MyProject;

+ 0
- 127
server/sonar-web/src/main/js/apps/overview/badges/ProjectBadges.tsx View File

@@ -1,127 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2020 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 * as React from 'react';
import { Button, ResetButtonLink } from 'sonar-ui-common/components/controls/buttons';
import Modal from 'sonar-ui-common/components/controls/Modal';
import { translate } from 'sonar-ui-common/helpers/l10n';
import CodeSnippet from '../../../components/common/CodeSnippet';
import { getBranchLikeQuery } from '../../../helpers/branch-like';
import { isSonarCloud } from '../../../helpers/system';
import { BranchLike } from '../../../types/branch-like';
import BadgeButton from './BadgeButton';
import BadgeParams from './BadgeParams';
import './styles.css';
import { BadgeOptions, BadgeType, getBadgeSnippet, getBadgeUrl } from './utils';

interface Props {
branchLike?: BranchLike;
metrics: T.Dict<T.Metric>;
project: string;
qualifier: string;
}

interface State {
open: boolean;
selectedType: BadgeType;
badgeOptions: BadgeOptions;
}

export default class ProjectBadges extends React.PureComponent<Props, State> {
state: State = {
open: false,
selectedType: BadgeType.measure,
badgeOptions: { color: 'white', metric: 'alert_status' }
};

handleClose = () => {
this.setState({ open: false });
};

handleOpen = () => {
this.setState({ open: true });
};

handleSelectBadge = (selectedType: BadgeType) => {
this.setState({ selectedType });
};

handleUpdateOptions = (options: Partial<BadgeOptions>) => {
this.setState(state => ({ badgeOptions: { ...state.badgeOptions, ...options } }));
};

render() {
const { branchLike, project, qualifier } = this.props;
const { selectedType, badgeOptions } = this.state;
const header = translate('overview.badges.title');
const fullBadgeOptions = { project, ...badgeOptions, ...getBranchLikeQuery(branchLike) };
const badges = isSonarCloud()
? [BadgeType.measure, BadgeType.qualityGate, BadgeType.marketing]
: [BadgeType.measure, BadgeType.qualityGate];
return (
<div className="overview-meta-card">
<Button className="js-project-badges" onClick={this.handleOpen}>
{translate('overview.badges.get_badge', qualifier)}
</Button>
{this.state.open && (
<Modal contentLabel={header} onRequestClose={this.handleClose}>
<header className="modal-head">
<h2>{header}</h2>
</header>
<div className="modal-body">
<p className="huge-spacer-bottom">
{translate('overview.badges.description', qualifier)}
</p>
<div className="badges-list spacer-bottom">
{badges.map(type => (
<BadgeButton
key={type}
onClick={this.handleSelectBadge}
selected={type === selectedType}
type={type}
url={getBadgeUrl(type, fullBadgeOptions)}
/>
))}
</div>
<p className="text-center note huge-spacer-bottom">
{translate('overview.badges', selectedType, 'description', qualifier)}
</p>
<BadgeParams
className="big-spacer-bottom"
metrics={this.props.metrics}
options={badgeOptions}
type={selectedType}
updateOptions={this.handleUpdateOptions}
/>
<CodeSnippet
isOneLine={true}
snippet={getBadgeSnippet(selectedType, fullBadgeOptions)}
/>
</div>
<footer className="modal-foot">
<ResetButtonLink className="js-modal-close" onClick={this.handleClose}>
{translate('close')}
</ResetButtonLink>
</footer>
</Modal>
)}
</div>
);
}
}

+ 0
- 180
server/sonar-web/src/main/js/apps/overview/badges/__tests__/__snapshots__/ProjectBadges-test.tsx.snap View File

@@ -1,180 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should display the modal after click on sonarcloud 1`] = `
<div
className="overview-meta-card"
>
<Button
className="js-project-badges"
onClick={[Function]}
>
overview.badges.get_badge.TRK
</Button>
</div>
`;

exports[`should display the modal after click on sonarcloud 2`] = `
<Modal
contentLabel="overview.badges.title"
onRequestClose={[Function]}
>
<header
className="modal-head"
>
<h2>
overview.badges.title
</h2>
</header>
<div
className="modal-body"
>
<p
className="huge-spacer-bottom"
>
overview.badges.description.TRK
</p>
<div
className="badges-list spacer-bottom"
>
<BadgeButton
key="measure"
onClick={[Function]}
selected={true}
type="measure"
url="host/api/project_badges/measure?branch=branch-6.6&project=foo&metric=alert_status"
/>
<BadgeButton
key="quality_gate"
onClick={[Function]}
selected={false}
type="quality_gate"
url="host/api/project_badges/quality_gate?branch=branch-6.6&project=foo"
/>
<BadgeButton
key="marketing"
onClick={[Function]}
selected={false}
type="marketing"
url="host/images/project_badges/sonarcloud-white.svg"
/>
</div>
<p
className="text-center note huge-spacer-bottom"
>
overview.badges.measure.description.TRK
</p>
<BadgeParams
className="big-spacer-bottom"
metrics={Object {}}
options={
Object {
"color": "white",
"metric": "alert_status",
}
}
type="measure"
updateOptions={[Function]}
/>
<CodeSnippet
isOneLine={true}
snippet="[![alert_status](host/api/project_badges/measure?branch=branch-6.6&project=foo&metric=alert_status)](/dashboard)"
/>
</div>
<footer
className="modal-foot"
>
<ResetButtonLink
className="js-modal-close"
onClick={[Function]}
>
close
</ResetButtonLink>
</footer>
</Modal>
`;

exports[`should display the modal after click on sonarqube 1`] = `
<div
className="overview-meta-card"
>
<Button
className="js-project-badges"
onClick={[Function]}
>
overview.badges.get_badge.TRK
</Button>
</div>
`;

exports[`should display the modal after click on sonarqube 2`] = `
<Modal
contentLabel="overview.badges.title"
onRequestClose={[Function]}
>
<header
className="modal-head"
>
<h2>
overview.badges.title
</h2>
</header>
<div
className="modal-body"
>
<p
className="huge-spacer-bottom"
>
overview.badges.description.TRK
</p>
<div
className="badges-list spacer-bottom"
>
<BadgeButton
key="measure"
onClick={[Function]}
selected={true}
type="measure"
url="host/api/project_badges/measure?branch=branch-6.6&project=foo&metric=alert_status"
/>
<BadgeButton
key="quality_gate"
onClick={[Function]}
selected={false}
type="quality_gate"
url="host/api/project_badges/quality_gate?branch=branch-6.6&project=foo"
/>
</div>
<p
className="text-center note huge-spacer-bottom"
>
overview.badges.measure.description.TRK
</p>
<BadgeParams
className="big-spacer-bottom"
metrics={Object {}}
options={
Object {
"color": "white",
"metric": "alert_status",
}
}
type="measure"
updateOptions={[Function]}
/>
<CodeSnippet
isOneLine={true}
snippet="[![alert_status](host/api/project_badges/measure?branch=branch-6.6&project=foo&metric=alert_status)](/dashboard)"
/>
</div>
<footer
className="modal-foot"
>
<ResetButtonLink
className="js-modal-close"
onClick={[Function]}
>
close
</ResetButtonLink>
</footer>
</Modal>
`;

+ 0
- 171
server/sonar-web/src/main/js/apps/overview/meta/MetaContainer.tsx View File

@@ -1,171 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2020 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 * as React from 'react';
import { connect } from 'react-redux';
import { lazyLoadComponent } from 'sonar-ui-common/components/lazyLoadComponent';
import { translate } from 'sonar-ui-common/helpers/l10n';
import PrivacyBadgeContainer from '../../../components/common/PrivacyBadgeContainer';
import { hasPrivateAccess } from '../../../helpers/organizations';
import { isLoggedIn } from '../../../helpers/users';
import {
getAppState,
getCurrentUser,
getMyOrganizations,
getOrganizationByKey,
Store
} from '../../../store/rootReducer';
import { BranchLike } from '../../../types/branch-like';
import MetaKey from './MetaKey';
import MetaLinks from './MetaLinks';
import MetaOrganizationKey from './MetaOrganizationKey';
import MetaQualityGate from './MetaQualityGate';
import MetaQualityProfiles from './MetaQualityProfiles';
import MetaTags from './MetaTags';

const ProjectBadges = lazyLoadComponent(() => import('../badges/ProjectBadges'), 'ProjectBadges');
const ProjectNotifications = lazyLoadComponent(
() => import('../notifications/ProjectNotifications'),
'ProjectNotifications'
);

interface StateToProps {
appState: T.AppState;
currentUser: T.CurrentUser;
organization?: T.Organization;
userOrganizations: T.Organization[];
}

interface OwnProps {
branchLike?: BranchLike;
component: T.Component;
history?: {
[metric: string]: Array<{ date: Date; value?: string }>;
};
measures?: T.MeasureEnhanced[];
metrics?: T.Dict<T.Metric>;
onComponentChange: (changes: {}) => void;
}

type Props = OwnProps & StateToProps;

export class Meta extends React.PureComponent<Props> {
renderQualityInfos() {
const { organizationsEnabled } = this.props.appState;
const { component, currentUser, organization, userOrganizations } = this.props;
const { qualifier, qualityProfiles, qualityGate } = component;
const isProject = qualifier === 'TRK';

if (
!isProject ||
(organizationsEnabled && !hasPrivateAccess(currentUser, organization, userOrganizations))
) {
return null;
}

return (
<div className="overview-meta-card" id="overview-meta-quality-gate">
{qualityGate && (
<MetaQualityGate
organization={organizationsEnabled ? component.organization : undefined}
qualityGate={qualityGate}
/>
)}

{qualityProfiles && qualityProfiles.length > 0 && (
<MetaQualityProfiles
headerClassName={qualityGate ? 'big-spacer-top' : undefined}
organization={organizationsEnabled ? component.organization : undefined}
profiles={qualityProfiles}
/>
)}
</div>
);
}

render() {
const { organizationsEnabled } = this.props.appState;
const { branchLike, component, currentUser, metrics, organization } = this.props;
const { qualifier, description, visibility } = component;

const isProject = qualifier === 'TRK';
const isApp = qualifier === 'APP';
const isPrivate = visibility === 'private';
const canUseBadges = !isPrivate && (isProject || isApp);
const canConfigureNotifications = isLoggedIn(currentUser);

return (
<div className="overview-meta">
<div className="overview-meta-card">
<h4 className="overview-meta-header">
{translate('overview.about_this_project', qualifier)}
{component.visibility && (
<PrivacyBadgeContainer
className="spacer-left pull-right"
organization={organization}
qualifier={component.qualifier}
tooltipProps={{ projectKey: component.key }}
visibility={component.visibility}
/>
)}
</h4>
{description !== undefined && <p className="overview-meta-description">{description}</p>}
{isProject && (
<MetaTags component={component} onComponentChange={this.props.onComponentChange} />
)}
</div>

{this.renderQualityInfos()}

{isProject && <MetaLinks component={component} />}

<div className="overview-meta-card">
<MetaKey componentKey={component.key} qualifier={component.qualifier} />
{organizationsEnabled && <MetaOrganizationKey organization={component.organization} />}
</div>

{(canUseBadges || canConfigureNotifications) && (
<div className="overview-meta-card">
{canUseBadges && metrics !== undefined && (
<ProjectBadges
branchLike={branchLike}
metrics={metrics}
project={component.key}
qualifier={component.qualifier}
/>
)}

{canConfigureNotifications && (
<ProjectNotifications className="spacer-top spacer-bottom" component={component} />
)}
</div>
)}
</div>
);
}
}

const mapStateToProps = (state: Store, { component }: OwnProps) => ({
appState: getAppState(state),
currentUser: getCurrentUser(state),
organization: getOrganizationByKey(state, component.organization),
userOrganizations: getMyOrganizations(state)
});

export default connect(mapStateToProps)(Meta);

+ 0
- 109
server/sonar-web/src/main/js/apps/overview/meta/MetaSize.tsx View File

@@ -1,109 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2020 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 * as classNames from 'classnames';
import * as React from 'react';
import SizeRating from 'sonar-ui-common/components/ui/SizeRating';
import { translate } from 'sonar-ui-common/helpers/l10n';
import { formatMeasure } from 'sonar-ui-common/helpers/measures';
import LanguageDistributionContainer from '../../../components/charts/LanguageDistributionContainer';
import DrilldownLink from '../../../components/shared/DrilldownLink';
import { BranchLike } from '../../../types/branch-like';

interface Props {
branchLike?: BranchLike;
component: T.LightComponent;
measures: T.MeasureEnhanced[];
}

export default class MetaSize extends React.PureComponent<Props> {
renderLoC = (ncloc?: T.MeasureEnhanced) => (
<div
className={classNames('overview-meta-size-ncloc', {
'is-half-width': this.props.component.qualifier === 'APP'
})}
id="overview-ncloc">
{ncloc && (
<span className="spacer-right">
<SizeRating value={Number(ncloc.value)} />
</span>
)}
{ncloc ? (
<DrilldownLink
branchLike={this.props.branchLike}
component={this.props.component.key}
metric="ncloc">
{formatMeasure(ncloc.value, 'SHORT_INT')}
</DrilldownLink>
) : (
<span>0</span>
)}
</div>
);

renderLoCDistribution = () => {
const languageDistribution = this.props.measures.find(
measure => measure.metric.key === 'ncloc_language_distribution'
);

const className =
this.props.component.qualifier === 'TRK' ? 'overview-meta-size-lang-dist' : 'big-spacer-top';

return languageDistribution && languageDistribution.value !== undefined ? (
<div className={className} id="overview-language-distribution">
<LanguageDistributionContainer distribution={languageDistribution.value} width={160} />
</div>
) : null;
};

renderProjects = () => {
const projects = this.props.measures.find(measure => measure.metric.key === 'projects');
return (
<div className="overview-meta-size-ncloc is-half-width" id="overview-projects">
{projects ? (
<DrilldownLink
branchLike={this.props.branchLike}
component={this.props.component.key}
metric="projects">
{formatMeasure(projects.value, 'SHORT_INT')}
</DrilldownLink>
) : (
<span>0</span>
)}
<div className="spacer-top text-muted">{translate('metric.projects.name')}</div>
</div>
);
};

render() {
const ncloc = this.props.measures.find(measure => measure.metric.key === 'ncloc');

if (ncloc == null && this.props.component.qualifier !== 'APP') {
return null;
}

return (
<div className="big-spacer-top" id="overview-size">
{this.props.component.qualifier === 'APP' && this.renderProjects()}
{this.renderLoC(ncloc)}
{this.renderLoCDistribution()}
</div>
);
}
}

+ 0
- 69
server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaContainer-test.tsx View File

@@ -1,69 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2020 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 { shallow } from 'enzyme';
import * as React from 'react';
import {
mockAppState,
mockComponent,
mockLoggedInUser,
mockOrganization
} from '../../../../helpers/testMocks';
import { Meta } from '../MetaContainer';

it('should render correctly', () => {
const wrapper = shallowRender();
expect(wrapper).toMatchSnapshot();
expect(metaQualityGateRendered(wrapper)).toBe(true);
});

it('should hide QG and QP links if the organization has a paid plan, and the user is not a member', () => {
const wrapper = shallowRender({
organization: mockOrganization({ key: 'other_key', subscription: 'PAID' })
});
expect(wrapper).toMatchSnapshot();
expect(metaQualityGateRendered(wrapper)).toBe(false);
});

it('should show QG and QP links if the organization has a paid plan, and the user is a member', () => {
const wrapper = shallowRender({
organization: mockOrganization({ subscription: 'PAID' })
});
expect(wrapper).toMatchSnapshot();
expect(metaQualityGateRendered(wrapper)).toBe(true);
});

function metaQualityGateRendered(wrapper: any) {
return wrapper.find('#overview-meta-quality-gate').exists();
}

function shallowRender(props: Partial<Meta['props']> = {}) {
return shallow(
<Meta
appState={mockAppState({ organizationsEnabled: true })}
component={mockComponent()}
currentUser={mockLoggedInUser()}
metrics={{}}
onComponentChange={jest.fn()}
organization={mockOrganization()}
userOrganizations={[mockOrganization()]}
{...props}
/>
);
}

+ 0
- 398
server/sonar-web/src/main/js/apps/overview/meta/__tests__/__snapshots__/MetaContainer-test.tsx.snap View File

@@ -1,398 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should hide QG and QP links if the organization has a paid plan, and the user is not a member 1`] = `
<div
className="overview-meta"
>
<div
className="overview-meta-card"
>
<h4
className="overview-meta-header"
>
overview.about_this_project.TRK
</h4>
<MetaTags
component={
Object {
"breadcrumbs": Array [],
"key": "my-project",
"name": "MyProject",
"organization": "foo",
"qualifier": "TRK",
"qualityGate": Object {
"isDefault": true,
"key": "30",
"name": "Sonar way",
},
"qualityProfiles": Array [
Object {
"deleted": false,
"key": "my-qp",
"language": "ts",
"name": "Sonar way",
},
],
"tags": Array [],
}
}
onComponentChange={[MockFunction]}
/>
</div>
<MetaLinks
component={
Object {
"breadcrumbs": Array [],
"key": "my-project",
"name": "MyProject",
"organization": "foo",
"qualifier": "TRK",
"qualityGate": Object {
"isDefault": true,
"key": "30",
"name": "Sonar way",
},
"qualityProfiles": Array [
Object {
"deleted": false,
"key": "my-qp",
"language": "ts",
"name": "Sonar way",
},
],
"tags": Array [],
}
}
/>
<div
className="overview-meta-card"
>
<MetaKey
componentKey="my-project"
qualifier="TRK"
/>
<MetaOrganizationKey
organization="foo"
/>
</div>
<div
className="overview-meta-card"
>
<ProjectBadges
metrics={Object {}}
project="my-project"
qualifier="TRK"
/>
<ProjectNotifications
className="spacer-top spacer-bottom"
component={
Object {
"breadcrumbs": Array [],
"key": "my-project",
"name": "MyProject",
"organization": "foo",
"qualifier": "TRK",
"qualityGate": Object {
"isDefault": true,
"key": "30",
"name": "Sonar way",
},
"qualityProfiles": Array [
Object {
"deleted": false,
"key": "my-qp",
"language": "ts",
"name": "Sonar way",
},
],
"tags": Array [],
}
}
/>
</div>
</div>
`;

exports[`should render correctly 1`] = `
<div
className="overview-meta"
>
<div
className="overview-meta-card"
>
<h4
className="overview-meta-header"
>
overview.about_this_project.TRK
</h4>
<MetaTags
component={
Object {
"breadcrumbs": Array [],
"key": "my-project",
"name": "MyProject",
"organization": "foo",
"qualifier": "TRK",
"qualityGate": Object {
"isDefault": true,
"key": "30",
"name": "Sonar way",
},
"qualityProfiles": Array [
Object {
"deleted": false,
"key": "my-qp",
"language": "ts",
"name": "Sonar way",
},
],
"tags": Array [],
}
}
onComponentChange={[MockFunction]}
/>
</div>
<div
className="overview-meta-card"
id="overview-meta-quality-gate"
>
<MetaQualityGate
organization="foo"
qualityGate={
Object {
"isDefault": true,
"key": "30",
"name": "Sonar way",
}
}
/>
<Connect(MetaQualityProfiles)
headerClassName="big-spacer-top"
organization="foo"
profiles={
Array [
Object {
"deleted": false,
"key": "my-qp",
"language": "ts",
"name": "Sonar way",
},
]
}
/>
</div>
<MetaLinks
component={
Object {
"breadcrumbs": Array [],
"key": "my-project",
"name": "MyProject",
"organization": "foo",
"qualifier": "TRK",
"qualityGate": Object {
"isDefault": true,
"key": "30",
"name": "Sonar way",
},
"qualityProfiles": Array [
Object {
"deleted": false,
"key": "my-qp",
"language": "ts",
"name": "Sonar way",
},
],
"tags": Array [],
}
}
/>
<div
className="overview-meta-card"
>
<MetaKey
componentKey="my-project"
qualifier="TRK"
/>
<MetaOrganizationKey
organization="foo"
/>
</div>
<div
className="overview-meta-card"
>
<ProjectBadges
metrics={Object {}}
project="my-project"
qualifier="TRK"
/>
<ProjectNotifications
className="spacer-top spacer-bottom"
component={
Object {
"breadcrumbs": Array [],
"key": "my-project",
"name": "MyProject",
"organization": "foo",
"qualifier": "TRK",
"qualityGate": Object {
"isDefault": true,
"key": "30",
"name": "Sonar way",
},
"qualityProfiles": Array [
Object {
"deleted": false,
"key": "my-qp",
"language": "ts",
"name": "Sonar way",
},
],
"tags": Array [],
}
}
/>
</div>
</div>
`;

exports[`should show QG and QP links if the organization has a paid plan, and the user is a member 1`] = `
<div
className="overview-meta"
>
<div
className="overview-meta-card"
>
<h4
className="overview-meta-header"
>
overview.about_this_project.TRK
</h4>
<MetaTags
component={
Object {
"breadcrumbs": Array [],
"key": "my-project",
"name": "MyProject",
"organization": "foo",
"qualifier": "TRK",
"qualityGate": Object {
"isDefault": true,
"key": "30",
"name": "Sonar way",
},
"qualityProfiles": Array [
Object {
"deleted": false,
"key": "my-qp",
"language": "ts",
"name": "Sonar way",
},
],
"tags": Array [],
}
}
onComponentChange={[MockFunction]}
/>
</div>
<div
className="overview-meta-card"
id="overview-meta-quality-gate"
>
<MetaQualityGate
organization="foo"
qualityGate={
Object {
"isDefault": true,
"key": "30",
"name": "Sonar way",
}
}
/>
<Connect(MetaQualityProfiles)
headerClassName="big-spacer-top"
organization="foo"
profiles={
Array [
Object {
"deleted": false,
"key": "my-qp",
"language": "ts",
"name": "Sonar way",
},
]
}
/>
</div>
<MetaLinks
component={
Object {
"breadcrumbs": Array [],
"key": "my-project",
"name": "MyProject",
"organization": "foo",
"qualifier": "TRK",
"qualityGate": Object {
"isDefault": true,
"key": "30",
"name": "Sonar way",
},
"qualityProfiles": Array [
Object {
"deleted": false,
"key": "my-qp",
"language": "ts",
"name": "Sonar way",
},
],
"tags": Array [],
}
}
/>
<div
className="overview-meta-card"
>
<MetaKey
componentKey="my-project"
qualifier="TRK"
/>
<MetaOrganizationKey
organization="foo"
/>
</div>
<div
className="overview-meta-card"
>
<ProjectBadges
metrics={Object {}}
project="my-project"
qualifier="TRK"
/>
<ProjectNotifications
className="spacer-top spacer-bottom"
component={
Object {
"breadcrumbs": Array [],
"key": "my-project",
"name": "MyProject",
"organization": "foo",
"qualifier": "TRK",
"qualityGate": Object {
"isDefault": true,
"key": "30",
"name": "Sonar way",
},
"qualityProfiles": Array [
Object {
"deleted": false,
"key": "my-qp",
"language": "ts",
"name": "Sonar way",
},
],
"tags": Array [],
}
}
/>
</div>
</div>
`;

+ 0
- 116
server/sonar-web/src/main/js/apps/overview/notifications/ProjectNotifications.tsx View File

@@ -1,116 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2020 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 * as React from 'react';
import { Button, ResetButtonLink } from 'sonar-ui-common/components/controls/buttons';
import Modal from 'sonar-ui-common/components/controls/Modal';
import ModalButton from 'sonar-ui-common/components/controls/ModalButton';
import { Alert } from 'sonar-ui-common/components/ui/Alert';
import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner';
import { translate } from 'sonar-ui-common/helpers/l10n';
import {
withNotifications,
WithNotificationsProps
} from '../../../components/hoc/withNotifications';
import NotificationsList from '../../account/notifications/NotificationsList';

interface Props {
className?: string;
component: T.Component;
}

export function ProjectNotifications(props: WithNotificationsProps & Props) {
const { channels, className, component, loading, notifications, perProjectTypes } = props;

const header = translate('my_account.notifications');

const handleAddNotification = ({ channel, type }: { channel: string; type: string }) => {
props.addNotification({ project: component.key, channel, type });
};

const handleRemoveNotification = ({ channel, type }: { channel: string; type: string }) => {
props.removeNotification({
project: component.key,
channel,
type
});
};

const getCheckboxId = (type: string, channel: string) => {
return `project-notification-${component.key}-${type}-${channel}`;
};

const projectNotifications = notifications.filter(n => n.project && n.project === component.key);

return (
<div className={className}>
<ModalButton
modal={({ onClose }) => (
<Modal contentLabel={header} onRequestClose={onClose}>
<header className="modal-head">
<h2>{header}</h2>
</header>
<div className="modal-body">
<Alert variant="info">{translate('notification.dispatcher.information')}</Alert>

<DeferredSpinner loading={loading}>
<table className="data zebra notifications-table">
<thead>
<tr>
<th aria-label={translate('project')} />
{channels.map(channel => (
<th className="text-center" key={channel}>
<h4>{translate('notification.channel', channel)}</h4>
</th>
))}
</tr>
</thead>

<NotificationsList
channels={channels}
checkboxId={getCheckboxId}
notifications={projectNotifications}
onAdd={handleAddNotification}
onRemove={handleRemoveNotification}
project={true}
types={perProjectTypes}
/>
</table>
</DeferredSpinner>
</div>
<footer className="modal-foot">
<ResetButtonLink className="js-modal-close" onClick={onClose}>
{translate('close')}
</ResetButtonLink>
</footer>
</Modal>
)}>
{({ onClick }) => (
<Button onClick={onClick}>
<span data-test="overview__edit-notifications">
{translate('my_profile.per_project_notifications.edit')}
</span>
</Button>
)}
</ModalButton>
</div>
);
}

export default withNotifications(ProjectNotifications);

+ 0
- 108
server/sonar-web/src/main/js/apps/overview/notifications/__tests__/__snapshots__/ProjectNotifications.tsx.snap View File

@@ -1,108 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should render correctly 1`] = `
<Modal
ariaHideApp={true}
bodyOpenClassName="ReactModal__Body--open"
className="modal"
closeTimeoutMS={0}
contentLabel="my_account.notifications"
isOpen={true}
onRequestClose={[MockFunction]}
overlayClassName="modal-overlay"
parentSelector={[Function]}
portalClassName="ReactModalPortal"
role="dialog"
shouldCloseOnEsc={true}
shouldCloseOnOverlayClick={true}
shouldFocusAfterRender={true}
shouldReturnFocusAfterClose={true}
>
<header
className="modal-head"
>
<h2>
my_account.notifications
</h2>
</header>
<div
className="modal-body"
>
<Alert
variant="info"
>
notification.dispatcher.information
</Alert>
<DeferredSpinner
loading={false}
timeout={100}
>
<table
className="data zebra notifications-table"
>
<thead>
<tr>
<th
aria-label="project"
/>
<th
className="text-center"
key="channel1"
>
<h4>
notification.channel.channel1
</h4>
</th>
<th
className="text-center"
key="channel2"
>
<h4>
notification.channel.channel2
</h4>
</th>
</tr>
</thead>
<NotificationsList
channels={
Array [
"channel1",
"channel2",
]
}
checkboxId={[Function]}
notifications={
Array [
Object {
"channel": "channel1",
"organization": "org",
"project": "foo",
"projectName": "Foo",
"type": "type-global",
},
]
}
onAdd={[Function]}
onRemove={[Function]}
project={true}
types={
Array [
"type-common",
]
}
/>
</table>
</DeferredSpinner>
</div>
<footer
className="modal-foot"
>
<ResetButtonLink
className="js-modal-close"
onClick={[MockFunction]}
>
close
</ResetButtonLink>
</footer>
</Modal>
`;

+ 0
- 67
server/sonar-web/src/main/js/apps/overview/styles.css View File

@@ -168,73 +168,6 @@
vertical-align: top;
}

/*
* Meta
TODO REMOVE ME!!
*/

.overview-meta {
background-color: var(--barBackgroundColor);
}

.overview-meta-card {
min-width: 200px;
box-sizing: border-box;
}

.overview-meta-card + .overview-meta-card {
margin-top: calc(2 * var(--gridSize));
padding-top: calc(2 * var(--gridSize) - 1px);
border-top: 1px solid var(--barBorderColor);
}

.overview-meta-description {
margin-top: calc(-0.5 * var(--gridSize));
line-height: 1.5;
color: var(--secondFontColor);
}

.overview-meta-header {
margin-bottom: calc(0.5 * var(--gridSize));
color: var(--baseFontColor);
}

.overview-meta-list > li {
/* 1px to not cut icons on the left */
padding-left: 1px;
padding-bottom: 4px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

.overview-meta-tags {
position: relative;
}

.overview-meta-size-lang-dist {
display: inline-block;
vertical-align: middle;
width: 160px;
min-height: 40px;
border-left: 1px solid var(--barBorderColor);
box-sizing: border-box;
}

.overview-key {
width: 100%;
background-color: transparent !important;
}

.overview-deleted-profile,
.overview-deprecated-rules {
margin: 4px -6px 4px;
padding: 3px 6px !important;
border: 1px solid var(--alertBorderError);
border-radius: 3px;
background-color: var(--alertBackgroundError);
}

/*
* Animations
*/

+ 1
- 1
server/sonar-web/src/main/js/apps/projectLinks/LinkRow.tsx View File

@@ -23,7 +23,7 @@ import ConfirmButton from 'sonar-ui-common/components/controls/ConfirmButton';
import ProjectLinkIcon from 'sonar-ui-common/components/icons/ProjectLinkIcon';
import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n';
import isValidUri from '../../app/utils/isValidUri';
import { getLinkName, isProvided } from './utils';
import { getLinkName, isProvided } from '../../helpers/projectLinks';

interface Props {
link: T.ProjectLink;

+ 1
- 1
server/sonar-web/src/main/js/apps/projectLinks/Table.tsx View File

@@ -19,8 +19,8 @@
*/
import * as React from 'react';
import { translate } from 'sonar-ui-common/helpers/l10n';
import { orderLinks } from '../../helpers/projectLinks';
import LinkRow from './LinkRow';
import { orderLinks } from './utils';

interface Props {
links: T.ProjectLink[];

server/sonar-web/src/main/js/apps/projectLinks/__tests__/utils-test.ts → server/sonar-web/src/main/js/helpers/__tests__/projectLinks-test.ts View File

@@ -17,11 +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 * as utils from '../utils';
import { getLinkName, isProvided, orderLinks } from '../projectLinks';

it('#isProvided', () => {
expect(utils.isProvided({ type: 'homepage' })).toBe(true);
expect(utils.isProvided({ type: 'custom' })).toBe(false);
expect(isProvided({ type: 'homepage' })).toBe(true);
expect(isProvided({ type: 'custom' })).toBe(false);
});

it('#orderLinks', () => {
@@ -29,12 +29,12 @@ it('#orderLinks', () => {
const issues = { type: 'issue' };
const foo = { name: 'foo', type: 'foo' };
const bar = { name: 'bar', type: 'bar' };
expect(utils.orderLinks([foo, homepage, issues, bar])).toEqual([homepage, issues, bar, foo]);
expect(utils.orderLinks([foo, bar])).toEqual([bar, foo]);
expect(utils.orderLinks([issues, homepage])).toEqual([homepage, issues]);
expect(orderLinks([foo, homepage, issues, bar])).toEqual([homepage, issues, bar, foo]);
expect(orderLinks([foo, bar])).toEqual([bar, foo]);
expect(orderLinks([issues, homepage])).toEqual([homepage, issues]);
});

it('#getLinkName', () => {
expect(utils.getLinkName({ type: 'homepage' })).toBe('project_links.homepage');
expect(utils.getLinkName({ name: 'foo', type: 'custom' })).toBe('foo');
expect(getLinkName({ type: 'homepage' })).toBe('project_links.homepage');
expect(getLinkName({ name: 'foo', type: 'custom' })).toBe('foo');
});

server/sonar-web/src/main/js/apps/projectLinks/utils.ts → server/sonar-web/src/main/js/helpers/projectLinks.ts View File


+ 22
- 11
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

@@ -1318,7 +1318,7 @@ project_quality_profile.successfully_updated={0} quality profile has been succes
#
#------------------------------------------------------------------------------
project_quality_gate.default_qgate=Default
project_quality_gate.successfully_updated=Quality gate has been successfully updated.
project_quality_gate.successfully_updated=Quality Gate has been successfully updated.

#------------------------------------------------------------------------------
#
@@ -1333,6 +1333,18 @@ projects_management.delete_selected_warning=You're about to delete {0} selected
projects_management.delete_all_warning=You're about to delete all {0} items.
projects_management.project_has_been_successfully_created=Project {project} has been successfully created.

#------------------------------------------------------------------------------
#
# PROJECT INFORMATION DRAWER
#
#------------------------------------------------------------------------------

project.info.title=Project information
application.info.title=Application information
project.info.description=Description
project.info.quality_gate=Quality Gate used
project.info.to_notifications=Set notifications
project.info.notifications=Set notifications

#------------------------------------------------------------------------------
#
@@ -2654,7 +2666,7 @@ overview.you_should_define_quality_gate=You should define a quality gate on this
overview.quality_gate.ignored_conditions=Some Quality Gate conditions on New Code were ignored because of the small number of New Lines
overview.quality_gate.ignored_conditions.tooltip=At the start of a new code period, if very few lines have been added or modified, it might be difficult to reach the desired level of code coverage or duplications. To prevent Quality Gate failure when there's little that can be done about it, Quality Gate conditions about duplications in new code and coverage on new code are ignored until the number of new lines is at least 20.
overview.quality_gate.conditions_on_new_code=Only conditions on new code that are defined in the Quality Gate are checked. See the {link} associated to the project for details.
overview.quality_profiles=Quality Profiles
overview.quality_profiles=Quality Profiles used
overview.new_code_period_x=New Code: {0}
overview.max_new_code_period_from_x=Max New Code from: {0}
overview.started_x=Started {0}
@@ -2724,11 +2736,10 @@ overview.complexity_tooltip.file={0} files have complexity around {1}
overview.deprecated_profile=This quality profile uses {0} deprecated rules and should be updated.
overview.deleted_profile={0} has been deleted since the last analysis.


overview.badges.get_badge.TRK=Get project badges
overview.badges.get_badge.VW=Get portfolio badges
overview.badges.get_badge.APP=Get application badges
overview.badges.title=Badges
overview.badges.title=Get project badges
overview.badges.description.TRK=Show the status of your project metrics on your README or website. Pick your style:
overview.badges.description.VW=Show the status of your portfolio metrics on your README or website. Pick your style:
overview.badges.description.APP=Show the status of your application metrics on your README or website. Pick your style:
@@ -2739,17 +2750,17 @@ overview.badges.options.colors.orange=Orange
overview.badges.options.formats.md=Markdown
overview.badges.options.formats.url=Image URL only
overview.badges.measure.alt=Standard badge
overview.badges.measure.description.TRK=This badge dynamically displays the current status of one metric of your project.
overview.badges.measure.description.VW=This badge dynamically displays the current status of one metric of your portfolio.
overview.badges.measure.description.APP=This badge dynamically displays the current status of one metric of your application.
overview.badges.measure.description.TRK=Displays the current status of one metric of your project.
overview.badges.measure.description.VW=Displays the current status of one metric of your portfolio.
overview.badges.measure.description.APP=Displays the current status of one metric of your application.
overview.badges.marketing.alt=Scanned on SonarCloud badge
overview.badges.marketing.description=This badge lets you advertise that you're using SonarCloud for code quality.
overview.badges.marketing.description.TRK=This badge lets you advertise that you're using SonarCloud for code quality.
overview.badges.quality_gate.alt=Quality Gate badge
overview.badges.quality_gate.description=This badge dynamically displays the current quality gate status of your project.
overview.badges.quality_gate.description.APP=This badge dynamically displays the current quality gate status of your application.
overview.badges.quality_gate.description.TRK=This badge dynamically displays the current quality gate status of your project.
overview.badges.quality_gate.description.VW=This badge dynamically displays the current quality gate status of your portfolio.
overview.badges.quality_gate.description=Displays the current quality gate status of your project.
overview.badges.quality_gate.description.APP=Displays the current quality gate status of your application.
overview.badges.quality_gate.description.TRK=Displays the current quality gate status of your project.
overview.badges.quality_gate.description.VW=Displays the current quality gate status of your portfolio.


#------------------------------------------------------------------------------

Loading…
Cancel
Save