* SONAR-11003 Always set organization parameter in api/rules/search * SONAR-11002 Show rules and hide quality profiles inside rules pagetags/7.5
@@ -19,9 +19,10 @@ | |||
*/ | |||
import * as React from 'react'; | |||
import { Helmet } from 'react-helmet'; | |||
import { connect } from 'react-redux'; | |||
import * as PropTypes from 'prop-types'; | |||
import { keyBy } from 'lodash'; | |||
import * as key from 'keymaster'; | |||
import { keyBy } from 'lodash'; | |||
import BulkChange from './BulkChange'; | |||
import FacetsList from './FacetsList'; | |||
import PageActions from './PageActions'; | |||
@@ -43,27 +44,36 @@ import { | |||
Activation, | |||
getOpen | |||
} from '../query'; | |||
import { searchRules, getRulesApp } from '../../../api/rules'; | |||
import { Paging, Rule, RuleActivation } from '../../../app/types'; | |||
import ScreenPositionHelper from '../../../components/common/ScreenPositionHelper'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { RawQuery } from '../../../helpers/query'; | |||
import ListFooter from '../../../components/controls/ListFooter'; | |||
import FiltersHeader from '../../../components/common/FiltersHeader'; | |||
import SearchBox from '../../../components/controls/SearchBox'; | |||
import { searchRules, getRulesApp } from '../../../api/rules'; | |||
import { searchQualityProfiles, Profile } from '../../../api/quality-profiles'; | |||
import { getCurrentUser, getMyOrganizations } from '../../../store/rootReducer'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { RawQuery } from '../../../helpers/query'; | |||
import { scrollToElement } from '../../../helpers/scrolling'; | |||
import { Paging, Rule, RuleActivation, Organization, CurrentUser } from '../../../app/types'; | |||
import '../../../components/search-navigator.css'; | |||
import '../styles.css'; | |||
import { hasPrivateAccess } from '../../../helpers/organizations'; | |||
const PAGE_SIZE = 100; | |||
const LIMIT_BEFORE_LOAD_MORE = 5; | |||
interface Props { | |||
interface StateToProps { | |||
currentUser: CurrentUser; | |||
userOrganizations: Organization[]; | |||
} | |||
interface OwnProps { | |||
location: { pathname: string; query: RawQuery }; | |||
organization?: { key: string }; | |||
organization: Organization | undefined; | |||
} | |||
type Props = OwnProps & StateToProps; | |||
interface State { | |||
actives?: Actives; | |||
canWrite?: boolean; | |||
@@ -81,7 +91,7 @@ interface State { | |||
// TODO redirect to default organization's rules page | |||
export default class App extends React.PureComponent<Props, State> { | |||
export class App extends React.PureComponent<Props, State> { | |||
mounted = false; | |||
static contextTypes = { | |||
@@ -103,9 +113,7 @@ export default class App extends React.PureComponent<Props, State> { | |||
componentDidMount() { | |||
this.mounted = true; | |||
// $FlowFixMe | |||
document.body.classList.add('white-page'); | |||
// $FlowFixMe | |||
document.documentElement.classList.add('white-page'); | |||
const footer = document.getElementById('footer'); | |||
if (footer) { | |||
@@ -116,11 +124,14 @@ export default class App extends React.PureComponent<Props, State> { | |||
} | |||
componentWillReceiveProps(nextProps: Props) { | |||
const openRule = this.getOpenRule(nextProps, this.state.rules); | |||
if (openRule && openRule.key !== this.state.selected) { | |||
this.setState({ selected: openRule.key }); | |||
} | |||
this.setState({ openRule, query: parseQuery(nextProps.location.query) }); | |||
this.setState(({ rules, selected }) => { | |||
const openRule = this.getOpenRule(nextProps, rules); | |||
return { | |||
openRule, | |||
query: parseQuery(nextProps.location.query), | |||
selected: openRule ? openRule.key : selected | |||
}; | |||
}); | |||
} | |||
componentDidUpdate(prevProps: Props, prevState: State) { | |||
@@ -219,7 +230,7 @@ export default class App extends React.PureComponent<Props, State> { | |||
fetchInitialData = () => { | |||
this.setState({ loading: true }); | |||
const organization = this.props.organization && this.props.organization.key; | |||
Promise.all([getRulesApp({ organization }), searchQualityProfiles({ organization })]).then( | |||
Promise.all([getRulesApp({ organization }), this.fetchQualityProfiles()]).then( | |||
([{ canWrite, repositories }, { profiles }]) => { | |||
this.setState({ | |||
canWrite, | |||
@@ -283,6 +294,14 @@ export default class App extends React.PureComponent<Props, State> { | |||
}, this.stopLoading); | |||
}; | |||
fetchQualityProfiles = () => { | |||
const { currentUser, organization, userOrganizations } = this.props; | |||
if (hasPrivateAccess(currentUser, organization, userOrganizations)) { | |||
return searchQualityProfiles({ organization: organization && organization.key }); | |||
} | |||
return { profiles: [] }; | |||
}; | |||
getSelectedIndex = ({ selected, rules } = this.state) => { | |||
const index = rules.findIndex(rule => rule.key === selected); | |||
return index !== -1 ? index : undefined; | |||
@@ -464,7 +483,11 @@ export default class App extends React.PureComponent<Props, State> { | |||
const { paging, rules } = this.state; | |||
const selectedIndex = this.getSelectedIndex(); | |||
const organization = this.props.organization && this.props.organization.key; | |||
const hideQualityProfiles = !hasPrivateAccess( | |||
this.props.currentUser, | |||
this.props.organization, | |||
this.props.userOrganizations | |||
); | |||
return ( | |||
<> | |||
<Suggestions suggestions="coding_rules" /> | |||
@@ -488,6 +511,7 @@ export default class App extends React.PureComponent<Props, State> { | |||
/> | |||
<FacetsList | |||
facets={this.state.facets} | |||
hideProfileFacet={hideQualityProfiles} | |||
onFacetToggle={this.handleFacetToggle} | |||
onFilterChange={this.handleFilterChange} | |||
openFacets={this.state.openFacets} | |||
@@ -509,7 +533,7 @@ export default class App extends React.PureComponent<Props, State> { | |||
<div className="layout-page-header-panel-inner layout-page-main-header-inner"> | |||
<div className="layout-page-main-inner"> | |||
{this.state.openRule ? ( | |||
<a href="#" className="js-back" onClick={this.handleBack}> | |||
<a className="js-back" href="#" onClick={this.handleBack}> | |||
{translate('coding_rules.return_to_list')} | |||
</a> | |||
) : ( | |||
@@ -537,6 +561,7 @@ export default class App extends React.PureComponent<Props, State> { | |||
<RuleDetails | |||
allowCustomRules={!this.context.organizationsEnabled} | |||
canWrite={this.state.canWrite} | |||
hideQualityProfiles={hideQualityProfiles} | |||
onActivate={this.handleRuleActivate} | |||
onDeactivate={this.handleRuleDeactivate} | |||
onDelete={this.handleRuleDelete} | |||
@@ -603,3 +628,10 @@ function parseFacets(rawFacets: { property: string; values: { count: number; val | |||
} | |||
return facets; | |||
} | |||
const mapStateToProps = (state: any) => ({ | |||
currentUser: getCurrentUser(state), | |||
userOrganizations: getMyOrganizations(state) | |||
}); | |||
export default connect<StateToProps, {}, OwnProps>(mapStateToProps)(App); |
@@ -34,6 +34,7 @@ import { Profile } from '../../../api/quality-profiles'; | |||
interface Props { | |||
facets?: Facets; | |||
hideProfileFacet?: boolean; | |||
onFacetToggle: (facet: FacetKey) => void; | |||
onFilterChange: (changes: Partial<Query>) => void; | |||
openFacets: OpenFacets; | |||
@@ -55,7 +56,6 @@ export default function FacetsList(props: Props) { | |||
props.query.compareToProfile !== undefined || | |||
props.selectedProfile === undefined || | |||
!props.query.activation; | |||
return ( | |||
<div className="search-navigator-facets-list"> | |||
<LanguageFacet | |||
@@ -75,8 +75,8 @@ export default function FacetsList(props: Props) { | |||
<TagFacet | |||
onChange={props.onFilterChange} | |||
onToggle={props.onFacetToggle} | |||
organization={props.organization} | |||
open={!!props.openFacets.tags} | |||
organization={props.organization} | |||
stats={props.facets && props.facets.tags} | |||
values={props.query.tags} | |||
/> | |||
@@ -84,8 +84,8 @@ export default function FacetsList(props: Props) { | |||
onChange={props.onFilterChange} | |||
onToggle={props.onFacetToggle} | |||
open={!!props.openFacets.repositories} | |||
stats={props.facets && props.facets.repositories} | |||
referencedRepositories={props.referencedRepositories} | |||
stats={props.facets && props.facets.repositories} | |||
values={props.query.repositories} | |||
/> | |||
<DefaultSeverityFacet | |||
@@ -116,31 +116,35 @@ export default function FacetsList(props: Props) { | |||
value={props.query.template} | |||
/> | |||
)} | |||
<ProfileFacet | |||
activation={props.query.activation} | |||
compareToProfile={props.query.compareToProfile} | |||
languages={props.query.languages} | |||
onChange={props.onFilterChange} | |||
onToggle={props.onFacetToggle} | |||
open={!!props.openFacets.profile} | |||
referencedProfiles={props.referencedProfiles} | |||
value={props.query.profile} | |||
/> | |||
<InheritanceFacet | |||
disabled={inheritanceDisabled} | |||
onChange={props.onFilterChange} | |||
onToggle={props.onFacetToggle} | |||
open={!!props.openFacets.inheritance} | |||
value={props.query.inheritance} | |||
/> | |||
<ActivationSeverityFacet | |||
disabled={activationSeverityDisabled} | |||
onChange={props.onFilterChange} | |||
onToggle={props.onFacetToggle} | |||
open={!!props.openFacets.activationSeverities} | |||
stats={props.facets && props.facets.activationSeverities} | |||
values={props.query.activationSeverities} | |||
/> | |||
{!props.hideProfileFacet && ( | |||
<> | |||
<ProfileFacet | |||
activation={props.query.activation} | |||
compareToProfile={props.query.compareToProfile} | |||
languages={props.query.languages} | |||
onChange={props.onFilterChange} | |||
onToggle={props.onFacetToggle} | |||
open={!!props.openFacets.profile} | |||
referencedProfiles={props.referencedProfiles} | |||
value={props.query.profile} | |||
/> | |||
<InheritanceFacet | |||
disabled={inheritanceDisabled} | |||
onChange={props.onFilterChange} | |||
onToggle={props.onFacetToggle} | |||
open={!!props.openFacets.inheritance} | |||
value={props.query.inheritance} | |||
/> | |||
<ActivationSeverityFacet | |||
disabled={activationSeverityDisabled} | |||
onChange={props.onFilterChange} | |||
onToggle={props.onFacetToggle} | |||
open={!!props.openFacets.activationSeverities} | |||
stats={props.facets && props.facets.activationSeverities} | |||
values={props.query.activationSeverities} | |||
/> | |||
</> | |||
)} | |||
</div> | |||
); | |||
} |
@@ -38,6 +38,7 @@ import { translate, translateWithParameters } from '../../../helpers/l10n'; | |||
interface Props { | |||
allowCustomRules?: boolean; | |||
canWrite?: boolean; | |||
hideQualityProfiles?: boolean; | |||
onActivate: (profile: string, rule: string, activation: Activation) => void; | |||
onDeactivate: (profile: string, rule: string) => void; | |||
onDelete: (rule: string) => void; | |||
@@ -148,7 +149,13 @@ export default class RuleDetails extends React.PureComponent<Props, State> { | |||
return <div className="coding-rule-details" />; | |||
} | |||
const { allowCustomRules, canWrite, organization, referencedProfiles } = this.props; | |||
const { | |||
allowCustomRules, | |||
canWrite, | |||
hideQualityProfiles, | |||
organization, | |||
referencedProfiles | |||
} = this.props; | |||
const { params = [] } = ruleDetails; | |||
const isCustom = !!ruleDetails.templateKey; | |||
@@ -225,17 +232,18 @@ export default class RuleDetails extends React.PureComponent<Props, State> { | |||
/> | |||
)} | |||
{!ruleDetails.isTemplate && ( | |||
<RuleDetailsProfiles | |||
activations={this.state.actives} | |||
canWrite={canWrite} | |||
onActivate={this.handleActivate} | |||
onDeactivate={this.handleDeactivate} | |||
organization={organization} | |||
referencedProfiles={referencedProfiles} | |||
ruleDetails={ruleDetails} | |||
/> | |||
)} | |||
{!ruleDetails.isTemplate && | |||
!hideQualityProfiles && ( | |||
<RuleDetailsProfiles | |||
activations={this.state.actives} | |||
canWrite={canWrite} | |||
onActivate={this.handleActivate} | |||
onDeactivate={this.handleDeactivate} | |||
organization={organization} | |||
referencedProfiles={referencedProfiles} | |||
ruleDetails={ruleDetails} | |||
/> | |||
)} | |||
{!ruleDetails.isTemplate && ( | |||
<RuleDetailsIssues organization={organization} ruleKey={ruleDetails.key} /> |
@@ -39,7 +39,6 @@ import { PopupPlacement } from '../../../components/ui/popups'; | |||
interface Props { | |||
canWrite: boolean | undefined; | |||
hidePermalink?: boolean; | |||
hideSimilarRulesFilter?: boolean; | |||
onFilterChange: (changes: Partial<Query>) => void; | |||
onTagsChange: (tags: string[]) => void; | |||
@@ -231,22 +230,21 @@ export default class RuleDetailsMeta extends React.PureComponent<Props> { | |||
}; | |||
render() { | |||
const { hidePermalink, ruleDetails } = this.props; | |||
const { ruleDetails } = this.props; | |||
const hasTypeData = !ruleDetails.isExternal || ruleDetails.type !== 'UNKNOWN'; | |||
return ( | |||
<div className="js-rule-meta"> | |||
<header className="page-header"> | |||
<div className="pull-right"> | |||
<span className="note text-middle">{ruleDetails.key}</span> | |||
{!ruleDetails.isExternal && | |||
!hidePermalink && ( | |||
<Link | |||
className="coding-rules-detail-permalink link-no-underline spacer-left text-middle" | |||
title={translate('permalink')} | |||
to={getRuleUrl(ruleDetails.key, this.props.organization)}> | |||
<LinkIcon /> | |||
</Link> | |||
)} | |||
{!ruleDetails.isExternal && ( | |||
<Link | |||
className="coding-rules-detail-permalink link-no-underline spacer-left text-middle" | |||
title={translate('permalink')} | |||
to={getRuleUrl(ruleDetails.key, this.props.organization)}> | |||
<LinkIcon /> | |||
</Link> | |||
)} | |||
{!this.props.hideSimilarRulesFilter && ( | |||
<SimilarRulesFilter onFilterChange={this.props.onFilterChange} rule={ruleDetails} /> | |||
)} |
@@ -85,14 +85,6 @@ it('should edit tags', () => { | |||
expect(onTagsChange).toBeCalledWith(['foo', 'bar']); | |||
}); | |||
it('should not display rule permalink', () => { | |||
expect( | |||
getWrapper({ hidePermalink: true }) | |||
.find('.coding-rules-detail-permalink') | |||
.exists() | |||
).toBeFalsy(); | |||
}); | |||
function getWrapper(props = {}) { | |||
return shallow( | |||
<RuleDetailsMeta |
@@ -38,7 +38,7 @@ export interface Props { | |||
onChange: (changes: Partial<Query>) => void; | |||
onToggle: (property: string) => void; | |||
open: boolean; | |||
organization: { key: string } | undefined; | |||
organization: string | undefined; | |||
stats: { [x: string]: number } | undefined; | |||
referencedUsers: { [login: string]: ReferencedUser }; | |||
} | |||
@@ -77,11 +77,7 @@ export default class AssigneeFacet extends React.PureComponent<Props> { | |||
}; | |||
handleSearch = (query: string) => { | |||
let organization = this.props.component && this.props.component.organization; | |||
if (this.props.organization && !organization) { | |||
organization = this.props.organization.key; | |||
} | |||
return searchAssignees(query, organization); | |||
return searchAssignees(query, this.props.organization); | |||
}; | |||
handleSelect = (option: { value: string }) => { |
@@ -63,6 +63,10 @@ export default class Sidebar extends React.PureComponent<Props> { | |||
const displayFilesFacet = component !== undefined; | |||
const displayAuthorFacet = !component || component.qualifier !== 'DEV'; | |||
const organizationKey = | |||
(component && component.organization) || | |||
(this.props.organization && this.props.organization.key); | |||
return ( | |||
<div className="search-navigator-facets-list"> | |||
<FacetMode facetMode={query.facetMode} onChange={this.props.onFilterChange} /> | |||
@@ -124,7 +128,7 @@ export default class Sidebar extends React.PureComponent<Props> { | |||
onChange={this.props.onFilterChange} | |||
onToggle={this.props.onFacetToggle} | |||
open={!!openFacets.rules} | |||
organization={this.props.organization && this.props.organization.key} | |||
organization={organizationKey} | |||
referencedRules={this.props.referencedRules} | |||
rules={query.rules} | |||
stats={facets.rules} | |||
@@ -136,7 +140,7 @@ export default class Sidebar extends React.PureComponent<Props> { | |||
onChange={this.props.onFilterChange} | |||
onToggle={this.props.onFacetToggle} | |||
open={!!openFacets.tags} | |||
organization={this.props.organization} | |||
organization={organizationKey} | |||
stats={facets.tags} | |||
tags={query.tags} | |||
/> | |||
@@ -200,7 +204,7 @@ export default class Sidebar extends React.PureComponent<Props> { | |||
onChange={this.props.onFilterChange} | |||
onToggle={this.props.onFacetToggle} | |||
open={!!openFacets.assignees} | |||
organization={this.props.organization} | |||
organization={organizationKey} | |||
referencedUsers={this.props.referencedUsers} | |||
stats={facets.assignees} | |||
/> |
@@ -38,7 +38,7 @@ interface Props { | |||
onChange: (changes: Partial<Query>) => void; | |||
onToggle: (property: string) => void; | |||
open: boolean; | |||
organization: { key: string } | undefined; | |||
organization: string | undefined; | |||
stats: { [x: string]: number } | undefined; | |||
tags: string[]; | |||
} | |||
@@ -74,11 +74,7 @@ export default class TagFacet extends React.PureComponent<Props> { | |||
}; | |||
handleSearch = (query: string) => { | |||
let organization = this.props.component && this.props.component.organization; | |||
if (this.props.organization && !organization) { | |||
organization = this.props.organization.key; | |||
} | |||
return searchIssueTags({ organization, ps: 50, q: query }).then(tags => | |||
return searchIssueTags({ organization: this.props.organization, ps: 50, q: query }).then(tags => | |||
tags.map(tag => ({ label: tag, value: tag })) | |||
); | |||
}; |
@@ -20,11 +20,15 @@ | |||
import * as React from 'react'; | |||
import { connect } from 'react-redux'; | |||
import { RouterState } from 'react-router'; | |||
import { getOrganizationByKey, getCurrentUser } from '../../../store/rootReducer'; | |||
import { | |||
getCurrentUser, | |||
getMyOrganizations, | |||
getOrganizationByKey | |||
} from '../../../store/rootReducer'; | |||
import handleRequiredAuthorization from '../../../app/utils/handleRequiredAuthorization'; | |||
import { Organization, CurrentUser, isLoggedIn } from '../../../app/types'; | |||
import { isCurrentUserMemberOf, hasPrivateAccess } from '../../../helpers/organizations'; | |||
import { getMyOrganizations } from '../../../store/organizations/duck'; | |||
import {} from '../../../store/organizations/duck'; | |||
interface StateToProps { | |||
currentUser: CurrentUser; |
@@ -47,6 +47,7 @@ export function OrganizationNavigationMenu({ | |||
organization, | |||
userOrganizations | |||
}: Props) { | |||
const hasPrivateRights = hasPrivateAccess(currentUser, organization, userOrganizations); | |||
return ( | |||
<NavBarTabs className="navbar-context-tabs"> | |||
<li> | |||
@@ -64,28 +65,25 @@ export function OrganizationNavigationMenu({ | |||
{translate('issues.page')} | |||
</Link> | |||
</li> | |||
{hasPrivateAccess(currentUser, organization, userOrganizations) && ( | |||
<> | |||
<li> | |||
<Link | |||
activeClassName="active" | |||
to={`/organizations/${organization.key}/quality_profiles`}> | |||
{translate('quality_profiles.page')} | |||
</Link> | |||
</li> | |||
<li> | |||
<Link activeClassName="active" to={`/organizations/${organization.key}/rules`}> | |||
{translate('coding_rules.page')} | |||
</Link> | |||
</li> | |||
<li> | |||
<Link activeClassName="active" to={getQualityGatesUrl(organization.key)}> | |||
{translate('quality_gates.page')} | |||
</Link> | |||
</li> | |||
</> | |||
{hasPrivateRights && ( | |||
<li> | |||
<Link activeClassName="active" to={`/organizations/${organization.key}/quality_profiles`}> | |||
{translate('quality_profiles.page')} | |||
</Link> | |||
</li> | |||
)} | |||
<li> | |||
<Link activeClassName="active" to={`/organizations/${organization.key}/rules`}> | |||
{translate('coding_rules.page')} | |||
</Link> | |||
</li> | |||
{hasPrivateRights && ( | |||
<li> | |||
<Link activeClassName="active" to={getQualityGatesUrl(organization.key)}> | |||
{translate('quality_gates.page')} | |||
</Link> | |||
</li> | |||
)} | |||
{isCurrentUserMemberOf(currentUser, organization, userOrganizations) && ( | |||
<li> | |||
<Link activeClassName="active" to={`/organizations/${organization.key}/members`}> | |||
@@ -93,7 +91,6 @@ export function OrganizationNavigationMenu({ | |||
</Link> | |||
</li> | |||
)} | |||
<OrganizationNavigationExtensions location={location} organization={organization} /> | |||
{organization.canAdmin && ( | |||
<OrganizationNavigationAdministration location={location} organization={organization} /> |
@@ -31,42 +31,40 @@ exports[`renders 1`] = ` | |||
issues.page | |||
</Link> | |||
</li> | |||
<React.Fragment> | |||
<li> | |||
<Link | |||
activeClassName="active" | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to="/organizations/foo/quality_profiles" | |||
> | |||
quality_profiles.page | |||
</Link> | |||
</li> | |||
<li> | |||
<Link | |||
activeClassName="active" | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to="/organizations/foo/rules" | |||
> | |||
coding_rules.page | |||
</Link> | |||
</li> | |||
<li> | |||
<Link | |||
activeClassName="active" | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to={ | |||
Object { | |||
"pathname": "/organizations/foo/quality_gates", | |||
} | |||
<li> | |||
<Link | |||
activeClassName="active" | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to="/organizations/foo/quality_profiles" | |||
> | |||
quality_profiles.page | |||
</Link> | |||
</li> | |||
<li> | |||
<Link | |||
activeClassName="active" | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to="/organizations/foo/rules" | |||
> | |||
coding_rules.page | |||
</Link> | |||
</li> | |||
<li> | |||
<Link | |||
activeClassName="active" | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to={ | |||
Object { | |||
"pathname": "/organizations/foo/quality_gates", | |||
} | |||
> | |||
quality_gates.page | |||
</Link> | |||
</li> | |||
</React.Fragment> | |||
} | |||
> | |||
quality_gates.page | |||
</Link> | |||
</li> | |||
<li> | |||
<Link | |||
activeClassName="active" | |||
@@ -125,42 +123,40 @@ exports[`renders for admin 1`] = ` | |||
issues.page | |||
</Link> | |||
</li> | |||
<React.Fragment> | |||
<li> | |||
<Link | |||
activeClassName="active" | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to="/organizations/foo/quality_profiles" | |||
> | |||
quality_profiles.page | |||
</Link> | |||
</li> | |||
<li> | |||
<Link | |||
activeClassName="active" | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to="/organizations/foo/rules" | |||
> | |||
coding_rules.page | |||
</Link> | |||
</li> | |||
<li> | |||
<Link | |||
activeClassName="active" | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to={ | |||
Object { | |||
"pathname": "/organizations/foo/quality_gates", | |||
} | |||
<li> | |||
<Link | |||
activeClassName="active" | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to="/organizations/foo/quality_profiles" | |||
> | |||
quality_profiles.page | |||
</Link> | |||
</li> | |||
<li> | |||
<Link | |||
activeClassName="active" | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to="/organizations/foo/rules" | |||
> | |||
coding_rules.page | |||
</Link> | |||
</li> | |||
<li> | |||
<Link | |||
activeClassName="active" | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to={ | |||
Object { | |||
"pathname": "/organizations/foo/quality_gates", | |||
} | |||
> | |||
quality_gates.page | |||
</Link> | |||
</li> | |||
</React.Fragment> | |||
} | |||
> | |||
quality_gates.page | |||
</Link> | |||
</li> | |||
<li> | |||
<Link | |||
activeClassName="active" |
@@ -53,6 +53,11 @@ const routes = [ | |||
{ indexRoute: { component: lazyLoad(() => import('../issues/components/AppContainer')) } } | |||
] | |||
}, | |||
{ | |||
path: 'rules', | |||
component: OrganizationContainer, | |||
childRoutes: codingRulesRoutes | |||
}, | |||
{ | |||
component: lazyLoad(() => | |||
import('./components/OrganizationAccessContainer').then(lib => ({ | |||
@@ -73,11 +78,6 @@ const routes = [ | |||
})) | |||
), | |||
childRoutes: [ | |||
{ | |||
path: 'rules', | |||
component: OrganizationContainer, | |||
childRoutes: codingRulesRoutes | |||
}, | |||
{ | |||
path: 'quality_profiles', | |||
childRoutes: qualityProfilesRoutes |
@@ -27,7 +27,7 @@ import BugsAndVulnerabilities from '../main/BugsAndVulnerabilities'; | |||
import CodeSmells from '../main/CodeSmells'; | |||
import Coverage from '../main/Coverage'; | |||
import Duplications from '../main/Duplications'; | |||
import Meta from '../meta/Meta'; | |||
import MetaContainer from '../meta/MetaContainer'; | |||
import throwGlobalError from '../../../app/utils/throwGlobalError'; | |||
import Suggestions from '../../../app/components/embed-docs-modal/Suggestions'; | |||
import { getMeasuresAndMeta } from '../../../api/measures'; | |||
@@ -257,7 +257,7 @@ export class OverviewApp extends React.PureComponent<Props, State> { | |||
{this.renderMain()} | |||
<div className="overview-sidebar page-sidebar-fixed"> | |||
<Meta | |||
<MetaContainer | |||
branchLike={branchLike} | |||
component={component} | |||
history={history} |
@@ -19,6 +19,7 @@ | |||
*/ | |||
import * as React from 'react'; | |||
import * as PropTypes from 'prop-types'; | |||
import { connect } from 'react-redux'; | |||
import MetaKey from './MetaKey'; | |||
import MetaOrganizationKey from './MetaOrganizationKey'; | |||
import MetaLinks from './MetaLinks'; | |||
@@ -28,13 +29,31 @@ import MetaSize from './MetaSize'; | |||
import MetaTags from './MetaTags'; | |||
import BadgesModal from '../badges/BadgesModal'; | |||
import AnalysesList from '../events/AnalysesList'; | |||
import { Visibility, Component, Metric, BranchLike } from '../../../app/types'; | |||
import { | |||
Visibility, | |||
Component, | |||
Metric, | |||
BranchLike, | |||
CurrentUser, | |||
Organization | |||
} from '../../../app/types'; | |||
import { History } from '../../../api/time-machine'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { MeasureEnhanced } from '../../../helpers/measures'; | |||
import { hasPrivateAccess } from '../../../helpers/organizations'; | |||
import { | |||
getCurrentUser, | |||
getMyOrganizations, | |||
getOrganizationByKey | |||
} from '../../../store/rootReducer'; | |||
interface Props { | |||
interface StateToProps { | |||
currentUser: CurrentUser; | |||
organization?: Organization; | |||
userOrganizations: Organization[]; | |||
} | |||
interface OwnProps { | |||
branchLike?: BranchLike; | |||
component: Component; | |||
history?: History; | |||
@@ -43,17 +62,23 @@ interface Props { | |||
onComponentChange: (changes: {}) => void; | |||
} | |||
export default class Meta extends React.PureComponent<Props> { | |||
type Props = OwnProps & StateToProps; | |||
export class Meta extends React.PureComponent<Props> { | |||
static contextTypes = { | |||
organizationsEnabled: PropTypes.bool | |||
}; | |||
renderQualityInfos() { | |||
const { organizationsEnabled } = this.context; | |||
const { organization, qualifier, qualityProfiles, qualityGate } = this.props.component; | |||
const { component, currentUser, organization, userOrganizations } = this.props; | |||
const { qualifier, qualityProfiles, qualityGate } = component; | |||
const isProject = qualifier === 'TRK'; | |||
if (!isProject || (organizationsEnabled && !hasPrivateAccess(organization))) { | |||
if ( | |||
!isProject || | |||
(organizationsEnabled && !hasPrivateAccess(currentUser, organization, userOrganizations)) | |||
) { | |||
return null; | |||
} | |||
@@ -61,7 +86,7 @@ export default class Meta extends React.PureComponent<Props> { | |||
<div className="overview-meta-card"> | |||
{qualityGate && ( | |||
<MetaQualityGate | |||
organization={organizationsEnabled ? organization : undefined} | |||
organization={organizationsEnabled ? component.organization : undefined} | |||
qualityGate={qualityGate} | |||
/> | |||
)} | |||
@@ -70,7 +95,7 @@ export default class Meta extends React.PureComponent<Props> { | |||
qualityProfiles.length > 0 && ( | |||
<MetaQualityProfiles | |||
headerClassName={qualityGate ? 'big-spacer-top' : undefined} | |||
organization={organizationsEnabled ? organization : undefined} | |||
organization={organizationsEnabled ? component.organization : undefined} | |||
profiles={qualityProfiles} | |||
/> | |||
)} | |||
@@ -130,3 +155,11 @@ export default class Meta extends React.PureComponent<Props> { | |||
); | |||
} | |||
} | |||
const mapStateToProps = (state: any, { component }: OwnProps) => ({ | |||
currentUser: getCurrentUser(state), | |||
organization: getOrganizationByKey(state, component.organization), | |||
userOrganizations: getMyOrganizations(state) | |||
}); | |||
export default connect<StateToProps, {}, OwnProps>(mapStateToProps)(Meta); |
@@ -17,8 +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 * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import React from 'react'; | |||
import MarkdownTips from '../MarkdownTips'; | |||
it('should render the tips', () => { |
@@ -19,13 +19,12 @@ | |||
*/ | |||
import * as React from 'react'; | |||
import { keyBy } from 'lodash'; | |||
import { getRuleDetails, getRulesApp } from '../../api/rules'; | |||
import { RuleDetails } from '../../app/types'; | |||
import DeferredSpinner from '../common/DeferredSpinner'; | |||
import RuleDetailsMeta from '../../apps/coding-rules/components/RuleDetailsMeta'; | |||
import RuleDetailsDescription from '../../apps/coding-rules/components/RuleDetailsDescription'; | |||
import { getRuleDetails, getRulesApp } from '../../api/rules'; | |||
import { RuleDetails } from '../../app/types'; | |||
import '../../apps/coding-rules/styles.css'; | |||
import { hasPrivateAccess } from '../../helpers/organizations'; | |||
interface Props { | |||
onLoad: (details: { name: string }) => void; | |||
@@ -96,7 +95,6 @@ export default class WorkspaceRuleDetails extends React.PureComponent<Props, Sta | |||
<> | |||
<RuleDetailsMeta | |||
canWrite={false} | |||
hidePermalink={!hasPrivateAccess(organizationKey)} | |||
hideSimilarRulesFilter={true} | |||
onFilterChange={this.noOp} | |||
onTagsChange={this.noOp} |
@@ -21,12 +21,6 @@ import * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import WorkspaceRuleDetails from '../WorkspaceRuleDetails'; | |||
import { waitAndUpdate } from '../../../helpers/testUtils'; | |||
import { OrganizationSubscription, Visibility } from '../../../app/types'; | |||
import { hasPrivateAccess } from '../../../helpers/organizations'; | |||
jest.mock('../../../helpers/organizations', () => ({ | |||
hasPrivateAccess: jest.fn().mockReturnValue(true) | |||
})); | |||
jest.mock('../../../api/rules', () => ({ | |||
getRulesApp: jest.fn(() => | |||
@@ -35,17 +29,6 @@ jest.mock('../../../api/rules', () => ({ | |||
getRuleDetails: jest.fn(() => Promise.resolve({ rule: { key: 'foo', name: 'Foo' } })) | |||
})); | |||
const organization = { | |||
key: 'foo', | |||
name: 'Foo', | |||
projectVisibility: Visibility.Public, | |||
subscription: OrganizationSubscription.Paid | |||
}; | |||
beforeEach(() => { | |||
(hasPrivateAccess as jest.Mock<any>).mockClear(); | |||
}); | |||
it('should render', async () => { | |||
const wrapper = shallow( | |||
<WorkspaceRuleDetails onLoad={jest.fn()} organizationKey={undefined} ruleKey="foo" /> | |||
@@ -64,13 +47,3 @@ it('should call back on load', async () => { | |||
await waitAndUpdate(wrapper); | |||
expect(onLoad).toBeCalledWith({ name: 'Foo' }); | |||
}); | |||
it('should render without permalink', async () => { | |||
(hasPrivateAccess as jest.Mock<any>).mockReturnValueOnce(false); | |||
const wrapper = shallow( | |||
<WorkspaceRuleDetails onLoad={jest.fn()} organizationKey={organization.key} ruleKey="foo" /> | |||
); | |||
await waitAndUpdate(wrapper); | |||
expect(wrapper.find('RuleDetailsMeta').prop('hidePermalink')).toBeTruthy(); | |||
}); |
@@ -15,7 +15,6 @@ exports[`should render 2`] = ` | |||
<React.Fragment> | |||
<RuleDetailsMeta | |||
canWrite={false} | |||
hidePermalink={false} | |||
hideSimilarRulesFilter={true} | |||
onFilterChange={[Function]} | |||
onTagsChange={[Function]} |
@@ -18,67 +18,40 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { hasPrivateAccess, isCurrentUserMemberOf } from '../organizations'; | |||
import { getCurrentUser, getMyOrganizations } from '../../store/rootReducer'; | |||
import { OrganizationSubscription } from '../../app/types'; | |||
jest.mock('../../app/utils/getStore', () => ({ | |||
default: () => ({ | |||
getState: jest.fn() | |||
}) | |||
})); | |||
const org = { key: 'foo', name: 'Foo', subscription: OrganizationSubscription.Paid }; | |||
const adminOrg = { key: 'bar', name: 'Bar', canAdmin: true }; | |||
const randomOrg = { key: 'bar', name: 'Bar' }; | |||
jest.mock('../../store/rootReducer', () => ({ | |||
getCurrentUser: jest.fn().mockReturnValue({ | |||
isLoggedIn: true, | |||
login: 'luke', | |||
name: 'Skywalker', | |||
showOnboardingTutorial: false | |||
}), | |||
getMyOrganizations: jest.fn().mockReturnValue([]) | |||
})); | |||
const organization = { | |||
key: 'foo', | |||
name: 'Foo', | |||
subscription: OrganizationSubscription.Paid | |||
const loggedIn = { | |||
isLoggedIn: true, | |||
login: 'luke', | |||
name: 'Skywalker' | |||
}; | |||
const loggedOut = { isLoggedIn: false }; | |||
beforeEach(() => { | |||
(getCurrentUser as jest.Mock<any>).mockClear(); | |||
(getMyOrganizations as jest.Mock<any>).mockClear(); | |||
}); | |||
describe('isCurrentUserMemberOf', () => { | |||
it('should be a member', () => { | |||
expect(isCurrentUserMemberOf({ key: 'bar', name: 'Bar', canAdmin: true })).toBeTruthy(); | |||
(getMyOrganizations as jest.Mock<any>).mockReturnValueOnce([organization]); | |||
expect(isCurrentUserMemberOf(organization)).toBeTruthy(); | |||
expect(isCurrentUserMemberOf(loggedIn, adminOrg, [])).toBeTruthy(); | |||
expect(isCurrentUserMemberOf(loggedIn, org, [org])).toBeTruthy(); | |||
}); | |||
it('should not be a member', () => { | |||
expect(isCurrentUserMemberOf(undefined)).toBeFalsy(); | |||
expect(isCurrentUserMemberOf(organization)).toBeFalsy(); | |||
(getMyOrganizations as jest.Mock<any>).mockReturnValueOnce([{ key: 'bar', name: 'Bar' }]); | |||
expect(isCurrentUserMemberOf(organization)).toBeFalsy(); | |||
(getCurrentUser as jest.Mock<any>).mockReturnValueOnce(loggedOut); | |||
expect(isCurrentUserMemberOf(organization)).toBeFalsy(); | |||
expect(isCurrentUserMemberOf(loggedIn, undefined, [])).toBeFalsy(); | |||
expect(isCurrentUserMemberOf(loggedIn, org, [])).toBeFalsy(); | |||
expect(isCurrentUserMemberOf(loggedIn, org, [randomOrg])).toBeFalsy(); | |||
expect(isCurrentUserMemberOf(loggedOut, org, [org])).toBeFalsy(); | |||
}); | |||
}); | |||
describe('hasPrivateAccess', () => { | |||
it('should have access', () => { | |||
expect(hasPrivateAccess({ key: 'bar', name: 'Bar' })).toBeTruthy(); | |||
(getMyOrganizations as jest.Mock<any>).mockReturnValueOnce([organization]); | |||
expect(hasPrivateAccess(organization)).toBeTruthy(); | |||
expect(hasPrivateAccess(loggedIn, randomOrg, [])).toBeTruthy(); | |||
expect(hasPrivateAccess(loggedIn, org, [org])).toBeTruthy(); | |||
}); | |||
it('should not have access', () => { | |||
expect(hasPrivateAccess(organization)).toBeFalsy(); | |||
expect(hasPrivateAccess(loggedIn, org, [])).toBeFalsy(); | |||
}); | |||
}); |
@@ -17,40 +17,31 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { Organization, isLoggedIn, OrganizationSubscription, CurrentUser } from '../app/types'; | |||
import getStore from '../app/utils/getStore'; | |||
import { Organization, isLoggedIn, OrganizationSubscription } from '../app/types'; | |||
import { getCurrentUser, getMyOrganizations, getOrganizationByKey } from '../store/rootReducer'; | |||
function getRealOrganization( | |||
organization?: Organization | string, | |||
state?: any | |||
): Organization | undefined { | |||
if (typeof organization === 'string') { | |||
state = state || getStore().getState(); | |||
return getOrganizationByKey(state, organization); | |||
} | |||
return organization; | |||
} | |||
function isPaidOrganization(organization: Organization | undefined): boolean { | |||
export function isPaidOrganization(organization: Organization | undefined): boolean { | |||
return Boolean(organization && organization.subscription === OrganizationSubscription.Paid); | |||
} | |||
export function hasPrivateAccess(organization: Organization | string | undefined): boolean { | |||
const realOrg = getRealOrganization(organization); | |||
return !isPaidOrganization(realOrg) || isCurrentUserMemberOf(realOrg); | |||
export function hasPrivateAccess( | |||
currentUser: CurrentUser, | |||
organization: Organization | undefined, | |||
userOrganizations: Organization[] | |||
): boolean { | |||
return ( | |||
!isPaidOrganization(organization) || | |||
isCurrentUserMemberOf(currentUser, organization, userOrganizations) | |||
); | |||
} | |||
export function isCurrentUserMemberOf(organization: Organization | string | undefined): boolean { | |||
const state = getStore().getState(); | |||
const currentUser = getCurrentUser(state); | |||
const userOrganizations = getMyOrganizations(state); | |||
const realOrg = getRealOrganization(organization, state); | |||
export function isCurrentUserMemberOf( | |||
currentUser: CurrentUser, | |||
organization: Organization | undefined, | |||
userOrganizations: Organization[] | |||
): boolean { | |||
return Boolean( | |||
realOrg && | |||
organization && | |||
isLoggedIn(currentUser) && | |||
(realOrg.canAdmin || userOrganizations.some(org => org.key === realOrg.key)) | |||
(organization.canAdmin || userOrganizations.some(org => org.key === organization.key)) | |||
); | |||
} |