diff options
author | Grégoire Aubert <gregoire.aubert@sonarsource.com> | 2018-09-28 11:51:12 +0200 |
---|---|---|
committer | SonarTech <sonartech@sonarsource.com> | 2018-10-10 20:20:57 +0200 |
commit | 7f339fd2f1f9f3b587b0ccb1860f6f30ff7902d0 (patch) | |
tree | 331485689cfa4e5a769e63cc0d6b69798f994f38 | |
parent | 5f7784e32941dcc2b3c549b74bdc084c2d6b4b2c (diff) | |
download | sonarqube-7f339fd2f1f9f3b587b0ccb1860f6f30ff7902d0.tar.gz sonarqube-7f339fd2f1f9f3b587b0ccb1860f6f30ff7902d0.zip |
SONAR-11271 Add new permissions and update layout to group them
23 files changed, 946 insertions, 726 deletions
diff --git a/server/sonar-web/src/main/js/app/types.ts b/server/sonar-web/src/main/js/app/types.ts index 13d556d491b..658d67ebed8 100644 --- a/server/sonar-web/src/main/js/app/types.ts +++ b/server/sonar-web/src/main/js/app/types.ts @@ -537,25 +537,32 @@ export enum PeriodMode { PreviousVersion = 'previous_version' } -export interface Permission { +export interface PermissionDefinition { key: string; name: string; description: string; } +export type PermissionDefinitions = Array<PermissionDefinition | PermissionDefinitionGroup>; + +export interface PermissionDefinitionGroup { + category: string; + permissions: PermissionDefinition[]; +} + export interface PermissionGroup { + description?: string; id?: string; name: string; - description?: string; permissions: string[]; } export interface PermissionUser { + avatar?: string; + email?: string; login: string; name: string; - email?: string; permissions: string[]; - avatar?: string; } export interface PermissionTemplate { diff --git a/server/sonar-web/src/main/js/apps/permission-templates/components/Template.js b/server/sonar-web/src/main/js/apps/permission-templates/components/Template.js index cb0dbd6680a..9b1aee495cd 100644 --- a/server/sonar-web/src/main/js/apps/permission-templates/components/Template.js +++ b/server/sonar-web/src/main/js/apps/permission-templates/components/Template.js @@ -20,12 +20,14 @@ import React from 'react'; import PropTypes from 'prop-types'; import Helmet from 'react-helmet'; -import { debounce } from 'lodash'; import TemplateHeader from './TemplateHeader'; import TemplateDetails from './TemplateDetails'; import HoldersList from '../../permissions/shared/components/HoldersList'; 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 { translate } from '../../../helpers/l10n'; @@ -165,11 +167,10 @@ export default class Template extends React.PureComponent { }; 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]; diff --git a/server/sonar-web/src/main/js/apps/permissions/global/components/AllHoldersList.tsx b/server/sonar-web/src/main/js/apps/permissions/global/components/AllHoldersList.tsx index 85a1946055f..9adccff9343 100644 --- a/server/sonar-web/src/main/js/apps/permissions/global/components/AllHoldersList.tsx +++ b/server/sonar-web/src/main/js/apps/permissions/global/components/AllHoldersList.tsx @@ -18,36 +18,50 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; +import { connect } from 'react-redux'; import SearchForm from '../../shared/components/SearchForm'; 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 { + 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; grantPermissionToGroup: (groupName: string, permission: string) => Promise<void>; grantPermissionToUser: (login: string, permission: string) => Promise<void>; groups: PermissionGroup[]; - groupsPaging: Paging; + groupsPaging?: Paging; loadHolders: () => void; loading?: boolean; - onLoadMore: (usersPageIndex: number, groupsPageIndex: number) => void; + onLoadMore: () => void; onFilter: (filter: string) => void; onSearch: (query: string) => void; - onSelectPermission: (permission: string) => void; organization?: Organization; query: string; revokePermissionFromGroup: (groupName: string, permission: string) => Promise<void>; revokePermissionFromUser: (login: string, permission: string) => Promise<void>; - selectedPermission?: string; 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) => { const hasPermission = user.permissions.includes(permission); if (hasPermission) { @@ -67,38 +81,34 @@ export default class AllHoldersList extends React.PureComponent<Props> { } }; - handleLoadMore = () => { - this.props.onLoadMore( - this.props.usersPaging.pageIndex + 1, - this.props.groupsPaging.pageIndex + 1 - ); - }; - render() { + const { filter, groups, groupsPaging, users, usersPaging } = this.props; 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 ( <> <HoldersList groups={this.props.groups} loading={this.props.loading} - onSelectPermission={this.props.onSelectPermission} onToggleGroup={this.handleToggleGroup} onToggleUser={this.handleToggleUser} permissions={permissions} - selectedPermission={this.props.selectedPermission} users={this.props.users}> <SearchForm filter={this.props.filter} @@ -107,8 +117,14 @@ export default class AllHoldersList extends React.PureComponent<Props> { query={this.props.query} /> </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); diff --git a/server/sonar-web/src/main/js/apps/permissions/global/components/App.tsx b/server/sonar-web/src/main/js/apps/permissions/global/components/App.tsx index fa13917f4ea..f34942f8cc2 100644 --- a/server/sonar-web/src/main/js/apps/permissions/global/components/App.tsx +++ b/server/sonar-web/src/main/js/apps/permissions/global/components/App.tsx @@ -34,14 +34,13 @@ interface Props { } interface State { - filter: string; + filter: 'all' | 'groups' | 'users'; groups: PermissionGroup[]; - groupsPaging: Paging; + groupsPaging?: Paging; loading: boolean; query: string; - selectedPermission?: string; users: PermissionUser[]; - usersPaging: Paging; + usersPaging?: Paging; } export class App extends React.PureComponent<Props, State> { @@ -52,11 +51,9 @@ export class App extends React.PureComponent<Props, State> { this.state = { filter: 'all', groups: [], - groupsPaging: { pageIndex: 1, pageSize: 100, total: 0 }, loading: true, query: '', - users: [], - usersPaging: { pageIndex: 1, pageSize: 100, total: 0 } + users: [] }; } @@ -71,37 +68,25 @@ export class App extends React.PureComponent<Props, State> { loadUsersAndGroups = (userPage?: number, groupsPage?: number) => { 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' ? api.getGlobalPermissionsUsers({ q: query || undefined, - permission: selectedPermission, organization: organization && organization.key, 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' ? api.getGlobalPermissionsGroups({ q: query || undefined, - permission: selectedPermission, organization: organization && organization.key, p: groupsPage }) - : Promise.resolve({ - paging: { pageIndex: 1, pageSize: 100, total: 0 }, - groups: [] - }); + : Promise.resolve({ paging: undefined, groups: [] }); return Promise.all([getUsers, getGroups]); }; @@ -122,10 +107,11 @@ export class App extends React.PureComponent<Props, State> { }; onLoadMore = () => { + const { usersPaging, groupsPaging } = this.state; this.setState({ loading: true }); 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]) => { if (this.mounted) { this.setState(({ groups, users }) => ({ @@ -139,7 +125,7 @@ export class App extends React.PureComponent<Props, State> { }, this.stopLoading); }; - onFilter = (filter: string) => { + onFilter = (filter: 'all' | 'groups' | 'users') => { this.setState({ filter }, this.loadHolders); }; @@ -147,15 +133,6 @@ export class App extends React.PureComponent<Props, State> { 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) => { return groups.map( candidate => @@ -315,11 +292,9 @@ export class App extends React.PureComponent<Props, State> { onFilter={this.onFilter} onLoadMore={this.onLoadMore} onSearch={this.onSearch} - onSelectPermission={this.onSelectPermission} query={this.state.query} revokePermissionFromGroup={this.revokePermissionFromGroup} revokePermissionFromUser={this.revokePermissionFromUser} - selectedPermission={this.state.selectedPermission} users={this.state.users} usersPaging={this.state.usersPaging} /> diff --git a/server/sonar-web/src/main/js/apps/permissions/project/components/AllHoldersList.tsx b/server/sonar-web/src/main/js/apps/permissions/project/components/AllHoldersList.tsx index ea9119ad4c3..f03354dad35 100644 --- a/server/sonar-web/src/main/js/apps/permissions/project/components/AllHoldersList.tsx +++ b/server/sonar-web/src/main/js/apps/permissions/project/components/AllHoldersList.tsx @@ -21,8 +21,8 @@ import * as React from 'react'; import { without } from 'lodash'; import SearchForm from '../../shared/components/SearchForm'; import HoldersList from '../../shared/components/HoldersList'; -import { translate } from '../../../../helpers/l10n'; -import { PERMISSIONS_ORDER_BY_QUALIFIER } from '../constants'; +import ListFooter from '../../../../components/controls/ListFooter'; +import { PERMISSIONS_ORDER_BY_QUALIFIER, convertToPermissionDefinitions } from '../../utils'; import { Component, Paging, @@ -30,7 +30,6 @@ import { PermissionUser, Visibility } from '../../../../app/types'; -import ListFooter from '../../../../components/controls/ListFooter'; interface Props { component: Component; @@ -38,8 +37,8 @@ interface Props { grantPermissionToGroup: (group: string, permission: string) => Promise<void>; grantPermissionToUser: (user: string, permission: string) => Promise<void>; groups: PermissionGroup[]; - groupsPaging: Paging; - onLoadMore: (usersPageIndex: number, groupsPageIndex: number) => void; + groupsPaging?: Paging; + onLoadMore: () => void; onFilterChange: (filter: string) => void; onPermissionSelect: (permissions?: string) => void; onQueryChange: (query: string) => void; @@ -48,7 +47,7 @@ interface Props { revokePermissionFromUser: (user: string, permission: string) => Promise<void>; selectedPermission?: string; users: PermissionUser[]; - usersPaging: Paging; + usersPaging?: Paging; visibility?: Visibility; } @@ -77,31 +76,24 @@ export default class AllHoldersList extends React.PureComponent<Props> { this.props.onPermissionSelect(permission); }; - handleLoadMore = () => { - this.props.onLoadMore( - this.props.usersPaging.pageIndex + 1, - this.props.groupsPaging.pageIndex + 1 - ); - }; - render() { + const { filter, groups, groupsPaging, users, usersPaging } = this.props; let order = PERMISSIONS_ORDER_BY_QUALIFIER[this.props.component.qualifier]; if (this.props.visibility === Visibility.Public) { 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 ( <> @@ -120,7 +112,7 @@ export default class AllHoldersList extends React.PureComponent<Props> { query={this.props.query} /> </HoldersList> - <ListFooter count={count} loadMore={this.handleLoadMore} total={total} /> + <ListFooter count={count} loadMore={this.props.onLoadMore} total={total} /> </> ); } diff --git a/server/sonar-web/src/main/js/apps/permissions/project/components/App.tsx b/server/sonar-web/src/main/js/apps/permissions/project/components/App.tsx index a67598d44c6..9e9f7678116 100644 --- a/server/sonar-web/src/main/js/apps/permissions/project/components/App.tsx +++ b/server/sonar-web/src/main/js/apps/permissions/project/components/App.tsx @@ -45,12 +45,12 @@ interface State { disclaimer: boolean; filter: string; groups: PermissionGroup[]; - groupsPaging: Paging; + groupsPaging?: Paging; loading: boolean; query: string; selectedPermission?: string; users: PermissionUser[]; - usersPaging: Paging; + usersPaging?: Paging; } export default class App extends React.PureComponent<Props, State> { @@ -62,11 +62,9 @@ export default class App extends React.PureComponent<Props, State> { disclaimer: false, filter: 'all', groups: [], - groupsPaging: { pageIndex: 1, pageSize: 100, total: 0 }, loading: true, query: '', - users: [], - usersPaging: { pageIndex: 1, pageSize: 100, total: 0 } + users: [] }; } @@ -85,55 +83,67 @@ export default class App extends React.PureComponent<Props, State> { } }; - 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) => { @@ -344,10 +354,6 @@ export default class App extends React.PureComponent<Props, State> { } }; - handleLoadMore = (usersPageIndex: number, groupsPageIndex: number) => { - this.loadHolders(usersPageIndex, groupsPageIndex); - }; - render() { const canTurnToPrivate = this.props.component.configuration != null && @@ -389,7 +395,7 @@ export default class App extends React.PureComponent<Props, State> { groups={this.state.groups} groupsPaging={this.state.groupsPaging} onFilterChange={this.handleFilterChange} - onLoadMore={this.handleLoadMore} + onLoadMore={this.onLoadMore} onPermissionSelect={this.handlePermissionSelect} onQueryChange={this.handleQueryChange} query={this.state.query} diff --git a/server/sonar-web/src/main/js/apps/permissions/project/constants.tsx b/server/sonar-web/src/main/js/apps/permissions/project/constants.tsx deleted file mode 100644 index 709fd13ab73..00000000000 --- a/server/sonar-web/src/main/js/apps/permissions/project/constants.tsx +++ /dev/null @@ -1,39 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -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 -}; diff --git a/server/sonar-web/src/main/js/apps/permissions/shared/components/GroupHolder.tsx b/server/sonar-web/src/main/js/apps/permissions/shared/components/GroupHolder.tsx index dac80f50c86..54b3c93a865 100644 --- a/server/sonar-web/src/main/js/apps/permissions/shared/components/GroupHolder.tsx +++ b/server/sonar-web/src/main/js/apps/permissions/shared/components/GroupHolder.tsx @@ -19,16 +19,16 @@ */ import * as React from 'react'; import { without } from 'lodash'; -import Checkbox from '../../../../components/controls/Checkbox'; +import PermissionCell from './PermissionCell'; import GroupIcon from '../../../../components/icons-components/GroupIcon'; -import { PermissionGroup } from '../../../../app/types'; +import { PermissionDefinitions, PermissionGroup } from '../../../../app/types'; +import { isPermissionDefinitionGroup } from '../../utils'; interface Props { group: PermissionGroup; - permissions: string[]; - selectedPermission?: string; - permissionsOrder: string[]; onToggle: (group: PermissionGroup, permission: string) => Promise<void>; + permissions: PermissionDefinitions; + selectedPermission?: string; } interface State { @@ -63,26 +63,11 @@ export default class GroupHolder extends React.PureComponent<Props, State> { }; 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; return ( <tr> - <td className="nowrap"> + <td className="nowrap text-middle"> <div className="display-inline-block text-middle big-spacer-right"> <GroupIcon /> </div> @@ -95,7 +80,16 @@ export default class GroupHolder extends React.PureComponent<Props, State> { </div> </div> </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> ); } diff --git a/server/sonar-web/src/main/js/apps/permissions/shared/components/HoldersList.tsx b/server/sonar-web/src/main/js/apps/permissions/shared/components/HoldersList.tsx index 791c42a1760..5dd7cbd16da 100644 --- a/server/sonar-web/src/main/js/apps/permissions/shared/components/HoldersList.tsx +++ b/server/sonar-web/src/main/js/apps/permissions/shared/components/HoldersList.tsx @@ -18,20 +18,21 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { groupBy } from 'lodash'; +import { partition, sortBy } from 'lodash'; import UserHolder from './UserHolder'; import GroupHolder from './GroupHolder'; import PermissionHeader from './PermissionHeader'; 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 { loading?: boolean; groups: PermissionGroup[]; - onSelectPermission: (permission: string) => void; + onSelectPermission?: (permission: string) => void; onToggleGroup: (group: PermissionGroup, permission: string) => Promise<void>; onToggleUser: (user: PermissionUser, permission: string) => Promise<void>; - permissions: Permission[]; + permissions: PermissionDefinitions; selectedPermission?: string; showPublicProjectsWarning?: boolean; users: PermissionUser[]; @@ -42,27 +43,6 @@ export default class HoldersList extends React.PureComponent<Props> { 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() { const columns = this.props.permissions.length + 1; return ( @@ -72,65 +52,71 @@ export default class HoldersList extends React.PureComponent<Props> { ); } - 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 - key={'user-' + user.login} + key={'user-' + item.login} onToggle={this.props.onToggleUser} - permissions={user.permissions} - permissionsOrder={permissionsOrder} + permissions={permissions} selectedPermission={this.props.selectedPermission} - user={user} + user={item} /> - ); - } - - renderGroup(group: PermissionGroup, permissionsOrder: string[]) { - return ( + ) : ( <GroupHolder - group={group} - key={'group-' + group.id} + group={item} + key={'group-' + item.id} onToggle={this.props.onToggleGroup} - permissions={group.permissions} - permissionsOrder={permissionsOrder} + permissions={permissions} selectedPermission={this.props.selectedPermission} /> ); } 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, item => item.permissions.length > 0 ); return ( <div className="boxed-group boxed-group-inner"> <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> {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 && itemWithoutPermissions.length > 0 && ( <> <tr> - <td className="divider" colSpan={6} /> + <td className="divider" colSpan={20} /> </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> </table> </div> diff --git a/server/sonar-web/src/main/js/apps/permissions/shared/components/PermissionCell.tsx b/server/sonar-web/src/main/js/apps/permissions/shared/components/PermissionCell.tsx new file mode 100644 index 00000000000..30bebc26c25 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permissions/shared/components/PermissionCell.tsx @@ -0,0 +1,74 @@ +/* + * 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> + ); + } + } +} diff --git a/server/sonar-web/src/main/js/apps/permissions/shared/components/PermissionHeader.tsx b/server/sonar-web/src/main/js/apps/permissions/shared/components/PermissionHeader.tsx index a353a5fe549..35fd48be2ff 100644 --- a/server/sonar-web/src/main/js/apps/permissions/shared/components/PermissionHeader.tsx +++ b/server/sonar-web/src/main/js/apps/permissions/shared/components/PermissionHeader.tsx @@ -18,15 +18,17 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; +import * as classNames from 'classnames'; import HelpTooltip from '../../../../components/controls/HelpTooltip'; import InstanceMessage from '../../../../components/common/InstanceMessage'; -import Tooltip from '../../../../components/controls/Tooltip'; 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 { - onSelectPermission: (permission: string) => void; - permission: Permission; + onSelectPermission?: (permission: string) => void; + permission: PermissionDefinition | PermissionDefinitionGroup; selectedPermission?: string; showPublicProjectsWarning?: boolean; } @@ -35,42 +37,68 @@ export default class PermissionHeader extends React.PureComponent<Props> { handlePermissionClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => { event.preventDefault(); 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> - ); + ); + } + return <InstanceMessage message={permission.description} />; } - return <InstanceMessage message={permission.description} />; }; 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 ( <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"> - <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> </th> ); diff --git a/server/sonar-web/src/main/js/apps/permissions/shared/components/UserHolder.tsx b/server/sonar-web/src/main/js/apps/permissions/shared/components/UserHolder.tsx index e3a775b02b5..728fe12cd98 100644 --- a/server/sonar-web/src/main/js/apps/permissions/shared/components/UserHolder.tsx +++ b/server/sonar-web/src/main/js/apps/permissions/shared/components/UserHolder.tsx @@ -19,17 +19,17 @@ */ import * as React from 'react'; import { without } from 'lodash'; +import PermissionCell from './PermissionCell'; import Avatar from '../../../../components/ui/Avatar'; -import Checkbox from '../../../../components/controls/Checkbox'; import { translate } from '../../../../helpers/l10n'; -import { PermissionUser } from '../../../../app/types'; +import { PermissionDefinitions, PermissionUser } from '../../../../app/types'; +import { isPermissionDefinitionGroup } from '../../utils'; interface Props { - user: PermissionUser; - permissions: string[]; - selectedPermission?: string; - permissionsOrder: string[]; onToggle: (user: PermissionUser, permission: string) => Promise<void>; + permissions: PermissionDefinitions; + selectedPermission?: string; + user: PermissionUser; } interface State { @@ -64,26 +64,22 @@ export default class UserHolder extends React.PureComponent<Props, State> { }; 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>') { return ( <tr> - <td className="nowrap"> + <td className="nowrap text-middle"> <div className="display-inline-block text-middle"> <div> <strong>{user.name}</strong> @@ -100,7 +96,7 @@ export default class UserHolder extends React.PureComponent<Props, State> { return ( <tr> - <td className="nowrap"> + <td className="nowrap text-middle"> <Avatar className="text-middle big-spacer-right" hash={user.avatar} diff --git a/server/sonar-web/src/main/js/apps/permissions/shared/components/__tests__/GroupHolder-test.tsx b/server/sonar-web/src/main/js/apps/permissions/shared/components/__tests__/GroupHolder-test.tsx index ce27bced242..9776e533260 100644 --- a/server/sonar-web/src/main/js/apps/permissions/shared/components/__tests__/GroupHolder-test.tsx +++ b/server/sonar-web/src/main/js/apps/permissions/shared/components/__tests__/GroupHolder-test.tsx @@ -33,29 +33,35 @@ const groupHolder = ( group={group} key="foo" 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'} /> ); -it('should display checkboxes for permissions', () => { +it('should render correctly', () => { 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); expect(wrapper.state().loading).toEqual([]); (wrapper.instance() as GroupHolder).handleCheck(true, 'baz'); wrapper.update(); expect(wrapper.state().loading).toEqual(['baz']); - expect(wrapper).toMatchSnapshot(); (wrapper.instance() as GroupHolder).handleCheck(true, 'bar'); wrapper.update(); expect(wrapper.state().loading).toEqual(['baz', 'bar']); - expect(wrapper).toMatchSnapshot(); await waitAndUpdate(wrapper); expect(wrapper.state().loading).toEqual([]); diff --git a/server/sonar-web/src/main/js/apps/permissions/shared/components/__tests__/HoldersList-test.tsx b/server/sonar-web/src/main/js/apps/permissions/shared/components/__tests__/HoldersList-test.tsx index 05def6fb2c7..e8e20d6bc31 100644 --- a/server/sonar-web/src/main/js/apps/permissions/shared/components/__tests__/HoldersList-test.tsx +++ b/server/sonar-web/src/main/js/apps/permissions/shared/components/__tests__/HoldersList-test.tsx @@ -21,7 +21,16 @@ import * as React from 'react'; import { shallow } from 'enzyme'; 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 = [ { id: 'foobar', name: 'Foobar', permissions: ['bar'] }, diff --git a/server/sonar-web/src/main/js/apps/permissions/shared/components/__tests__/PermissionCell-test.tsx b/server/sonar-web/src/main/js/apps/permissions/shared/components/__tests__/PermissionCell-test.tsx new file mode 100644 index 00000000000..1abf375540e --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permissions/shared/components/__tests__/PermissionCell-test.tsx @@ -0,0 +1,89 @@ +/* + * 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(); +}); diff --git a/server/sonar-web/src/main/js/apps/permissions/shared/components/__tests__/UserHolder-test.tsx b/server/sonar-web/src/main/js/apps/permissions/shared/components/__tests__/UserHolder-test.tsx index f3164e608dd..c04db2be6b0 100644 --- a/server/sonar-web/src/main/js/apps/permissions/shared/components/__tests__/UserHolder-test.tsx +++ b/server/sonar-web/src/main/js/apps/permissions/shared/components/__tests__/UserHolder-test.tsx @@ -32,30 +32,36 @@ const userHolder = ( <UserHolder key="foo" 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'} user={user} /> ); -it('should display checkboxes for permissions', () => { +it('should render correctly', () => { 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); expect(wrapper.state().loading).toEqual([]); (wrapper.instance() as UserHolder).handleCheck(true, 'baz'); wrapper.update(); expect(wrapper.state().loading).toEqual(['baz']); - expect(wrapper).toMatchSnapshot(); (wrapper.instance() as UserHolder).handleCheck(true, 'bar'); wrapper.update(); expect(wrapper.state().loading).toEqual(['baz', 'bar']); - expect(wrapper).toMatchSnapshot(); await waitAndUpdate(wrapper); expect(wrapper.state().loading).toEqual([]); diff --git a/server/sonar-web/src/main/js/apps/permissions/shared/components/__tests__/__snapshots__/GroupHolder-test.tsx.snap b/server/sonar-web/src/main/js/apps/permissions/shared/components/__tests__/__snapshots__/GroupHolder-test.tsx.snap index 170c88a26a3..25b18f84153 100644 --- a/server/sonar-web/src/main/js/apps/permissions/shared/components/__tests__/__snapshots__/GroupHolder-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/permissions/shared/components/__tests__/__snapshots__/GroupHolder-test.tsx.snap @@ -1,9 +1,9 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`should disabled checkboxes when waiting for promise to return 1`] = ` +exports[`should render correctly 1`] = ` <tr> <td - className="nowrap" + className="nowrap text-middle" > <div className="display-inline-block text-middle big-spacer-right" @@ -28,169 +28,59 @@ exports[`should disabled checkboxes when waiting for promise to return 1`] = ` /> </div> </td> - <td - className="text-center text-middle" - key="bar" - style={ + <PermissionCell + key="admin" + loading={Array []} + onCheck={[Function]} + permission={ 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 { - "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 { - "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 { - "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 { - "backgroundColor": "transparent", + "id": "foobar", + "name": "Foobar", + "permissions": Array [ + "bar", + ], } } - > - <Checkbox - checked={false} - disabled={false} - id="baz" - onCheck={[Function]} - thirdState={false} - /> - </td> + selectedPermission="bar" + /> </tr> `; diff --git a/server/sonar-web/src/main/js/apps/permissions/shared/components/__tests__/__snapshots__/HoldersList-test.tsx.snap b/server/sonar-web/src/main/js/apps/permissions/shared/components/__tests__/__snapshots__/HoldersList-test.tsx.snap index 811717f06f4..df11bc04eb6 100644 --- a/server/sonar-web/src/main/js/apps/permissions/shared/components/__tests__/__snapshots__/HoldersList-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/permissions/shared/components/__tests__/__snapshots__/HoldersList-test.tsx.snap @@ -13,13 +13,35 @@ exports[`should display users and groups 1`] = ` className="nowrap bordered-bottom" /> <PermissionHeader - key="bar" + key="foo" onSelectPermission={[MockFunction]} permission={ 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" @@ -27,41 +49,31 @@ exports[`should display users and groups 1`] = ` </tr> </thead> <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 key="user-barbaz" onToggle={[MockFunction]} permissions={ 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" @@ -78,23 +90,37 @@ exports[`should display users and groups 1`] = ` <GroupHolder group={ Object { - "id": "foobar", - "name": "Foobar", + "id": "barbaz", + "name": "Barbaz", "permissions": Array [ "bar", ], } } - key="group-foobar" + key="group-barbaz" onToggle={[MockFunction]} permissions={ 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" @@ -104,12 +130,26 @@ exports[`should display users and groups 1`] = ` onToggle={[MockFunction]} permissions={ 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" @@ -123,12 +163,53 @@ exports[`should display users and groups 1`] = ` } } /> - <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 group={ Object { @@ -139,10 +220,28 @@ exports[`should display users and groups 1`] = ` } key="group-abc" onToggle={[MockFunction]} - permissions={Array []} - permissionsOrder={ + permissions={ 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" @@ -150,10 +249,28 @@ exports[`should display users and groups 1`] = ` <UserHolder key="user-bcd" onToggle={[MockFunction]} - permissions={Array []} - permissionsOrder={ + permissions={ 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" diff --git a/server/sonar-web/src/main/js/apps/permissions/shared/components/__tests__/__snapshots__/PermissionCell-test.tsx.snap b/server/sonar-web/src/main/js/apps/permissions/shared/components/__tests__/__snapshots__/PermissionCell-test.tsx.snap new file mode 100644 index 00000000000..2972dee2385 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permissions/shared/components/__tests__/__snapshots__/PermissionCell-test.tsx.snap @@ -0,0 +1,84 @@ +// 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> +`; diff --git a/server/sonar-web/src/main/js/apps/permissions/shared/components/__tests__/__snapshots__/UserHolder-test.tsx.snap b/server/sonar-web/src/main/js/apps/permissions/shared/components/__tests__/__snapshots__/UserHolder-test.tsx.snap index 6288d3a1f77..1adb2bb5f86 100644 --- a/server/sonar-web/src/main/js/apps/permissions/shared/components/__tests__/__snapshots__/UserHolder-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/permissions/shared/components/__tests__/__snapshots__/UserHolder-test.tsx.snap @@ -1,9 +1,9 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`should disabled checkboxes when waiting for promise to return 1`] = ` +exports[`should render correctly 1`] = ` <tr> <td - className="nowrap" + className="nowrap text-middle" > <Connect(Avatar) className="text-middle big-spacer-right" @@ -28,169 +28,59 @@ exports[`should disabled checkboxes when waiting for promise to return 1`] = ` /> </div> </td> - <td - className="text-center text-middle" - key="bar" - style={ + <PermissionCell + key="admin" + loading={Array []} + onCheck={[Function]} + permission={ 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 { - "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" - style={ + loading={Array []} + onCheck={[Function]} + permission={ 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 { - "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> `; diff --git a/server/sonar-web/src/main/js/apps/permissions/styles.css b/server/sonar-web/src/main/js/apps/permissions/styles.css index 59ba1212b87..a99ab42b908 100644 --- a/server/sonar-web/src/main/js/apps/permissions/styles.css +++ b/server/sonar-web/src/main/js/apps/permissions/styles.css @@ -17,22 +17,24 @@ * along with this program; if not, write to the Free Software Foundation, * 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 { + display: inline-block; width: 100px; } .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; - padding: 0; + width: 100%; } diff --git a/server/sonar-web/src/main/js/apps/permissions/utils.ts b/server/sonar-web/src/main/js/apps/permissions/utils.ts new file mode 100644 index 00000000000..98de45fa190 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permissions/utils.ts @@ -0,0 +1,87 @@ +/* + * 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); +} diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index a20dc46664d..aaac8fa91d7 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -1998,21 +1998,25 @@ metric.wont_fix_issues.name=Won't Fix Issues global_permissions.permission=Permission global_permissions.users=Users global_permissions.groups=Groups +global_permissions.administer=Administer +global_permissions.creator=Create global_permissions.admin=Administer System 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.gateadmin=Administer Quality Gates +global_permissions.gateadmin=Quality Gates global_permissions.gateadmin.desc=Ability to perform any action on quality gates. 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.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.filter_by_x_permission=Filter by "{0}" permission 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.applicationcreator=Applications +global_permissions.applicationcreator.desc=Ability to create an application. +global_permissions.portfoliocreator=Portfolios +global_permissions.portfoliocreator.desc=Ability to create a portfolio. #------------------------------------------------------------------------------ # |