@@ -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) |
@@ -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) |
@@ -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) |
@@ -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) |
@@ -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) |
@@ -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) |
@@ -162,7 +162,7 @@ export interface Component { | |||
isFavorite?: boolean; | |||
analysisDate?: string; | |||
tags: string[]; | |||
visibility: string; | |||
visibility: Visibility; | |||
leakPeriodDate?: string; | |||
} | |||
@@ -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 |
@@ -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 () => { |
@@ -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} |
@@ -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); | |||
}); |
@@ -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); | |||
} |
@@ -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 { |
@@ -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 }; |
@@ -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(); | |||
}); | |||
} |
@@ -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 |
@@ -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> |
@@ -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 }} |
@@ -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,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(); | |||
}); |
@@ -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) |
@@ -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 && ( |
@@ -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'); | |||
} | |||
@@ -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 }); | |||
} | |||
); | |||
}; |
@@ -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> |
@@ -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)} |
@@ -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> |
@@ -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(); | |||
}); |
@@ -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(); | |||
}); |
@@ -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" |
@@ -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" |
@@ -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 { |
@@ -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]} />) |
@@ -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( |
@@ -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 && ( |
@@ -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); | |||
} |
@@ -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> |
@@ -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] }) { |
@@ -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] }) { |
@@ -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(); | |||
@@ -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 } |
@@ -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 |
@@ -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); |
@@ -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} | |||
/> | |||
); | |||
} |
@@ -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(); | |||
}); |
@@ -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> | |||
`; |
@@ -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> | |||
`; |
@@ -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 -->'; |
@@ -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> | |||
); |
@@ -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} /> | |||
</> | |||
); |
@@ -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(); | |||
}); |
@@ -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(); | |||
}); |
@@ -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> | |||
`; |
@@ -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`] = ` |
@@ -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> | |||
); | |||
} |
@@ -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)) | |||
); | |||
} |
@@ -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 |