Parcourir la source

SONAR-11271 Add new permissions and update layout to group them

tags/7.5
Grégoire Aubert il y a 5 ans
Parent
révision
7f339fd2f1
23 fichiers modifiés avec 946 ajouts et 726 suppressions
  1. 11
    4
      server/sonar-web/src/main/js/app/types.ts
  2. 8
    7
      server/sonar-web/src/main/js/apps/permission-templates/components/Template.js
  3. 47
    31
      server/sonar-web/src/main/js/apps/permissions/global/components/AllHoldersList.tsx
  4. 13
    38
      server/sonar-web/src/main/js/apps/permissions/global/components/App.tsx
  5. 18
    26
      server/sonar-web/src/main/js/apps/permissions/project/components/AllHoldersList.tsx
  6. 63
    57
      server/sonar-web/src/main/js/apps/permissions/project/components/App.tsx
  7. 0
    39
      server/sonar-web/src/main/js/apps/permissions/project/constants.tsx
  8. 16
    22
      server/sonar-web/src/main/js/apps/permissions/shared/components/GroupHolder.tsx
  9. 42
    56
      server/sonar-web/src/main/js/apps/permissions/shared/components/HoldersList.tsx
  10. 74
    0
      server/sonar-web/src/main/js/apps/permissions/shared/components/PermissionCell.tsx
  11. 58
    30
      server/sonar-web/src/main/js/apps/permissions/shared/components/PermissionHeader.tsx
  12. 18
    22
      server/sonar-web/src/main/js/apps/permissions/shared/components/UserHolder.tsx
  13. 12
    6
      server/sonar-web/src/main/js/apps/permissions/shared/components/__tests__/GroupHolder-test.tsx
  14. 10
    1
      server/sonar-web/src/main/js/apps/permissions/shared/components/__tests__/HoldersList-test.tsx
  15. 89
    0
      server/sonar-web/src/main/js/apps/permissions/shared/components/__tests__/PermissionCell-test.tsx
  16. 12
    6
      server/sonar-web/src/main/js/apps/permissions/shared/components/__tests__/UserHolder-test.tsx
  17. 43
    153
      server/sonar-web/src/main/js/apps/permissions/shared/components/__tests__/__snapshots__/GroupHolder-test.tsx.snap
  18. 178
    61
      server/sonar-web/src/main/js/apps/permissions/shared/components/__tests__/__snapshots__/HoldersList-test.tsx.snap
  19. 84
    0
      server/sonar-web/src/main/js/apps/permissions/shared/components/__tests__/__snapshots__/PermissionCell-test.tsx.snap
  20. 43
    153
      server/sonar-web/src/main/js/apps/permissions/shared/components/__tests__/__snapshots__/UserHolder-test.tsx.snap
  21. 11
    9
      server/sonar-web/src/main/js/apps/permissions/styles.css
  22. 87
    0
      server/sonar-web/src/main/js/apps/permissions/utils.ts
  23. 9
    5
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 11
- 4
server/sonar-web/src/main/js/app/types.ts Voir le fichier

PreviousVersion = 'previous_version' PreviousVersion = 'previous_version'
} }


export interface Permission {
export interface PermissionDefinition {
key: string; key: string;
name: string; name: string;
description: string; description: string;
} }


export type PermissionDefinitions = Array<PermissionDefinition | PermissionDefinitionGroup>;

export interface PermissionDefinitionGroup {
category: string;
permissions: PermissionDefinition[];
}

export interface PermissionGroup { export interface PermissionGroup {
description?: string;
id?: string; id?: string;
name: string; name: string;
description?: string;
permissions: string[]; permissions: string[];
} }


export interface PermissionUser { export interface PermissionUser {
avatar?: string;
email?: string;
login: string; login: string;
name: string; name: string;
email?: string;
permissions: string[]; permissions: string[];
avatar?: string;
} }


export interface PermissionTemplate { export interface PermissionTemplate {

+ 8
- 7
server/sonar-web/src/main/js/apps/permission-templates/components/Template.js Voir le fichier

import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Helmet from 'react-helmet'; import Helmet from 'react-helmet';
import { debounce } from 'lodash';
import TemplateHeader from './TemplateHeader'; import TemplateHeader from './TemplateHeader';
import TemplateDetails from './TemplateDetails'; import TemplateDetails from './TemplateDetails';
import HoldersList from '../../permissions/shared/components/HoldersList'; import HoldersList from '../../permissions/shared/components/HoldersList';
import SearchForm from '../../permissions/shared/components/SearchForm'; import SearchForm from '../../permissions/shared/components/SearchForm';
import { PERMISSIONS_ORDER_FOR_PROJECT } from '../../permissions/project/constants';
import {
convertToPermissionDefinitions,
PERMISSIONS_ORDER_FOR_PROJECT_TEMPLATE
} from '../../permissions/utils';
import * as api from '../../../api/permissions'; import * as api from '../../../api/permissions';
import { translate } from '../../../helpers/l10n'; import { translate } from '../../../helpers/l10n';


}; };


render() { render() {
const permissions = PERMISSIONS_ORDER_FOR_PROJECT.map(p => ({
key: p,
name: translate('projects_role', p),
description: translate('projects_role', p, 'desc')
}));
const permissions = convertToPermissionDefinitions(
PERMISSIONS_ORDER_FOR_PROJECT_TEMPLATE,
'projects_role'
);


const allUsers = [...this.state.users]; const allUsers = [...this.state.users];



+ 47
- 31
server/sonar-web/src/main/js/apps/permissions/global/components/AllHoldersList.tsx Voir le fichier

* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/ */
import * as React from 'react'; import * as React from 'react';
import { connect } from 'react-redux';
import SearchForm from '../../shared/components/SearchForm'; import SearchForm from '../../shared/components/SearchForm';
import HoldersList from '../../shared/components/HoldersList'; import HoldersList from '../../shared/components/HoldersList';
import { translate } from '../../../../helpers/l10n';
import { Organization, Paging, PermissionGroup, PermissionUser } from '../../../../app/types';
import ListFooter from '../../../../components/controls/ListFooter'; import ListFooter from '../../../../components/controls/ListFooter';
import {
AppState,
Organization,
Paging,
PermissionGroup,
PermissionUser
} from '../../../../app/types';
import {
PERMISSIONS_ORDER_GLOBAL,
convertToPermissionDefinitions,
PERMISSIONS_ORDER_GLOBAL_GOV
} from '../../utils';
import { Store, getAppState } from '../../../../store/rootReducer';


const PERMISSIONS_ORDER = ['admin', 'profileadmin', 'gateadmin', 'scan', 'provisioning'];
interface StateProps {
appState: Pick<AppState, 'qualifiers'>;
}


interface Props {
interface OwnProps {
filter: string; filter: string;
grantPermissionToGroup: (groupName: string, permission: string) => Promise<void>; grantPermissionToGroup: (groupName: string, permission: string) => Promise<void>;
grantPermissionToUser: (login: string, permission: string) => Promise<void>; grantPermissionToUser: (login: string, permission: string) => Promise<void>;
groups: PermissionGroup[]; groups: PermissionGroup[];
groupsPaging: Paging;
groupsPaging?: Paging;
loadHolders: () => void; loadHolders: () => void;
loading?: boolean; loading?: boolean;
onLoadMore: (usersPageIndex: number, groupsPageIndex: number) => void;
onLoadMore: () => void;
onFilter: (filter: string) => void; onFilter: (filter: string) => void;
onSearch: (query: string) => void; onSearch: (query: string) => void;
onSelectPermission: (permission: string) => void;
organization?: Organization; organization?: Organization;
query: string; query: string;
revokePermissionFromGroup: (groupName: string, permission: string) => Promise<void>; revokePermissionFromGroup: (groupName: string, permission: string) => Promise<void>;
revokePermissionFromUser: (login: string, permission: string) => Promise<void>; revokePermissionFromUser: (login: string, permission: string) => Promise<void>;
selectedPermission?: string;
users: PermissionUser[]; users: PermissionUser[];
usersPaging: Paging;
usersPaging?: Paging;
} }


export default class AllHoldersList extends React.PureComponent<Props> {
type Props = StateProps & OwnProps;

export class AllHoldersList extends React.PureComponent<Props> {
handleToggleUser = (user: PermissionUser, permission: string) => { handleToggleUser = (user: PermissionUser, permission: string) => {
const hasPermission = user.permissions.includes(permission); const hasPermission = user.permissions.includes(permission);
if (hasPermission) { if (hasPermission) {
} }
}; };


handleLoadMore = () => {
this.props.onLoadMore(
this.props.usersPaging.pageIndex + 1,
this.props.groupsPaging.pageIndex + 1
);
};

render() { render() {
const { filter, groups, groupsPaging, users, usersPaging } = this.props;
const l10nPrefix = this.props.organization ? 'organizations_permissions' : 'global_permissions'; const l10nPrefix = this.props.organization ? 'organizations_permissions' : 'global_permissions';
const permissions = PERMISSIONS_ORDER.map(p => ({
key: p,
name: translate(l10nPrefix, p),
description: translate(l10nPrefix, p, 'desc')
}));
const governanceInstalled = this.props.appState.qualifiers.includes('VW');
const permissions = convertToPermissionDefinitions(
governanceInstalled ? PERMISSIONS_ORDER_GLOBAL_GOV : PERMISSIONS_ORDER_GLOBAL,
l10nPrefix
);


const count =
(this.props.filter !== 'users' ? this.props.groups.length : 0) +
(this.props.filter !== 'groups' ? this.props.users.length : 0);
const total =
(this.props.filter !== 'users' ? this.props.groupsPaging.total : 0) +
(this.props.filter !== 'groups' ? this.props.usersPaging.total : 0);
let count = 0;
let total = 0;
if (filter !== 'users') {
count += groups.length;
total += groupsPaging ? groupsPaging.total : groups.length;
}
if (filter !== 'groups') {
count += users.length;
total += usersPaging ? usersPaging.total : users.length;
}


return ( return (
<> <>
<HoldersList <HoldersList
groups={this.props.groups} groups={this.props.groups}
loading={this.props.loading} loading={this.props.loading}
onSelectPermission={this.props.onSelectPermission}
onToggleGroup={this.handleToggleGroup} onToggleGroup={this.handleToggleGroup}
onToggleUser={this.handleToggleUser} onToggleUser={this.handleToggleUser}
permissions={permissions} permissions={permissions}
selectedPermission={this.props.selectedPermission}
users={this.props.users}> users={this.props.users}>
<SearchForm <SearchForm
filter={this.props.filter} filter={this.props.filter}
query={this.props.query} query={this.props.query}
/> />
</HoldersList> </HoldersList>
<ListFooter count={count} loadMore={this.handleLoadMore} total={total} />
<ListFooter count={count} loadMore={this.props.onLoadMore} total={total} />
</> </>
); );
} }
} }

const mapStateToProps = (state: Store): StateProps => ({
appState: getAppState(state)
});

export default connect(mapStateToProps)(AllHoldersList);

+ 13
- 38
server/sonar-web/src/main/js/apps/permissions/global/components/App.tsx Voir le fichier

} }


interface State { interface State {
filter: string;
filter: 'all' | 'groups' | 'users';
groups: PermissionGroup[]; groups: PermissionGroup[];
groupsPaging: Paging;
groupsPaging?: Paging;
loading: boolean; loading: boolean;
query: string; query: string;
selectedPermission?: string;
users: PermissionUser[]; users: PermissionUser[];
usersPaging: Paging;
usersPaging?: Paging;
} }


export class App extends React.PureComponent<Props, State> { export class App extends React.PureComponent<Props, State> {
this.state = { this.state = {
filter: 'all', filter: 'all',
groups: [], groups: [],
groupsPaging: { pageIndex: 1, pageSize: 100, total: 0 },
loading: true, loading: true,
query: '', query: '',
users: [],
usersPaging: { pageIndex: 1, pageSize: 100, total: 0 }
users: []
}; };
} }




loadUsersAndGroups = (userPage?: number, groupsPage?: number) => { loadUsersAndGroups = (userPage?: number, groupsPage?: number) => {
const { organization } = this.props; const { organization } = this.props;
const { filter, query, selectedPermission } = this.state;
const { filter, query } = this.state;


const getUsers =
const getUsers: Promise<{ paging?: Paging; users: PermissionUser[] }> =
filter !== 'groups' filter !== 'groups'
? api.getGlobalPermissionsUsers({ ? api.getGlobalPermissionsUsers({
q: query || undefined, q: query || undefined,
permission: selectedPermission,
organization: organization && organization.key, organization: organization && organization.key,
p: userPage p: userPage
}) })
: Promise.resolve({
paging: {
pageIndex: 1,
pageSize: 100,
total: 0
},
users: []
});
: Promise.resolve({ paging: undefined, users: [] });


const getGroups =
const getGroups: Promise<{ paging?: Paging; groups: PermissionGroup[] }> =
filter !== 'users' filter !== 'users'
? api.getGlobalPermissionsGroups({ ? api.getGlobalPermissionsGroups({
q: query || undefined, q: query || undefined,
permission: selectedPermission,
organization: organization && organization.key, organization: organization && organization.key,
p: groupsPage p: groupsPage
}) })
: Promise.resolve({
paging: { pageIndex: 1, pageSize: 100, total: 0 },
groups: []
});
: Promise.resolve({ paging: undefined, groups: [] });


return Promise.all([getUsers, getGroups]); return Promise.all([getUsers, getGroups]);
}; };
}; };


onLoadMore = () => { onLoadMore = () => {
const { usersPaging, groupsPaging } = this.state;
this.setState({ loading: true }); this.setState({ loading: true });
return this.loadUsersAndGroups( return this.loadUsersAndGroups(
this.state.usersPaging.pageIndex + 1,
this.state.groupsPaging.pageIndex + 1
usersPaging ? usersPaging.pageIndex + 1 : 1,
groupsPaging ? groupsPaging.pageIndex + 1 : 1
).then(([usersResponse, groupsResponse]) => { ).then(([usersResponse, groupsResponse]) => {
if (this.mounted) { if (this.mounted) {
this.setState(({ groups, users }) => ({ this.setState(({ groups, users }) => ({
}, this.stopLoading); }, this.stopLoading);
}; };


onFilter = (filter: string) => {
onFilter = (filter: 'all' | 'groups' | 'users') => {
this.setState({ filter }, this.loadHolders); this.setState({ filter }, this.loadHolders);
}; };


this.setState({ query }, this.loadHolders); this.setState({ query }, this.loadHolders);
}; };


onSelectPermission = (permission: string) => {
this.setState(
({ selectedPermission }) => ({
selectedPermission: selectedPermission !== permission ? permission : undefined
}),
this.loadHolders
);
};

addPermissionToGroup = (groups: PermissionGroup[], group: string, permission: string) => { addPermissionToGroup = (groups: PermissionGroup[], group: string, permission: string) => {
return groups.map( return groups.map(
candidate => candidate =>
onFilter={this.onFilter} onFilter={this.onFilter}
onLoadMore={this.onLoadMore} onLoadMore={this.onLoadMore}
onSearch={this.onSearch} onSearch={this.onSearch}
onSelectPermission={this.onSelectPermission}
query={this.state.query} query={this.state.query}
revokePermissionFromGroup={this.revokePermissionFromGroup} revokePermissionFromGroup={this.revokePermissionFromGroup}
revokePermissionFromUser={this.revokePermissionFromUser} revokePermissionFromUser={this.revokePermissionFromUser}
selectedPermission={this.state.selectedPermission}
users={this.state.users} users={this.state.users}
usersPaging={this.state.usersPaging} usersPaging={this.state.usersPaging}
/> />

+ 18
- 26
server/sonar-web/src/main/js/apps/permissions/project/components/AllHoldersList.tsx Voir le fichier

import { without } from 'lodash'; import { without } from 'lodash';
import SearchForm from '../../shared/components/SearchForm'; import SearchForm from '../../shared/components/SearchForm';
import HoldersList from '../../shared/components/HoldersList'; import HoldersList from '../../shared/components/HoldersList';
import { translate } from '../../../../helpers/l10n';
import { PERMISSIONS_ORDER_BY_QUALIFIER } from '../constants';
import ListFooter from '../../../../components/controls/ListFooter';
import { PERMISSIONS_ORDER_BY_QUALIFIER, convertToPermissionDefinitions } from '../../utils';
import { import {
Component, Component,
Paging, Paging,
PermissionUser, PermissionUser,
Visibility Visibility
} from '../../../../app/types'; } from '../../../../app/types';
import ListFooter from '../../../../components/controls/ListFooter';


interface Props { interface Props {
component: Component; component: Component;
grantPermissionToGroup: (group: string, permission: string) => Promise<void>; grantPermissionToGroup: (group: string, permission: string) => Promise<void>;
grantPermissionToUser: (user: string, permission: string) => Promise<void>; grantPermissionToUser: (user: string, permission: string) => Promise<void>;
groups: PermissionGroup[]; groups: PermissionGroup[];
groupsPaging: Paging;
onLoadMore: (usersPageIndex: number, groupsPageIndex: number) => void;
groupsPaging?: Paging;
onLoadMore: () => void;
onFilterChange: (filter: string) => void; onFilterChange: (filter: string) => void;
onPermissionSelect: (permissions?: string) => void; onPermissionSelect: (permissions?: string) => void;
onQueryChange: (query: string) => void; onQueryChange: (query: string) => void;
revokePermissionFromUser: (user: string, permission: string) => Promise<void>; revokePermissionFromUser: (user: string, permission: string) => Promise<void>;
selectedPermission?: string; selectedPermission?: string;
users: PermissionUser[]; users: PermissionUser[];
usersPaging: Paging;
usersPaging?: Paging;
visibility?: Visibility; visibility?: Visibility;
} }


this.props.onPermissionSelect(permission); this.props.onPermissionSelect(permission);
}; };


handleLoadMore = () => {
this.props.onLoadMore(
this.props.usersPaging.pageIndex + 1,
this.props.groupsPaging.pageIndex + 1
);
};

render() { render() {
const { filter, groups, groupsPaging, users, usersPaging } = this.props;
let order = PERMISSIONS_ORDER_BY_QUALIFIER[this.props.component.qualifier]; let order = PERMISSIONS_ORDER_BY_QUALIFIER[this.props.component.qualifier];
if (this.props.visibility === Visibility.Public) { if (this.props.visibility === Visibility.Public) {
order = without(order, 'user', 'codeviewer'); order = without(order, 'user', 'codeviewer');
} }
const permissions = convertToPermissionDefinitions(order, 'projects_role');


const permissions = order.map(p => ({
key: p,
name: translate('projects_role', p),
description: translate('projects_role', p, 'desc')
}));

const count =
(this.props.filter !== 'users' ? this.props.groups.length : 0) +
(this.props.filter !== 'groups' ? this.props.users.length : 0);
const total =
(this.props.filter !== 'users' ? this.props.groupsPaging.total : 0) +
(this.props.filter !== 'groups' ? this.props.usersPaging.total : 0);
let count = 0;
let total = 0;
if (filter !== 'users') {
count += groups.length;
total += groupsPaging ? groupsPaging.total : groups.length;
}
if (filter !== 'groups') {
count += users.length;
total += usersPaging ? usersPaging.total : users.length;
}


return ( return (
<> <>
query={this.props.query} query={this.props.query}
/> />
</HoldersList> </HoldersList>
<ListFooter count={count} loadMore={this.handleLoadMore} total={total} />
<ListFooter count={count} loadMore={this.props.onLoadMore} total={total} />
</> </>
); );
} }

+ 63
- 57
server/sonar-web/src/main/js/apps/permissions/project/components/App.tsx Voir le fichier

disclaimer: boolean; disclaimer: boolean;
filter: string; filter: string;
groups: PermissionGroup[]; groups: PermissionGroup[];
groupsPaging: Paging;
groupsPaging?: Paging;
loading: boolean; loading: boolean;
query: string; query: string;
selectedPermission?: string; selectedPermission?: string;
users: PermissionUser[]; users: PermissionUser[];
usersPaging: Paging;
usersPaging?: Paging;
} }


export default class App extends React.PureComponent<Props, State> { export default class App extends React.PureComponent<Props, State> {
disclaimer: false, disclaimer: false,
filter: 'all', filter: 'all',
groups: [], groups: [],
groupsPaging: { pageIndex: 1, pageSize: 100, total: 0 },
loading: true, loading: true,
query: '', query: '',
users: [],
usersPaging: { pageIndex: 1, pageSize: 100, total: 0 }
users: []
}; };
} }


} }
}; };


loadHolders = (usersPageIndex?: number, groupsPageIndex?: number) => {
if (this.mounted) {
this.setState({ loading: true });

const { component } = this.props;
const { filter, query, selectedPermission } = this.state;

const getUsers =
filter !== 'groups'
? api.getPermissionsUsersForComponent({
projectKey: component.key,
q: query || undefined,
permission: selectedPermission,
organization: component.organization,
p: usersPageIndex
})
: Promise.resolve({
paging: { pageIndex: 1, pageSize: 100, total: 0 },
users: []
});
loadUsersAndGroups = (userPage?: number, groupsPage?: number) => {
const { component } = this.props;
const { filter, query, selectedPermission } = this.state;

const getUsers: Promise<{ paging?: Paging; users: PermissionUser[] }> =
filter !== 'groups'
? api.getPermissionsUsersForComponent({
projectKey: component.key,
q: query || undefined,
permission: selectedPermission,
organization: component.organization,
p: userPage
})
: Promise.resolve({ paging: undefined, users: [] });

const getGroups: Promise<{ paging?: Paging; groups: PermissionGroup[] }> =
filter !== 'users'
? api.getPermissionsGroupsForComponent({
projectKey: component.key,
q: query || undefined,
permission: selectedPermission,
organization: component.organization,
p: groupsPage
})
: Promise.resolve({ paging: undefined, groups: [] });

return Promise.all([getUsers, getGroups]);
};


const getGroups =
filter !== 'users'
? api.getPermissionsGroupsForComponent({
projectKey: component.key,
q: query || undefined,
permission: selectedPermission,
organization: component.organization,
p: groupsPageIndex
})
: Promise.resolve({
paging: { pageIndex: 1, pageSize: 100, total: 0 },
groups: []
});
loadHolders = () => {
this.setState({ loading: true });
return this.loadUsersAndGroups().then(([usersResponse, groupsResponse]) => {
if (this.mounted) {
this.setState({
groups: groupsResponse.groups,
groupsPaging: groupsResponse.paging,
loading: false,
users: usersResponse.users,
usersPaging: usersResponse.paging
});
}
}, this.stopLoading);
};


Promise.all([getUsers, getGroups]).then(responses => {
if (this.mounted) {
this.setState(state => ({
loading: false,
groups: groupsPageIndex
? [...state.groups, ...responses[1].groups]
: responses[1].groups,
groupsPaging: responses[1].paging,
users: usersPageIndex ? [...state.users, ...responses[0].users] : responses[0].users,
usersPaging: responses[0].paging
}));
}
}, this.stopLoading);
}
onLoadMore = () => {
const { usersPaging, groupsPaging } = this.state;
this.setState({ loading: true });
return this.loadUsersAndGroups(
usersPaging ? usersPaging.pageIndex + 1 : 1,
groupsPaging ? groupsPaging.pageIndex + 1 : 1
).then(([usersResponse, groupsResponse]) => {
if (this.mounted) {
this.setState(({ groups, users }) => ({
groups: [...groups, ...groupsResponse.groups],
groupsPaging: groupsResponse.paging,
loading: false,
users: [...users, ...usersResponse.users],
usersPaging: usersResponse.paging
}));
}
}, this.stopLoading);
}; };


handleFilterChange = (filter: string) => { handleFilterChange = (filter: string) => {
} }
}; };


handleLoadMore = (usersPageIndex: number, groupsPageIndex: number) => {
this.loadHolders(usersPageIndex, groupsPageIndex);
};

render() { render() {
const canTurnToPrivate = const canTurnToPrivate =
this.props.component.configuration != null && this.props.component.configuration != null &&
groups={this.state.groups} groups={this.state.groups}
groupsPaging={this.state.groupsPaging} groupsPaging={this.state.groupsPaging}
onFilterChange={this.handleFilterChange} onFilterChange={this.handleFilterChange}
onLoadMore={this.handleLoadMore}
onLoadMore={this.onLoadMore}
onPermissionSelect={this.handlePermissionSelect} onPermissionSelect={this.handlePermissionSelect}
onQueryChange={this.handleQueryChange} onQueryChange={this.handleQueryChange}
query={this.state.query} query={this.state.query}

+ 0
- 39
server/sonar-web/src/main/js/apps/permissions/project/constants.tsx Voir le fichier

/*
* SonarQube
* Copyright (C) 2009-2018 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
export const PERMISSIONS_ORDER_FOR_PROJECT = [
'user',
'codeviewer',
'issueadmin',
'securityhotspotadmin',
'admin',
'scan'
];

export const PERMISSIONS_ORDER_FOR_VIEW = ['user', 'admin'];

export const PERMISSIONS_ORDER_FOR_DEV = ['user', 'admin'];

export const PERMISSIONS_ORDER_BY_QUALIFIER: { [index: string]: string[] } = {
TRK: PERMISSIONS_ORDER_FOR_PROJECT,
VW: PERMISSIONS_ORDER_FOR_VIEW,
SVW: PERMISSIONS_ORDER_FOR_VIEW,
APP: PERMISSIONS_ORDER_FOR_VIEW,
DEV: PERMISSIONS_ORDER_FOR_DEV
};

+ 16
- 22
server/sonar-web/src/main/js/apps/permissions/shared/components/GroupHolder.tsx Voir le fichier

*/ */
import * as React from 'react'; import * as React from 'react';
import { without } from 'lodash'; import { without } from 'lodash';
import Checkbox from '../../../../components/controls/Checkbox';
import PermissionCell from './PermissionCell';
import GroupIcon from '../../../../components/icons-components/GroupIcon'; import GroupIcon from '../../../../components/icons-components/GroupIcon';
import { PermissionGroup } from '../../../../app/types';
import { PermissionDefinitions, PermissionGroup } from '../../../../app/types';
import { isPermissionDefinitionGroup } from '../../utils';


interface Props { interface Props {
group: PermissionGroup; group: PermissionGroup;
permissions: string[];
selectedPermission?: string;
permissionsOrder: string[];
onToggle: (group: PermissionGroup, permission: string) => Promise<void>; onToggle: (group: PermissionGroup, permission: string) => Promise<void>;
permissions: PermissionDefinitions;
selectedPermission?: string;
} }


interface State { interface State {
}; };


render() { render() {
const { selectedPermission } = this.props;
const permissionCells = this.props.permissionsOrder.map(permission => (
<td
className="text-center text-middle"
key={permission}
style={{ backgroundColor: permission === selectedPermission ? '#d9edf7' : 'transparent' }}>
<Checkbox
checked={this.props.permissions.includes(permission)}
disabled={this.state.loading.includes(permission)}
id={permission}
onCheck={this.handleCheck}
/>
</td>
));

const { group } = this.props; const { group } = this.props;


return ( return (
<tr> <tr>
<td className="nowrap">
<td className="nowrap text-middle">
<div className="display-inline-block text-middle big-spacer-right"> <div className="display-inline-block text-middle big-spacer-right">
<GroupIcon /> <GroupIcon />
</div> </div>
</div> </div>
</div> </div>
</td> </td>
{permissionCells}
{this.props.permissions.map(permission => (
<PermissionCell
key={isPermissionDefinitionGroup(permission) ? permission.category : permission.key}
loading={this.state.loading}
onCheck={this.handleCheck}
permission={permission}
permissionItem={group}
selectedPermission={this.props.selectedPermission}
/>
))}
</tr> </tr>
); );
} }

+ 42
- 56
server/sonar-web/src/main/js/apps/permissions/shared/components/HoldersList.tsx Voir le fichier

* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/ */
import * as React from 'react'; import * as React from 'react';
import { groupBy } from 'lodash';
import { partition, sortBy } from 'lodash';
import UserHolder from './UserHolder'; import UserHolder from './UserHolder';
import GroupHolder from './GroupHolder'; import GroupHolder from './GroupHolder';
import PermissionHeader from './PermissionHeader'; import PermissionHeader from './PermissionHeader';
import { translate } from '../../../../helpers/l10n'; import { translate } from '../../../../helpers/l10n';
import { Permission, PermissionGroup, PermissionUser } from '../../../../app/types';
import { PermissionGroup, PermissionUser, PermissionDefinitions } from '../../../../app/types';
import { isPermissionDefinitionGroup } from '../../utils';


interface Props { interface Props {
loading?: boolean; loading?: boolean;
groups: PermissionGroup[]; groups: PermissionGroup[];
onSelectPermission: (permission: string) => void;
onSelectPermission?: (permission: string) => void;
onToggleGroup: (group: PermissionGroup, permission: string) => Promise<void>; onToggleGroup: (group: PermissionGroup, permission: string) => Promise<void>;
onToggleUser: (user: PermissionUser, permission: string) => Promise<void>; onToggleUser: (user: PermissionUser, permission: string) => Promise<void>;
permissions: Permission[];
permissions: PermissionDefinitions;
selectedPermission?: string; selectedPermission?: string;
showPublicProjectsWarning?: boolean; showPublicProjectsWarning?: boolean;
users: PermissionUser[]; users: PermissionUser[];
return (item as PermissionUser).login !== undefined; return (item as PermissionUser).login !== undefined;
} }


renderTableHeader() {
const { onSelectPermission, selectedPermission, showPublicProjectsWarning } = this.props;
const cells = this.props.permissions.map(p => (
<PermissionHeader
key={p.key}
onSelectPermission={onSelectPermission}
permission={p}
selectedPermission={selectedPermission}
showPublicProjectsWarning={showPublicProjectsWarning}
/>
));
return (
<thead>
<tr>
<td className="nowrap bordered-bottom">{this.props.children}</td>
{cells}
</tr>
</thead>
);
}

renderEmpty() { renderEmpty() {
const columns = this.props.permissions.length + 1; const columns = this.props.permissions.length + 1;
return ( return (
); );
} }


renderItem(item: PermissionUser | PermissionGroup, permissionsOrder: string[]) {
return this.isPermissionUser(item)
? this.renderUser(item, permissionsOrder)
: this.renderGroup(item, permissionsOrder);
}

renderUser(user: PermissionUser, permissionsOrder: string[]) {
return (
renderItem(item: PermissionUser | PermissionGroup, permissions: PermissionDefinitions) {
return this.isPermissionUser(item) ? (
<UserHolder <UserHolder
key={'user-' + user.login}
key={'user-' + item.login}
onToggle={this.props.onToggleUser} onToggle={this.props.onToggleUser}
permissions={user.permissions}
permissionsOrder={permissionsOrder}
permissions={permissions}
selectedPermission={this.props.selectedPermission} selectedPermission={this.props.selectedPermission}
user={user}
user={item}
/> />
);
}

renderGroup(group: PermissionGroup, permissionsOrder: string[]) {
return (
) : (
<GroupHolder <GroupHolder
group={group}
key={'group-' + group.id}
group={item}
key={'group-' + item.id}
onToggle={this.props.onToggleGroup} onToggle={this.props.onToggleGroup}
permissions={group.permissions}
permissionsOrder={permissionsOrder}
permissions={permissions}
selectedPermission={this.props.selectedPermission} selectedPermission={this.props.selectedPermission}
/> />
); );
} }


render() { render() {
const permissionsOrder = this.props.permissions.map(p => p.key);
const items = [...this.props.users, ...this.props.groups].sort((a, b) => {
return a.name < b.name ? -1 : 1;
const { permissions } = this.props;
const items = sortBy([...this.props.users, ...this.props.groups], item => {
if (this.isPermissionUser(item) && item.login === '<creator>') {
return 0;
}
return item.name;
}); });

const { true: itemWithPermissions = [], false: itemWithoutPermissions = [] } = groupBy(
const [itemWithPermissions, itemWithoutPermissions] = partition(
items, items,
item => item.permissions.length > 0 item => item.permissions.length > 0
); );
return ( return (
<div className="boxed-group boxed-group-inner"> <div className="boxed-group boxed-group-inner">
<table className="data zebra permissions-table"> <table className="data zebra permissions-table">
{this.renderTableHeader()}
<thead>
<tr>
<td className="nowrap bordered-bottom">{this.props.children}</td>
{permissions.map(permission => (
<PermissionHeader
key={
isPermissionDefinitionGroup(permission) ? permission.category : permission.key
}
onSelectPermission={this.props.onSelectPermission}
permission={permission}
selectedPermission={this.props.selectedPermission}
showPublicProjectsWarning={this.props.showPublicProjectsWarning}
/>
))}
</tr>
</thead>
<tbody> <tbody>
{items.length === 0 && !this.props.loading && this.renderEmpty()} {items.length === 0 && !this.props.loading && this.renderEmpty()}
{itemWithPermissions.map(item => this.renderItem(item, permissionsOrder))}
{itemWithPermissions.map(item => this.renderItem(item, permissions))}
{itemWithPermissions.length > 0 && {itemWithPermissions.length > 0 &&
itemWithoutPermissions.length > 0 && ( itemWithoutPermissions.length > 0 && (
<> <>
<tr> <tr>
<td className="divider" colSpan={6} />
<td className="divider" colSpan={20} />
</tr> </tr>
<tr /> {/* Keep correct zebra colors in the table */}
<tr />
{/* Keep correct zebra colors in the table */}
</> </>
)} )}
{itemWithoutPermissions.map(item => this.renderItem(item, permissionsOrder))}
{itemWithoutPermissions.map(item => this.renderItem(item, permissions))}
</tbody> </tbody>
</table> </table>
</div> </div>

+ 74
- 0
server/sonar-web/src/main/js/apps/permissions/shared/components/PermissionCell.tsx Voir le fichier

/*
* SonarQube
* Copyright (C) 2009-2018 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import * as classNames from 'classnames';
import {
PermissionDefinition,
PermissionDefinitionGroup,
PermissionGroup,
PermissionUser
} from '../../../../app/types';
import { isPermissionDefinitionGroup } from '../../utils';
import Checkbox from '../../../../components/controls/Checkbox';

interface Props {
loading: string[];
onCheck: (checked: boolean, permission?: string) => void;
permission: PermissionDefinition | PermissionDefinitionGroup;
permissionItem: PermissionGroup | PermissionUser;
selectedPermission?: string;
}

export default class PermissionCell extends React.PureComponent<Props> {
render() {
const { loading, onCheck, permission, permissionItem, selectedPermission } = this.props;
if (isPermissionDefinitionGroup(permission)) {
return (
<td className="text-middle">
{permission.permissions.map(permission => (
<div key={permission.key}>
<Checkbox
checked={permissionItem.permissions.includes(permission.key)}
disabled={loading.includes(permission.key)}
id={permission.key}
onCheck={onCheck}>
<span className="little-spacer-left">{permission.name}</span>
</Checkbox>
</div>
))}
</td>
);
} else {
return (
<td
className={classNames('permission-column text-center text-middle', {
selected: permission.key === selectedPermission
})}>
<Checkbox
checked={permissionItem.permissions.includes(permission.key)}
disabled={loading.includes(permission.key)}
id={permission.key}
onCheck={onCheck}
/>
</td>
);
}
}
}

+ 58
- 30
server/sonar-web/src/main/js/apps/permissions/shared/components/PermissionHeader.tsx Voir le fichier

* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/ */
import * as React from 'react'; import * as React from 'react';
import * as classNames from 'classnames';
import HelpTooltip from '../../../../components/controls/HelpTooltip'; import HelpTooltip from '../../../../components/controls/HelpTooltip';
import InstanceMessage from '../../../../components/common/InstanceMessage'; import InstanceMessage from '../../../../components/common/InstanceMessage';
import Tooltip from '../../../../components/controls/Tooltip';
import { translate, translateWithParameters } from '../../../../helpers/l10n'; import { translate, translateWithParameters } from '../../../../helpers/l10n';
import { Permission } from '../../../../app/types';
import { PermissionDefinition, PermissionDefinitionGroup } from '../../../../app/types';
import { isPermissionDefinitionGroup } from '../../utils';
import Tooltip from '../../../../components/controls/Tooltip';


interface Props { interface Props {
onSelectPermission: (permission: string) => void;
permission: Permission;
onSelectPermission?: (permission: string) => void;
permission: PermissionDefinition | PermissionDefinitionGroup;
selectedPermission?: string; selectedPermission?: string;
showPublicProjectsWarning?: boolean; showPublicProjectsWarning?: boolean;
} }
handlePermissionClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => { handlePermissionClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
event.preventDefault(); event.preventDefault();
event.currentTarget.blur(); event.currentTarget.blur();
this.props.onSelectPermission(this.props.permission.key);
const { permission } = this.props;
if (this.props.onSelectPermission && !isPermissionDefinitionGroup(permission)) {
this.props.onSelectPermission(permission.key);
}
}; };


renderTooltip = (permission: Permission) => {
if (this.props.showPublicProjectsWarning && ['user', 'codeviewer'].includes(permission.key)) {
return (
<div>
<InstanceMessage message={permission.description} />
<div className="alert alert-warning spacer-top">
{translate('projects_role.public_projects_warning')}
getTooltipOverlay = () => {
const { permission } = this.props;

if (isPermissionDefinitionGroup(permission)) {
return permission.permissions.map(permission => (
<>
<b className="little-spacer-right">{permission.name}:</b>
<InstanceMessage key={permission.key} message={permission.description} />
<br />
</>
));
} else {
if (this.props.showPublicProjectsWarning && ['user', 'codeviewer'].includes(permission.key)) {
return (
<div>
<InstanceMessage message={permission.description} />
<div className="alert alert-warning spacer-top">
{translate('projects_role.public_projects_warning')}
</div>
</div> </div>
</div>
);
);
}
return <InstanceMessage message={permission.description} />;
} }
return <InstanceMessage message={permission.description} />;
}; };


render() { render() {
const { permission, selectedPermission } = this.props;
const { onSelectPermission, permission } = this.props;
let name;
if (isPermissionDefinitionGroup(permission)) {
name = translate('global_permissions', permission.category);
} else {
name = onSelectPermission ? (
<Tooltip
overlay={translateWithParameters(
'global_permissions.filter_by_x_permission',
permission.name
)}>
<a href="#" onClick={this.handlePermissionClick}>
{permission.name}
</a>
</Tooltip>
) : (
permission.name
);
}
return ( return (
<th <th
className="permission-column text-center"
style={{
backgroundColor: permission.key === selectedPermission ? '#d9edf7' : 'transparent'
}}>
className={classNames('permission-column text-center text-middle', {
selected:
!isPermissionDefinitionGroup(permission) &&
permission.key === this.props.selectedPermission
})}>
<div className="permission-column-inner"> <div className="permission-column-inner">
<Tooltip
overlay={translateWithParameters(
'global_permissions.filter_by_x_permission',
permission.name
)}>
<a className="text-middle" href="#" onClick={this.handlePermissionClick}>
{permission.name}
</a>
</Tooltip>
<HelpTooltip className="spacer-left" overlay={this.renderTooltip(permission)} />
{name}
<HelpTooltip className="spacer-left" overlay={this.getTooltipOverlay()} />
</div> </div>
</th> </th>
); );

+ 18
- 22
server/sonar-web/src/main/js/apps/permissions/shared/components/UserHolder.tsx Voir le fichier

*/ */
import * as React from 'react'; import * as React from 'react';
import { without } from 'lodash'; import { without } from 'lodash';
import PermissionCell from './PermissionCell';
import Avatar from '../../../../components/ui/Avatar'; import Avatar from '../../../../components/ui/Avatar';
import Checkbox from '../../../../components/controls/Checkbox';
import { translate } from '../../../../helpers/l10n'; import { translate } from '../../../../helpers/l10n';
import { PermissionUser } from '../../../../app/types';
import { PermissionDefinitions, PermissionUser } from '../../../../app/types';
import { isPermissionDefinitionGroup } from '../../utils';


interface Props { interface Props {
user: PermissionUser;
permissions: string[];
selectedPermission?: string;
permissionsOrder: string[];
onToggle: (user: PermissionUser, permission: string) => Promise<void>; onToggle: (user: PermissionUser, permission: string) => Promise<void>;
permissions: PermissionDefinitions;
selectedPermission?: string;
user: PermissionUser;
} }


interface State { interface State {
}; };


render() { render() {
const { selectedPermission } = this.props;
const permissionCells = this.props.permissionsOrder.map(permission => (
<td
className="text-center text-middle"
key={permission}
style={{ backgroundColor: permission === selectedPermission ? '#d9edf7' : 'transparent' }}>
<Checkbox
checked={this.props.permissions.includes(permission)}
disabled={this.state.loading.includes(permission)}
id={permission}
onCheck={this.handleCheck}
/>
</td>
const { user } = this.props;
const permissionCells = this.props.permissions.map(permission => (
<PermissionCell
key={isPermissionDefinitionGroup(permission) ? permission.category : permission.key}
loading={this.state.loading}
onCheck={this.handleCheck}
permission={permission}
permissionItem={user}
selectedPermission={this.props.selectedPermission}
/>
)); ));


const { user } = this.props;
if (user.login === '<creator>') { if (user.login === '<creator>') {
return ( return (
<tr> <tr>
<td className="nowrap">
<td className="nowrap text-middle">
<div className="display-inline-block text-middle"> <div className="display-inline-block text-middle">
<div> <div>
<strong>{user.name}</strong> <strong>{user.name}</strong>


return ( return (
<tr> <tr>
<td className="nowrap">
<td className="nowrap text-middle">
<Avatar <Avatar
className="text-middle big-spacer-right" className="text-middle big-spacer-right"
hash={user.avatar} hash={user.avatar}

+ 12
- 6
server/sonar-web/src/main/js/apps/permissions/shared/components/__tests__/GroupHolder-test.tsx Voir le fichier

group={group} group={group}
key="foo" key="foo"
onToggle={jest.fn(() => Promise.resolve())} onToggle={jest.fn(() => Promise.resolve())}
permissions={['bar']}
permissionsOrder={['bar', 'baz']}
permissions={[
{
category: 'admin',
permissions: [
{ key: 'foo', name: 'Foo', description: '' },
{ key: 'bar', name: 'Bar', description: '' }
]
},
{ key: 'baz', name: 'Baz', description: '' }
]}
selectedPermission={'bar'} selectedPermission={'bar'}
/> />
); );


it('should display checkboxes for permissions', () => {
it('should render correctly', () => {
expect(shallow(groupHolder)).toMatchSnapshot(); expect(shallow(groupHolder)).toMatchSnapshot();
}); });


it('should disabled checkboxes when waiting for promise to return', async () => {
it('should disabled PermissionCell checkboxes when waiting for promise to return', async () => {
const wrapper = shallow(groupHolder); const wrapper = shallow(groupHolder);
expect(wrapper.state().loading).toEqual([]); expect(wrapper.state().loading).toEqual([]);


(wrapper.instance() as GroupHolder).handleCheck(true, 'baz'); (wrapper.instance() as GroupHolder).handleCheck(true, 'baz');
wrapper.update(); wrapper.update();
expect(wrapper.state().loading).toEqual(['baz']); expect(wrapper.state().loading).toEqual(['baz']);
expect(wrapper).toMatchSnapshot();


(wrapper.instance() as GroupHolder).handleCheck(true, 'bar'); (wrapper.instance() as GroupHolder).handleCheck(true, 'bar');
wrapper.update(); wrapper.update();
expect(wrapper.state().loading).toEqual(['baz', 'bar']); expect(wrapper.state().loading).toEqual(['baz', 'bar']);
expect(wrapper).toMatchSnapshot();


await waitAndUpdate(wrapper); await waitAndUpdate(wrapper);
expect(wrapper.state().loading).toEqual([]); expect(wrapper.state().loading).toEqual([]);

+ 10
- 1
server/sonar-web/src/main/js/apps/permissions/shared/components/__tests__/HoldersList-test.tsx Voir le fichier

import { shallow } from 'enzyme'; import { shallow } from 'enzyme';
import HoldersList from '../HoldersList'; import HoldersList from '../HoldersList';


const permissions = [{ key: 'bar', name: 'bar', description: 'foo' }];
const permissions = [
{ key: 'foo', name: 'Foo', description: '' },
{
category: 'admin',
permissions: [
{ key: 'bar', name: 'Bar', description: '' },
{ key: 'baz', name: 'Baz', description: '' }
]
}
];


const groups = [ const groups = [
{ id: 'foobar', name: 'Foobar', permissions: ['bar'] }, { id: 'foobar', name: 'Foobar', permissions: ['bar'] },

+ 89
- 0
server/sonar-web/src/main/js/apps/permissions/shared/components/__tests__/PermissionCell-test.tsx Voir le fichier

/*
* SonarQube
* Copyright (C) 2009-2018 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import { shallow } from 'enzyme';
import PermissionCell from '../PermissionCell';

const permissionItem = {
id: 'foobar',
name: 'Foobar',
permissions: ['bar']
};

const permission = { key: 'baz', name: 'Baz', description: '' };
const permissionGroup = {
category: 'admin',
permissions: [
{ key: 'foo', name: 'Foo', description: '' },
{ key: 'bar', name: 'Bar', description: '' }
]
};
it('should display unchecked checkbox', () => {
expect(
shallow(
<PermissionCell
loading={[]}
onCheck={jest.fn()}
permission={permission}
permissionItem={permissionItem}
/>
)
).toMatchSnapshot();
});

it('should display multiple checkboxes with one checked', () => {
expect(
shallow(
<PermissionCell
loading={[]}
onCheck={jest.fn()}
permission={permissionGroup}
permissionItem={permissionItem}
/>
)
).toMatchSnapshot();
});

it('should display disabled checkbox', () => {
expect(
shallow(
<PermissionCell
loading={['baz']}
onCheck={jest.fn()}
permission={permission}
permissionItem={permissionItem}
/>
)
).toMatchSnapshot();
});

it('should display selected checkbox', () => {
expect(
shallow(
<PermissionCell
loading={[]}
onCheck={jest.fn()}
permission={permission}
permissionItem={permissionItem}
selectedPermission="baz"
/>
)
).toMatchSnapshot();
});

+ 12
- 6
server/sonar-web/src/main/js/apps/permissions/shared/components/__tests__/UserHolder-test.tsx Voir le fichier

<UserHolder <UserHolder
key="foo" key="foo"
onToggle={jest.fn(() => Promise.resolve())} onToggle={jest.fn(() => Promise.resolve())}
permissions={['bar']}
permissionsOrder={['bar', 'baz']}
permissions={[
{
category: 'admin',
permissions: [
{ key: 'foo', name: 'Foo', description: '' },
{ key: 'bar', name: 'Bar', description: '' }
]
},
{ key: 'baz', name: 'Baz', description: '' }
]}
selectedPermission={'bar'} selectedPermission={'bar'}
user={user} user={user}
/> />
); );


it('should display checkboxes for permissions', () => {
it('should render correctly', () => {
expect(shallow(userHolder)).toMatchSnapshot(); expect(shallow(userHolder)).toMatchSnapshot();
}); });


it('should disabled checkboxes when waiting for promise to return', async () => {
it('should disabled PermissionCell checkboxes when waiting for promise to return', async () => {
const wrapper = shallow(userHolder); const wrapper = shallow(userHolder);
expect(wrapper.state().loading).toEqual([]); expect(wrapper.state().loading).toEqual([]);


(wrapper.instance() as UserHolder).handleCheck(true, 'baz'); (wrapper.instance() as UserHolder).handleCheck(true, 'baz');
wrapper.update(); wrapper.update();
expect(wrapper.state().loading).toEqual(['baz']); expect(wrapper.state().loading).toEqual(['baz']);
expect(wrapper).toMatchSnapshot();


(wrapper.instance() as UserHolder).handleCheck(true, 'bar'); (wrapper.instance() as UserHolder).handleCheck(true, 'bar');
wrapper.update(); wrapper.update();
expect(wrapper.state().loading).toEqual(['baz', 'bar']); expect(wrapper.state().loading).toEqual(['baz', 'bar']);
expect(wrapper).toMatchSnapshot();


await waitAndUpdate(wrapper); await waitAndUpdate(wrapper);
expect(wrapper.state().loading).toEqual([]); expect(wrapper.state().loading).toEqual([]);

+ 43
- 153
server/sonar-web/src/main/js/apps/permissions/shared/components/__tests__/__snapshots__/GroupHolder-test.tsx.snap Voir le fichier

// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP


exports[`should disabled checkboxes when waiting for promise to return 1`] = `
exports[`should render correctly 1`] = `
<tr> <tr>
<td <td
className="nowrap"
className="nowrap text-middle"
> >
<div <div
className="display-inline-block text-middle big-spacer-right" className="display-inline-block text-middle big-spacer-right"
/> />
</div> </div>
</td> </td>
<td
className="text-center text-middle"
key="bar"
style={
<PermissionCell
key="admin"
loading={Array []}
onCheck={[Function]}
permission={
Object { Object {
"backgroundColor": "#d9edf7",
}
}
>
<Checkbox
checked={true}
disabled={false}
id="bar"
onCheck={[Function]}
thirdState={false}
/>
</td>
<td
className="text-center text-middle"
key="baz"
style={
Object {
"backgroundColor": "transparent",
}
}
>
<Checkbox
checked={false}
disabled={true}
id="baz"
onCheck={[Function]}
thirdState={false}
/>
</td>
</tr>
`;

exports[`should disabled checkboxes when waiting for promise to return 2`] = `
<tr>
<td
className="nowrap"
>
<div
className="display-inline-block text-middle big-spacer-right"
>
<GroupIcon />
</div>
<div
className="display-inline-block text-middle"
>
<div>
<strong>
Foobar
</strong>
</div>
<div
className="little-spacer-top"
style={
"category": "admin",
"permissions": Array [
Object { Object {
"whiteSpace": "normal",
}
}
/>
</div>
</td>
<td
className="text-center text-middle"
key="bar"
style={
Object {
"backgroundColor": "#d9edf7",
"description": "",
"key": "foo",
"name": "Foo",
},
Object {
"description": "",
"key": "bar",
"name": "Bar",
},
],
} }
} }
>
<Checkbox
checked={true}
disabled={true}
id="bar"
onCheck={[Function]}
thirdState={false}
/>
</td>
<td
className="text-center text-middle"
key="baz"
style={
permissionItem={
Object { Object {
"backgroundColor": "transparent",
"id": "foobar",
"name": "Foobar",
"permissions": Array [
"bar",
],
} }
} }
>
<Checkbox
checked={false}
disabled={true}
id="baz"
onCheck={[Function]}
thirdState={false}
/>
</td>
</tr>
`;

exports[`should display checkboxes for permissions 1`] = `
<tr>
<td
className="nowrap"
>
<div
className="display-inline-block text-middle big-spacer-right"
>
<GroupIcon />
</div>
<div
className="display-inline-block text-middle"
>
<div>
<strong>
Foobar
</strong>
</div>
<div
className="little-spacer-top"
style={
Object {
"whiteSpace": "normal",
}
}
/>
</div>
</td>
<td
className="text-center text-middle"
key="bar"
style={
selectedPermission="bar"
/>
<PermissionCell
key="baz"
loading={Array []}
onCheck={[Function]}
permission={
Object { Object {
"backgroundColor": "#d9edf7",
"description": "",
"key": "baz",
"name": "Baz",
} }
} }
>
<Checkbox
checked={true}
disabled={false}
id="bar"
onCheck={[Function]}
thirdState={false}
/>
</td>
<td
className="text-center text-middle"
key="baz"
style={
permissionItem={
Object { Object {
"backgroundColor": "transparent",
"id": "foobar",
"name": "Foobar",
"permissions": Array [
"bar",
],
} }
} }
>
<Checkbox
checked={false}
disabled={false}
id="baz"
onCheck={[Function]}
thirdState={false}
/>
</td>
selectedPermission="bar"
/>
</tr> </tr>
`; `;

+ 178
- 61
server/sonar-web/src/main/js/apps/permissions/shared/components/__tests__/__snapshots__/HoldersList-test.tsx.snap Voir le fichier

className="nowrap bordered-bottom" className="nowrap bordered-bottom"
/> />
<PermissionHeader <PermissionHeader
key="bar"
key="foo"
onSelectPermission={[MockFunction]} onSelectPermission={[MockFunction]}
permission={ permission={
Object { Object {
"description": "foo",
"key": "bar",
"name": "bar",
"description": "",
"key": "foo",
"name": "Foo",
}
}
selectedPermission="bar"
/>
<PermissionHeader
key="admin"
onSelectPermission={[MockFunction]}
permission={
Object {
"category": "admin",
"permissions": Array [
Object {
"description": "",
"key": "bar",
"name": "Bar",
},
Object {
"description": "",
"key": "baz",
"name": "Baz",
},
],
} }
} }
selectedPermission="bar" selectedPermission="bar"
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<GroupHolder
group={
Object {
"id": "barbaz",
"name": "Barbaz",
"permissions": Array [
"bar",
],
}
}
key="group-barbaz"
onToggle={[MockFunction]}
permissions={
Array [
"bar",
]
}
permissionsOrder={
Array [
"bar",
]
}
selectedPermission="bar"
/>
<UserHolder <UserHolder
key="user-barbaz" key="user-barbaz"
onToggle={[MockFunction]} onToggle={[MockFunction]}
permissions={ permissions={
Array [ Array [
"bar",
]
}
permissionsOrder={
Array [
"bar",
Object {
"description": "",
"key": "foo",
"name": "Foo",
},
Object {
"category": "admin",
"permissions": Array [
Object {
"description": "",
"key": "bar",
"name": "Bar",
},
Object {
"description": "",
"key": "baz",
"name": "Baz",
},
],
},
] ]
} }
selectedPermission="bar" selectedPermission="bar"
<GroupHolder <GroupHolder
group={ group={
Object { Object {
"id": "foobar",
"name": "Foobar",
"id": "barbaz",
"name": "Barbaz",
"permissions": Array [ "permissions": Array [
"bar", "bar",
], ],
} }
} }
key="group-foobar"
key="group-barbaz"
onToggle={[MockFunction]} onToggle={[MockFunction]}
permissions={ permissions={
Array [ Array [
"bar",
]
}
permissionsOrder={
Array [
"bar",
Object {
"description": "",
"key": "foo",
"name": "Foo",
},
Object {
"category": "admin",
"permissions": Array [
Object {
"description": "",
"key": "bar",
"name": "Bar",
},
Object {
"description": "",
"key": "baz",
"name": "Baz",
},
],
},
] ]
} }
selectedPermission="bar" selectedPermission="bar"
onToggle={[MockFunction]} onToggle={[MockFunction]}
permissions={ permissions={
Array [ Array [
"bar",
]
}
permissionsOrder={
Array [
"bar",
Object {
"description": "",
"key": "foo",
"name": "Foo",
},
Object {
"category": "admin",
"permissions": Array [
Object {
"description": "",
"key": "bar",
"name": "Bar",
},
Object {
"description": "",
"key": "baz",
"name": "Baz",
},
],
},
] ]
} }
selectedPermission="bar" selectedPermission="bar"
} }
} }
/> />
<tr>
<td
className="divider"
colSpan={6}
/>
</tr>
<GroupHolder
group={
Object {
"id": "foobar",
"name": "Foobar",
"permissions": Array [
"bar",
],
}
}
key="group-foobar"
onToggle={[MockFunction]}
permissions={
Array [
Object {
"description": "",
"key": "foo",
"name": "Foo",
},
Object {
"category": "admin",
"permissions": Array [
Object {
"description": "",
"key": "bar",
"name": "Bar",
},
Object {
"description": "",
"key": "baz",
"name": "Baz",
},
],
},
]
}
selectedPermission="bar"
/>
<React.Fragment>
<tr>
<td
className="divider"
colSpan={20}
/>
</tr>
<tr />
</React.Fragment>
<GroupHolder <GroupHolder
group={ group={
Object { Object {
} }
key="group-abc" key="group-abc"
onToggle={[MockFunction]} onToggle={[MockFunction]}
permissions={Array []}
permissionsOrder={
permissions={
Array [ Array [
"bar",
Object {
"description": "",
"key": "foo",
"name": "Foo",
},
Object {
"category": "admin",
"permissions": Array [
Object {
"description": "",
"key": "bar",
"name": "Bar",
},
Object {
"description": "",
"key": "baz",
"name": "Baz",
},
],
},
] ]
} }
selectedPermission="bar" selectedPermission="bar"
<UserHolder <UserHolder
key="user-bcd" key="user-bcd"
onToggle={[MockFunction]} onToggle={[MockFunction]}
permissions={Array []}
permissionsOrder={
permissions={
Array [ Array [
"bar",
Object {
"description": "",
"key": "foo",
"name": "Foo",
},
Object {
"category": "admin",
"permissions": Array [
Object {
"description": "",
"key": "bar",
"name": "Bar",
},
Object {
"description": "",
"key": "baz",
"name": "Baz",
},
],
},
] ]
} }
selectedPermission="bar" selectedPermission="bar"

+ 84
- 0
server/sonar-web/src/main/js/apps/permissions/shared/components/__tests__/__snapshots__/PermissionCell-test.tsx.snap Voir le fichier

// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should display disabled checkbox 1`] = `
<td
className="permission-column text-center text-middle"
>
<Checkbox
checked={false}
disabled={true}
id="baz"
onCheck={[MockFunction]}
thirdState={false}
/>
</td>
`;

exports[`should display multiple checkboxes with one checked 1`] = `
<td
className="text-middle"
>
<div
key="foo"
>
<Checkbox
checked={false}
disabled={false}
id="foo"
onCheck={[MockFunction]}
thirdState={false}
>
<span
className="little-spacer-left"
>
Foo
</span>
</Checkbox>
</div>
<div
key="bar"
>
<Checkbox
checked={true}
disabled={false}
id="bar"
onCheck={[MockFunction]}
thirdState={false}
>
<span
className="little-spacer-left"
>
Bar
</span>
</Checkbox>
</div>
</td>
`;

exports[`should display selected checkbox 1`] = `
<td
className="permission-column text-center text-middle selected"
>
<Checkbox
checked={false}
disabled={false}
id="baz"
onCheck={[MockFunction]}
thirdState={false}
/>
</td>
`;

exports[`should display unchecked checkbox 1`] = `
<td
className="permission-column text-center text-middle"
>
<Checkbox
checked={false}
disabled={false}
id="baz"
onCheck={[MockFunction]}
thirdState={false}
/>
</td>
`;

+ 43
- 153
server/sonar-web/src/main/js/apps/permissions/shared/components/__tests__/__snapshots__/UserHolder-test.tsx.snap Voir le fichier

// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP


exports[`should disabled checkboxes when waiting for promise to return 1`] = `
exports[`should render correctly 1`] = `
<tr> <tr>
<td <td
className="nowrap"
className="nowrap text-middle"
> >
<Connect(Avatar) <Connect(Avatar)
className="text-middle big-spacer-right" className="text-middle big-spacer-right"
/> />
</div> </div>
</td> </td>
<td
className="text-center text-middle"
key="bar"
style={
<PermissionCell
key="admin"
loading={Array []}
onCheck={[Function]}
permission={
Object { Object {
"backgroundColor": "#d9edf7",
"category": "admin",
"permissions": Array [
Object {
"description": "",
"key": "foo",
"name": "Foo",
},
Object {
"description": "",
"key": "bar",
"name": "Bar",
},
],
} }
} }
>
<Checkbox
checked={true}
disabled={false}
id="bar"
onCheck={[Function]}
thirdState={false}
/>
</td>
<td
className="text-center text-middle"
key="baz"
style={
permissionItem={
Object { Object {
"backgroundColor": "transparent",
"login": "john doe",
"name": "John Doe",
"permissions": Array [
"bar",
],
} }
} }
>
<Checkbox
checked={false}
disabled={true}
id="baz"
onCheck={[Function]}
thirdState={false}
/>
</td>
</tr>
`;

exports[`should disabled checkboxes when waiting for promise to return 2`] = `
<tr>
<td
className="nowrap"
>
<Connect(Avatar)
className="text-middle big-spacer-right"
name="John Doe"
size={36}
/>
<div
className="display-inline-block text-middle"
>
<div>
<strong>
John Doe
</strong>
<span
className="note spacer-left"
>
john doe
</span>
</div>
<div
className="little-spacer-top"
/>
</div>
</td>
<td
className="text-center text-middle"
key="bar"
style={
Object {
"backgroundColor": "#d9edf7",
}
}
>
<Checkbox
checked={true}
disabled={true}
id="bar"
onCheck={[Function]}
thirdState={false}
/>
</td>
<td
className="text-center text-middle"
selectedPermission="bar"
/>
<PermissionCell
key="baz" key="baz"
style={
loading={Array []}
onCheck={[Function]}
permission={
Object { Object {
"backgroundColor": "transparent",
"description": "",
"key": "baz",
"name": "Baz",
} }
} }
>
<Checkbox
checked={false}
disabled={true}
id="baz"
onCheck={[Function]}
thirdState={false}
/>
</td>
</tr>
`;

exports[`should display checkboxes for permissions 1`] = `
<tr>
<td
className="nowrap"
>
<Connect(Avatar)
className="text-middle big-spacer-right"
name="John Doe"
size={36}
/>
<div
className="display-inline-block text-middle"
>
<div>
<strong>
John Doe
</strong>
<span
className="note spacer-left"
>
john doe
</span>
</div>
<div
className="little-spacer-top"
/>
</div>
</td>
<td
className="text-center text-middle"
key="bar"
style={
permissionItem={
Object { Object {
"backgroundColor": "#d9edf7",
"login": "john doe",
"name": "John Doe",
"permissions": Array [
"bar",
],
} }
} }
>
<Checkbox
checked={true}
disabled={false}
id="bar"
onCheck={[Function]}
thirdState={false}
/>
</td>
<td
className="text-center text-middle"
key="baz"
style={
Object {
"backgroundColor": "transparent",
}
}
>
<Checkbox
checked={false}
disabled={false}
id="baz"
onCheck={[Function]}
thirdState={false}
/>
</td>
selectedPermission="bar"
/>
</tr> </tr>
`; `;

+ 11
- 9
server/sonar-web/src/main/js/apps/permissions/styles.css Voir le fichier

* along with this program; if not, write to the Free Software Foundation, * along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/ */
.permissions-table > tbody > tr > td {
border-bottom: 10px solid #fff !important;
}


.permissions-table .permission-column {
width: 1px;
.permissions-table .permission-column.selected {
background-color: #d9edf7;
} }


.permissions-table .permission-column-inner { .permissions-table .permission-column-inner {
display: inline-block;
width: 100px; width: 100px;
} }


.permissions-table .divider { .permissions-table .divider {
background: #e6e6e6;
border-bottom: 20px solid #fff !important;
border-top: 20px solid #fff !important;
background: #fff;
padding: 16px 0;
}
.permissions-table .divider::after {
display: block;
content: '';
background: var(--barBorderColor);
height: 1px; height: 1px;
padding: 0;
width: 100%;
} }

+ 87
- 0
server/sonar-web/src/main/js/apps/permissions/utils.ts Voir le fichier

/*
* SonarQube
* Copyright (C) 2009-2018 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { translate } from '../../helpers/l10n';
import { PermissionDefinition, PermissionDefinitionGroup } from '../../app/types';

export const PERMISSIONS_ORDER_FOR_PROJECT_TEMPLATE = [
'user',
'codeviewer',
'issueadmin',
'securityhotspotadmin',
'admin',
'scan'
];

export const PERMISSIONS_ORDER_GLOBAL = [
'admin',
{ category: 'administer', permissions: ['gateadmin', 'profileadmin'] },
'scan',
{ category: 'creator', permissions: ['provisioning'] }
];

export const PERMISSIONS_ORDER_GLOBAL_GOV = [
'admin',
{ category: 'administer', permissions: ['gateadmin', 'profileadmin'] },
'scan',
{ category: 'creator', permissions: ['provisioning', 'applicationcreator', 'portfoliocreator'] }
];

export const PERMISSIONS_ORDER_FOR_VIEW = ['user', 'admin'];

export const PERMISSIONS_ORDER_FOR_DEV = ['user', 'admin'];

export const PERMISSIONS_ORDER_BY_QUALIFIER: { [index: string]: string[] } = {
TRK: PERMISSIONS_ORDER_FOR_PROJECT_TEMPLATE,
VW: PERMISSIONS_ORDER_FOR_VIEW,
SVW: PERMISSIONS_ORDER_FOR_VIEW,
APP: PERMISSIONS_ORDER_FOR_VIEW,
DEV: PERMISSIONS_ORDER_FOR_DEV
};

function convertToPermissionDefinition(permission: string, l10nPrefix: string) {
return {
key: permission,
name: translate(l10nPrefix, permission),
description: translate(l10nPrefix, permission, 'desc')
};
}

export function convertToPermissionDefinitions(
permissions: Array<string | { category: string; permissions: string[] }>,
l10nPrefix: string
): Array<PermissionDefinition | PermissionDefinitionGroup> {
return permissions.map(permission => {
if (typeof permission === 'object') {
return {
category: permission.category,
permissions: permission.permissions.map(permission =>
convertToPermissionDefinition(permission, l10nPrefix)
)
};
}
return convertToPermissionDefinition(permission, l10nPrefix);
});
}

export function isPermissionDefinitionGroup(
permission?: PermissionDefinition | PermissionDefinitionGroup
): permission is PermissionDefinitionGroup {
return Boolean(permission && (permission as PermissionDefinitionGroup).category);
}

+ 9
- 5
sonar-core/src/main/resources/org/sonar/l10n/core.properties Voir le fichier

global_permissions.permission=Permission global_permissions.permission=Permission
global_permissions.users=Users global_permissions.users=Users
global_permissions.groups=Groups global_permissions.groups=Groups
global_permissions.administer=Administer
global_permissions.creator=Create
global_permissions.admin=Administer System global_permissions.admin=Administer System
global_permissions.admin.desc=Ability to perform all administration functions for the instance. global_permissions.admin.desc=Ability to perform all administration functions for the instance.
global_permissions.profileadmin=Administer Quality Profiles
global_permissions.profileadmin=Quality Profiles
global_permissions.profileadmin.desc=Ability to perform any action on quality profiles. global_permissions.profileadmin.desc=Ability to perform any action on quality profiles.
global_permissions.gateadmin=Administer Quality Gates
global_permissions.gateadmin=Quality Gates
global_permissions.gateadmin.desc=Ability to perform any action on quality gates. global_permissions.gateadmin.desc=Ability to perform any action on quality gates.
global_permissions.scan=Execute Analysis global_permissions.scan=Execute Analysis
global_permissions.scan.desc=Ability to get all settings required to perform an analysis (including the secured settings like passwords) and to push analysis results to the {instance} server. global_permissions.scan.desc=Ability to get all settings required to perform an analysis (including the secured settings like passwords) and to push analysis results to the {instance} server.
global_permissions.provisioning=Create Projects
global_permissions.provisioning=Projects
global_permissions.provisioning.desc=Ability to initialize a project so its settings can be configured before the first analysis. global_permissions.provisioning.desc=Ability to initialize a project so its settings can be configured before the first analysis.
global_permissions.filter_by_x_permission=Filter by "{0}" permission global_permissions.filter_by_x_permission=Filter by "{0}" permission
global_permissions.restore_access=Restore Access global_permissions.restore_access=Restore Access
global_permissions.restore_access.message=You will receive {browse} and {administer} permissions on the project. Do you want to continue? global_permissions.restore_access.message=You will receive {browse} and {administer} permissions on the project. Do you want to continue?


global_permissions.applicationcreator=Applications
global_permissions.applicationcreator.desc=Ability to create an application.
global_permissions.portfoliocreator=Portfolios
global_permissions.portfoliocreator.desc=Ability to create a portfolio.


#------------------------------------------------------------------------------ #------------------------------------------------------------------------------
# #

Chargement…
Annuler
Enregistrer