Quellcode durchsuchen

SONAR-10963 Improve privacy badges of projects and organization

tags/7.5
Grégoire Aubert vor 6 Jahren
Ursprung
Commit
3bf2e0779d
57 geänderte Dateien mit 693 neuen und 234 gelöschten Zeilen
  1. 5
    0
      server/sonar-docs/src/tooltips/organizations/subscription-paid-plan.md
  2. 5
    0
      server/sonar-docs/src/tooltips/project/visibility-private.md
  3. 5
    0
      server/sonar-docs/src/tooltips/project/visibility-public-admin.md
  4. 5
    0
      server/sonar-docs/src/tooltips/project/visibility-public-paid-org-admin.md
  5. 5
    0
      server/sonar-docs/src/tooltips/project/visibility-public-paid-org.md
  6. 5
    0
      server/sonar-docs/src/tooltips/project/visibility-public.md
  7. 1
    1
      server/sonar-web/src/main/js/api/components.ts
  8. 3
    3
      server/sonar-web/src/main/js/api/permissions.ts
  9. 5
    4
      server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx
  10. 0
    4
      server/sonar-web/src/main/js/app/components/nav/component/ComponentNavHeader.tsx
  11. 2
    22
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavHeader-test.tsx
  12. 13
    1
      server/sonar-web/src/main/js/app/styles/components/badges.css
  13. 1
    1
      server/sonar-web/src/main/js/app/types.ts
  14. 18
    5
      server/sonar-web/src/main/js/apps/organizations/components/OrganizationPage.tsx
  15. 20
    24
      server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationPage-test.tsx
  16. 6
    0
      server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationPage-test.tsx.snap
  17. 14
    3
      server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigation.tsx
  18. 19
    3
      server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationMeta.tsx
  19. 2
    0
      server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigation-test.tsx
  20. 20
    6
      server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigationMeta-test.tsx
  21. 6
    0
      server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigation-test.tsx.snap
  22. 11
    2
      server/sonar-web/src/main/js/apps/overview/meta/MetaContainer.tsx
  23. 2
    1
      server/sonar-web/src/main/js/apps/permissions/project/components/AllHoldersList.js
  24. 8
    7
      server/sonar-web/src/main/js/apps/permissions/project/components/App.js
  25. 10
    0
      server/sonar-web/src/main/js/apps/portfolio/components/App.tsx
  26. 14
    8
      server/sonar-web/src/main/js/apps/projects/components/ProjectCardLeak.tsx
  27. 10
    4
      server/sonar-web/src/main/js/apps/projects/components/ProjectCardOverall.tsx
  28. 12
    9
      server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCardLeak-test.tsx
  29. 19
    10
      server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCardOverall-test.tsx
  30. 18
    1
      server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCardLeak-test.tsx.snap
  31. 18
    1
      server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCardOverall-test.tsx.snap
  32. 3
    1
      server/sonar-web/src/main/js/apps/projects/types.ts
  33. 2
    1
      server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/Risk-test.tsx
  34. 2
    1
      server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/SimpleBubbleChart-test.tsx
  35. 7
    7
      server/sonar-web/src/main/js/apps/projectsManagement/App.tsx
  36. 2
    2
      server/sonar-web/src/main/js/apps/projectsManagement/AppContainer.tsx
  37. 9
    7
      server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.tsx
  38. 2
    2
      server/sonar-web/src/main/js/apps/projectsManagement/__tests__/App-test.tsx
  39. 2
    2
      server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ChangeVisibilityForm-test.tsx
  40. 2
    2
      server/sonar-web/src/main/js/apps/projectsManagement/__tests__/CreateProjectForm-test.tsx
  41. 1
    1
      server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Projects-test.tsx
  42. 14
    2
      server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ProjectRow-test.tsx.snap
  43. 123
    0
      server/sonar-web/src/main/js/components/common/PrivacyBadgeContainer.tsx
  44. 69
    0
      server/sonar-web/src/main/js/components/common/__tests__/PrivacyBadgeContainer-test.tsx
  45. 0
    26
      server/sonar-web/src/main/js/components/common/__tests__/PrivateBadge-test.tsx
  46. 53
    0
      server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/PrivacyBadgeContainer-test.tsx.snap
  47. 0
    13
      server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/PrivateBadge-test.tsx.snap
  48. 18
    2
      server/sonar-web/src/main/js/components/docs/DocMarkdownBlock.tsx
  49. 7
    1
      server/sonar-web/src/main/js/components/docs/DocTooltip.tsx
  50. 35
    15
      server/sonar-web/src/main/js/components/docs/DocTooltipLink.tsx
  51. 12
    0
      server/sonar-web/src/main/js/components/docs/__tests__/DocMarkdownBlock-test.tsx
  52. 6
    0
      server/sonar-web/src/main/js/components/docs/__tests__/DocTooltipLink-test.tsx
  53. 9
    0
      server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocMarkdownBlock-test.tsx.snap
  54. 17
    13
      server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocTooltipLink-test.tsx.snap
  55. 8
    14
      server/sonar-web/src/main/js/components/icons-components/VisibleIcon.tsx
  56. 3
    1
      server/sonar-web/src/main/js/helpers/organizations.ts
  57. 5
    1
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 5
- 0
server/sonar-docs/src/tooltips/organizations/subscription-paid-plan.md Datei anzeigen

@@ -0,0 +1,5 @@
This organization is subscribed to a paid plan, allowing private projects. Its private projects, members, Quality Profiles and Quality Gates are visible to members only.

---

See also: [Organization and Project Privacy](/organizations/organization-and-project-privacy)

+ 5
- 0
server/sonar-docs/src/tooltips/project/visibility-private.md Datei anzeigen

@@ -0,0 +1,5 @@
This project is private. Only the members of this organization are able to browse it and its source code.

---

See also: [Organization and Project Privacy](/organizations/organization-and-project-privacy)

+ 5
- 0
server/sonar-docs/src/tooltips/project/visibility-public-admin.md Datei anzeigen

@@ -0,0 +1,5 @@
This project is public, which means anyone is able to browse its source code. Subscribe to a paid plan to get unlimited private projects in [Administration > Billing](/#sonarcloud#/organizations/#organization#/extension/billing/billing).

---

See also: [Pricing](/sonarcloud-pricing)

+ 5
- 0
server/sonar-docs/src/tooltips/project/visibility-public-paid-org-admin.md Datei anzeigen

@@ -0,0 +1,5 @@
This project is public, which means anyone is able to browse its source code. Go to your project's [Administration > Permissions](/#sonarcloud#/project_roles?id=#projectKey#) to make it private.

---

See also: [Organization and Project Privacy](/organizations/organization-and-project-privacy)

+ 5
- 0
server/sonar-docs/src/tooltips/project/visibility-public-paid-org.md Datei anzeigen

@@ -0,0 +1,5 @@
This project is public, which means anyone is able to browse its source code. Contact the project administrator to make it private.

---

See also: [Organization and Project Privacy](/organizations/organization-and-project-privacy)

+ 5
- 0
server/sonar-docs/src/tooltips/project/visibility-public.md Datei anzeigen

@@ -0,0 +1,5 @@
This project is public, which means anyone is able to browse its source code. Contact the organization administrator if you want to make it private.

---

See also: [Pricing](/sonarcloud-pricing)

+ 1
- 1
server/sonar-web/src/main/js/api/components.ts Datei anzeigen

@@ -162,7 +162,7 @@ export interface Component {
isFavorite?: boolean;
analysisDate?: string;
tags: string[];
visibility: string;
visibility: Visibility;
leakPeriodDate?: string;
}


+ 3
- 3
server/sonar-web/src/main/js/api/permissions.ts Datei anzeigen

@@ -18,7 +18,7 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { BaseSearchProjectsParameters } from './components';
import { PermissionTemplate } from '../app/types';
import { PermissionTemplate, Visibility } from '../app/types';
import throwGlobalError from '../app/utils/throwGlobalError';
import { getJSON, post, postJSON, RequestData } from '../helpers/request';

@@ -294,14 +294,14 @@ export function getPermissionTemplateGroups(

export function changeProjectVisibility(
project: string,
visibility: string
visibility: Visibility
): Promise<void | Response> {
return post('/api/projects/update_visibility', { project, visibility }).catch(throwGlobalError);
}

export function changeProjectDefaultVisibility(
organization: string,
projectVisibility: string
projectVisibility: Visibility
): Promise<void | Response> {
return post('/api/projects/update_default_visibility', { organization, projectVisibility }).catch(
throwGlobalError

+ 5
- 4
server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx Datei anzeigen

@@ -29,7 +29,8 @@ import {
MainBranch,
LongLivingBranch,
PullRequest,
BranchType
BranchType,
Visibility
} from '../../types';
import { STATUSES } from '../../../apps/background-tasks/constants';
import { waitAndUpdate } from '../../../helpers/testUtils';
@@ -80,12 +81,12 @@ it('changes component', () => {
(wrapper.instance() as ComponentContainer).mounted = true;
wrapper.setState({
branches: [{ isMain: true }],
component: { qualifier: 'TRK', visibility: 'public' },
component: { qualifier: 'TRK', visibility: Visibility.Public },
loading: false
});

(wrapper.find(Inner).prop('onComponentChange') as Function)({ visibility: 'private' });
expect(wrapper.state().component).toEqual({ qualifier: 'TRK', visibility: 'private' });
(wrapper.find(Inner).prop('onComponentChange') as Function)({ visibility: Visibility.Private });
expect(wrapper.state().component).toEqual({ qualifier: 'TRK', visibility: Visibility.Private });
});

it("loads branches for module's project", async () => {

+ 0
- 4
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavHeader.tsx Datei anzeigen

@@ -27,7 +27,6 @@ import { getOrganizationByKey, areThereCustomOrganizations } from '../../../../s
import OrganizationAvatar from '../../../../components/common/OrganizationAvatar';
import OrganizationHelmet from '../../../../components/common/OrganizationHelmet';
import OrganizationLink from '../../../../components/ui/OrganizationLink';
import PrivateBadge from '../../../../components/common/PrivateBadge';
import { collapsePath, limitComponentName } from '../../../../helpers/path';
import { getProjectUrl } from '../../../../helpers/urls';

@@ -67,9 +66,6 @@ export function ComponentNavHeader(props: Props) {
</>
)}
{renderBreadcrumbs(component.breadcrumbs)}
{component.visibility === 'private' && (
<PrivateBadge className="spacer-left" qualifier={component.qualifier} />
)}
{props.currentBranchLike && (
<ComponentNavBranch
branchLikes={props.branchLikes}

+ 2
- 22
server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavHeader-test.tsx Datei anzeigen

@@ -29,7 +29,7 @@ it('should not render breadcrumbs with one element', () => {
name: 'My Project',
organization: 'org',
qualifier: 'TRK',
visibility: 'public'
visibility: Visibility.Public
};
const result = shallow(
<ComponentNavHeader
@@ -49,7 +49,7 @@ it('should render organization', () => {
name: 'My Project',
organization: 'foo',
qualifier: 'TRK',
visibility: 'public'
visibility: Visibility.Public
};
const organization = {
key: 'foo',
@@ -67,23 +67,3 @@ it('should render organization', () => {
);
expect(result).toMatchSnapshot();
});

it('renders private badge', () => {
const component = {
breadcrumbs: [{ key: 'my-project', name: 'My Project', qualifier: 'TRK' }],
key: 'my-project',
name: 'My Project',
organization: 'org',
qualifier: 'TRK',
visibility: 'private'
};
const result = shallow(
<ComponentNavHeader
branchLikes={[]}
component={component}
currentBranchLike={undefined}
shouldOrganizationBeDisplayed={false}
/>
);
expect(result.find('PrivateBadge')).toHaveLength(1);
});

+ 13
- 1
server/sonar-web/src/main/js/app/styles/components/badges.css Datei anzeigen

@@ -148,6 +148,18 @@ a.badge-focus:active {

.outline-badge.active {
color: var(--baseFontColor);
border: 1px solid var(--blue);
border-color: var(--blue);
background-color: var(--lightBlue);
}

.outline-badge.badge-info {
border-color: var(--blue);
}

.outline-badge.badge-icon {
padding-left: calc(var(--gridSize) / 2);
}

.outline-badge.badge-icon svg {
height: calc(var(--smallControlHeight) - 2px);
}

+ 1
- 1
server/sonar-web/src/main/js/app/types.ts Datei anzeigen

@@ -67,7 +67,7 @@ export interface Component extends LightComponent {
qualityGate?: { isDefault?: boolean; key: string; name: string };
tags?: string[];
version?: string;
visibility?: string;
visibility?: Visibility;
}

interface ComponentConfiguration {

+ 18
- 5
server/sonar-web/src/main/js/apps/organizations/components/OrganizationPage.tsx Datei anzeigen

@@ -21,11 +21,15 @@ import * as React from 'react';
import Helmet from 'react-helmet';
import { connect } from 'react-redux';
import OrganizationNavigation from '../navigation/OrganizationNavigation';
import { fetchOrganization } from '../actions';
import NotFound from '../../../app/components/NotFound';
import Suggestions from '../../../app/components/embed-docs-modal/Suggestions';
import { fetchOrganization } from '../actions';
import { getOrganizationByKey } from '../../../store/rootReducer';
import { Organization } from '../../../app/types';
import { Organization, CurrentUser } from '../../../app/types';
import {
getOrganizationByKey,
getCurrentUser,
getMyOrganizations
} from '../../../store/rootReducer';

interface OwnProps {
children?: React.ReactNode;
@@ -34,7 +38,9 @@ interface OwnProps {
}

interface StateProps {
currentUser: CurrentUser;
organization?: Organization;
userOrganizations: Organization[];
}

interface DispatchToProps {
@@ -92,7 +98,12 @@ export class OrganizationPage extends React.PureComponent<Props, State> {
<div>
<Helmet defaultTitle={organization.name} titleTemplate={'%s - ' + organization.name} />
<Suggestions suggestions="organization_space" />
<OrganizationNavigation location={this.props.location} organization={organization} />
<OrganizationNavigation
currentUser={this.props.currentUser}
location={this.props.location}
organization={organization}
userOrganizations={this.props.userOrganizations}
/>
{this.props.children}
</div>
);
@@ -100,7 +111,9 @@ export class OrganizationPage extends React.PureComponent<Props, State> {
}

const mapStateToProps = (state: any, ownProps: OwnProps) => ({
organization: getOrganizationByKey(state, ownProps.params.organizationKey)
currentUser: getCurrentUser(state),
organization: getOrganizationByKey(state, ownProps.params.organizationKey),
userOrganizations: getMyOrganizations(state)
});

const mapDispatchToProps = { fetchOrganization: fetchOrganization as any };

+ 20
- 24
server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationPage-test.tsx Datei anzeigen

@@ -21,17 +21,14 @@ import * as React from 'react';
import { shallow } from 'enzyme';
import { OrganizationPage } from '../OrganizationPage';

const fetchOrganization = () => Promise.resolve();
const fetchOrganization = jest.fn().mockResolvedValue(undefined);

beforeEach(() => {
fetchOrganization.mockClear();
});

it('smoke test', () => {
const wrapper = shallow(
<OrganizationPage
fetchOrganization={fetchOrganization}
location={{ pathname: 'foo' }}
params={{ organizationKey: 'foo' }}>
<div>hello</div>
</OrganizationPage>
);
const wrapper = getWrapper();
expect(wrapper.type()).toBeNull();

const organization = { key: 'foo', name: 'Foo', isDefault: false, canAdmin: false };
@@ -40,29 +37,28 @@ it('smoke test', () => {
});

it('not found', () => {
const wrapper = shallow(
<OrganizationPage
fetchOrganization={fetchOrganization}
location={{ pathname: 'foo' }}
params={{ organizationKey: 'foo' }}>
<div>hello</div>
</OrganizationPage>
);
const wrapper = getWrapper();
wrapper.setState({ loading: false });
expect(wrapper).toMatchSnapshot();
});

it('should correctly update when the organization changes', () => {
const fetchOrganization = jest.fn(() => Promise.resolve());
const wrapper = shallow(
const wrapper = getWrapper();
wrapper.setProps({ params: { organizationKey: 'bar' } });
expect(fetchOrganization).toHaveBeenCalledTimes(2);
expect(fetchOrganization.mock.calls).toMatchSnapshot();
});

function getWrapper(props = {}) {
return shallow(
<OrganizationPage
currentUser={{ isLoggedIn: false }}
fetchOrganization={fetchOrganization}
location={{ pathname: 'foo' }}
params={{ organizationKey: 'foo' }}>
params={{ organizationKey: 'foo' }}
userOrganizations={[]}
{...props}>
<div>hello</div>
</OrganizationPage>
);
wrapper.setProps({ params: { organizationKey: 'bar' } });
expect(fetchOrganization).toHaveBeenCalledTimes(2);
expect(fetchOrganization.mock.calls).toMatchSnapshot();
});
}

+ 6
- 0
server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationPage-test.tsx.snap Datei anzeigen

@@ -25,6 +25,11 @@ exports[`smoke test 1`] = `
suggestions="organization_space"
/>
<OrganizationNavigation
currentUser={
Object {
"isLoggedIn": false,
}
}
location={
Object {
"pathname": "foo",
@@ -38,6 +43,7 @@ exports[`smoke test 1`] = `
"name": "Foo",
}
}
userOrganizations={Array []}
/>
<div>
hello

+ 14
- 3
server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigation.tsx Datei anzeigen

@@ -23,19 +23,30 @@ import OrganizationNavigationMeta from './OrganizationNavigationMeta';
import OrganizationNavigationMenuContainer from './OrganizationNavigationMenuContainer';
import * as theme from '../../../app/theme';
import ContextNavBar from '../../../components/nav/ContextNavBar';
import { Organization } from '../../../app/types';
import { Organization, CurrentUser } from '../../../app/types';

interface Props {
currentUser: CurrentUser;
location: { pathname: string };
organization: Organization;
userOrganizations: Organization[];
}

export default function OrganizationNavigation({ location, organization }: Props) {
export default function OrganizationNavigation({
currentUser,
location,
organization,
userOrganizations
}: Props) {
return (
<ContextNavBar height={theme.contextNavHeightRaw} id="context-navigation">
<div className="navbar-context-justified">
<OrganizationNavigationHeaderContainer organization={organization} />
<OrganizationNavigationMeta organization={organization} />
<OrganizationNavigationMeta
currentUser={currentUser}
organization={organization}
userOrganizations={userOrganizations}
/>
</div>
<OrganizationNavigationMenuContainer location={location} organization={organization} />
</ContextNavBar>

+ 19
- 3
server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationMeta.tsx Datei anzeigen

@@ -18,16 +18,25 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import { Organization, HomePageType } from '../../../app/types';
import HomePageSelect from '../../../components/controls/HomePageSelect';
import DocTooltip from '../../../components/docs/DocTooltip';
import { translate } from '../../../helpers/l10n';
import { isSonarCloud } from '../../../helpers/system';
import { hasPrivateAccess, isPaidOrganization } from '../../../helpers/organizations';
import { CurrentUser, HomePageType, Organization } from '../../../app/types';

interface Props {
currentUser: CurrentUser;
organization: Organization;
userOrganizations: Organization[];
}

export default function OrganizationNavigationMeta({ organization }: Props) {
export default function OrganizationNavigationMeta({
currentUser,
organization,
userOrganizations
}: Props) {
const onSonarCloud = isSonarCloud();
return (
<div className="navbar-context-meta">
{organization.url != null && (
@@ -39,10 +48,17 @@ export default function OrganizationNavigationMeta({ organization }: Props) {
{organization.url}
</a>
)}
{onSonarCloud &&
isPaidOrganization(organization) &&
hasPrivateAccess(currentUser, organization, userOrganizations) && (
<DocTooltip className="spacer-right" doc="organizations/subscription-paid-plan">
<div className="outline-badge">{translate('organization.paid_plan.badge')}</div>
</DocTooltip>
)}
<div className="text-muted">
<strong>{translate('organization.key')}:</strong> {organization.key}
</div>
{isSonarCloud() && (
{onSonarCloud && (
<div className="navbar-context-meta-secondary">
<HomePageSelect
currentPage={{ type: HomePageType.Organization, organization: organization.key }}

+ 2
- 0
server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigation-test.tsx Datei anzeigen

@@ -26,12 +26,14 @@ it('render', () => {
expect(
shallow(
<OrganizationNavigation
currentUser={{ isLoggedIn: false }}
location={{ pathname: '/organizations/foo' }}
organization={{
key: 'foo',
name: 'Foo',
projectVisibility: Visibility.Public
}}
userOrganizations={[]}
/>
)
).toMatchSnapshot();

+ 20
- 6
server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigationMeta-test.tsx Datei anzeigen

@@ -20,20 +20,34 @@
import * as React from 'react';
import { shallow } from 'enzyme';
import OrganizationNavigationMeta from '../OrganizationNavigationMeta';
import { Visibility } from '../../../../app/types';
import { OrganizationSubscription } from '../../../../app/types';

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

const organization = { key: 'foo', name: 'Foo', subscription: OrganizationSubscription.Free };

it('renders', () => {
expect(
shallow(
<OrganizationNavigationMeta
organization={{
key: 'foo',
name: 'Foo',
projectVisibility: Visibility.Public
}}
currentUser={{ isLoggedIn: false }}
organization={organization}
userOrganizations={[]}
/>
)
).toMatchSnapshot();
});

it('renders with private badge', () => {
expect(
shallow(
<OrganizationNavigationMeta
currentUser={{ isLoggedIn: true }}
organization={{ ...organization, subscription: OrganizationSubscription.Paid }}
userOrganizations={[organization]}
/>
)
.find('DocTooltip')
.exists()
).toBeTruthy();
});

+ 6
- 0
server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigation-test.tsx.snap Datei anzeigen

@@ -18,6 +18,11 @@ exports[`render 1`] = `
}
/>
<OrganizationNavigationMeta
currentUser={
Object {
"isLoggedIn": false,
}
}
organization={
Object {
"key": "foo",
@@ -25,6 +30,7 @@ exports[`render 1`] = `
"projectVisibility": "public",
}
}
userOrganizations={Array []}
/>
</div>
<Connect(OrganizationNavigationMenu)

+ 11
- 2
server/sonar-web/src/main/js/apps/overview/meta/MetaContainer.tsx Datei anzeigen

@@ -46,6 +46,7 @@ import {
getMyOrganizations,
getOrganizationByKey
} from '../../../store/rootReducer';
import PrivacyBadgeContainer from '../../../components/common/PrivacyBadgeContainer';

interface StateToProps {
currentUser: CurrentUser;
@@ -105,18 +106,26 @@ export class Meta extends React.PureComponent<Props> {

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

const isProject = qualifier === 'TRK';
const isApp = qualifier === 'APP';
const isPrivate = visibility === Visibility.Private;

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 && (

+ 2
- 1
server/sonar-web/src/main/js/apps/permissions/project/components/AllHoldersList.js Datei anzeigen

@@ -24,6 +24,7 @@ import SearchForm from '../../shared/components/SearchForm';
import HoldersList from '../../shared/components/HoldersList';
import { translate } from '../../../../helpers/l10n';
import { PERMISSIONS_ORDER_BY_QUALIFIER } from '../constants';
import { Visibility } from '../../../../app/types';

/*::
type Props = {|
@@ -89,7 +90,7 @@ export default class AllHoldersList extends React.PureComponent {

render() {
let order = PERMISSIONS_ORDER_BY_QUALIFIER[this.props.component.qualifier];
if (this.props.visibility === 'public') {
if (this.props.visibility === Visibility.Public) {
order = without(order, 'user', 'codeviewer');
}


+ 8
- 7
server/sonar-web/src/main/js/apps/permissions/project/components/App.js Datei anzeigen

@@ -30,6 +30,7 @@ import PageError from '../../shared/components/PageError';
import * as api from '../../../../api/permissions';
import { translate } from '../../../../helpers/l10n';
import '../../styles.css';
import { Visibility } from '../../../../app/types';

/*::
export type Props = {|
@@ -284,7 +285,7 @@ export default class App extends React.PureComponent {
};

handleVisibilityChange = (visibility /*: string */) => {
if (visibility === 'public') {
if (visibility === Visibility.Public) {
this.openDisclaimer();
} else {
this.turnProjectToPrivate();
@@ -292,25 +293,25 @@ export default class App extends React.PureComponent {
};

turnProjectToPublic = () => {
this.props.onComponentChange({ visibility: 'public' });
api.changeProjectVisibility(this.props.component.key, 'public').then(
this.props.onComponentChange({ visibility: Visibility.Public });
api.changeProjectVisibility(this.props.component.key, Visibility.Public).then(
() => {
this.loadHolders();
},
error => {
this.props.onComponentChange({ visibility: 'private' });
this.props.onComponentChange({ visibility: Visibility.Private });
}
);
};

turnProjectToPrivate = () => {
this.props.onComponentChange({ visibility: 'private' });
api.changeProjectVisibility(this.props.component.key, 'private').then(
this.props.onComponentChange({ visibility: Visibility.Private });
api.changeProjectVisibility(this.props.component.key, Visibility.Private).then(
() => {
this.loadHolders();
},
error => {
this.props.onComponentChange({ visibility: 'public' });
this.props.onComponentChange({ visibility: Visibility.Public });
}
);
};

+ 10
- 0
server/sonar-web/src/main/js/apps/portfolio/components/App.tsx Datei anzeigen

@@ -36,6 +36,7 @@ import { fetchMetrics } from '../../../store/rootActions';
import { getMetrics } from '../../../store/rootReducer';
import { Metric, Component } from '../../../app/types';
import '../styles.css';
import PrivacyBadgeContainer from '../../../components/common/PrivacyBadgeContainer';

interface OwnProps {
component: Component;
@@ -190,6 +191,15 @@ export class App extends React.PureComponent<Props, State> {
<div className="portfolio-meta-card">
<h4 className="portfolio-meta-header">
{translate('overview.about_this_portfolio')}
{component.visibility && (
<PrivacyBadgeContainer
className="spacer-left pull-right"
organization={component.organization}
qualifier={component.qualifier}
tooltipProps={{ projectKey: component.key }}
visibility={component.visibility}
/>
)}
</h4>
<Summary component={component} measures={measures || {}} />
</div>

+ 14
- 8
server/sonar-web/src/main/js/apps/projects/components/ProjectCardLeak.tsx Datei anzeigen

@@ -26,20 +26,19 @@ import Favorite from '../../../components/controls/Favorite';
import DateFromNow from '../../../components/intl/DateFromNow';
import DateTimeFormatter from '../../../components/intl/DateTimeFormatter';
import TagsList from '../../../components/tags/TagsList';
import PrivateBadge from '../../../components/common/PrivateBadge';
import PrivacyBadgeContainer from '../../../components/common/PrivacyBadgeContainer';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { Project } from '../types';
import { Organization } from '../../../app/types';

interface Props {
height: number;
organization?: { key: string };
organization: Organization | undefined;
project: Project;
}

export default function ProjectCardLeak({ height, organization, project }: Props) {
const { measures } = project;

const isPrivate = project.visibility === 'private';
const hasTags = project.tags.length > 0;

return (
@@ -60,23 +59,30 @@ export default function ProjectCardLeak({ height, organization, project }: Props
)}
<Link to={{ pathname: '/dashboard', query: { id: project.key } }}>{project.name}</Link>
</h2>
{project.analysisDate && <ProjectCardQualityGate status={measures!['alert_status']} />}
{project.analysisDate && <ProjectCardQualityGate status={measures['alert_status']} />}
<div className="project-card-header-right">
{isPrivate && <PrivateBadge className="spacer-left" qualifier="TRK" />}
<PrivacyBadgeContainer
className="spacer-left"
organization={organization || project.organization}
qualifier="TRK"
tooltipProps={{ projectKey: project.key }}
visibility={project.visibility}
/>

{hasTags && <TagsList className="spacer-left note" tags={project.tags} />}
</div>
</div>
{project.analysisDate &&
project.leakPeriodDate && (
<div className="project-card-dates note text-right pull-right">
<DateFromNow date={project.leakPeriodDate!}>
<DateFromNow date={project.leakPeriodDate}>
{fromNow => (
<span className="project-card-leak-date pull-right">
{translateWithParameters('projects.leak_period_x', fromNow)}
</span>
)}
</DateFromNow>
<DateTimeFormatter date={project.analysisDate!}>
<DateTimeFormatter date={project.analysisDate}>
{formattedDate => (
<span>
{translateWithParameters('projects.last_analysis_on_x', formattedDate)}

+ 10
- 4
server/sonar-web/src/main/js/apps/projects/components/ProjectCardOverall.tsx Datei anzeigen

@@ -25,20 +25,20 @@ import ProjectCardOrganizationContainer from './ProjectCardOrganizationContainer
import Favorite from '../../../components/controls/Favorite';
import DateTimeFormatter from '../../../components/intl/DateTimeFormatter';
import TagsList from '../../../components/tags/TagsList';
import PrivateBadge from '../../../components/common/PrivateBadge';
import PrivacyBadgeContainer from '../../../components/common/PrivacyBadgeContainer';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { Project } from '../types';
import { Organization } from '../../../app/types';

interface Props {
height: number;
organization?: { key: string };
organization: Organization | undefined;
project: Project;
}

export default function ProjectCardOverall({ height, organization, project }: Props) {
const { measures } = project;

const isPrivate = project.visibility === 'private';
const hasTags = project.tags.length > 0;

return (
@@ -61,7 +61,13 @@ export default function ProjectCardOverall({ height, organization, project }: Pr
</h2>
{project.analysisDate && <ProjectCardQualityGate status={measures['alert_status']} />}
<div className="project-card-header-right">
{isPrivate && <PrivateBadge className="spacer-left" qualifier="TRK" />}
<PrivacyBadgeContainer
className="spacer-left"
organization={organization || project.organization}
qualifier="TRK"
tooltipProps={{ projectKey: project.key }}
visibility={project.visibility}
/>
{hasTags && <TagsList className="spacer-left note" tags={project.tags} />}
</div>
</div>

+ 12
- 9
server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCardLeak-test.tsx Datei anzeigen

@@ -20,6 +20,7 @@
import * as React from 'react';
import { shallow } from 'enzyme';
import ProjectCardLeak from '../ProjectCardLeak';
import { Visibility } from '../../../../app/types';

const MEASURES = {
alert_status: 'OK',
@@ -36,11 +37,11 @@ const PROJECT = {
name: 'Foo',
organization: { key: 'org', name: 'org' },
tags: [],
visibility: 'public'
visibility: Visibility.Public
};

it('should display analysis date and leak start date', () => {
const card = shallow(<ProjectCardLeak height={100} project={PROJECT} />);
const card = shallow(<ProjectCardLeak height={100} organization={undefined} project={PROJECT} />);
expect(card.find('.project-card-dates').exists()).toBeTruthy();
expect(card.find('.project-card-dates').find('DateFromNow')).toHaveLength(1);
expect(card.find('.project-card-dates').find('DateTimeFormatter')).toHaveLength(1);
@@ -48,28 +49,30 @@ it('should display analysis date and leak start date', () => {

it('should not display analysis date or leak start date', () => {
const project = { ...PROJECT, analysisDate: undefined };
const card = shallow(<ProjectCardLeak height={100} project={project} />);
const card = shallow(<ProjectCardLeak height={100} organization={undefined} project={project} />);
expect(card.find('.project-card-dates').exists()).toBeFalsy();
});

it('should display tags', () => {
const project = { ...PROJECT, tags: ['foo', 'bar'] };
expect(
shallow(<ProjectCardLeak height={100} project={project} />)
shallow(<ProjectCardLeak height={100} organization={undefined} project={project} />)
.find('TagsList')
.exists()
).toBeTruthy();
});

it('should private badge', () => {
const project = { ...PROJECT, visibility: 'private' };
it('should display private badge', () => {
const project = { ...PROJECT, visibility: Visibility.Private };
expect(
shallow(<ProjectCardLeak height={100} project={project} />)
.find('PrivateBadge')
shallow(<ProjectCardLeak height={100} organization={undefined} project={project} />)
.find('Connect(PrivacyBadge)')
.exists()
).toBeTruthy();
});

it('should display the leak measures and quality gate', () => {
expect(shallow(<ProjectCardLeak height={100} project={PROJECT} />)).toMatchSnapshot();
expect(
shallow(<ProjectCardLeak height={100} organization={undefined} project={PROJECT} />)
).toMatchSnapshot();
});

+ 19
- 10
server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCardOverall-test.tsx Datei anzeigen

@@ -20,6 +20,7 @@
import * as React from 'react';
import { shallow } from 'enzyme';
import ProjectCardOverall from '../ProjectCardOverall';
import { Visibility } from '../../../../app/types';

const MEASURES = {
alert_status: 'OK',
@@ -35,17 +36,23 @@ const PROJECT = {
name: 'Foo',
organization: { key: 'org', name: 'org' },
tags: [],
visibility: 'public'
visibility: Visibility.Public
};

it('should display analysis date (and not leak period) when defined', () => {
expect(
shallow(<ProjectCardOverall height={100} project={PROJECT} />)
shallow(<ProjectCardOverall height={100} organization={undefined} project={PROJECT} />)
.find('.project-card-dates')
.exists()
).toBeTruthy();
expect(
shallow(<ProjectCardOverall height={100} project={{ ...PROJECT, analysisDate: undefined }} />)
shallow(
<ProjectCardOverall
height={100}
organization={undefined}
project={{ ...PROJECT, analysisDate: undefined }}
/>
)
.find('.project-card-dates')
.exists()
).toBeFalsy();
@@ -54,7 +61,7 @@ it('should display analysis date (and not leak period) when defined', () => {
it('should not display the quality gate', () => {
const project = { ...PROJECT, analysisDate: undefined };
expect(
shallow(<ProjectCardOverall height={100} project={project} />)
shallow(<ProjectCardOverall height={100} organization={undefined} project={project} />)
.find('ProjectCardOverallQualityGate')
.exists()
).toBeFalsy();
@@ -63,21 +70,23 @@ it('should not display the quality gate', () => {
it('should display tags', () => {
const project = { ...PROJECT, tags: ['foo', 'bar'] };
expect(
shallow(<ProjectCardOverall height={100} project={project} />)
shallow(<ProjectCardOverall height={100} organization={undefined} project={project} />)
.find('TagsList')
.exists()
).toBeTruthy();
});

it('should private badge', () => {
const project = { ...PROJECT, visibility: 'private' };
it('should display private badge', () => {
const project = { ...PROJECT, visibility: Visibility.Private };
expect(
shallow(<ProjectCardOverall height={100} project={project} />)
.find('PrivateBadge')
shallow(<ProjectCardOverall height={100} organization={undefined} project={project} />)
.find('Connect(PrivacyBadge)')
.exists()
).toBeTruthy();
});

it('should display the overall measures and quality gate', () => {
expect(shallow(<ProjectCardOverall height={100} project={PROJECT} />)).toMatchSnapshot();
expect(
shallow(<ProjectCardOverall height={100} organization={undefined} project={PROJECT} />)
).toMatchSnapshot();
});

+ 18
- 1
server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCardLeak-test.tsx.snap Datei anzeigen

@@ -47,7 +47,24 @@ exports[`should display the leak measures and quality gate 1`] = `
/>
<div
className="project-card-header-right"
/>
>
<Connect(PrivacyBadge)
className="spacer-left"
organization={
Object {
"key": "org",
"name": "org",
}
}
qualifier="TRK"
tooltipProps={
Object {
"projectKey": "foo",
}
}
visibility="public"
/>
</div>
</div>
<div
className="project-card-dates note text-right pull-right"

+ 18
- 1
server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCardOverall-test.tsx.snap Datei anzeigen

@@ -47,7 +47,24 @@ exports[`should display the overall measures and quality gate 1`] = `
/>
<div
className="project-card-header-right"
/>
>
<Connect(PrivacyBadge)
className="spacer-left"
organization={
Object {
"key": "org",
"name": "org",
}
}
qualifier="TRK"
tooltipProps={
Object {
"projectKey": "foo",
}
}
visibility="public"
/>
</div>
</div>
<div
className="project-card-dates note text-right"

+ 3
- 1
server/sonar-web/src/main/js/apps/projects/types.ts Datei anzeigen

@@ -17,6 +17,8 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { Visibility } from '../../app/types';

export interface Project {
analysisDate?: string;
isFavorite?: boolean;
@@ -26,7 +28,7 @@ export interface Project {
name: string;
organization?: { key: string; name: string };
tags: string[];
visibility: string;
visibility: Visibility;
}

export interface Facet {

+ 2
- 1
server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/Risk-test.tsx Datei anzeigen

@@ -20,6 +20,7 @@
import * as React from 'react';
import { shallow } from 'enzyme';
import Risk from '../Risk';
import { Visibility } from '../../../../app/types';

it('renders', () => {
const project1 = {
@@ -27,7 +28,7 @@ it('renders', () => {
measures: { complexity: '17.2', coverage: '53.5', ncloc: '1734' },
name: 'Foo',
tags: [],
visibility: 'public'
visibility: Visibility.Public
};
expect(
shallow(<Risk displayOrganizations={false} helpText="foobar" projects={[project1]} />)

+ 2
- 1
server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/SimpleBubbleChart-test.tsx Datei anzeigen

@@ -20,6 +20,7 @@
import * as React from 'react';
import { shallow } from 'enzyme';
import SimpleBubbleChart from '../SimpleBubbleChart';
import { Visibility } from '../../../../app/types';

it('renders', () => {
const project1 = {
@@ -28,7 +29,7 @@ it('renders', () => {
measures: { complexity: '17.2', coverage: '53.5', ncloc: '1734', security_rating: '2' },
name: 'Foo',
tags: [],
visibility: 'public'
visibility: Visibility.Public
};
expect(
shallow(

+ 7
- 7
server/sonar-web/src/main/js/apps/projectsManagement/App.tsx Datei anzeigen

@@ -35,7 +35,7 @@ import { translate } from '../../helpers/l10n';
export interface Props {
currentUser: { login: string };
hasProvisionPermission?: boolean;
onVisibilityChange: (visibility: string) => void;
onVisibilityChange: (visibility: Visibility) => void;
organization: Organization;
topLevelQualifiers: string[];
}
@@ -215,19 +215,19 @@ export default class App extends React.PureComponent<Props, State> {

<Projects
currentUser={this.props.currentUser}
ready={this.state.ready}
projects={this.state.projects}
selection={this.state.selection}
onProjectSelected={this.onProjectSelected}
onProjectDeselected={this.onProjectDeselected}
onProjectSelected={this.onProjectSelected}
organization={this.props.organization}
projects={this.state.projects}
ready={this.state.ready}
selection={this.state.selection}
/>

<ListFooter
ready={this.state.ready}
count={this.state.projects.length}
total={this.state.total}
loadMore={this.loadMore}
ready={this.state.ready}
total={this.state.total}
/>

{this.state.createProjectForm && (

+ 2
- 2
server/sonar-web/src/main/js/apps/projectsManagement/AppContainer.tsx Datei anzeigen

@@ -35,7 +35,7 @@ interface StateProps {

interface DispatchProps {
fetchOrganization: (organization: string) => void;
onVisibilityChange: (organization: Organization, visibility: string) => void;
onVisibilityChange: (organization: Organization, visibility: Visibility) => void;
}

interface OwnProps {
@@ -51,7 +51,7 @@ class AppContainer extends React.PureComponent<OwnProps & StateProps & DispatchP
}
}

handleVisibilityChange = (visibility: string) => {
handleVisibilityChange = (visibility: Visibility) => {
if (this.props.organization) {
this.props.onVisibilityChange(this.props.organization, visibility);
}

+ 9
- 7
server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.tsx Datei anzeigen

@@ -21,8 +21,7 @@ import * as React from 'react';
import { Link } from 'react-router';
import ProjectRowActions from './ProjectRowActions';
import { Project } from './utils';
import { Visibility } from '../../app/types';
import PrivateBadge from '../../components/common/PrivateBadge';
import PrivacyBadgeContainer from '../../components/common/PrivacyBadgeContainer';
import Checkbox from '../../components/controls/Checkbox';
import QualifierIcon from '../../components/icons-components/QualifierIcon';
import DateTooltipFormatter from '../../components/intl/DateTooltipFormatter';
@@ -41,7 +40,7 @@ export default class ProjectRow extends React.PureComponent<Props> {
};

render() {
const { project, selected } = this.props;
const { organization, project, selected } = this.props;

return (
<tr>
@@ -58,9 +57,12 @@ export default class ProjectRow extends React.PureComponent<Props> {
</td>

<td className="thin nowrap">
{project.visibility === Visibility.Private && (
<PrivateBadge qualifier={project.qualifier} />
)}
<PrivacyBadgeContainer
organization={organization}
qualifier={project.qualifier}
tooltipProps={{ projectKey: project.key }}
visibility={project.visibility}
/>
</td>

<td className="nowrap">
@@ -78,7 +80,7 @@ export default class ProjectRow extends React.PureComponent<Props> {
<td className="thin nowrap">
<ProjectRowActions
currentUser={this.props.currentUser}
organization={this.props.organization}
organization={organization}
project={project}
/>
</td>

+ 2
- 2
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/App-test.tsx Datei anzeigen

@@ -129,8 +129,8 @@ it('creates project', () => {
it('changes default project visibility', () => {
const onVisibilityChange = jest.fn();
const wrapper = shallowRender({ onVisibilityChange });
wrapper.find('Header').prop<Function>('onVisibilityChange')('private');
expect(onVisibilityChange).toBeCalledWith('private');
wrapper.find('Header').prop<Function>('onVisibilityChange')(Visibility.Private);
expect(onVisibilityChange).toBeCalledWith(Visibility.Private);
});

function shallowRender(props?: { [P in keyof Props]?: Props[P] }) {

+ 2
- 2
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ChangeVisibilityForm-test.tsx Datei anzeigen

@@ -53,13 +53,13 @@ it('changes visibility', () => {
click(wrapper.find('a[data-visibility="private"]'), {
currentTarget: {
blur() {},
dataset: { visibility: 'private' }
dataset: { visibility: Visibility.Private }
}
});
expect(wrapper).toMatchSnapshot();

click(wrapper.find('.js-confirm'));
expect(onConfirm).toBeCalledWith('private');
expect(onConfirm).toBeCalledWith(Visibility.Private);
});

function shallowRender(props?: { [P in keyof Props]?: Props[P] }) {

+ 2
- 2
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/CreateProjectForm-test.tsx Datei anzeigen

@@ -51,7 +51,7 @@ it('creates project', async () => {
change(wrapper.find('input[name="key"]'), 'key', {
currentTarget: { name: 'key', value: 'key' }
});
wrapper.find('VisibilitySelector').prop<Function>('onChange')('private');
wrapper.find('VisibilitySelector').prop<Function>('onChange')(Visibility.Private);
wrapper.update();
expect(wrapper).toMatchSnapshot();

@@ -60,7 +60,7 @@ it('creates project', async () => {
name: 'name',
organization: 'org',
project: 'key',
visibility: 'private'
visibility: Visibility.Private
});
expect(wrapper).toMatchSnapshot();


+ 1
- 1
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Projects-test.tsx Datei anzeigen

@@ -22,7 +22,7 @@ import { shallow } from 'enzyme';
import Projects from '../Projects';
import { Visibility } from '../../../app/types';

const organization = { key: 'org', name: 'org', projectVisibility: 'public' };
const organization = { key: 'org', name: 'org', projectVisibility: Visibility.Public };
const projects = [
{ key: 'a', name: 'A', qualifier: 'TRK', visibility: Visibility.Public },
{ key: 'b', name: 'B', qualifier: 'TRK', visibility: Visibility.Public }

+ 14
- 2
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ProjectRow-test.tsx.snap Datei anzeigen

@@ -39,8 +39,14 @@ exports[`renders 1`] = `
<td
className="thin nowrap"
>
<PrivateBadge
<Connect(PrivacyBadge)
qualifier="TRK"
tooltipProps={
Object {
"projectKey": "project",
}
}
visibility="private"
/>
</td>
<td
@@ -122,8 +128,14 @@ exports[`renders 2`] = `
<td
className="thin nowrap"
>
<PrivateBadge
<Connect(PrivacyBadge)
qualifier="TRK"
tooltipProps={
Object {
"projectKey": "project",
}
}
visibility="private"
/>
</td>
<td

+ 123
- 0
server/sonar-web/src/main/js/components/common/PrivacyBadgeContainer.tsx Datei anzeigen

@@ -0,0 +1,123 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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 * as classNames from 'classnames';
import { connect } from 'react-redux';
import * as theme from '../../app/theme';
import Tooltip from '../controls/Tooltip';
import { translate } from '../../helpers/l10n';
import { Visibility, Organization, CurrentUser } from '../../app/types';
import { isSonarCloud } from '../../helpers/system';
import { isCurrentUserMemberOf, isPaidOrganization } from '../../helpers/organizations';
import { getCurrentUser, getOrganizationByKey, getMyOrganizations } from '../../store/rootReducer';
import VisibleIcon from '../icons-components/VisibleIcon';
import DocTooltip from '../docs/DocTooltip';

interface StateToProps {
currentUser: CurrentUser;
organization?: Organization;
userOrganizations: Organization[];
}

interface OwnProps {
className?: string;
organization: Organization | string | undefined;
qualifier: string;
tooltipProps?: { projectKey: string };
visibility: Visibility;
}

interface Props extends OwnProps, StateToProps {
organization: Organization | undefined;
}

export function PrivacyBadge({
className,
currentUser,
organization,
qualifier,
userOrganizations,
tooltipProps,
visibility
}: Props) {
const onSonarCloud = isSonarCloud();
if (
visibility !== Visibility.Private &&
(!onSonarCloud || !isCurrentUserMemberOf(currentUser, organization, userOrganizations))
) {
return null;
}

let icon = null;
if (isPaidOrganization(organization) && visibility === Visibility.Public) {
icon = <VisibleIcon className="little-spacer-right" fill={theme.blue} />;
}

const badge = (
<div
className={classNames('outline-badge', className, {
'badge-info': Boolean(icon),
'badge-icon': Boolean(icon)
})}>
{icon}
{translate('visibility', visibility)}
</div>
);

if (onSonarCloud && organization) {
let docUrl = `project/visibility-${visibility}`;
if (visibility === Visibility.Public) {
if (icon) {
docUrl += '-paid-org';
}
if (organization.canAdmin) {
docUrl += '-admin';
}
}

return (
<DocTooltip
className={className}
doc={docUrl}
overlayProps={{ ...tooltipProps, organization: organization.key }}>
{badge}
</DocTooltip>
);
}

return (
<Tooltip overlay={translate('visibility', visibility, 'description', qualifier)}>
{badge}
</Tooltip>
);
}

const mapStateToProps = (state: any, { organization }: OwnProps) => {
if (typeof organization === 'string') {
organization = getOrganizationByKey(state, organization);
}
return {
currentUser: getCurrentUser(state),
organization,
userOrganizations: getMyOrganizations(state)
};
};

export default connect<StateToProps, {}, OwnProps>(mapStateToProps)(PrivacyBadge);

+ 69
- 0
server/sonar-web/src/main/js/components/common/__tests__/PrivacyBadgeContainer-test.tsx Datei anzeigen

@@ -0,0 +1,69 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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 { shallow } from 'enzyme';
import { PrivacyBadge } from '../PrivacyBadgeContainer';
import { Visibility, OrganizationSubscription } from '../../../app/types';
import { isSonarCloud } from '../../../helpers/system';

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

const organization = { key: 'foo', name: 'Foo' };
const loggedInUser = { isLoggedIn: true, login: 'luke', name: 'Skywalker' };

it('renders', () => {
expect(getWrapper()).toMatchSnapshot();
});

it('do not render', () => {
expect(getWrapper({ visibility: Visibility.Public })).toMatchSnapshot();
});

it('renders public', () => {
(isSonarCloud as jest.Mock<any>).mockReturnValueOnce(true);
expect(getWrapper({ visibility: Visibility.Public })).toMatchSnapshot();
});

it('renders public with icon', () => {
(isSonarCloud as jest.Mock<any>).mockReturnValueOnce(true);
expect(
getWrapper({
organization: {
...organization,
canAdmin: true,
subscription: OrganizationSubscription.Paid
},
visibility: Visibility.Public
})
).toMatchSnapshot();
});

function getWrapper(props = {}) {
return shallow(
<PrivacyBadge
currentUser={loggedInUser}
organization={organization}
qualifier="TRK"
userOrganizations={[organization]}
visibility={Visibility.Private}
{...props}
/>
);
}

+ 0
- 26
server/sonar-web/src/main/js/components/common/__tests__/PrivateBadge-test.tsx Datei anzeigen

@@ -1,26 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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 { shallow } from 'enzyme';
import PrivateBadge from '../PrivateBadge';

it('renders', () => {
expect(shallow(<PrivateBadge qualifier="TRK" />)).toMatchSnapshot();
});

+ 53
- 0
server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/PrivacyBadgeContainer-test.tsx.snap Datei anzeigen

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

exports[`do not render 1`] = `""`;

exports[`renders 1`] = `
<Tooltip
overlay="visibility.private.description.TRK"
>
<div
className="outline-badge"
>
visibility.private
</div>
</Tooltip>
`;

exports[`renders public 1`] = `
<DocTooltip
doc="project/visibility-public"
overlayProps={
Object {
"organization": "foo",
}
}
>
<div
className="outline-badge"
>
visibility.public
</div>
</DocTooltip>
`;

exports[`renders public with icon 1`] = `
<DocTooltip
doc="project/visibility-public-paid-org-admin"
overlayProps={
Object {
"organization": "foo",
}
}
>
<div
className="outline-badge badge-info badge-icon"
>
<VisibleIcon
className="little-spacer-right"
fill="#4b9fd5"
/>
visibility.public
</div>
</DocTooltip>
`;

+ 0
- 13
server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/PrivateBadge-test.tsx.snap Datei anzeigen

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

exports[`renders 1`] = `
<Tooltip
overlay="visibility.private.description.TRK"
>
<div
className="outline-badge"
>
visibility.private
</div>
</Tooltip>
`;

+ 18
- 2
server/sonar-web/src/main/js/components/docs/DocMarkdownBlock.tsx Datei anzeigen

@@ -29,13 +29,20 @@ import { separateFrontMatter } from '../../helpers/markdown';
import { isSonarCloud } from '../../helpers/system';

interface Props {
childProps?: { [k: string]: string };
className?: string;
content: string | undefined;
displayH1?: boolean;
isTooltip?: boolean;
}

export default function DocMarkdownBlock({ className, content, displayH1, isTooltip }: Props) {
export default function DocMarkdownBlock({
childProps,
className,
content,
displayH1,
isTooltip
}: Props) {
const parsed = separateFrontMatter(content || '');
return (
<div className={classNames('markdown', className)}>
@@ -48,7 +55,7 @@ export default function DocMarkdownBlock({ className, content, displayH1, isTool
// do not render outer <div />
div: React.Fragment,
// use custom link to render documentation anchors
a: isTooltip ? DocTooltipLink : DocLink,
a: isTooltip ? withChildProps(DocTooltipLink, childProps) : DocLink,
// used to handle `@include`
p: DocParagraph,
// use custom img tag to render documentation images
@@ -62,6 +69,15 @@ export default function DocMarkdownBlock({ className, content, displayH1, isTool
);
}

function withChildProps<P>(
WrappedComponent: React.ComponentType<P & { customProps?: { [k: string]: string } }>,
childProps?: { [k: string]: string }
) {
return function withChildProps(props: P) {
return <WrappedComponent customProps={childProps} {...props} />;
};
}

function filterContent(content: string) {
const beginning = isSonarCloud() ? '<!-- sonarqube -->' : '<!-- sonarcloud -->';
const ending = isSonarCloud() ? '<!-- /sonarqube -->' : '<!-- /sonarcloud -->';

+ 7
- 1
server/sonar-web/src/main/js/components/docs/DocTooltip.tsx Datei anzeigen

@@ -28,6 +28,7 @@ interface Props {
children?: React.ReactNode;
/** Key of the documentation chunk */
doc: string;
overlayProps?: { [k: string]: string };
}

interface State {
@@ -82,7 +83,12 @@ export default class DocTooltip extends React.PureComponent<Props, State> {
{this.state.loading ? (
<i className="spinner" />
) : (
<DocMarkdownBlock className="cut-margins" content={this.state.content} isTooltip={true} />
<DocMarkdownBlock
childProps={this.props.overlayProps}
className="cut-margins"
content={this.state.content}
isTooltip={true}
/>
)}
</div>
);

+ 35
- 15
server/sonar-web/src/main/js/components/docs/DocTooltipLink.tsx Datei anzeigen

@@ -19,25 +19,45 @@
*/
import * as React from 'react';
import { Link } from 'react-router';
import { forEach } from 'lodash';
import DetachIcon from '../../components/icons-components/DetachIcon';

export default function DocTooltipLink(props: React.AnchorHTMLAttributes<HTMLAnchorElement>) {
const { children, href, ...other } = props;
interface OwnProps {
customProps?: { [k: string]: string };
}

type Props = OwnProps & React.AnchorHTMLAttributes<HTMLAnchorElement>;

const SONARCLOUD_LINK = '/#sonarcloud#/';

export default function DocTooltipLink({ children, customProps, href, ...other }: Props) {
if (customProps) {
forEach(customProps, (value, key) => {
if (href) {
href = href.replace(`#${key}#`, encodeURIComponent(value));
}
});
}

if (href && href.startsWith('/')) {
if (href.startsWith(SONARCLOUD_LINK)) {
href = `/${href.substr(SONARCLOUD_LINK.length)}`;
} else {
href = `/documentation/${href.substr(1)}`;
}

return (
<Link rel="noopener noreferrer" target="_blank" to={href} {...other}>
{children}
</Link>
);
}

return (
<>
{href && href.startsWith('/') ? (
<Link
rel="noopener noreferrer"
target="_blank"
to={`/documentation/${href.substr(1)}`}
{...other}>
{children}
</Link>
) : (
<a href={href} rel="noopener noreferrer" target="_blank" {...other}>
{children}
</a>
)}
<a href={href} rel="noopener noreferrer" target="_blank" {...other}>
{children}
</a>
<DetachIcon className="little-spacer-left little-spacer-right vertical-baseline" size={12} />
</>
);

+ 12
- 0
server/sonar-web/src/main/js/components/docs/__tests__/DocMarkdownBlock-test.tsx Datei anzeigen

@@ -71,3 +71,15 @@ text`;
(isSonarCloud as jest.Mock).mockImplementation(() => true);
expect(shallow(<DocMarkdownBlock content={content} />)).toMatchSnapshot();
});

it('should render with custom props for links', () => {
expect(
shallow(
<DocMarkdownBlock
childProps={{ foo: 'bar' }}
content="some [link](#quality-profiles)"
isTooltip={true}
/>
).find('withChildProps')
).toMatchSnapshot();
});

+ 6
- 0
server/sonar-web/src/main/js/components/docs/__tests__/DocTooltipLink-test.tsx Datei anzeigen

@@ -28,3 +28,9 @@ it('should render simple link', () => {
it('should render internal link', () => {
expect(shallow(<DocTooltipLink href="/foo/bar" />)).toMatchSnapshot();
});

it('should render links with custom props', () => {
expect(
shallow(<DocTooltipLink customProps={{ bar: 'baz' }} href="/#sonarcloud#/foo/#bar#" />)
).toMatchSnapshot();
});

+ 9
- 0
server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocMarkdownBlock-test.tsx.snap Datei anzeigen

@@ -104,3 +104,12 @@ exports[`should render use custom component for links 1`] = `
link
</DocLink>
`;

exports[`should render with custom props for links 1`] = `
<withChildProps
href="#quality-profiles"
key="h-3"
>
link
</withChildProps>
`;

+ 17
- 13
server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocTooltipLink-test.tsx.snap Datei anzeigen

@@ -1,19 +1,23 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should render internal link 1`] = `
<React.Fragment>
<Link
onlyActiveOnIndex={false}
rel="noopener noreferrer"
style={Object {}}
target="_blank"
to="/documentation/foo/bar"
/>
<DetachIcon
className="little-spacer-left little-spacer-right vertical-baseline"
size={12}
/>
</React.Fragment>
<Link
onlyActiveOnIndex={false}
rel="noopener noreferrer"
style={Object {}}
target="_blank"
to="/documentation/foo/bar"
/>
`;

exports[`should render links with custom props 1`] = `
<Link
onlyActiveOnIndex={false}
rel="noopener noreferrer"
style={Object {}}
target="_blank"
to="/foo/baz"
/>
`;

exports[`should render simple link 1`] = `

server/sonar-web/src/main/js/components/common/PrivateBadge.tsx → server/sonar-web/src/main/js/components/icons-components/VisibleIcon.tsx Datei anzeigen

@@ -18,21 +18,15 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import * as classNames from 'classnames';
import Tooltip from '../controls/Tooltip';
import { translate } from '../../helpers/l10n';
import Icon, { IconProps } from './Icon';

interface Props {
className?: string;
qualifier: string;
}

export default function PrivateBadge({ className, qualifier }: Props) {
export default function VisibleIcon({ className, fill = 'currentColor', size }: IconProps) {
return (
<Tooltip overlay={translate('visibility.private.description', qualifier)}>
<div className={classNames('outline-badge', className)}>
{translate('visibility.private')}
</div>
</Tooltip>
<Icon className={className} size={size}>
<path
d="M13.524 8.403q-1.093-1.697-2.74-2.539 0.439 0.748 0.439 1.618 0 1.331-0.946 2.276t-2.276 0.946-2.276-0.946-0.946-2.276q0-0.87 0.439-1.618-1.647 0.842-2.74 2.539 0.957 1.474 2.399 2.348t3.125 0.874 3.125-0.874 2.399-2.348zM8.345 5.641q0-0.144-0.101-0.245t-0.245-0.101q-0.899 0-1.543 0.644t-0.644 1.543q0 0.144 0.101 0.245t0.245 0.101 0.245-0.101 0.101-0.245q0-0.619 0.439-1.057t1.057-0.439q0.144 0 0.245-0.101t0.101-0.245zM14.444 8.403q0 0.245-0.144 0.496-1.007 1.654-2.708 2.65t-3.593 0.996-3.593-1-2.708-2.647q-0.144-0.252-0.144-0.496t0.144-0.496q1.007-1.647 2.708-2.647t3.593-1 3.593 1 2.708 2.647q0.144 0.252 0.144 0.496z"
style={{ fill }}
/>
</Icon>
);
}

+ 3
- 1
server/sonar-web/src/main/js/helpers/organizations.ts Datei anzeigen

@@ -42,6 +42,8 @@ export function isCurrentUserMemberOf(
return Boolean(
organization &&
isLoggedIn(currentUser) &&
(organization.canAdmin || userOrganizations.some(org => org.key === organization.key))
(organization.canAdmin ||
organization.isAdmin ||
userOrganizations.some(org => org.key === organization.key))
);
}

+ 5
- 1
sonar-core/src/main/resources/org/sonar/l10n/core.properties Datei anzeigen

@@ -459,7 +459,9 @@ sidebar.tools=Tools

visibility.both=Public, Private
visibility.public=Public
visibility.public.description=This project is public. Anyone can browse and see the source code.
visibility.public.description.TRK=This project is public. Anyone can browse and see the source code.
visibility.public.description.VW=This portfolio is public. Anyone can browse it.
visibility.public.description.APP=This application is public. Anyone can browse it.
visibility.public.description.short=Anyone can browse and see the source code.
visibility.private=Private
visibility.private.description.TRK=This project is private. Only authorized users can browse and see the source code.
@@ -2573,11 +2575,13 @@ organization.members.manage_groups=Manage groups
organization.members.members_groups={0}'s groups:
organization.members.manage_a_team=Manage a team
organization.members.add_to_members=Add to members
organization.paid_plan.badge=Paid plan
organization.default_visibility_of_new_projects=Default visibility of new projects:
organization.change_visibility_form.header=Set Default Visibility of New Projects
organization.change_visibility_form.warning=This will not change the visibility of already existing projects.
organization.change_visibility_form.submit=Change Default Visibility


#------------------------------------------------------------------------------
#
# EMBEDED DOCS

Laden…
Abbrechen
Speichern