Explorar el Código

SONAR-10945 Hide QP in rules pages

* SONAR-11003 Always set organization parameter in api/rules/search
* SONAR-11002 Show rules and hide quality profiles inside rules page
tags/7.5
Grégoire Aubert hace 6 años
padre
commit
df81c0dd7c
Se han modificado 21 ficheros con 297 adiciones y 303 borrados
  1. 50
    18
      server/sonar-web/src/main/js/apps/coding-rules/components/App.tsx
  2. 32
    28
      server/sonar-web/src/main/js/apps/coding-rules/components/FacetsList.tsx
  3. 20
    12
      server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetails.tsx
  4. 9
    11
      server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsMeta.tsx
  5. 0
    8
      server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/RuleDetailsMeta-test.tsx
  6. 2
    6
      server/sonar-web/src/main/js/apps/issues/sidebar/AssigneeFacet.tsx
  7. 7
    3
      server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.tsx
  8. 2
    6
      server/sonar-web/src/main/js/apps/issues/sidebar/TagFacet.tsx
  9. 6
    2
      server/sonar-web/src/main/js/apps/organizations/components/OrganizationAccessContainer.tsx
  10. 19
    22
      server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationMenuContainer.tsx
  11. 66
    70
      server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationMenuContainer-test.tsx.snap
  12. 5
    5
      server/sonar-web/src/main/js/apps/organizations/routes.ts
  13. 2
    2
      server/sonar-web/src/main/js/apps/overview/components/OverviewApp.tsx
  14. 40
    7
      server/sonar-web/src/main/js/apps/overview/meta/MetaContainer.tsx
  15. 1
    1
      server/sonar-web/src/main/js/components/common/__tests__/MarkdownTips-test.tsx
  16. 0
    0
      server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/MarkdownTips-test.tsx.snap
  17. 2
    4
      server/sonar-web/src/main/js/components/workspace/WorkspaceRuleDetails.tsx
  18. 0
    27
      server/sonar-web/src/main/js/components/workspace/__tests__/WorkspaceRuleDetails-test.tsx
  19. 0
    1
      server/sonar-web/src/main/js/components/workspace/__tests__/__snapshots__/WorkspaceRuleDetails-test.tsx.snap
  20. 16
    43
      server/sonar-web/src/main/js/helpers/__tests__/organizations-test.ts
  21. 18
    27
      server/sonar-web/src/main/js/helpers/organizations.ts

+ 50
- 18
server/sonar-web/src/main/js/apps/coding-rules/components/App.tsx Ver fichero

@@ -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);

+ 32
- 28
server/sonar-web/src/main/js/apps/coding-rules/components/FacetsList.tsx Ver fichero

@@ -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>
);
}

+ 20
- 12
server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetails.tsx Ver fichero

@@ -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} />

+ 9
- 11
server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsMeta.tsx Ver fichero

@@ -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} />
)}

+ 0
- 8
server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/RuleDetailsMeta-test.tsx Ver fichero

@@ -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

+ 2
- 6
server/sonar-web/src/main/js/apps/issues/sidebar/AssigneeFacet.tsx Ver fichero

@@ -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 }) => {

+ 7
- 3
server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.tsx Ver fichero

@@ -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}
/>

+ 2
- 6
server/sonar-web/src/main/js/apps/issues/sidebar/TagFacet.tsx Ver fichero

@@ -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 }))
);
};

+ 6
- 2
server/sonar-web/src/main/js/apps/organizations/components/OrganizationAccessContainer.tsx Ver fichero

@@ -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;

+ 19
- 22
server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationMenuContainer.tsx Ver fichero

@@ -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} />

+ 66
- 70
server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationMenuContainer-test.tsx.snap Ver fichero

@@ -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"

+ 5
- 5
server/sonar-web/src/main/js/apps/organizations/routes.ts Ver fichero

@@ -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

+ 2
- 2
server/sonar-web/src/main/js/apps/overview/components/OverviewApp.tsx Ver fichero

@@ -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}

server/sonar-web/src/main/js/apps/overview/meta/Meta.tsx → server/sonar-web/src/main/js/apps/overview/meta/MetaContainer.tsx Ver fichero

@@ -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);

server/sonar-web/src/main/js/components/common/__tests__/MarkdownTips-test.js → server/sonar-web/src/main/js/components/common/__tests__/MarkdownTips-test.tsx Ver fichero

@@ -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', () => {

server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/MarkdownTips-test.js.snap → server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/MarkdownTips-test.tsx.snap Ver fichero


+ 2
- 4
server/sonar-web/src/main/js/components/workspace/WorkspaceRuleDetails.tsx Ver fichero

@@ -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}

+ 0
- 27
server/sonar-web/src/main/js/components/workspace/__tests__/WorkspaceRuleDetails-test.tsx Ver fichero

@@ -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();
});

+ 0
- 1
server/sonar-web/src/main/js/components/workspace/__tests__/__snapshots__/WorkspaceRuleDetails-test.tsx.snap Ver fichero

@@ -15,7 +15,6 @@ exports[`should render 2`] = `
<React.Fragment>
<RuleDetailsMeta
canWrite={false}
hidePermalink={false}
hideSimilarRulesFilter={true}
onFilterChange={[Function]}
onTagsChange={[Function]}

+ 16
- 43
server/sonar-web/src/main/js/helpers/__tests__/organizations-test.ts Ver fichero

@@ -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();
});
});

+ 18
- 27
server/sonar-web/src/main/js/helpers/organizations.ts Ver fichero

@@ -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))
);
}

Cargando…
Cancelar
Guardar