Browse Source

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

tags/7.5
Grégoire Aubert 5 years ago
parent
commit
7f339fd2f1
23 changed files with 946 additions and 726 deletions
  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 View File

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

+ 8
- 7
server/sonar-web/src/main/js/apps/permission-templates/components/Template.js View File

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


+ 47
- 31
server/sonar-web/src/main/js/apps/permissions/global/components/AllHoldersList.tsx View File

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

+ 13
- 38
server/sonar-web/src/main/js/apps/permissions/global/components/App.tsx View File

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

+ 18
- 26
server/sonar-web/src/main/js/apps/permissions/project/components/AllHoldersList.tsx View File

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

+ 63
- 57
server/sonar-web/src/main/js/apps/permissions/project/components/App.tsx View File

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

+ 0
- 39
server/sonar-web/src/main/js/apps/permissions/project/constants.tsx View File

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

+ 16
- 22
server/sonar-web/src/main/js/apps/permissions/shared/components/GroupHolder.tsx View File

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

+ 42
- 56
server/sonar-web/src/main/js/apps/permissions/shared/components/HoldersList.tsx View File

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

+ 74
- 0
server/sonar-web/src/main/js/apps/permissions/shared/components/PermissionCell.tsx View File

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

+ 58
- 30
server/sonar-web/src/main/js/apps/permissions/shared/components/PermissionHeader.tsx View File

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

+ 18
- 22
server/sonar-web/src/main/js/apps/permissions/shared/components/UserHolder.tsx View File

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

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

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

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

@@ -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'] },

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

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

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

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

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

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

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

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

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

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

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

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

+ 11
- 9
server/sonar-web/src/main/js/apps/permissions/styles.css View File

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

+ 87
- 0
server/sonar-web/src/main/js/apps/permissions/utils.ts View File

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

+ 9
- 5
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

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

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

Loading…
Cancel
Save