diff options
author | Jeremy Davis <jeremy.davis@sonarsource.com> | 2021-10-18 19:19:19 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2021-10-22 20:03:28 +0000 |
commit | 6770559c5db408687b25b483140d2d4530e7f9a7 (patch) | |
tree | f98c42cbfc9256c7b9e129195c61cb3caed71e4f /server/sonar-web | |
parent | 0ee35ea91483d3e7122ffe8f97a4543b1895dadd (diff) | |
download | sonarqube-6770559c5db408687b25b483140d2d4530e7f9a7.tar.gz sonarqube-6770559c5db408687b25b483140d2d4530e7f9a7.zip |
SONAR-15441 QG permission management handles groups
Diffstat (limited to 'server/sonar-web')
18 files changed, 586 insertions, 165 deletions
diff --git a/server/sonar-web/src/main/js/api/quality-gates.ts b/server/sonar-web/src/main/js/api/quality-gates.ts index c4c41090810..1c86cc84767 100644 --- a/server/sonar-web/src/main/js/api/quality-gates.ts +++ b/server/sonar-web/src/main/js/api/quality-gates.ts @@ -21,7 +21,9 @@ import throwGlobalError from '../app/utils/throwGlobalError'; import { getJSON, post, postJSON } from '../helpers/request'; import { BranchParameters } from '../types/branch-like'; import { + AddDeleteGroupPermissionsParameters, AddDeleteUserPermissionsParameters, + Group, QualityGateApplicationStatus, QualityGateProjectStatus, SearchPermissionsParameters @@ -141,3 +143,15 @@ export function removeUser(data: AddDeleteUserPermissionsParameters) { export function searchUsers(data: SearchPermissionsParameters): Promise<{ users: T.UserBase[] }> { return getJSON('/api/qualitygates/search_users', data).catch(throwGlobalError); } + +export function addGroup(data: AddDeleteGroupPermissionsParameters) { + return post('/api/qualitygates/add_group', data).catch(throwGlobalError); +} + +export function removeGroup(data: AddDeleteGroupPermissionsParameters) { + return post('/api/qualitygates/remove_group', data).catch(throwGlobalError); +} + +export function searchGroups(data: SearchPermissionsParameters): Promise<{ groups: Group[] }> { + return getJSON('/api/qualitygates/search_groups', data).catch(throwGlobalError); +} diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/PermissionItem.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/PermissionItem.tsx index 44a99ffe2d1..4c3067e598a 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/PermissionItem.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/PermissionItem.tsx @@ -19,26 +19,32 @@ */ import * as React from 'react'; import { DeleteButton } from '../../../components/controls/buttons'; +import GroupIcon from '../../../components/icons/GroupIcon'; import Avatar from '../../../components/ui/Avatar'; +import { Group, isUser } from '../../../types/quality-gates'; -interface Props { - onClickDelete: (user: T.UserBase) => void; - user: T.UserBase; +export interface PermissionItemProps { + onClickDelete: (item: T.UserBase | Group) => void; + item: T.UserBase | Group; } -export default function PermissionItem(props: Props) { - const { user } = props; +export default function PermissionItem(props: PermissionItemProps) { + const { item } = props; return ( <div className="display-flex-center permission-list-item padded"> - <Avatar className="spacer-right" hash={user.avatar} name={user.name} size={32} /> + {isUser(item) ? ( + <Avatar className="spacer-right" hash={item.avatar} name={item.name} size={32} /> + ) : ( + <GroupIcon className="pull-left spacer-right" size={32} /> + )} <div className="overflow-hidden flex-1"> - <strong>{user.name}</strong> - <div className="note">{user.login}</div> + <strong>{item.name}</strong> + {isUser(item) && <div className="note">{item.login}</div>} </div> - <DeleteButton onClick={() => props.onClickDelete(user)} /> + <DeleteButton onClick={() => props.onClickDelete(item)} /> </div> ); } diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatePermissions.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatePermissions.tsx index 0e0c415eeb3..2391486abe1 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatePermissions.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatePermissions.tsx @@ -19,7 +19,15 @@ */ import { sortBy } from 'lodash'; import * as React from 'react'; -import { addUser, removeUser, searchUsers } from '../../../api/quality-gates'; +import { + addGroup, + addUser, + removeGroup, + removeUser, + searchGroups, + searchUsers +} from '../../../api/quality-gates'; +import { Group, isUser, SearchPermissionsParameters } from '../../../types/quality-gates'; import QualityGatePermissionsRenderer from './QualityGatePermissionsRenderer'; interface Props { @@ -27,16 +35,18 @@ interface Props { } interface State { + groups: Group[]; submitting: boolean; loading: boolean; showAddModal: boolean; - userPermissionToDelete?: T.UserBase; + permissionToDelete?: T.UserBase | Group; users: T.UserBase[]; } export default class QualityGatePermissions extends React.Component<Props, State> { mounted = false; state: State = { + groups: [], submitting: false, loading: true, showAddModal: false, @@ -63,15 +73,17 @@ export default class QualityGatePermissions extends React.Component<Props, State const { qualityGate } = this.props; this.setState({ loading: true }); - const { users } = await searchUsers({ - qualityGate: qualityGate.id, + const params: SearchPermissionsParameters = { + gateName: qualityGate.name, selected: 'selected' - }).catch(() => ({ - users: [] - })); + }; + const [{ users }, { groups }] = await Promise.all([ + searchUsers(params).catch(() => ({ users: [] })), + searchGroups(params).catch(() => ({ groups: [] })) + ]); if (this.mounted) { - this.setState({ loading: false, users }); + this.setState({ groups, loading: false, users }); } }; @@ -83,58 +95,83 @@ export default class QualityGatePermissions extends React.Component<Props, State this.setState({ showAddModal: true }); }; - handleSubmitAddPermission = async (user: T.UserBase) => { + handleSubmitAddPermission = async (item: T.UserBase | Group) => { const { qualityGate } = this.props; this.setState({ submitting: true }); let error = false; try { - await addUser({ qualityGate: qualityGate.id, userLogin: user.login }); + if (isUser(item)) { + await addUser({ gateName: qualityGate.name, login: item.login }); + } else { + await addGroup({ gateName: qualityGate.name, groupName: item.name }); + } } catch { error = true; } + if (this.mounted && !error) { + if (isUser(item)) { + this.setState(({ users }) => ({ + showAddModal: false, + users: sortBy(users.concat(item), u => u.name) + })); + } else { + this.setState(({ groups }) => ({ + showAddModal: false, + groups: sortBy(groups.concat(item), g => g.name) + })); + } + } + if (this.mounted) { - this.setState(({ users }) => { - return { - submitting: false, - showAddModal: error, - users: sortBy(users.concat(user), u => u.name) - }; + this.setState({ + submitting: false }); } }; handleCloseDeletePermission = () => { - this.setState({ userPermissionToDelete: undefined }); + this.setState({ permissionToDelete: undefined }); }; - handleClickDeletePermission = (userPermissionToDelete?: T.UserBase) => { - this.setState({ userPermissionToDelete }); + handleClickDeletePermission = (permissionToDelete?: T.UserBase | Group) => { + this.setState({ permissionToDelete }); }; - handleConfirmDeletePermission = async (user: T.UserBase) => { + handleConfirmDeletePermission = async (item: T.UserBase | Group) => { const { qualityGate } = this.props; let error = false; try { - await removeUser({ qualityGate: qualityGate.id, userLogin: user.login }); + if (isUser(item)) { + await removeUser({ gateName: qualityGate.name, login: item.login }); + } else { + await removeGroup({ gateName: qualityGate.name, groupName: item.name }); + } } catch { error = true; } if (this.mounted && !error) { - this.setState(({ users }) => ({ - users: users.filter(u => u.login !== user.login) - })); + if (isUser(item)) { + this.setState(({ users }) => ({ + users: users.filter(u => u.login !== item.login) + })); + } else { + this.setState(({ groups }) => ({ + groups: groups.filter(g => g.name !== item.name) + })); + } } }; render() { const { qualityGate } = this.props; - const { submitting, loading, showAddModal, userPermissionToDelete, users } = this.state; + const { groups, submitting, loading, showAddModal, permissionToDelete, users } = this.state; return ( <QualityGatePermissionsRenderer + groups={groups} loading={loading} onClickAddPermission={this.handleClickAddPermission} onCloseAddPermission={this.handleCloseAddPermission} @@ -142,10 +179,10 @@ export default class QualityGatePermissions extends React.Component<Props, State onCloseDeletePermission={this.handleCloseDeletePermission} onClickDeletePermission={this.handleClickDeletePermission} onConfirmDeletePermission={this.handleConfirmDeletePermission} + permissionToDelete={permissionToDelete} qualityGate={qualityGate} showAddModal={showAddModal} submitting={submitting} - userPermissionToDelete={userPermissionToDelete} users={users} /> ); diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatePermissionsAddModal.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatePermissionsAddModal.tsx index ce7c92f7b63..a4ae9357c1b 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatePermissionsAddModal.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatePermissionsAddModal.tsx @@ -19,12 +19,13 @@ */ import { debounce } from 'lodash'; import * as React from 'react'; -import { searchUsers } from '../../../api/quality-gates'; +import { searchGroups, searchUsers } from '../../../api/quality-gates'; +import { Group, SearchPermissionsParameters } from '../../../types/quality-gates'; import QualityGatePermissionsAddModalRenderer from './QualityGatePermissionsAddModalRenderer'; interface Props { onClose: () => void; - onSubmit: (selectedUser: T.UserBase) => void; + onSubmit: (selection: T.UserBase | Group) => void; qualityGate: T.QualityGate; submitting: boolean; } @@ -32,8 +33,8 @@ interface Props { interface State { loading: boolean; query?: string; - searchResults: T.UserBase[]; - selection?: T.UserBase; + searchResults: Array<T.UserBase | Group>; + selection?: T.UserBase | Group; } const DEBOUNCE_DELAY = 250; @@ -58,21 +59,29 @@ export default class QualityGatePermissionsAddModal extends React.Component<Prop this.mounted = false; } - handleSearch = (query: string) => { + handleSearch = async (query: string) => { const { qualityGate } = this.props; this.setState({ loading: true }); - searchUsers({ qualityGate: qualityGate.id, q: query, selected: 'deselected' }).then( - result => { - if (this.mounted) { - this.setState({ loading: false, searchResults: result.users }); - } - }, - () => { - if (this.mounted) { - this.setState({ loading: false }); - } + + const queryParams: SearchPermissionsParameters = { + gateName: qualityGate.name, + q: query, + selected: 'deselected' + }; + + try { + const [{ users }, { groups }] = await Promise.all([ + searchUsers(queryParams), + searchGroups(queryParams) + ]); + if (this.mounted) { + this.setState({ loading: false, searchResults: [...users, ...groups] }); } - ); + } catch { + if (this.mounted) { + this.setState({ loading: false }); + } + } }; handleInputChange = (query: string) => { @@ -82,7 +91,7 @@ export default class QualityGatePermissionsAddModal extends React.Component<Prop } }; - handleSelection = (selection: T.UserBase) => { + handleSelection = (selection: T.UserBase | Group) => { this.setState({ selection }); }; diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatePermissionsAddModalRenderer.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatePermissionsAddModalRenderer.tsx index dd4f7355f5d..ba33e972406 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatePermissionsAddModalRenderer.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatePermissionsAddModalRenderer.tsx @@ -21,22 +21,24 @@ import * as React from 'react'; import { ResetButtonLink, SubmitButton } from '../../../components/controls/buttons'; import Modal from '../../../components/controls/Modal'; import Select from '../../../components/controls/Select'; +import GroupIcon from '../../../components/icons/GroupIcon'; import Avatar from '../../../components/ui/Avatar'; import { translate, translateWithParameters } from '../../../helpers/l10n'; +import { Group, isUser } from '../../../types/quality-gates'; export interface QualityGatePermissionsAddModalRendererProps { onClose: () => void; onInputChange: (query: string) => void; onSubmit: (event: React.SyntheticEvent<HTMLFormElement>) => void; - onSelection: (selection: T.UserBase) => void; + onSelection: (selection: T.UserBase | Group) => void; submitting: boolean; loading: boolean; query: string; - searchResults: T.UserBase[]; - selection?: T.UserBase; + searchResults: Array<T.UserBase | Group>; + selection?: T.UserBase | Group; } -type Option = T.UserBase & { value: string }; +type Option = (T.UserBase | Group) & { value: string }; export default function QualityGatePermissionsAddModalRenderer( props: QualityGatePermissionsAddModalRendererProps @@ -68,7 +70,7 @@ export default function QualityGatePermissionsAddModalRenderer( onChange={props.onSelection} onInputChange={props.onInputChange} optionRenderer={optionRenderer} - options={searchResults.map(r => ({ ...r, value: r.login }))} + options={searchResults.map(r => ({ ...r, value: isUser(r) ? r.login : r.name }))} placeholder="" searchable={true} value={selection} @@ -89,9 +91,13 @@ export default function QualityGatePermissionsAddModalRenderer( function optionRenderer(option: Option) { return ( <> - <Avatar hash={option.avatar} name={option.name} size={16} /> + {isUser(option) ? ( + <Avatar hash={option.avatar} name={option.name} size={16} /> + ) : ( + <GroupIcon size={16} /> + )} <strong className="spacer-left">{option.name}</strong> - <span className="note little-spacer-left">{option.login}</span> + {isUser(option) && <span className="note little-spacer-left">{option.login}</span>} </> ); } diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatePermissionsRenderer.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatePermissionsRenderer.tsx index bf29803a3a8..2eda6d6dad1 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatePermissionsRenderer.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatePermissionsRenderer.tsx @@ -23,39 +23,52 @@ import { Button } from '../../../components/controls/buttons'; import ConfirmModal from '../../../components/controls/ConfirmModal'; import DeferredSpinner from '../../../components/ui/DeferredSpinner'; import { translate } from '../../../helpers/l10n'; +import { Group, isUser } from '../../../types/quality-gates'; import PermissionItem from './PermissionItem'; import QualityGatePermissionsAddModal from './QualityGatePermissionsAddModal'; export interface QualityGatePermissionsRendererProps { + groups: Group[]; loading: boolean; onClickAddPermission: () => void; onCloseAddPermission: () => void; - onSubmitAddPermission: (user: T.UserBase) => void; + onSubmitAddPermission: (item: T.UserBase | Group) => void; onCloseDeletePermission: () => void; - onConfirmDeletePermission: (user: T.UserBase) => void; - onClickDeletePermission: (user: T.UserBase) => void; + onConfirmDeletePermission: (item: T.UserBase | Group) => void; + onClickDeletePermission: (item: T.UserBase | Group) => void; + permissionToDelete?: T.UserBase | Group; qualityGate: T.QualityGate; showAddModal: boolean; submitting: boolean; - userPermissionToDelete?: T.UserBase; users: T.UserBase[]; } export default function QualityGatePermissionsRenderer(props: QualityGatePermissionsRendererProps) { - const { loading, qualityGate, showAddModal, submitting, userPermissionToDelete, users } = props; + const { + groups, + loading, + permissionToDelete, + qualityGate, + showAddModal, + submitting, + users + } = props; return ( <div className="quality-gate-permissions"> - <header className="display-flex-center spacer-bottom"> - <h3>{translate('quality_gates.permissions')}</h3> - </header> + <h3 className="spacer-bottom">{translate('quality_gates.permissions')}</h3> <p className="spacer-bottom">{translate('quality_gates.permissions.help')}</p> <div> <DeferredSpinner loading={loading}> <ul> {users.map(user => ( <li key={user.login}> - <PermissionItem onClickDelete={props.onClickDeletePermission} user={user} /> + <PermissionItem onClickDelete={props.onClickDeletePermission} item={user} /> + </li> + ))} + {groups.map(group => ( + <li key={group.name}> + <PermissionItem onClickDelete={props.onClickDeletePermission} item={group} /> </li> ))} </ul> @@ -75,19 +88,25 @@ export default function QualityGatePermissionsRenderer(props: QualityGatePermiss /> )} - {userPermissionToDelete && ( + {permissionToDelete && ( <ConfirmModal - header={translate('users.remove')} + header={ + isUser(permissionToDelete) ? translate('users.remove') : translate('groups.remove') + } confirmButtonText={translate('remove')} isDestructive={true} - confirmData={userPermissionToDelete} + confirmData={permissionToDelete} onClose={props.onCloseDeletePermission} onConfirm={props.onConfirmDeletePermission}> <FormattedMessage - defaultMessage={translate('users.remove.confirmation')} - id="users.remove.confirmation" + defaultMessage={ + isUser(permissionToDelete) + ? translate('users.remove.confirmation') + : translate('groups.remove.confirmation') + } + id="remove.confirmation" values={{ - user: <strong>{userPermissionToDelete.name}</strong> + user: <strong>{permissionToDelete.name}</strong> }} /> </ConfirmModal> diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/PermissionItem-test.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/PermissionItem-test.tsx index 4724e340757..1afc96619e8 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/PermissionItem-test.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/PermissionItem-test.tsx @@ -20,12 +20,13 @@ import { shallow } from 'enzyme'; import * as React from 'react'; import { mockUser } from '../../../../helpers/testMocks'; -import PermissionItem from '../PermissionItem'; +import PermissionItem, { PermissionItemProps } from '../PermissionItem'; it('should render correctly', () => { - expect(shallowRender()).toMatchSnapshot(); + expect(shallowRender()).toMatchSnapshot('user'); + expect(shallowRender({ item: { name: 'groupname' } })).toMatchSnapshot('group'); }); -function shallowRender() { - return shallow(<PermissionItem onClickDelete={jest.fn()} user={mockUser()} />); +function shallowRender(overrides: Partial<PermissionItemProps> = {}) { + return shallow(<PermissionItem onClickDelete={jest.fn()} item={mockUser()} {...overrides} />); } diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGatePermissions-test.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGatePermissions-test.tsx index 04eccce3a1c..cd63d9c9845 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGatePermissions-test.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGatePermissions-test.tsx @@ -19,7 +19,14 @@ */ import { shallow } from 'enzyme'; import * as React from 'react'; -import { addUser, removeUser, searchUsers } from '../../../../api/quality-gates'; +import { + addGroup, + addUser, + removeGroup, + removeUser, + searchGroups, + searchUsers +} from '../../../../api/quality-gates'; import { mockQualityGate } from '../../../../helpers/mocks/quality-gates'; import { mockUserBase } from '../../../../helpers/mocks/users'; import { waitAndUpdate } from '../../../../helpers/testUtils'; @@ -28,7 +35,10 @@ import QualityGatePermissions from '../QualityGatePermissions'; jest.mock('../../../../api/quality-gates', () => ({ addUser: jest.fn().mockResolvedValue(undefined), removeUser: jest.fn().mockResolvedValue(undefined), - searchUsers: jest.fn().mockResolvedValue({ users: [] }) + searchUsers: jest.fn().mockResolvedValue({ users: [] }), + addGroup: jest.fn().mockResolvedValue(undefined), + removeGroup: jest.fn().mockResolvedValue(undefined), + searchGroups: jest.fn().mockResolvedValue({ groups: [] }) })); beforeEach(() => { @@ -39,20 +49,32 @@ it('should render correctly', () => { expect(shallowRender()).toMatchSnapshot(); }); -it('should fetch users', async () => { +it('should fetch users and groups', async () => { const wrapper = shallowRender(); await waitAndUpdate(wrapper); - expect(searchUsers).toBeCalledWith({ qualityGate: '1', selected: 'selected' }); + expect(searchUsers).toBeCalledWith({ gateName: 'qualitygate', selected: 'selected' }); + expect(searchGroups).toBeCalledWith({ gateName: 'qualitygate', selected: 'selected' }); }); -it('should fetch users on update', async () => { +it('should handle errors when fetching users and groups', async () => { + (searchUsers as jest.Mock).mockRejectedValueOnce('nope'); + (searchGroups as jest.Mock).mockRejectedValueOnce('nope'); + + const wrapper = shallowRender(); + await waitAndUpdate(wrapper); + + expect(wrapper.state().groups).toHaveLength(0); + expect(wrapper.state().users).toHaveLength(0); +}); + +it('should fetch users and groups on update', async () => { const wrapper = shallowRender(); await waitAndUpdate(wrapper); (searchUsers as jest.Mock).mockClear(); - wrapper.setProps({ qualityGate: mockQualityGate({ id: '2' }) }); - expect(searchUsers).toBeCalledWith({ qualityGate: '2', selected: 'selected' }); + wrapper.setProps({ qualityGate: mockQualityGate({ id: '2', name: 'qg2' }) }); + expect(searchUsers).toBeCalledWith({ gateName: 'qg2', selected: 'selected' }); }); it('should handleCloseAddPermission', () => { @@ -69,23 +91,48 @@ it('should handleClickAddPermission', () => { expect(wrapper.state().showAddModal).toBe(true); }); -it('should handleSubmitAddPermission', async () => { +it('should handleSubmitAddPermission for a user', async () => { const wrapper = shallowRender(); await waitAndUpdate(wrapper); expect(wrapper.state().users).toHaveLength(0); + expect(wrapper.state().groups).toHaveLength(0); wrapper.instance().handleSubmitAddPermission(mockUserBase({ login: 'user1', name: 'User One' })); expect(wrapper.state().submitting).toBe(true); - expect(addUser).toBeCalledWith({ qualityGate: '1', userLogin: 'user1' }); + expect(addUser).toBeCalledWith({ gateName: 'qualitygate', login: 'user1' }); + expect(addGroup).not.toBeCalled(); await waitAndUpdate(wrapper); expect(wrapper.state().submitting).toBe(false); expect(wrapper.state().showAddModal).toBe(false); expect(wrapper.state().users).toHaveLength(1); + expect(wrapper.state().groups).toHaveLength(0); +}); + +it('should handleSubmitAddPermission for a group', async () => { + const wrapper = shallowRender(); + + await waitAndUpdate(wrapper); + + expect(wrapper.state().users).toHaveLength(0); + expect(wrapper.state().groups).toHaveLength(0); + + wrapper.instance().handleSubmitAddPermission({ name: 'group' }); + expect(wrapper.state().submitting).toBe(true); + + expect(addUser).not.toBeCalled(); + expect(addGroup).toBeCalledWith({ gateName: 'qualitygate', groupName: 'group' }); + + await waitAndUpdate(wrapper); + + expect(wrapper.state().submitting).toBe(false); + expect(wrapper.state().showAddModal).toBe(false); + expect(wrapper.state().users).toHaveLength(0); + expect(wrapper.state().groups).toHaveLength(1); }); it('should handleSubmitAddPermission if it returns an error', async () => { @@ -96,51 +143,81 @@ it('should handleSubmitAddPermission if it returns an error', async () => { expect(wrapper.state().users).toHaveLength(0); + wrapper.setState({ showAddModal: true }); wrapper.instance().handleSubmitAddPermission(mockUserBase({ login: 'user1', name: 'User One' })); expect(wrapper.state().submitting).toBe(true); - expect(addUser).toBeCalledWith({ qualityGate: '1', userLogin: 'user1' }); + expect(addUser).toBeCalledWith({ gateName: 'qualitygate', login: 'user1' }); await waitAndUpdate(wrapper); expect(wrapper.state().submitting).toBe(false); expect(wrapper.state().showAddModal).toBe(true); - expect(wrapper.state().users).toHaveLength(1); + expect(wrapper.state().users).toHaveLength(0); }); it('should handleCloseDeletePermission', () => { const wrapper = shallowRender(); - wrapper.setState({ userPermissionToDelete: mockUserBase() }); + wrapper.setState({ permissionToDelete: mockUserBase() }); wrapper.instance().handleCloseDeletePermission(); - expect(wrapper.state().userPermissionToDelete).toBeUndefined(); + expect(wrapper.state().permissionToDelete).toBeUndefined(); }); it('should handleClickDeletePermission', () => { const user = mockUserBase(); const wrapper = shallowRender(); - wrapper.setState({ userPermissionToDelete: undefined }); + wrapper.setState({ permissionToDelete: undefined }); wrapper.instance().handleClickDeletePermission(user); - expect(wrapper.state().userPermissionToDelete).toBe(user); + expect(wrapper.state().permissionToDelete).toBe(user); }); -it('should handleConfirmDeletePermission', async () => { +it('should handleConfirmDeletePermission for a user', async () => { const deleteThisUser = mockUserBase(); + const deleteThisGroup = { name: 'deletableGroup' }; (searchUsers as jest.Mock).mockResolvedValueOnce({ users: [deleteThisUser] }); + (searchGroups as jest.Mock).mockResolvedValueOnce({ groups: [deleteThisGroup] }); const wrapper = shallowRender(); await waitAndUpdate(wrapper); expect(wrapper.state().users).toHaveLength(1); + expect(wrapper.state().groups).toHaveLength(1); wrapper.instance().handleConfirmDeletePermission(deleteThisUser); - expect(removeUser).toBeCalledWith({ qualityGate: '1', userLogin: deleteThisUser.login }); + expect(removeUser).toBeCalledWith({ gateName: 'qualitygate', login: deleteThisUser.login }); + expect(removeGroup).not.toBeCalled(); await waitAndUpdate(wrapper); - expect(wrapper.state().userPermissionToDelete).toBeUndefined(); + expect(wrapper.state().permissionToDelete).toBeUndefined(); expect(wrapper.state().users).toHaveLength(0); + expect(wrapper.state().groups).toHaveLength(1); +}); + +it('should handleConfirmDeletePermission for a group', async () => { + const deleteThisUser = mockUserBase(); + const deleteThisGroup = { name: 'deletableGroup' }; + (searchUsers as jest.Mock).mockResolvedValueOnce({ users: [deleteThisUser] }); + (searchGroups as jest.Mock).mockResolvedValueOnce({ groups: [deleteThisGroup] }); + const wrapper = shallowRender(); + + await waitAndUpdate(wrapper); + + expect(wrapper.state().users).toHaveLength(1); + expect(wrapper.state().groups).toHaveLength(1); + + wrapper.instance().handleConfirmDeletePermission(deleteThisGroup); + + expect(removeUser).not.toBeCalled(); + expect(removeGroup).toBeCalledWith({ gateName: 'qualitygate', groupName: deleteThisGroup.name }); + + await waitAndUpdate(wrapper); + + expect(wrapper.state().permissionToDelete).toBeUndefined(); + expect(wrapper.state().users).toHaveLength(1); + expect(wrapper.state().groups).toHaveLength(0); }); it('should handleConfirmDeletePermission if it returns an error', async () => { @@ -155,11 +232,11 @@ it('should handleConfirmDeletePermission if it returns an error', async () => { wrapper.instance().handleConfirmDeletePermission(deleteThisUser); - expect(removeUser).toBeCalledWith({ qualityGate: '1', userLogin: deleteThisUser.login }); + expect(removeUser).toBeCalledWith({ gateName: 'qualitygate', login: deleteThisUser.login }); await waitAndUpdate(wrapper); - expect(wrapper.state().userPermissionToDelete).toBeUndefined(); + expect(wrapper.state().permissionToDelete).toBeUndefined(); expect(wrapper.state().users).toHaveLength(1); }); diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGatePermissionsAddModal-test.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGatePermissionsAddModal-test.tsx index 766688a5fff..5a0b6b79e92 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGatePermissionsAddModal-test.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGatePermissionsAddModal-test.tsx @@ -19,14 +19,15 @@ */ import { shallow } from 'enzyme'; import * as React from 'react'; -import { searchUsers } from '../../../../api/quality-gates'; +import { searchGroups, searchUsers } from '../../../../api/quality-gates'; import { mockQualityGate } from '../../../../helpers/mocks/quality-gates'; import { mockUserBase } from '../../../../helpers/mocks/users'; import { mockEvent, waitAndUpdate } from '../../../../helpers/testUtils'; import QualityGatePermissionsAddModal from '../QualityGatePermissionsAddModal'; jest.mock('../../../../api/quality-gates', () => ({ - searchUsers: jest.fn().mockResolvedValue({ users: [] }) + searchUsers: jest.fn().mockResolvedValue({ users: [] }), + searchGroups: jest.fn().mockResolvedValue({ groups: [] }) })); beforeEach(() => { @@ -34,12 +35,12 @@ beforeEach(() => { }); it('should render correctly', () => { - expect(shallowRender()).toMatchSnapshot('default'); - expect(shallowRender({ submitting: true })).toMatchSnapshot('submitting'); + expect(shallowRender()).toMatchSnapshot(); }); -it('should fetch users', async () => { +it('should fetch users and groups', async () => { (searchUsers as jest.Mock).mockResolvedValueOnce({ users: [mockUserBase()] }); + (searchGroups as jest.Mock).mockResolvedValueOnce({ groups: [{ name: 'group' }] }); const wrapper = shallowRender(); @@ -48,12 +49,17 @@ it('should fetch users', async () => { wrapper.instance().handleSearch(query); expect(wrapper.state().loading).toBe(true); - expect(searchUsers).toBeCalledWith({ qualityGate: '1', q: query, selected: 'deselected' }); + expect(searchUsers).toBeCalledWith({ gateName: 'qualitygate', q: query, selected: 'deselected' }); + expect(searchGroups).toBeCalledWith({ + gateName: 'qualitygate', + q: query, + selected: 'deselected' + }); await waitAndUpdate(wrapper); expect(wrapper.state().loading).toBe(false); - expect(wrapper.state().searchResults).toHaveLength(1); + expect(wrapper.state().searchResults).toHaveLength(2); }); it('should handle input change', () => { diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGatePermissionsAddModalRenderer-test.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGatePermissionsAddModalRenderer-test.tsx index 6fddecf8d0f..768106fe36b 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGatePermissionsAddModalRenderer-test.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGatePermissionsAddModalRenderer-test.tsx @@ -32,9 +32,9 @@ it('should render correctly', () => { expect(shallowRender({ selection: mockUserBase(), submitting: true })).toMatchSnapshot( 'submitting' ); - expect(shallowRender({ query: 'ab', searchResults: [mockUserBase()] })).toMatchSnapshot( - 'query and results' - ); + expect( + shallowRender({ query: 'ab', searchResults: [mockUserBase(), { name: 'group name' }] }) + ).toMatchSnapshot('query and results'); }); it('should render options correctly', () => { @@ -42,7 +42,8 @@ it('should render options correctly', () => { const { optionRenderer = () => null } = wrapper.find(Select).props(); - expect(optionRenderer({ avatar: 'avatar', name: 'name', login: 'login' })).toMatchSnapshot(); + expect(optionRenderer({ avatar: 'A', name: 'name', login: 'login' })).toMatchSnapshot('user'); + expect(optionRenderer({ name: 'group name' })).toMatchSnapshot('group'); }); function shallowRender(overrides: Partial<QualityGatePermissionsAddModalRendererProps> = {}) { diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGatePermissionsRenderer-test.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGatePermissionsRenderer-test.tsx index d190f4b89af..0f61f0f00f7 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGatePermissionsRenderer-test.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGatePermissionsRenderer-test.tsx @@ -27,17 +27,24 @@ import QualityGatePermissionsRenderer, { } from '../QualityGatePermissionsRenderer'; it('should render correctly', () => { - expect(shallowRender()).toMatchSnapshot('with users'); + expect(shallowRender()).toMatchSnapshot('with users and groups'); expect(shallowRender({ users: [] })).toMatchSnapshot('with no users'); + expect(shallowRender({ groups: [] })).toMatchSnapshot('with no groups'); + expect(shallowRender({ groups: [], users: [] })).toMatchSnapshot('with no users or groups'); expect(shallowRender({ showAddModal: true })).toMatchSnapshot('show add modal'); - expect(shallowRender({ userPermissionToDelete: mockUserBase() })).toMatchSnapshot( - 'show remove modal' + expect(shallowRender({ permissionToDelete: mockUserBase() })).toMatchSnapshot( + 'show remove modal for user' + ); + + expect(shallowRender({ permissionToDelete: { name: 'deletable group' } })).toMatchSnapshot( + 'show remove modal for group' ); }); function shallowRender(overrides: Partial<QualityGatePermissionsRendererProps> = {}) { return shallow( <QualityGatePermissionsRenderer + groups={[{ name: 'group' }]} loading={false} onClickAddPermission={jest.fn()} onCloseAddPermission={jest.fn()} diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/PermissionItem-test.tsx.snap b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/PermissionItem-test.tsx.snap index 41a7f6881c1..5ddfe0ab0bc 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/PermissionItem-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/PermissionItem-test.tsx.snap @@ -1,6 +1,27 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`should render correctly 1`] = ` +exports[`should render correctly: group 1`] = ` +<div + className="display-flex-center permission-list-item padded" +> + <GroupIcon + className="pull-left spacer-right" + size={32} + /> + <div + className="overflow-hidden flex-1" + > + <strong> + groupname + </strong> + </div> + <DeleteButton + onClick={[Function]} + /> +</div> +`; + +exports[`should render correctly: user 1`] = ` <div className="display-flex-center permission-list-item padded" > diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/QualityGatePermissions-test.tsx.snap b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/QualityGatePermissions-test.tsx.snap index dc40f7963b4..703b2a5babf 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/QualityGatePermissions-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/QualityGatePermissions-test.tsx.snap @@ -2,6 +2,7 @@ exports[`should render correctly 1`] = ` <QualityGatePermissionsRenderer + groups={Array []} loading={true} onClickAddPermission={[Function]} onClickDeletePermission={[Function]} diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/QualityGatePermissionsAddModal-test.tsx.snap b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/QualityGatePermissionsAddModal-test.tsx.snap index 6711d9c224d..e4f85804b48 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/QualityGatePermissionsAddModal-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/QualityGatePermissionsAddModal-test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`should render correctly: default 1`] = ` +exports[`should render correctly 1`] = ` <QualityGatePermissionsAddModalRenderer loading={false} onClose={[MockFunction]} @@ -12,16 +12,3 @@ exports[`should render correctly: default 1`] = ` submitting={false} /> `; - -exports[`should render correctly: submitting 1`] = ` -<QualityGatePermissionsAddModalRenderer - loading={false} - onClose={[MockFunction]} - onInputChange={[Function]} - onSelection={[Function]} - onSubmit={[Function]} - query="" - searchResults={Array []} - submitting={true} -/> -`; diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/QualityGatePermissionsAddModalRenderer-test.tsx.snap b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/QualityGatePermissionsAddModalRenderer-test.tsx.snap index 41c4bb96a8d..db838460f7d 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/QualityGatePermissionsAddModalRenderer-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/QualityGatePermissionsAddModalRenderer-test.tsx.snap @@ -99,6 +99,10 @@ exports[`should render correctly: query and results 1`] = ` "login": "userlogin", "value": "userlogin", }, + Object { + "name": "group name", + "value": "group name", + }, ] } placeholder="" @@ -315,10 +319,23 @@ exports[`should render correctly: submitting 1`] = ` </Modal> `; -exports[`should render options correctly 1`] = ` +exports[`should render options correctly: group 1`] = ` +<React.Fragment> + <GroupIcon + size={16} + /> + <strong + className="spacer-left" + > + group name + </strong> +</React.Fragment> +`; + +exports[`should render options correctly: user 1`] = ` <React.Fragment> <Connect(Avatar) - hash="avatar" + hash="A" name="name" size={16} /> diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/QualityGatePermissionsRenderer-test.tsx.snap b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/QualityGatePermissionsRenderer-test.tsx.snap index 22f6cdc7bc0..42ebeba1913 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/QualityGatePermissionsRenderer-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/QualityGatePermissionsRenderer-test.tsx.snap @@ -4,13 +4,11 @@ exports[`should render correctly: show add modal 1`] = ` <div className="quality-gate-permissions" > - <header - className="display-flex-center spacer-bottom" + <h3 + className="spacer-bottom" > - <h3> - quality_gates.permissions - </h3> - </header> + quality_gates.permissions + </h3> <p className="spacer-bottom" > @@ -25,8 +23,7 @@ exports[`should render correctly: show add modal 1`] = ` key="john.doe" > <PermissionItem - onClickDelete={[MockFunction]} - user={ + item={ Object { "active": true, "local": true, @@ -34,6 +31,19 @@ exports[`should render correctly: show add modal 1`] = ` "name": "John Doe", } } + onClickDelete={[MockFunction]} + /> + </li> + <li + key="group" + > + <PermissionItem + item={ + Object { + "name": "group", + } + } + onClickDelete={[MockFunction]} /> </li> </ul> @@ -59,17 +69,15 @@ exports[`should render correctly: show add modal 1`] = ` </div> `; -exports[`should render correctly: show remove modal 1`] = ` +exports[`should render correctly: show remove modal for group 1`] = ` <div className="quality-gate-permissions" > - <header - className="display-flex-center spacer-bottom" + <h3 + className="spacer-bottom" > - <h3> - quality_gates.permissions - </h3> - </header> + quality_gates.permissions + </h3> <p className="spacer-bottom" > @@ -84,8 +92,89 @@ exports[`should render correctly: show remove modal 1`] = ` key="john.doe" > <PermissionItem + item={ + Object { + "active": true, + "local": true, + "login": "john.doe", + "name": "John Doe", + } + } onClickDelete={[MockFunction]} - user={ + /> + </li> + <li + key="group" + > + <PermissionItem + item={ + Object { + "name": "group", + } + } + onClickDelete={[MockFunction]} + /> + </li> + </ul> + </DeferredSpinner> + </div> + <Button + className="big-spacer-top" + onClick={[MockFunction]} + > + quality_gates.permissions.grant + </Button> + <ConfirmModal + confirmButtonText="remove" + confirmData={ + Object { + "name": "deletable group", + } + } + header="groups.remove" + isDestructive={true} + onClose={[MockFunction]} + onConfirm={[MockFunction]} + > + <FormattedMessage + defaultMessage="groups.remove.confirmation" + id="remove.confirmation" + values={ + Object { + "user": <strong> + deletable group + </strong>, + } + } + /> + </ConfirmModal> +</div> +`; + +exports[`should render correctly: show remove modal for user 1`] = ` +<div + className="quality-gate-permissions" +> + <h3 + className="spacer-bottom" + > + quality_gates.permissions + </h3> + <p + className="spacer-bottom" + > + quality_gates.permissions.help + </p> + <div> + <DeferredSpinner + loading={false} + > + <ul> + <li + key="john.doe" + > + <PermissionItem + item={ Object { "active": true, "local": true, @@ -93,6 +182,19 @@ exports[`should render correctly: show remove modal 1`] = ` "name": "John Doe", } } + onClickDelete={[MockFunction]} + /> + </li> + <li + key="group" + > + <PermissionItem + item={ + Object { + "name": "group", + } + } + onClickDelete={[MockFunction]} /> </li> </ul> @@ -118,7 +220,7 @@ exports[`should render correctly: show remove modal 1`] = ` > <FormattedMessage defaultMessage="users.remove.confirmation" - id="users.remove.confirmation" + id="remove.confirmation" values={ Object { "user": <strong />, @@ -129,17 +231,104 @@ exports[`should render correctly: show remove modal 1`] = ` </div> `; +exports[`should render correctly: with no groups 1`] = ` +<div + className="quality-gate-permissions" +> + <h3 + className="spacer-bottom" + > + quality_gates.permissions + </h3> + <p + className="spacer-bottom" + > + quality_gates.permissions.help + </p> + <div> + <DeferredSpinner + loading={false} + > + <ul> + <li + key="john.doe" + > + <PermissionItem + item={ + Object { + "active": true, + "local": true, + "login": "john.doe", + "name": "John Doe", + } + } + onClickDelete={[MockFunction]} + /> + </li> + </ul> + </DeferredSpinner> + </div> + <Button + className="big-spacer-top" + onClick={[MockFunction]} + > + quality_gates.permissions.grant + </Button> +</div> +`; + exports[`should render correctly: with no users 1`] = ` <div className="quality-gate-permissions" > - <header - className="display-flex-center spacer-bottom" + <h3 + className="spacer-bottom" > - <h3> - quality_gates.permissions - </h3> - </header> + quality_gates.permissions + </h3> + <p + className="spacer-bottom" + > + quality_gates.permissions.help + </p> + <div> + <DeferredSpinner + loading={false} + > + <ul> + <li + key="group" + > + <PermissionItem + item={ + Object { + "name": "group", + } + } + onClickDelete={[MockFunction]} + /> + </li> + </ul> + </DeferredSpinner> + </div> + <Button + className="big-spacer-top" + onClick={[MockFunction]} + > + quality_gates.permissions.grant + </Button> +</div> +`; + +exports[`should render correctly: with no users or groups 1`] = ` +<div + className="quality-gate-permissions" +> + <h3 + className="spacer-bottom" + > + quality_gates.permissions + </h3> <p className="spacer-bottom" > @@ -161,17 +350,15 @@ exports[`should render correctly: with no users 1`] = ` </div> `; -exports[`should render correctly: with users 1`] = ` +exports[`should render correctly: with users and groups 1`] = ` <div className="quality-gate-permissions" > - <header - className="display-flex-center spacer-bottom" + <h3 + className="spacer-bottom" > - <h3> - quality_gates.permissions - </h3> - </header> + quality_gates.permissions + </h3> <p className="spacer-bottom" > @@ -186,8 +373,7 @@ exports[`should render correctly: with users 1`] = ` key="john.doe" > <PermissionItem - onClickDelete={[MockFunction]} - user={ + item={ Object { "active": true, "local": true, @@ -195,6 +381,19 @@ exports[`should render correctly: with users 1`] = ` "name": "John Doe", } } + onClickDelete={[MockFunction]} + /> + </li> + <li + key="group" + > + <PermissionItem + item={ + Object { + "name": "group", + } + } + onClickDelete={[MockFunction]} /> </li> </ul> diff --git a/server/sonar-web/src/main/js/components/icons/GroupIcon.tsx b/server/sonar-web/src/main/js/components/icons/GroupIcon.tsx index bbe6d88db56..9a1b7b50591 100644 --- a/server/sonar-web/src/main/js/components/icons/GroupIcon.tsx +++ b/server/sonar-web/src/main/js/components/icons/GroupIcon.tsx @@ -23,7 +23,7 @@ import Icon, { IconProps } from './Icon'; export default function GroupIcon({ fill, size = 36, ...iconProps }: IconProps) { return ( - <Icon viewBox="0 0 36 36" {...iconProps}> + <Icon viewBox="0 0 36 36" size={size} {...iconProps}> <g transform="matrix(0.0625,0,0,0.0625,3,4)"> <path d="M148.25,224C121.25,224.833 99.167,235.5 82,256L48.5,256C34.833,256 23.333,252.625 14,245.875C4.667,239.125 0,229.25 0,216.25C0,157.417 10.333,128 31,128C32,128 35.625,129.75 41.875,133.25C48.125,136.75 56.25,140.292 66.25,143.875C76.25,147.458 86.167,149.25 96,149.25C107.167,149.25 118.25,147.333 129.25,143.5C128.417,149.667 128,155.167 128,160C128,183.167 134.75,204.5 148.25,224ZM416,383.25C416,403.25 409.917,419.042 397.75,430.625C385.583,442.208 369.417,448 349.25,448L130.75,448C110.583,448 94.417,442.208 82.25,430.625C70.083,419.042 64,403.25 64,383.25C64,374.417 64.292,365.792 64.875,357.375C65.458,348.958 66.625,339.875 68.375,330.125C70.125,320.375 72.333,311.333 75,303C77.667,294.667 81.25,286.542 85.75,278.625C90.25,270.708 95.417,263.958 101.25,258.375C107.083,252.792 114.208,248.333 122.625,245C131.042,241.667 140.333,240 150.5,240C152.167,240 155.75,241.792 161.25,245.375C166.75,248.958 172.833,252.958 179.5,257.375C186.167,261.792 195.083,265.792 206.25,269.375C217.417,272.958 228.667,274.75 240,274.75C251.333,274.75 262.583,272.958 273.75,269.375C284.917,265.792 293.833,261.792 300.5,257.375C307.167,252.958 313.25,248.958 318.75,245.375C324.25,241.792 327.833,240 329.5,240C339.667,240 348.958,241.667 357.375,245C365.792,248.333 372.917,252.792 378.75,258.375C384.583,263.958 389.75,270.708 394.25,278.625C398.75,286.542 402.333,294.667 405,303C407.667,311.333 409.875,320.375 411.625,330.125C413.375,339.875 414.542,348.958 415.125,357.375C415.708,365.792 416,374.417 416,383.25ZM160,64C160,81.667 153.75,96.75 141.25,109.25C128.75,121.75 113.667,128 96,128C78.333,128 63.25,121.75 50.75,109.25C38.25,96.75 32,81.667 32,64C32,46.333 38.25,31.25 50.75,18.75C63.25,6.25 78.333,0 96,0C113.667,0 128.75,6.25 141.25,18.75C153.75,31.25 160,46.333 160,64ZM336,160C336,186.5 326.625,209.125 307.875,227.875C289.125,246.625 266.5,256 240,256C213.5,256 190.875,246.625 172.125,227.875C153.375,209.125 144,186.5 144,160C144,133.5 153.375,110.875 172.125,92.125C190.875,73.375 213.5,64 240,64C266.5,64 289.125,73.375 307.875,92.125C326.625,110.875 336,133.5 336,160ZM480,216.25C480,229.25 475.333,239.125 466,245.875C456.667,252.625 445.167,256 431.5,256L398,256C380.833,235.5 358.75,224.833 331.75,224C345.25,204.5 352,183.167 352,160C352,155.167 351.583,149.667 350.75,143.5C361.75,147.333 372.833,149.25 384,149.25C393.833,149.25 403.75,147.458 413.75,143.875C423.75,140.292 431.875,136.75 438.125,133.25C444.375,129.75 448,128 449,128C469.667,128 480,157.417 480,216.25ZM448,64C448,81.667 441.75,96.75 429.25,109.25C416.75,121.75 401.667,128 384,128C366.333,128 351.25,121.75 338.75,109.25C326.25,96.75 320,81.667 320,64C320,46.333 326.25,31.25 338.75,18.75C351.25,6.25 366.333,0 384,0C401.667,0 416.75,6.25 429.25,18.75C441.75,31.25 448,46.333 448,64Z" diff --git a/server/sonar-web/src/main/js/types/quality-gates.ts b/server/sonar-web/src/main/js/types/quality-gates.ts index 425b401d5de..1814b8fc89d 100644 --- a/server/sonar-web/src/main/js/types/quality-gates.ts +++ b/server/sonar-web/src/main/js/types/quality-gates.ts @@ -82,12 +82,25 @@ export interface QualityGateStatusConditionEnhanced extends QualityGateStatusCon } export interface SearchPermissionsParameters { - qualityGate: string; + gateName: string; q?: string; selected?: 'all' | 'selected' | 'deselected'; } export interface AddDeleteUserPermissionsParameters { - qualityGate: string; - userLogin: string; + gateName: string; + login: string; +} + +export interface AddDeleteGroupPermissionsParameters { + gateName: string; + groupName: string; +} + +export interface Group { + name: string; +} + +export function isUser(item: T.UserBase | Group): item is T.UserBase { + return item && (item as T.UserBase).login !== undefined; } |