diff options
57 files changed, 693 insertions, 234 deletions
diff --git a/server/sonar-docs/src/tooltips/organizations/subscription-paid-plan.md b/server/sonar-docs/src/tooltips/organizations/subscription-paid-plan.md new file mode 100644 index 00000000000..757d33d3da2 --- /dev/null +++ b/server/sonar-docs/src/tooltips/organizations/subscription-paid-plan.md @@ -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) diff --git a/server/sonar-docs/src/tooltips/project/visibility-private.md b/server/sonar-docs/src/tooltips/project/visibility-private.md new file mode 100644 index 00000000000..99d9647adae --- /dev/null +++ b/server/sonar-docs/src/tooltips/project/visibility-private.md @@ -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) diff --git a/server/sonar-docs/src/tooltips/project/visibility-public-admin.md b/server/sonar-docs/src/tooltips/project/visibility-public-admin.md new file mode 100644 index 00000000000..defcc6b5ca3 --- /dev/null +++ b/server/sonar-docs/src/tooltips/project/visibility-public-admin.md @@ -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) diff --git a/server/sonar-docs/src/tooltips/project/visibility-public-paid-org-admin.md b/server/sonar-docs/src/tooltips/project/visibility-public-paid-org-admin.md new file mode 100644 index 00000000000..54b8a3d76d8 --- /dev/null +++ b/server/sonar-docs/src/tooltips/project/visibility-public-paid-org-admin.md @@ -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) diff --git a/server/sonar-docs/src/tooltips/project/visibility-public-paid-org.md b/server/sonar-docs/src/tooltips/project/visibility-public-paid-org.md new file mode 100644 index 00000000000..3be0178c809 --- /dev/null +++ b/server/sonar-docs/src/tooltips/project/visibility-public-paid-org.md @@ -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) diff --git a/server/sonar-docs/src/tooltips/project/visibility-public.md b/server/sonar-docs/src/tooltips/project/visibility-public.md new file mode 100644 index 00000000000..b0d5c067689 --- /dev/null +++ b/server/sonar-docs/src/tooltips/project/visibility-public.md @@ -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) diff --git a/server/sonar-web/src/main/js/api/components.ts b/server/sonar-web/src/main/js/api/components.ts index 7ae4d59aa41..a069c38b17e 100644 --- a/server/sonar-web/src/main/js/api/components.ts +++ b/server/sonar-web/src/main/js/api/components.ts @@ -162,7 +162,7 @@ export interface Component { isFavorite?: boolean; analysisDate?: string; tags: string[]; - visibility: string; + visibility: Visibility; leakPeriodDate?: string; } diff --git a/server/sonar-web/src/main/js/api/permissions.ts b/server/sonar-web/src/main/js/api/permissions.ts index 189ed4ca8de..e14367bb6a7 100644 --- a/server/sonar-web/src/main/js/api/permissions.ts +++ b/server/sonar-web/src/main/js/api/permissions.ts @@ -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 diff --git a/server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx b/server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx index 9052c611e68..83a4afb2822 100644 --- a/server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx +++ b/server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx @@ -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 () => { diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavHeader.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavHeader.tsx index 150c9c50923..e01fb137866 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavHeader.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavHeader.tsx @@ -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} diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavHeader-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavHeader-test.tsx index 676a38a577b..ff1fdacdf46 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavHeader-test.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavHeader-test.tsx @@ -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); -}); diff --git a/server/sonar-web/src/main/js/app/styles/components/badges.css b/server/sonar-web/src/main/js/app/styles/components/badges.css index 9d0e5a5a2d9..05d1f81238f 100644 --- a/server/sonar-web/src/main/js/app/styles/components/badges.css +++ b/server/sonar-web/src/main/js/app/styles/components/badges.css @@ -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); +} diff --git a/server/sonar-web/src/main/js/app/types.ts b/server/sonar-web/src/main/js/app/types.ts index bddfe372c4a..b546c8a212f 100644 --- a/server/sonar-web/src/main/js/app/types.ts +++ b/server/sonar-web/src/main/js/app/types.ts @@ -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 { diff --git a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationPage.tsx b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationPage.tsx index 4cc5dd277f2..8d135c8fede 100644 --- a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationPage.tsx +++ b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationPage.tsx @@ -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 }; diff --git a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationPage-test.tsx b/server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationPage-test.tsx index e1230f3a18c..5bf896954b5 100644 --- a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationPage-test.tsx +++ b/server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationPage-test.tsx @@ -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(); -}); +} diff --git a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationPage-test.tsx.snap b/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationPage-test.tsx.snap index 9802e06cdcb..c6b612738c1 100644 --- a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationPage-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationPage-test.tsx.snap @@ -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 diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigation.tsx b/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigation.tsx index 112d2ef75ce..d74444d133b 100644 --- a/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigation.tsx +++ b/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigation.tsx @@ -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> diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationMeta.tsx b/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationMeta.tsx index 6815ae5dae9..10e2496c277 100644 --- a/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationMeta.tsx +++ b/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationMeta.tsx @@ -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 }} diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigation-test.tsx b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigation-test.tsx index 6f780c28e2c..ecfdd94f780 100644 --- a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigation-test.tsx +++ b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigation-test.tsx @@ -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(); diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigationMeta-test.tsx b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigationMeta-test.tsx index 96d9f772c63..5009da6beb4 100644 --- a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigationMeta-test.tsx +++ b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigationMeta-test.tsx @@ -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(); +}); diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigation-test.tsx.snap b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigation-test.tsx.snap index 240404ba19f..0da0558483a 100644 --- a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigation-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigation-test.tsx.snap @@ -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) diff --git a/server/sonar-web/src/main/js/apps/overview/meta/MetaContainer.tsx b/server/sonar-web/src/main/js/apps/overview/meta/MetaContainer.tsx index 62ee5647128..3e7749343a5 100644 --- a/server/sonar-web/src/main/js/apps/overview/meta/MetaContainer.tsx +++ b/server/sonar-web/src/main/js/apps/overview/meta/MetaContainer.tsx @@ -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 && ( diff --git a/server/sonar-web/src/main/js/apps/permissions/project/components/AllHoldersList.js b/server/sonar-web/src/main/js/apps/permissions/project/components/AllHoldersList.js index 51d563afbe1..1fbf421aa71 100644 --- a/server/sonar-web/src/main/js/apps/permissions/project/components/AllHoldersList.js +++ b/server/sonar-web/src/main/js/apps/permissions/project/components/AllHoldersList.js @@ -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'); } diff --git a/server/sonar-web/src/main/js/apps/permissions/project/components/App.js b/server/sonar-web/src/main/js/apps/permissions/project/components/App.js index 5815264b6a4..dc5888fcd08 100644 --- a/server/sonar-web/src/main/js/apps/permissions/project/components/App.js +++ b/server/sonar-web/src/main/js/apps/permissions/project/components/App.js @@ -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 }); } ); }; diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/App.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/App.tsx index 188130d59e1..5e9b873b841 100644 --- a/server/sonar-web/src/main/js/apps/portfolio/components/App.tsx +++ b/server/sonar-web/src/main/js/apps/portfolio/components/App.tsx @@ -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> diff --git a/server/sonar-web/src/main/js/apps/projects/components/ProjectCardLeak.tsx b/server/sonar-web/src/main/js/apps/projects/components/ProjectCardLeak.tsx index b6a3c63c2b4..912bf578736 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/ProjectCardLeak.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/ProjectCardLeak.tsx @@ -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)} diff --git a/server/sonar-web/src/main/js/apps/projects/components/ProjectCardOverall.tsx b/server/sonar-web/src/main/js/apps/projects/components/ProjectCardOverall.tsx index f653d14975d..859572fb80a 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/ProjectCardOverall.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/ProjectCardOverall.tsx @@ -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> diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCardLeak-test.tsx b/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCardLeak-test.tsx index 3a25da57bf3..c2982771b7c 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCardLeak-test.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCardLeak-test.tsx @@ -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(); }); diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCardOverall-test.tsx b/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCardOverall-test.tsx index fc3e58bbd80..4ed5e6753ab 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCardOverall-test.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCardOverall-test.tsx @@ -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(); }); diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCardLeak-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCardLeak-test.tsx.snap index fa106ed2ac6..1c7f8335013 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCardLeak-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCardLeak-test.tsx.snap @@ -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" diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCardOverall-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCardOverall-test.tsx.snap index 263d0bf5fd0..039ca9bd89c 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCardOverall-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCardOverall-test.tsx.snap @@ -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" diff --git a/server/sonar-web/src/main/js/apps/projects/types.ts b/server/sonar-web/src/main/js/apps/projects/types.ts index 2d339fb7d4c..e997ca3eae6 100644 --- a/server/sonar-web/src/main/js/apps/projects/types.ts +++ b/server/sonar-web/src/main/js/apps/projects/types.ts @@ -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 { diff --git a/server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/Risk-test.tsx b/server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/Risk-test.tsx index 688230948db..23d9bb2830c 100644 --- a/server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/Risk-test.tsx +++ b/server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/Risk-test.tsx @@ -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]} />) diff --git a/server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/SimpleBubbleChart-test.tsx b/server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/SimpleBubbleChart-test.tsx index 4224630f25d..5d0acd4b3c3 100644 --- a/server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/SimpleBubbleChart-test.tsx +++ b/server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/SimpleBubbleChart-test.tsx @@ -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( diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/App.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/App.tsx index 34d6db3ebae..2a5bdbeee27 100644 --- a/server/sonar-web/src/main/js/apps/projectsManagement/App.tsx +++ b/server/sonar-web/src/main/js/apps/projectsManagement/App.tsx @@ -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 && ( diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/AppContainer.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/AppContainer.tsx index d208e4e5b9b..42f537dbbe6 100644 --- a/server/sonar-web/src/main/js/apps/projectsManagement/AppContainer.tsx +++ b/server/sonar-web/src/main/js/apps/projectsManagement/AppContainer.tsx @@ -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); } diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.tsx index 5537721739e..8853af4a079 100644 --- a/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.tsx +++ b/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.tsx @@ -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> diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/App-test.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/App-test.tsx index f8b0f32445d..3299c3e33b4 100644 --- a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/App-test.tsx +++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/App-test.tsx @@ -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] }) { diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ChangeVisibilityForm-test.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ChangeVisibilityForm-test.tsx index 1b78dda0438..9d462d5d584 100644 --- a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ChangeVisibilityForm-test.tsx +++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ChangeVisibilityForm-test.tsx @@ -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] }) { diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/CreateProjectForm-test.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/CreateProjectForm-test.tsx index 96ea19b42a2..bc9e0ff3a6d 100644 --- a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/CreateProjectForm-test.tsx +++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/CreateProjectForm-test.tsx @@ -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(); diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Projects-test.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Projects-test.tsx index 25b2a6ac78d..63a914ea80a 100644 --- a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Projects-test.tsx +++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Projects-test.tsx @@ -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 } diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ProjectRow-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ProjectRow-test.tsx.snap index 6d05b0406f7..f97f8899d35 100644 --- a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ProjectRow-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ProjectRow-test.tsx.snap @@ -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 diff --git a/server/sonar-web/src/main/js/components/common/PrivacyBadgeContainer.tsx b/server/sonar-web/src/main/js/components/common/PrivacyBadgeContainer.tsx new file mode 100644 index 00000000000..5b05edbeb4d --- /dev/null +++ b/server/sonar-web/src/main/js/components/common/PrivacyBadgeContainer.tsx @@ -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); diff --git a/server/sonar-web/src/main/js/components/common/__tests__/PrivacyBadgeContainer-test.tsx b/server/sonar-web/src/main/js/components/common/__tests__/PrivacyBadgeContainer-test.tsx new file mode 100644 index 00000000000..336a20948da --- /dev/null +++ b/server/sonar-web/src/main/js/components/common/__tests__/PrivacyBadgeContainer-test.tsx @@ -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} + /> + ); +} diff --git a/server/sonar-web/src/main/js/components/common/__tests__/PrivateBadge-test.tsx b/server/sonar-web/src/main/js/components/common/__tests__/PrivateBadge-test.tsx deleted file mode 100644 index 462b6f1aa04..00000000000 --- a/server/sonar-web/src/main/js/components/common/__tests__/PrivateBadge-test.tsx +++ /dev/null @@ -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(); -}); diff --git a/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/PrivacyBadgeContainer-test.tsx.snap b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/PrivacyBadgeContainer-test.tsx.snap new file mode 100644 index 00000000000..b0aef67a7db --- /dev/null +++ b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/PrivacyBadgeContainer-test.tsx.snap @@ -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> +`; diff --git a/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/PrivateBadge-test.tsx.snap b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/PrivateBadge-test.tsx.snap deleted file mode 100644 index b52275d7c3b..00000000000 --- a/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/PrivateBadge-test.tsx.snap +++ /dev/null @@ -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> -`; diff --git a/server/sonar-web/src/main/js/components/docs/DocMarkdownBlock.tsx b/server/sonar-web/src/main/js/components/docs/DocMarkdownBlock.tsx index 2d1f1c6579f..8e9c594a4fd 100644 --- a/server/sonar-web/src/main/js/components/docs/DocMarkdownBlock.tsx +++ b/server/sonar-web/src/main/js/components/docs/DocMarkdownBlock.tsx @@ -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 -->'; diff --git a/server/sonar-web/src/main/js/components/docs/DocTooltip.tsx b/server/sonar-web/src/main/js/components/docs/DocTooltip.tsx index 754008144a4..3f7d4ec0195 100644 --- a/server/sonar-web/src/main/js/components/docs/DocTooltip.tsx +++ b/server/sonar-web/src/main/js/components/docs/DocTooltip.tsx @@ -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> ); diff --git a/server/sonar-web/src/main/js/components/docs/DocTooltipLink.tsx b/server/sonar-web/src/main/js/components/docs/DocTooltipLink.tsx index 897b3568e89..7160ab51370 100644 --- a/server/sonar-web/src/main/js/components/docs/DocTooltipLink.tsx +++ b/server/sonar-web/src/main/js/components/docs/DocTooltipLink.tsx @@ -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} /> </> ); diff --git a/server/sonar-web/src/main/js/components/docs/__tests__/DocMarkdownBlock-test.tsx b/server/sonar-web/src/main/js/components/docs/__tests__/DocMarkdownBlock-test.tsx index 898ff22a5a3..16deb2d6838 100644 --- a/server/sonar-web/src/main/js/components/docs/__tests__/DocMarkdownBlock-test.tsx +++ b/server/sonar-web/src/main/js/components/docs/__tests__/DocMarkdownBlock-test.tsx @@ -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(); +}); diff --git a/server/sonar-web/src/main/js/components/docs/__tests__/DocTooltipLink-test.tsx b/server/sonar-web/src/main/js/components/docs/__tests__/DocTooltipLink-test.tsx index 21a44671719..e57b122d21a 100644 --- a/server/sonar-web/src/main/js/components/docs/__tests__/DocTooltipLink-test.tsx +++ b/server/sonar-web/src/main/js/components/docs/__tests__/DocTooltipLink-test.tsx @@ -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(); +}); diff --git a/server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocMarkdownBlock-test.tsx.snap b/server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocMarkdownBlock-test.tsx.snap index e5364db5d6a..9e7a8b437fb 100644 --- a/server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocMarkdownBlock-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocMarkdownBlock-test.tsx.snap @@ -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> +`; diff --git a/server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocTooltipLink-test.tsx.snap b/server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocTooltipLink-test.tsx.snap index efb4518b4ae..007500cb059 100644 --- a/server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocTooltipLink-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocTooltipLink-test.tsx.snap @@ -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`] = ` diff --git a/server/sonar-web/src/main/js/components/common/PrivateBadge.tsx b/server/sonar-web/src/main/js/components/icons-components/VisibleIcon.tsx index 3fdad3d849e..c22ea4722f1 100644 --- a/server/sonar-web/src/main/js/components/common/PrivateBadge.tsx +++ b/server/sonar-web/src/main/js/components/icons-components/VisibleIcon.tsx @@ -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> ); } diff --git a/server/sonar-web/src/main/js/helpers/organizations.ts b/server/sonar-web/src/main/js/helpers/organizations.ts index 3d57db7ff3b..97a57c14238 100644 --- a/server/sonar-web/src/main/js/helpers/organizations.ts +++ b/server/sonar-web/src/main/js/helpers/organizations.ts @@ -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)) ); } diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index cddf7f289fd..96bf8b0b98c 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -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 |