diff options
Diffstat (limited to 'server/sonar-web/src/main')
21 files changed, 297 insertions, 303 deletions
diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/App.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/App.tsx index 04a60d87c0a..19f179aca95 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/App.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/App.tsx @@ -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); diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/FacetsList.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/FacetsList.tsx index e384fcb6e16..9eb8edb79ed 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/FacetsList.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/FacetsList.tsx @@ -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> ); } diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetails.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetails.tsx index 21481a1f240..4521cc0d810 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetails.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetails.tsx @@ -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} /> diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsMeta.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsMeta.tsx index def6cd3f6d5..110956c04fb 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsMeta.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsMeta.tsx @@ -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} /> )} diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/RuleDetailsMeta-test.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/RuleDetailsMeta-test.tsx index fd496d8f03e..1380e6c0bfc 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/RuleDetailsMeta-test.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/RuleDetailsMeta-test.tsx @@ -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 diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/AssigneeFacet.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/AssigneeFacet.tsx index b4896bafea6..26d0204509e 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/AssigneeFacet.tsx +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/AssigneeFacet.tsx @@ -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 }) => { diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.tsx index c850ffa0750..77611af9140 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.tsx +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.tsx @@ -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} /> diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/TagFacet.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/TagFacet.tsx index dab0d43d298..91e30bf9de5 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/TagFacet.tsx +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/TagFacet.tsx @@ -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 })) ); }; diff --git a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationAccessContainer.tsx b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationAccessContainer.tsx index 2b5e9f301d4..4cef029f3df 100644 --- a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationAccessContainer.tsx +++ b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationAccessContainer.tsx @@ -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; diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationMenuContainer.tsx b/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationMenuContainer.tsx index 1fda2012f13..07ee53cd586 100644 --- a/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationMenuContainer.tsx +++ b/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationMenuContainer.tsx @@ -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} /> diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationMenuContainer-test.tsx.snap b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationMenuContainer-test.tsx.snap index a51bf8cc513..1177b7604b7 100644 --- a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationMenuContainer-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationMenuContainer-test.tsx.snap @@ -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" diff --git a/server/sonar-web/src/main/js/apps/organizations/routes.ts b/server/sonar-web/src/main/js/apps/organizations/routes.ts index 18aa7c3f4ba..24cd7999dcd 100644 --- a/server/sonar-web/src/main/js/apps/organizations/routes.ts +++ b/server/sonar-web/src/main/js/apps/organizations/routes.ts @@ -54,6 +54,11 @@ const routes = [ ] }, { + path: 'rules', + component: OrganizationContainer, + childRoutes: codingRulesRoutes + }, + { component: lazyLoad(() => import('./components/OrganizationAccessContainer').then(lib => ({ default: lib.OrganizationMembersAccess @@ -74,11 +79,6 @@ const routes = [ ), childRoutes: [ { - path: 'rules', - component: OrganizationContainer, - childRoutes: codingRulesRoutes - }, - { path: 'quality_profiles', childRoutes: qualityProfilesRoutes }, diff --git a/server/sonar-web/src/main/js/apps/overview/components/OverviewApp.tsx b/server/sonar-web/src/main/js/apps/overview/components/OverviewApp.tsx index df32445c412..402725bdc71 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/OverviewApp.tsx +++ b/server/sonar-web/src/main/js/apps/overview/components/OverviewApp.tsx @@ -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} diff --git a/server/sonar-web/src/main/js/apps/overview/meta/Meta.tsx b/server/sonar-web/src/main/js/apps/overview/meta/MetaContainer.tsx index 5eb22f4735d..62ee5647128 100644 --- a/server/sonar-web/src/main/js/apps/overview/meta/Meta.tsx +++ b/server/sonar-web/src/main/js/apps/overview/meta/MetaContainer.tsx @@ -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); diff --git a/server/sonar-web/src/main/js/components/common/__tests__/MarkdownTips-test.js b/server/sonar-web/src/main/js/components/common/__tests__/MarkdownTips-test.tsx index 48da22f6eb0..5b0dfe18c18 100644 --- a/server/sonar-web/src/main/js/components/common/__tests__/MarkdownTips-test.js +++ b/server/sonar-web/src/main/js/components/common/__tests__/MarkdownTips-test.tsx @@ -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', () => { diff --git a/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/MarkdownTips-test.js.snap b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/MarkdownTips-test.tsx.snap index baeceb6aa90..baeceb6aa90 100644 --- a/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/MarkdownTips-test.js.snap +++ b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/MarkdownTips-test.tsx.snap diff --git a/server/sonar-web/src/main/js/components/workspace/WorkspaceRuleDetails.tsx b/server/sonar-web/src/main/js/components/workspace/WorkspaceRuleDetails.tsx index 647f4a4fade..a1a62dcb3e5 100644 --- a/server/sonar-web/src/main/js/components/workspace/WorkspaceRuleDetails.tsx +++ b/server/sonar-web/src/main/js/components/workspace/WorkspaceRuleDetails.tsx @@ -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} diff --git a/server/sonar-web/src/main/js/components/workspace/__tests__/WorkspaceRuleDetails-test.tsx b/server/sonar-web/src/main/js/components/workspace/__tests__/WorkspaceRuleDetails-test.tsx index e755c5686d5..13ff053fcc3 100644 --- a/server/sonar-web/src/main/js/components/workspace/__tests__/WorkspaceRuleDetails-test.tsx +++ b/server/sonar-web/src/main/js/components/workspace/__tests__/WorkspaceRuleDetails-test.tsx @@ -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(); -}); diff --git a/server/sonar-web/src/main/js/components/workspace/__tests__/__snapshots__/WorkspaceRuleDetails-test.tsx.snap b/server/sonar-web/src/main/js/components/workspace/__tests__/__snapshots__/WorkspaceRuleDetails-test.tsx.snap index 566b4a80db2..3b316292cb5 100644 --- a/server/sonar-web/src/main/js/components/workspace/__tests__/__snapshots__/WorkspaceRuleDetails-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/workspace/__tests__/__snapshots__/WorkspaceRuleDetails-test.tsx.snap @@ -15,7 +15,6 @@ exports[`should render 2`] = ` <React.Fragment> <RuleDetailsMeta canWrite={false} - hidePermalink={false} hideSimilarRulesFilter={true} onFilterChange={[Function]} onTagsChange={[Function]} diff --git a/server/sonar-web/src/main/js/helpers/__tests__/organizations-test.ts b/server/sonar-web/src/main/js/helpers/__tests__/organizations-test.ts index 17429129955..a483c995c6c 100644 --- a/server/sonar-web/src/main/js/helpers/__tests__/organizations-test.ts +++ b/server/sonar-web/src/main/js/helpers/__tests__/organizations-test.ts @@ -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(); }); }); diff --git a/server/sonar-web/src/main/js/helpers/organizations.ts b/server/sonar-web/src/main/js/helpers/organizations.ts index 8489cb9ed12..3d57db7ff3b 100644 --- a/server/sonar-web/src/main/js/helpers/organizations.ts +++ b/server/sonar-web/src/main/js/helpers/organizations.ts @@ -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)) ); } |