@@ -0,0 +1,195 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2023 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 { cloneDeep } from 'lodash'; | |||
import { | |||
mockClusterSysInfo, | |||
mockGroup, | |||
mockIdentityProvider, | |||
mockPaging, | |||
mockUser, | |||
} from '../../helpers/testMocks'; | |||
import { Group, IdentityProvider, Paging, SysInfoCluster, UserSelected } from '../../types/types'; | |||
import { getSystemInfo } from '../system'; | |||
import { getIdentityProviders } from '../users'; | |||
import { | |||
createGroup, | |||
deleteGroup, | |||
getUsersInGroup, | |||
searchUsersGroups, | |||
updateGroup, | |||
} from '../user_groups'; | |||
export default class GroupsServiceMock { | |||
isManaged = false; | |||
paging: Paging; | |||
groups: Group[]; | |||
readOnlyGroups = [ | |||
mockGroup({ name: 'managed-group', managed: true }), | |||
mockGroup({ name: 'local-group', managed: false }), | |||
]; | |||
constructor() { | |||
this.groups = cloneDeep(this.readOnlyGroups); | |||
this.paging = mockPaging({ | |||
pageIndex: 1, | |||
pageSize: 2, | |||
total: 200, | |||
}); | |||
jest.mocked(getSystemInfo).mockImplementation(this.handleGetSystemInfo); | |||
jest.mocked(getIdentityProviders).mockImplementation(this.handleGetIdentityProviders); | |||
jest.mocked(searchUsersGroups).mockImplementation((p) => this.handleSearchUsersGroups(p)); | |||
jest.mocked(createGroup).mockImplementation((g) => this.handleCreateGroup(g)); | |||
jest.mocked(deleteGroup).mockImplementation((g) => this.handleDeleteGroup(g)); | |||
jest.mocked(updateGroup).mockImplementation((g) => this.handleUpdateGroup(g)); | |||
jest.mocked(getUsersInGroup).mockImplementation(this.handlegetUsersInGroup); | |||
} | |||
reset() { | |||
this.groups = cloneDeep(this.readOnlyGroups); | |||
} | |||
setIsManaged(managed: boolean) { | |||
this.isManaged = managed; | |||
} | |||
setPaging(paging: Partial<Paging>) { | |||
this.paging = { ...this.paging, ...paging }; | |||
} | |||
handleCreateGroup = (group: { name: string; description?: string }): Promise<Group> => { | |||
const newGroup = mockGroup(group); | |||
this.groups.push(newGroup); | |||
return this.reply(newGroup); | |||
}; | |||
handleDeleteGroup = (group: { name: string }): Promise<Record<string, never>> => { | |||
if (!this.groups.some((g) => g.name === group.name)) { | |||
return Promise.reject(); | |||
} | |||
const groupToDelete = this.groups.find((g) => g.name === group.name); | |||
if (groupToDelete?.managed) { | |||
return Promise.reject(); | |||
} | |||
this.groups = this.groups.filter((g) => g.name !== group.name); | |||
return this.reply({}); | |||
}; | |||
handleUpdateGroup = (group: { | |||
currentName: string; | |||
name?: string; | |||
description?: string; | |||
}): Promise<Record<string, never>> => { | |||
if (!this.groups.some((g) => group.currentName === g.name)) { | |||
return Promise.reject(); | |||
} | |||
this.groups.map((g) => { | |||
if (g.name === group.currentName) { | |||
if (group.name !== undefined) { | |||
g.name = group.name; | |||
} | |||
if (group.description !== undefined) { | |||
g.description = group.description; | |||
} | |||
} | |||
}); | |||
return this.reply({}); | |||
}; | |||
handlegetUsersInGroup = (): Promise<Paging & { users: UserSelected[] }> => { | |||
return this.reply({ | |||
...this.paging, | |||
users: [ | |||
{ | |||
...mockUser({ name: 'alice' }), | |||
selected: true, | |||
} as UserSelected, | |||
{ | |||
...mockUser({ name: 'bob' }), | |||
selected: false, | |||
} as UserSelected, | |||
], | |||
}); | |||
}; | |||
handleSearchUsersGroups = (data: { | |||
f?: string; | |||
p?: number; | |||
ps?: number; | |||
q?: string; | |||
managed: boolean | undefined; | |||
}): Promise<{ groups: Group[]; paging: Paging }> => { | |||
const { paging } = this; | |||
if (data.p !== undefined && data.p !== paging.pageIndex) { | |||
this.setPaging({ pageIndex: paging.pageIndex++ }); | |||
const groups = [ | |||
mockGroup({ name: `local-group ${this.groups.length + 4}` }), | |||
mockGroup({ name: `local-group ${this.groups.length + 5}` }), | |||
]; | |||
return this.reply({ paging, groups }); | |||
} | |||
if (this.isManaged) { | |||
if (data.managed === undefined) { | |||
return this.reply({ | |||
paging, | |||
groups: this.groups.filter((g) => (data?.q ? g.name.includes(data.q) : true)), | |||
}); | |||
} | |||
const groups = this.groups.filter((group) => group.managed === data.managed); | |||
return this.reply({ | |||
paging, | |||
groups: groups.filter((g) => (data?.q ? g.name.includes(data.q) : true)), | |||
}); | |||
} | |||
return this.reply({ | |||
paging, | |||
groups: this.groups.filter((g) => (data?.q ? g.name.includes(data.q) : true)), | |||
}); | |||
}; | |||
handleGetIdentityProviders = (): Promise<{ identityProviders: IdentityProvider[] }> => { | |||
return this.reply({ identityProviders: [mockIdentityProvider()] }); | |||
}; | |||
handleGetSystemInfo = (): Promise<SysInfoCluster> => { | |||
return this.reply( | |||
mockClusterSysInfo( | |||
this.isManaged | |||
? { | |||
System: { | |||
'High Availability': true, | |||
'Server ID': 'asd564-asd54a-5dsfg45', | |||
'External Users and Groups Provisioning': 'Okta', | |||
}, | |||
} | |||
: {} | |||
) | |||
); | |||
}; | |||
reply<T>(response: T): Promise<T> { | |||
return Promise.resolve(cloneDeep(response)); | |||
} | |||
} |
@@ -26,6 +26,7 @@ export function searchUsersGroups(data: { | |||
p?: number; | |||
ps?: number; | |||
q?: string; | |||
managed: boolean | undefined; | |||
}): Promise<{ groups: Group[]; paging: Paging }> { | |||
return getJSON('/api/user_groups/search', data).catch(throwGlobalError); | |||
} |
@@ -22,6 +22,7 @@ import * as React from 'react'; | |||
import { Helmet } from 'react-helmet-async'; | |||
import { getSystemInfo } from '../../../api/system'; | |||
import { createGroup, deleteGroup, searchUsersGroups, updateGroup } from '../../../api/user_groups'; | |||
import ButtonToggle from '../../../components/controls/ButtonToggle'; | |||
import ListFooter from '../../../components/controls/ListFooter'; | |||
import SearchBox from '../../../components/controls/SearchBox'; | |||
import Suggestions from '../../../components/embed-docs-modal/Suggestions'; | |||
@@ -41,11 +42,17 @@ interface State { | |||
paging?: Paging; | |||
query: string; | |||
manageProvider?: string; | |||
managed: boolean | undefined; | |||
} | |||
export default class App extends React.PureComponent<{}, State> { | |||
mounted = false; | |||
state: State = { loading: true, query: '' }; | |||
state: State = { | |||
loading: true, | |||
query: '', | |||
managed: undefined, | |||
paging: { pageIndex: 1, pageSize: 100, total: 1000 }, | |||
}; | |||
componentDidMount() { | |||
this.mounted = true; | |||
@@ -53,18 +60,19 @@ export default class App extends React.PureComponent<{}, State> { | |||
this.fetchManageInstance(); | |||
} | |||
componentDidUpdate(_prevProps: {}, prevState: State) { | |||
if (prevState.query !== this.state.query || prevState.managed !== this.state.managed) { | |||
this.fetchGroups(); | |||
} | |||
if (prevState !== undefined && prevState.paging?.pageIndex !== this.state.paging?.pageIndex) { | |||
this.fetchMoreGroups(); | |||
} | |||
} | |||
componentWillUnmount() { | |||
this.mounted = false; | |||
} | |||
makeFetchGroupsRequest = (data?: { p?: number; q?: string }) => { | |||
this.setState({ loading: true }); | |||
return searchUsersGroups({ | |||
q: this.state.query, | |||
...data, | |||
}); | |||
}; | |||
async fetchManageInstance() { | |||
const info = (await getSystemInfo()) as SysInfoCluster; | |||
if (this.mounted) { | |||
@@ -80,9 +88,14 @@ export default class App extends React.PureComponent<{}, State> { | |||
} | |||
}; | |||
fetchGroups = async (data?: { p?: number; q?: string }) => { | |||
fetchGroups = async () => { | |||
const { query: q, managed } = this.state; | |||
this.setState({ loading: true }); | |||
try { | |||
const { groups, paging } = await this.makeFetchGroupsRequest(data); | |||
const { groups, paging } = await searchUsersGroups({ | |||
q, | |||
managed, | |||
}); | |||
if (this.mounted) { | |||
this.setState({ groups, loading: false, paging }); | |||
} | |||
@@ -92,11 +105,13 @@ export default class App extends React.PureComponent<{}, State> { | |||
}; | |||
fetchMoreGroups = async () => { | |||
const { paging: currentPaging } = this.state; | |||
const { query: q, managed, paging: currentPaging } = this.state; | |||
if (currentPaging && currentPaging.total > currentPaging.pageIndex * currentPaging.pageSize) { | |||
try { | |||
const { groups, paging } = await this.makeFetchGroupsRequest({ | |||
p: currentPaging.pageIndex + 1, | |||
const { groups, paging } = await searchUsersGroups({ | |||
p: currentPaging.pageIndex, | |||
q, | |||
managed, | |||
}); | |||
if (this.mounted) { | |||
this.setState(({ groups: existingGroups = [] }) => ({ | |||
@@ -111,15 +126,10 @@ export default class App extends React.PureComponent<{}, State> { | |||
} | |||
}; | |||
search = (query: string) => { | |||
this.fetchGroups({ q: query }); | |||
this.setState({ query }); | |||
}; | |||
refresh = async () => { | |||
const { paging, query } = this.state; | |||
const { paging } = this.state; | |||
await this.fetchGroups({ q: query }); | |||
await this.fetchGroups(); | |||
// reload all pages in order | |||
if (paging && paging.pageIndex > 1) { | |||
@@ -130,22 +140,6 @@ export default class App extends React.PureComponent<{}, State> { | |||
} | |||
}; | |||
closeDeleteForm = () => { | |||
this.setState({ groupToBeDeleted: undefined }); | |||
}; | |||
closeEditForm = () => { | |||
this.setState({ editedGroup: undefined }); | |||
}; | |||
openDeleteForm = (group: Group) => { | |||
this.setState({ groupToBeDeleted: group }); | |||
}; | |||
openEditForm = (group: Group) => { | |||
this.setState({ editedGroup: group }); | |||
}; | |||
handleCreate = async (data: { description: string; name: string }) => { | |||
await createGroup({ ...data }); | |||
@@ -200,8 +194,16 @@ export default class App extends React.PureComponent<{}, State> { | |||
}; | |||
render() { | |||
const { editedGroup, groupToBeDeleted, groups, loading, paging, query, manageProvider } = | |||
this.state; | |||
const { | |||
editedGroup, | |||
groupToBeDeleted, | |||
groups, | |||
loading, | |||
paging, | |||
query, | |||
manageProvider, | |||
managed, | |||
} = this.state; | |||
const showAnyone = 'anyone'.includes(query.toLowerCase()); | |||
@@ -212,22 +214,45 @@ export default class App extends React.PureComponent<{}, State> { | |||
<main className="page page-limited" id="groups-page"> | |||
<Header onCreate={this.handleCreate} manageProvider={manageProvider} /> | |||
<SearchBox | |||
className="big-spacer-bottom" | |||
id="groups-search" | |||
minLength={2} | |||
onChange={this.search} | |||
placeholder={translate('search.search_by_name')} | |||
value={query} | |||
/> | |||
<div className="display-flex-justify-start big-spacer-bottom big-spacer-top"> | |||
{manageProvider !== undefined && ( | |||
<div className="big-spacer-right"> | |||
<ButtonToggle | |||
value={managed === undefined ? 'all' : managed} | |||
disabled={loading} | |||
options={[ | |||
{ label: translate('all'), value: 'all' }, | |||
{ label: translate('managed'), value: true }, | |||
{ label: translate('local'), value: false }, | |||
]} | |||
onCheck={(filterOption) => { | |||
if (filterOption === 'all') { | |||
this.setState({ managed: undefined }); | |||
} else { | |||
this.setState({ managed: filterOption as boolean }); | |||
} | |||
}} | |||
/> | |||
</div> | |||
)} | |||
<SearchBox | |||
className="big-spacer-bottom" | |||
id="groups-search" | |||
minLength={2} | |||
onChange={(q) => this.setState({ query: q })} | |||
placeholder={translate('search.search_by_name')} | |||
value={query} | |||
/> | |||
</div> | |||
{groups !== undefined && ( | |||
<List | |||
groups={groups} | |||
onDelete={this.openDeleteForm} | |||
onEdit={this.openEditForm} | |||
onDelete={(groupToBeDeleted) => this.setState({ groupToBeDeleted })} | |||
onEdit={(editedGroup) => this.setState({ editedGroup })} | |||
onEditMembers={this.refresh} | |||
showAnyone={showAnyone} | |||
manageProvider={manageProvider} | |||
/> | |||
)} | |||
@@ -236,7 +261,11 @@ export default class App extends React.PureComponent<{}, State> { | |||
<ListFooter | |||
count={showAnyone ? groups.length + 1 : groups.length} | |||
loading={loading} | |||
loadMore={this.fetchMoreGroups} | |||
loadMore={() => { | |||
if (paging.total > paging.pageIndex * paging.pageSize) { | |||
this.setState({ paging: { ...paging, pageIndex: paging.pageIndex + 1 } }); | |||
} | |||
}} | |||
ready={!loading} | |||
total={showAnyone ? paging.total + 1 : paging.total} | |||
/> | |||
@@ -246,7 +275,7 @@ export default class App extends React.PureComponent<{}, State> { | |||
{groupToBeDeleted && ( | |||
<DeleteForm | |||
group={groupToBeDeleted} | |||
onClose={this.closeDeleteForm} | |||
onClose={() => this.setState({ groupToBeDeleted: undefined })} | |||
onSubmit={this.handleDelete} | |||
/> | |||
)} | |||
@@ -256,7 +285,7 @@ export default class App extends React.PureComponent<{}, State> { | |||
confirmButtonText={translate('update_verb')} | |||
group={editedGroup} | |||
header={translate('groups.update_group')} | |||
onClose={this.closeEditForm} | |||
onClose={() => this.setState({ editedGroup: undefined })} | |||
onSubmit={this.handleEdit} | |||
/> | |||
)} |
@@ -20,7 +20,7 @@ | |||
import * as React from 'react'; | |||
import { ButtonIcon } from '../../../components/controls/buttons'; | |||
import BulletListIcon from '../../../components/icons/BulletListIcon'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { translateWithParameters } from '../../../helpers/l10n'; | |||
import { Group } from '../../../types/types'; | |||
import EditMembersModal from './EditMembersModal'; | |||
@@ -61,10 +61,10 @@ export default class EditMembers extends React.PureComponent<Props, State> { | |||
return ( | |||
<> | |||
<ButtonIcon | |||
aria-label={translate('groups.users.edit')} | |||
aria-label={translateWithParameters('groups.users.edit', this.props.group.name)} | |||
className="button-small" | |||
onClick={this.handleMembersClick} | |||
title={translate('groups.users.edit')} | |||
title={translateWithParameters('groups.users.edit', this.props.group.name)} | |||
> | |||
<BulletListIcon /> | |||
</ButtonIcon> |
@@ -25,12 +25,12 @@ import { Alert } from '../../../components/ui/Alert'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import Form from './Form'; | |||
interface Props { | |||
interface HeaderProps { | |||
onCreate: (data: { description: string; name: string }) => Promise<void>; | |||
manageProvider?: string; | |||
} | |||
export default function Header(props: Props) { | |||
export default function Header(props: HeaderProps) { | |||
const { manageProvider } = props; | |||
const [createModal, setCreateModal] = React.useState(false); | |||
@@ -29,9 +29,12 @@ interface Props { | |||
onEdit: (group: Group) => void; | |||
onEditMembers: () => void; | |||
showAnyone: boolean; | |||
manageProvider: string | undefined; | |||
} | |||
export default function List(props: Props) { | |||
const { groups, manageProvider, showAnyone } = props; | |||
return ( | |||
<div className="boxed-group boxed-group-inner"> | |||
<table className="data zebra zebra-hover" id="groups-list"> | |||
@@ -46,7 +49,7 @@ export default function List(props: Props) { | |||
</tr> | |||
</thead> | |||
<tbody> | |||
{props.showAnyone && ( | |||
{showAnyone && ( | |||
<tr className="js-anyone" key="anyone"> | |||
<td className="width-20"> | |||
<strong className="js-group-name">{translate('groups.anyone')}</strong> | |||
@@ -61,13 +64,14 @@ export default function List(props: Props) { | |||
</tr> | |||
)} | |||
{sortBy(props.groups, (group) => group.name.toLowerCase()).map((group) => ( | |||
{sortBy(groups, (group) => group.name.toLowerCase()).map((group) => ( | |||
<ListItem | |||
group={group} | |||
key={group.name} | |||
onDelete={props.onDelete} | |||
onEdit={props.onEdit} | |||
onEditMembers={props.onEditMembers} | |||
manageProvider={manageProvider} | |||
/> | |||
))} | |||
</tbody> |
@@ -22,7 +22,7 @@ import ActionsDropdown, { | |||
ActionsDropdownDivider, | |||
ActionsDropdownItem, | |||
} from '../../../components/controls/ActionsDropdown'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { translate, translateWithParameters } from '../../../helpers/l10n'; | |||
import { Group } from '../../../types/types'; | |||
import EditMembers from './EditMembers'; | |||
@@ -31,41 +31,63 @@ export interface ListItemProps { | |||
onDelete: (group: Group) => void; | |||
onEdit: (group: Group) => void; | |||
onEditMembers: () => void; | |||
manageProvider: string | undefined; | |||
} | |||
export default function ListItem(props: ListItemProps) { | |||
const { group } = props; | |||
const { manageProvider, group } = props; | |||
const { name, managed, membersCount, description } = group; | |||
const isManaged = () => { | |||
return manageProvider !== undefined; | |||
}; | |||
const isGroupLocal = () => { | |||
return isManaged() && !managed; | |||
}; | |||
return ( | |||
<tr data-id={group.name}> | |||
<tr data-id={name}> | |||
<td className="width-20"> | |||
<strong className="js-group-name">{group.name}</strong> | |||
<strong className="js-group-name">{name}</strong> | |||
{group.default && <span className="little-spacer-left">({translate('default')})</span>} | |||
{isGroupLocal() && <span className="little-spacer-left badge">{translate('local')}</span>} | |||
</td> | |||
<td className="thin text-middle text-right little-padded-right">{group.membersCount}</td> | |||
<td className="thin text-middle text-right little-padded-right">{membersCount}</td> | |||
<td className="little-padded-left"> | |||
{!group.default && <EditMembers group={group} onEdit={props.onEditMembers} />} | |||
{!group.default && !isManaged() && ( | |||
<EditMembers group={group} onEdit={props.onEditMembers} /> | |||
)} | |||
</td> | |||
<td className="width-40"> | |||
<span className="js-group-description">{group.description}</span> | |||
<span className="js-group-description">{description}</span> | |||
</td> | |||
<td className="thin nowrap text-right"> | |||
{!group.default && ( | |||
<ActionsDropdown> | |||
<ActionsDropdownItem className="js-group-update" onClick={() => props.onEdit(group)}> | |||
{translate('update_details')} | |||
</ActionsDropdownItem> | |||
<ActionsDropdownDivider /> | |||
<ActionsDropdownItem | |||
className="js-group-delete" | |||
destructive={true} | |||
onClick={() => props.onDelete(group)} | |||
> | |||
{translate('delete')} | |||
</ActionsDropdownItem> | |||
{!group.default && (!isManaged() || isGroupLocal()) && ( | |||
<ActionsDropdown label={translateWithParameters('groups.edit', group.name)}> | |||
{!isManaged() && ( | |||
<> | |||
<ActionsDropdownItem | |||
className="js-group-update" | |||
onClick={() => props.onEdit(group)} | |||
> | |||
{translate('update_details')} | |||
</ActionsDropdownItem> | |||
<ActionsDropdownDivider /> | |||
</> | |||
)} | |||
{(!isManaged() || isGroupLocal()) && ( | |||
<ActionsDropdownItem | |||
className="js-group-delete" | |||
destructive={true} | |||
onClick={() => props.onDelete(group)} | |||
> | |||
{translate('delete')} | |||
</ActionsDropdownItem> | |||
)} | |||
</ActionsDropdown> | |||
)} | |||
</td> |
@@ -1,191 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2023 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 { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { | |||
createGroup, | |||
deleteGroup, | |||
searchUsersGroups, | |||
updateGroup, | |||
} from '../../../../api/user_groups'; | |||
import { mockGroup } from '../../../../helpers/testMocks'; | |||
import { waitAndUpdate } from '../../../../helpers/testUtils'; | |||
import App from '../App'; | |||
jest.mock('../../../../api/user_groups', () => ({ | |||
createGroup: jest.fn().mockResolvedValue({ | |||
default: false, | |||
description: 'Desc foo', | |||
membersCount: 0, | |||
name: 'Foo', | |||
}), | |||
deleteGroup: jest.fn().mockResolvedValue({}), | |||
searchUsersGroups: jest.fn().mockResolvedValue({ | |||
paging: { pageIndex: 1, pageSize: 2, total: 4 }, | |||
groups: [ | |||
{ | |||
default: false, | |||
description: 'Owners of organization foo', | |||
membersCount: 1, | |||
name: 'Owners', | |||
}, | |||
{ | |||
default: true, | |||
description: 'Members of organization foo', | |||
membersCount: 2, | |||
name: 'Members', | |||
}, | |||
], | |||
}), | |||
updateGroup: jest.fn().mockResolvedValue({}), | |||
})); | |||
jest.mock('../../../../api/system', () => ({ | |||
getSystemInfo: jest.fn().mockResolvedValue({ System: {} }), | |||
})); | |||
beforeEach(() => { | |||
jest.clearAllMocks(); | |||
}); | |||
it('should render correctly', async () => { | |||
const wrapper = shallowRender(); | |||
expect(wrapper).toMatchSnapshot(); | |||
await waitAndUpdate(wrapper); | |||
expect(searchUsersGroups).toHaveBeenCalledWith({ q: '' }); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); | |||
it('should correctly handle creation', async () => { | |||
const wrapper = shallowRender(); | |||
await waitAndUpdate(wrapper); | |||
expect(wrapper.state('groups')).toHaveLength(2); | |||
wrapper.instance().handleCreate({ description: 'Desc foo', name: 'foo' }); | |||
await waitAndUpdate(wrapper); | |||
expect(createGroup).toHaveBeenCalled(); | |||
}); | |||
it('should correctly handle deletion', async () => { | |||
const wrapper = shallowRender(); | |||
await waitAndUpdate(wrapper); | |||
expect(wrapper.state('groups')).toHaveLength(2); | |||
wrapper.setState({ groupToBeDeleted: mockGroup({ name: 'Members' }) }); | |||
wrapper.instance().handleDelete(); | |||
await waitAndUpdate(wrapper); | |||
expect(deleteGroup).toHaveBeenCalled(); | |||
expect(wrapper.state().groupToBeDeleted).toBeUndefined(); | |||
}); | |||
it('should ignore deletion', async () => { | |||
const wrapper = shallowRender(); | |||
await waitAndUpdate(wrapper); | |||
wrapper.setState({ groupToBeDeleted: undefined }); | |||
wrapper.instance().handleDelete(); | |||
expect(deleteGroup).not.toHaveBeenCalled(); | |||
}); | |||
it('should correctly handle edition', async () => { | |||
const wrapper = shallowRender(); | |||
await waitAndUpdate(wrapper); | |||
wrapper.setState({ editedGroup: mockGroup({ name: 'Owners' }) }); | |||
wrapper.instance().handleEdit({ description: 'foo', name: 'bar' }); | |||
await waitAndUpdate(wrapper); | |||
expect(updateGroup).toHaveBeenCalled(); | |||
expect(wrapper.state('groups')).toContainEqual({ | |||
default: false, | |||
description: 'foo', | |||
membersCount: 1, | |||
name: 'bar', | |||
}); | |||
}); | |||
it('should ignore edition', async () => { | |||
const wrapper = shallowRender(); | |||
await waitAndUpdate(wrapper); | |||
wrapper.setState({ editedGroup: undefined }); | |||
wrapper.instance().handleEdit({ description: 'nope', name: 'nuhuh' }); | |||
expect(updateGroup).not.toHaveBeenCalled(); | |||
}); | |||
it('should fetch more groups', async () => { | |||
const wrapper = shallowRender(); | |||
await waitAndUpdate(wrapper); | |||
wrapper.find('ListFooter').prop<Function>('loadMore')(); | |||
await waitAndUpdate(wrapper); | |||
expect(searchUsersGroups).toHaveBeenCalledWith({ p: 2, q: '' }); | |||
expect(wrapper.state('groups')).toHaveLength(4); | |||
}); | |||
it('should search for groups', async () => { | |||
const wrapper = shallowRender(); | |||
await waitAndUpdate(wrapper); | |||
wrapper.find('SearchBox').prop<Function>('onChange')('foo'); | |||
expect(searchUsersGroups).toHaveBeenCalledWith({ q: 'foo' }); | |||
expect(wrapper.state('query')).toBe('foo'); | |||
}); | |||
it('should handle edit modal', async () => { | |||
const editedGroup = mockGroup(); | |||
const wrapper = shallowRender(); | |||
await waitAndUpdate(wrapper); | |||
expect(wrapper.state().editedGroup).toBeUndefined(); | |||
wrapper.instance().openEditForm(editedGroup); | |||
expect(wrapper.state().editedGroup).toEqual(editedGroup); | |||
wrapper.instance().closeEditForm(); | |||
expect(wrapper.state().editedGroup).toBeUndefined(); | |||
}); | |||
it('should handle delete modal', async () => { | |||
const groupToBeDeleted = mockGroup(); | |||
const wrapper = shallowRender(); | |||
await waitAndUpdate(wrapper); | |||
expect(wrapper.state().groupToBeDeleted).toBeUndefined(); | |||
wrapper.instance().openDeleteForm(groupToBeDeleted); | |||
expect(wrapper.state().groupToBeDeleted).toEqual(groupToBeDeleted); | |||
wrapper.instance().closeDeleteForm(); | |||
expect(wrapper.state().groupToBeDeleted).toBeUndefined(); | |||
}); | |||
it('should refresh correctly', async () => { | |||
const wrapper = shallowRender(); | |||
await waitAndUpdate(wrapper); | |||
const query = 'preserve me'; | |||
wrapper.setState({ paging: { pageIndex: 2, pageSize: 2, total: 5 }, query }); | |||
(searchUsersGroups as jest.Mock).mockClear(); | |||
wrapper.instance().refresh(); | |||
await waitAndUpdate(wrapper); | |||
expect(searchUsersGroups).toHaveBeenNthCalledWith(1, { q: query }); | |||
expect(searchUsersGroups).toHaveBeenNthCalledWith(2, { q: query, p: 2 }); | |||
}); | |||
function shallowRender(props: Partial<App['props']> = {}) { | |||
return shallow<App>(<App {...props} />); | |||
} |
@@ -1,29 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2023 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 { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import DeleteForm from '../DeleteForm'; | |||
it('should render', () => { | |||
const group = { id: 3, name: 'Foo', membersCount: 5 }; | |||
expect( | |||
shallow(<DeleteForm group={group} onClose={jest.fn()} onSubmit={jest.fn()} />).dive() | |||
).toMatchSnapshot(); | |||
}); |
@@ -19,11 +19,12 @@ | |||
*/ | |||
import { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { mockGroup } from '../../../../helpers/testMocks'; | |||
import { click } from '../../../../helpers/testUtils'; | |||
import EditMembers from '../EditMembers'; | |||
it('should edit members', () => { | |||
const group = { id: 3, name: 'Foo', membersCount: 5 }; | |||
const group = mockGroup({ name: 'Foo', membersCount: 5 }); | |||
const onEdit = jest.fn(); | |||
const wrapper = shallow(<EditMembers group={group} onEdit={onEdit} />); |
@@ -21,10 +21,11 @@ import { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { addUserToGroup, getUsersInGroup, removeUserFromGroup } from '../../../../api/user_groups'; | |||
import SelectList, { SelectListFilter } from '../../../../components/controls/SelectList'; | |||
import { mockGroup } from '../../../../helpers/testMocks'; | |||
import { waitAndUpdate } from '../../../../helpers/testUtils'; | |||
import EditMembersModal from '../EditMembersModal'; | |||
const group = { id: 1, name: 'foo', membersCount: 1 }; | |||
const group = mockGroup({ name: 'foo', membersCount: 1 }); | |||
jest.mock('../../../../api/user_groups', () => ({ | |||
getUsersInGroup: jest.fn().mockResolvedValue({ |
@@ -1,49 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2023 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 { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { change, click, submit } from '../../../../helpers/testUtils'; | |||
import Form from '../Form'; | |||
it('should render form', async () => { | |||
const onClose = jest.fn(); | |||
const onSubmit = jest.fn(() => Promise.resolve()); | |||
const wrapper = shallow( | |||
<Form | |||
confirmButtonText="confirmButtonText" | |||
header="header" | |||
onClose={onClose} | |||
onSubmit={onSubmit} | |||
/> | |||
).dive(); | |||
expect(wrapper).toMatchSnapshot(); | |||
change(wrapper.find('[name="name"]'), 'foo'); | |||
change(wrapper.find('[name="description"]'), 'bar'); | |||
submit(wrapper.find('form')); | |||
expect(onSubmit).toHaveBeenCalledWith({ description: 'bar', name: 'foo' }); | |||
await new Promise(setImmediate); | |||
expect(onClose).toHaveBeenCalled(); | |||
onClose.mockClear(); | |||
click(wrapper.find('ResetButtonLink')); | |||
expect(onClose).toHaveBeenCalled(); | |||
}); |
@@ -0,0 +1,255 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2023 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 { screen } from '@testing-library/react'; | |||
import userEvent from '@testing-library/user-event'; | |||
import * as React from 'react'; | |||
import { act } from 'react-dom/test-utils'; | |||
import { byRole, byText } from 'testing-library-selector'; | |||
import GroupsServiceMock from '../../../../api/mocks/GroupsServiceMock'; | |||
import { renderApp } from '../../../../helpers/testReactTestingUtils'; | |||
import App from '../App'; | |||
jest.mock('../../../../api/users'); | |||
jest.mock('../../../../api/system'); | |||
jest.mock('../../../../api/user_groups'); | |||
const handler = new GroupsServiceMock(); | |||
const ui = { | |||
createGroupButton: byRole('button', { name: 'groups.create_group' }), | |||
infoManageMode: byText(/groups\.page\.managed_description/), | |||
description: byText('user_groups.page.description'), | |||
allFilter: byRole('button', { name: 'all' }), | |||
managedFilter: byRole('button', { name: 'managed' }), | |||
localFilter: byRole('button', { name: 'local' }), | |||
searchInput: byRole('searchbox', { name: 'search.search_by_name' }), | |||
updateButton: byRole('button', { name: 'update_details' }), | |||
updateDialog: byRole('dialog', { name: 'groups.update_group' }), | |||
updateDialogButton: byRole('button', { name: 'update_verb' }), | |||
deleteButton: byRole('button', { name: 'delete' }), | |||
deleteDialog: byRole('dialog', { name: 'groups.delete_group' }), | |||
deleteDialogButton: byRole('button', { name: 'delete' }), | |||
showMore: byRole('button', { name: 'show_more' }), | |||
nameInput: byRole('textbox', { name: 'name field_required' }), | |||
descriptionInput: byRole('textbox', { name: 'description' }), | |||
createGroupDialogButton: byRole('button', { name: 'create' }), | |||
editGroupDialogButton: byRole('button', { name: 'groups.create_group' }), | |||
createGroupDialog: byRole('dialog', { name: 'groups.create_group' }), | |||
membersDialog: byRole('dialog', { name: 'users.update' }), | |||
managedGroupRow: byRole('row', { name: 'managed-group 1' }), | |||
managedGroupEditMembersButton: byRole('button', { name: 'groups.users.edit.managed-group' }), | |||
managedEditButton: byRole('button', { name: 'groups.edit.managed-group' }), | |||
localGroupRow: byRole('row', { name: 'local-group 1' }), | |||
localGroupEditMembersButton: byRole('button', { name: 'groups.users.edit.local-group' }), | |||
localGroupRow2: byRole('row', { name: 'local-group 2 1 group 2 is loco!' }), | |||
editedLocalGroupRow: byRole('row', { name: 'local-group 3 1 group 3 rocks!' }), | |||
localEditButton: byRole('button', { name: 'groups.edit.local-group' }), | |||
localGroupRowWithLocalBadge: byRole('row', { | |||
name: 'local-group local 1', | |||
}), | |||
}; | |||
describe('in non managed mode', () => { | |||
beforeEach(() => { | |||
handler.setIsManaged(false); | |||
handler.reset(); | |||
}); | |||
it('should render all groups', async () => { | |||
renderGroupsApp(); | |||
expect(await ui.localGroupRow.find()).toBeInTheDocument(); | |||
expect(ui.managedGroupRow.get()).toBeInTheDocument(); | |||
expect(ui.localGroupRowWithLocalBadge.query()).not.toBeInTheDocument(); | |||
}); | |||
it('should be able to create a group', async () => { | |||
const user = userEvent.setup(); | |||
renderGroupsApp(); | |||
expect(await ui.description.find()).toBeInTheDocument(); | |||
await user.click(ui.createGroupButton.get()); | |||
expect(ui.createGroupDialog.get()).toBeInTheDocument(); | |||
await user.type(ui.nameInput.get(), 'local-group 2'); | |||
await user.type(ui.descriptionInput.get(), 'group 2 is loco!'); | |||
await act(async () => { | |||
await user.click(ui.createGroupDialogButton.get()); | |||
}); | |||
expect(await ui.localGroupRow2.find()).toBeInTheDocument(); | |||
}); | |||
it('should be able to delete a group', async () => { | |||
const user = userEvent.setup(); | |||
renderGroupsApp(); | |||
await user.click(await ui.localEditButton.find()); | |||
await user.click(await ui.deleteButton.find()); | |||
expect(await ui.deleteDialog.find()).toBeInTheDocument(); | |||
await act(async () => { | |||
await user.click(ui.deleteDialogButton.get()); | |||
}); | |||
expect(await ui.managedGroupRow.find()).toBeInTheDocument(); | |||
expect(ui.localGroupRow.query()).not.toBeInTheDocument(); | |||
}); | |||
it('should be able to edit a group', async () => { | |||
const user = userEvent.setup(); | |||
renderGroupsApp(); | |||
await user.click(await ui.localEditButton.find()); | |||
await user.click(await ui.updateButton.find()); | |||
expect(ui.updateDialog.get()).toBeInTheDocument(); | |||
await user.clear(ui.nameInput.get()); | |||
await user.type(ui.nameInput.get(), 'local-group 3'); | |||
await user.clear(ui.descriptionInput.get()); | |||
await user.type(ui.descriptionInput.get(), 'group 3 rocks!'); | |||
expect(ui.updateDialog.get()).toBeInTheDocument(); | |||
await act(async () => { | |||
await user.click(ui.updateDialogButton.get()); | |||
}); | |||
expect(await ui.managedGroupRow.find()).toBeInTheDocument(); | |||
expect(await ui.editedLocalGroupRow.find()).toBeInTheDocument(); | |||
}); | |||
it('should be able to edit the members of a group', async () => { | |||
const user = userEvent.setup(); | |||
renderGroupsApp(); | |||
expect(await ui.localGroupRow.find()).toBeInTheDocument(); | |||
expect(await ui.localGroupEditMembersButton.find()).toBeInTheDocument(); | |||
await user.click(ui.localGroupEditMembersButton.get()); | |||
expect(await ui.membersDialog.find()).toBeInTheDocument(); | |||
}); | |||
it('should be able search a group', async () => { | |||
const user = userEvent.setup(); | |||
renderGroupsApp(); | |||
expect(await ui.localGroupRow.find()).toBeInTheDocument(); | |||
expect(ui.managedGroupRow.get()).toBeInTheDocument(); | |||
await user.type(await ui.searchInput.find(), 'local'); | |||
expect(await ui.localGroupRow.find()).toBeInTheDocument(); | |||
expect(ui.managedGroupRow.query()).not.toBeInTheDocument(); | |||
}); | |||
it('should be able load more group', async () => { | |||
const user = userEvent.setup(); | |||
renderGroupsApp(); | |||
// including the anyone (deprecated) group | |||
expect(await screen.findAllByRole('row')).toHaveLength(4); | |||
await user.click(await ui.showMore.find()); | |||
expect(await screen.findAllByRole('row')).toHaveLength(6); | |||
}); | |||
}); | |||
describe('in manage mode', () => { | |||
beforeEach(() => { | |||
handler.setIsManaged(true); | |||
handler.reset(); | |||
}); | |||
it('should not be able to create a group', async () => { | |||
renderGroupsApp(); | |||
expect(await ui.createGroupButton.find()).toBeDisabled(); | |||
expect(ui.infoManageMode.get()).toBeInTheDocument(); | |||
}); | |||
it('should ONLY be able to delete a local group', async () => { | |||
const user = userEvent.setup(); | |||
renderGroupsApp(); | |||
expect(await ui.localGroupRowWithLocalBadge.find()).toBeInTheDocument(); | |||
await user.click(await ui.localFilter.find()); | |||
await user.click(await ui.localEditButton.find()); | |||
expect(ui.updateButton.query()).not.toBeInTheDocument(); | |||
await user.click(await ui.deleteButton.find()); | |||
expect(await ui.deleteDialog.find()).toBeInTheDocument(); | |||
await act(async () => { | |||
await user.click(ui.deleteDialogButton.get()); | |||
}); | |||
expect(ui.localGroupRowWithLocalBadge.query()).not.toBeInTheDocument(); | |||
}); | |||
it('should not be able to delete or edit a managed group', async () => { | |||
renderGroupsApp(); | |||
expect(await ui.managedGroupRow.find()).toBeInTheDocument(); | |||
expect(ui.managedEditButton.query()).not.toBeInTheDocument(); | |||
expect(ui.managedGroupEditMembersButton.query()).not.toBeInTheDocument(); | |||
}); | |||
it('should render list of all groups', async () => { | |||
renderGroupsApp(); | |||
expect(await ui.allFilter.find()).toBeInTheDocument(); | |||
expect(ui.localGroupRowWithLocalBadge.get()).toBeInTheDocument(); | |||
expect(ui.managedGroupRow.get()).toBeInTheDocument(); | |||
}); | |||
it('should render list of managed groups', async () => { | |||
const user = userEvent.setup(); | |||
renderGroupsApp(); | |||
await user.click(await ui.managedFilter.find()); | |||
expect(ui.localGroupRow.query()).not.toBeInTheDocument(); | |||
expect(ui.managedGroupRow.get()).toBeInTheDocument(); | |||
}); | |||
it('should render list of local groups', async () => { | |||
const user = userEvent.setup(); | |||
renderGroupsApp(); | |||
await user.click(await ui.localFilter.find()); | |||
expect(ui.localGroupRowWithLocalBadge.get()).toBeInTheDocument(); | |||
expect(ui.managedGroupRow.query()).not.toBeInTheDocument(); | |||
}); | |||
}); | |||
function renderGroupsApp() { | |||
return renderApp('admin/groups', <App />); | |||
} |
@@ -1,35 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2023 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 { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { click } from '../../../../helpers/testUtils'; | |||
import Header from '../Header'; | |||
it('should create new group', () => { | |||
const onCreate = jest.fn(() => Promise.resolve()); | |||
const wrapper = shallow(<Header onCreate={onCreate} />); | |||
expect(wrapper).toMatchSnapshot(); | |||
click(wrapper.find('[id="groups-create"]')); | |||
expect(wrapper).toMatchSnapshot(); | |||
wrapper.find('Form').prop<Function>('onSubmit')({ name: 'foo', description: 'bar' }); | |||
expect(onCreate).toHaveBeenCalledWith({ name: 'foo', description: 'bar' }); | |||
}); |
@@ -19,6 +19,7 @@ | |||
*/ | |||
import { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { mockGroup } from '../../../../helpers/testMocks'; | |||
import List from '../List'; | |||
it('should render', () => { | |||
@@ -31,9 +32,9 @@ it('should not render "Anyone"', () => { | |||
function shallowRender(showAnyone = true) { | |||
const groups = [ | |||
{ id: 1, name: 'sonar-users', description: '', membersCount: 55, default: true }, | |||
{ id: 2, name: 'foo', description: 'foobar', membersCount: 0, default: false }, | |||
{ id: 3, name: 'bar', description: 'barbar', membersCount: 1, default: false }, | |||
mockGroup({ name: 'sonar-users', description: '', membersCount: 55, default: true }), | |||
mockGroup({ name: 'foo', description: 'foobar', membersCount: 0, default: false }), | |||
mockGroup({ name: 'bar', description: 'barbar', membersCount: 1, default: false }), | |||
]; | |||
return shallow( | |||
<List | |||
@@ -42,6 +43,7 @@ function shallowRender(showAnyone = true) { | |||
onEdit={jest.fn()} | |||
onEditMembers={jest.fn()} | |||
showAnyone={showAnyone} | |||
manageProvider={undefined} | |||
/> | |||
); | |||
} |
@@ -34,6 +34,7 @@ function shallowRender(overrides: Partial<ListItemProps> = {}) { | |||
onDelete={jest.fn()} | |||
onEdit={jest.fn()} | |||
onEditMembers={jest.fn()} | |||
manageProvider={undefined} | |||
{...overrides} | |||
/> | |||
); |
@@ -1,94 +0,0 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render correctly 1`] = ` | |||
<Fragment> | |||
<Suggestions | |||
suggestions="user_groups" | |||
/> | |||
<Helmet | |||
defer={false} | |||
encodeSpecialCharacters={true} | |||
prioritizeSeoTags={false} | |||
title="user_groups.page" | |||
/> | |||
<main | |||
className="page page-limited" | |||
id="groups-page" | |||
> | |||
<Header | |||
onCreate={[Function]} | |||
/> | |||
<SearchBox | |||
className="big-spacer-bottom" | |||
id="groups-search" | |||
minLength={2} | |||
onChange={[Function]} | |||
placeholder="search.search_by_name" | |||
value="" | |||
/> | |||
</main> | |||
</Fragment> | |||
`; | |||
exports[`should render correctly 2`] = ` | |||
<Fragment> | |||
<Suggestions | |||
suggestions="user_groups" | |||
/> | |||
<Helmet | |||
defer={false} | |||
encodeSpecialCharacters={true} | |||
prioritizeSeoTags={false} | |||
title="user_groups.page" | |||
/> | |||
<main | |||
className="page page-limited" | |||
id="groups-page" | |||
> | |||
<Header | |||
onCreate={[Function]} | |||
/> | |||
<SearchBox | |||
className="big-spacer-bottom" | |||
id="groups-search" | |||
minLength={2} | |||
onChange={[Function]} | |||
placeholder="search.search_by_name" | |||
value="" | |||
/> | |||
<List | |||
groups={ | |||
[ | |||
{ | |||
"default": false, | |||
"description": "Owners of organization foo", | |||
"membersCount": 1, | |||
"name": "Owners", | |||
}, | |||
{ | |||
"default": true, | |||
"description": "Members of organization foo", | |||
"membersCount": 2, | |||
"name": "Members", | |||
}, | |||
] | |||
} | |||
onDelete={[Function]} | |||
onEdit={[Function]} | |||
onEditMembers={[Function]} | |||
showAnyone={true} | |||
/> | |||
<div | |||
id="groups-list-footer" | |||
> | |||
<ListFooter | |||
count={3} | |||
loadMore={[Function]} | |||
loading={false} | |||
ready={true} | |||
total={5} | |||
/> | |||
</div> | |||
</main> | |||
</Fragment> | |||
`; |
@@ -1,45 +0,0 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render 1`] = ` | |||
<Modal | |||
contentLabel="groups.delete_group" | |||
onRequestClose={[MockFunction]} | |||
> | |||
<form | |||
onSubmit={[Function]} | |||
> | |||
<header | |||
className="modal-head" | |||
> | |||
<h2> | |||
groups.delete_group | |||
</h2> | |||
</header> | |||
<div | |||
className="modal-body" | |||
> | |||
groups.delete_group.confirmation.Foo | |||
</div> | |||
<footer | |||
className="modal-foot" | |||
> | |||
<DeferredSpinner | |||
className="spacer-right" | |||
loading={false} | |||
/> | |||
<SubmitButton | |||
className="button-red" | |||
disabled={false} | |||
> | |||
delete | |||
</SubmitButton> | |||
<ResetButtonLink | |||
disabled={false} | |||
onClick={[Function]} | |||
> | |||
cancel | |||
</ResetButtonLink> | |||
</footer> | |||
</form> | |||
</Modal> | |||
`; |
@@ -3,10 +3,10 @@ | |||
exports[`should edit members 1`] = ` | |||
<Fragment> | |||
<ButtonIcon | |||
aria-label="groups.users.edit" | |||
aria-label="groups.users.edit.Foo" | |||
className="button-small" | |||
onClick={[Function]} | |||
title="groups.users.edit" | |||
title="groups.users.edit.Foo" | |||
> | |||
<BulletListIcon /> | |||
</ButtonIcon> | |||
@@ -16,17 +16,17 @@ exports[`should edit members 1`] = ` | |||
exports[`should edit members 2`] = ` | |||
<Fragment> | |||
<ButtonIcon | |||
aria-label="groups.users.edit" | |||
aria-label="groups.users.edit.Foo" | |||
className="button-small" | |||
onClick={[Function]} | |||
title="groups.users.edit" | |||
title="groups.users.edit.Foo" | |||
> | |||
<BulletListIcon /> | |||
</ButtonIcon> | |||
<EditMembersModal | |||
group={ | |||
{ | |||
"id": 3, | |||
"managed": false, | |||
"membersCount": 5, | |||
"name": "Foo", | |||
} | |||
@@ -39,10 +39,10 @@ exports[`should edit members 2`] = ` | |||
exports[`should edit members 3`] = ` | |||
<Fragment> | |||
<ButtonIcon | |||
aria-label="groups.users.edit" | |||
aria-label="groups.users.edit.Foo" | |||
className="button-small" | |||
onClick={[Function]} | |||
title="groups.users.edit" | |||
title="groups.users.edit.Foo" | |||
> | |||
<BulletListIcon /> | |||
</ButtonIcon> |
@@ -1,82 +0,0 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render form 1`] = ` | |||
<Modal | |||
contentLabel="header" | |||
onRequestClose={[MockFunction]} | |||
size="small" | |||
> | |||
<form | |||
onSubmit={[Function]} | |||
> | |||
<header | |||
className="modal-head" | |||
> | |||
<h2> | |||
header | |||
</h2> | |||
</header> | |||
<div | |||
className="modal-body" | |||
> | |||
<MandatoryFieldsExplanation | |||
className="modal-field" | |||
/> | |||
<div | |||
className="modal-field" | |||
> | |||
<label | |||
htmlFor="create-group-name" | |||
> | |||
name | |||
<MandatoryFieldMarker /> | |||
</label> | |||
<input | |||
autoFocus={true} | |||
id="create-group-name" | |||
maxLength={255} | |||
name="name" | |||
onChange={[Function]} | |||
required={true} | |||
size={50} | |||
type="text" | |||
value="" | |||
/> | |||
</div> | |||
<div | |||
className="modal-field" | |||
> | |||
<label | |||
htmlFor="create-group-description" | |||
> | |||
description | |||
</label> | |||
<textarea | |||
id="create-group-description" | |||
name="description" | |||
onChange={[Function]} | |||
value="" | |||
/> | |||
</div> | |||
</div> | |||
<footer | |||
className="modal-foot" | |||
> | |||
<DeferredSpinner | |||
className="spacer-right" | |||
loading={false} | |||
/> | |||
<SubmitButton | |||
disabled={false} | |||
> | |||
confirmButtonText | |||
</SubmitButton> | |||
<ResetButtonLink | |||
onClick={[Function]} | |||
> | |||
cancel | |||
</ResetButtonLink> | |||
</footer> | |||
</form> | |||
</Modal> | |||
`; |
@@ -1,69 +0,0 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should create new group 1`] = ` | |||
<Fragment> | |||
<div | |||
className="page-header" | |||
id="groups-header" | |||
> | |||
<h2 | |||
className="page-title" | |||
> | |||
user_groups.page | |||
</h2> | |||
<div | |||
className="page-actions" | |||
> | |||
<Button | |||
disabled={false} | |||
id="groups-create" | |||
onClick={[Function]} | |||
> | |||
groups.create_group | |||
</Button> | |||
</div> | |||
<p | |||
className="page-description" | |||
> | |||
user_groups.page.description | |||
</p> | |||
</div> | |||
</Fragment> | |||
`; | |||
exports[`should create new group 2`] = ` | |||
<Fragment> | |||
<div | |||
className="page-header" | |||
id="groups-header" | |||
> | |||
<h2 | |||
className="page-title" | |||
> | |||
user_groups.page | |||
</h2> | |||
<div | |||
className="page-actions" | |||
> | |||
<Button | |||
disabled={false} | |||
id="groups-create" | |||
onClick={[Function]} | |||
> | |||
groups.create_group | |||
</Button> | |||
</div> | |||
<p | |||
className="page-description" | |||
> | |||
user_groups.page.description | |||
</p> | |||
</div> | |||
<Form | |||
confirmButtonText="create" | |||
header="groups.create_group" | |||
onClose={[Function]} | |||
onSubmit={[MockFunction]} | |||
/> | |||
</Fragment> | |||
`; |
@@ -64,7 +64,7 @@ exports[`should render 1`] = ` | |||
{ | |||
"default": false, | |||
"description": "barbar", | |||
"id": 3, | |||
"managed": false, | |||
"membersCount": 1, | |||
"name": "bar", | |||
} | |||
@@ -79,7 +79,7 @@ exports[`should render 1`] = ` | |||
{ | |||
"default": false, | |||
"description": "foobar", | |||
"id": 2, | |||
"managed": false, | |||
"membersCount": 0, | |||
"name": "foo", | |||
} | |||
@@ -94,7 +94,7 @@ exports[`should render 1`] = ` | |||
{ | |||
"default": true, | |||
"description": "", | |||
"id": 1, | |||
"managed": false, | |||
"membersCount": 55, | |||
"name": "sonar-users", | |||
} |
@@ -24,6 +24,7 @@ exports[`should render correctly 1`] = ` | |||
<EditMembers | |||
group={ | |||
{ | |||
"managed": false, | |||
"membersCount": 1, | |||
"name": "Foo", | |||
} | |||
@@ -41,7 +42,9 @@ exports[`should render correctly 1`] = ` | |||
<td | |||
className="thin nowrap text-right" | |||
> | |||
<ActionsDropdown> | |||
<ActionsDropdown | |||
label="groups.edit.Foo" | |||
> | |||
<ActionsDropdownItem | |||
className="js-group-update" | |||
onClick={[Function]} |
@@ -22,12 +22,10 @@ import { FormattedMessage } from 'react-intl'; | |||
import DocLink from '../../components/common/DocLink'; | |||
import { Button } from '../../components/controls/buttons'; | |||
import { Alert } from '../../components/ui/Alert'; | |||
import DeferredSpinner from '../../components/ui/DeferredSpinner'; | |||
import { translate } from '../../helpers/l10n'; | |||
import UserForm from './components/UserForm'; | |||
interface Props { | |||
loading: boolean; | |||
onUpdateUsers: () => void; | |||
manageProvider?: string; | |||
} | |||
@@ -35,11 +33,10 @@ interface Props { | |||
export default function Header(props: Props) { | |||
const [openUserForm, setOpenUserForm] = React.useState(false); | |||
const { manageProvider, loading } = props; | |||
const { manageProvider } = props; | |||
return ( | |||
<div className="page-header null-spacer-bottom"> | |||
<h2 className="page-title">{translate('users.page')}</h2> | |||
<DeferredSpinner loading={loading} /> | |||
<div className="page-actions"> | |||
<Button |
@@ -138,21 +138,22 @@ export class UsersApp extends React.PureComponent<Props, State> { | |||
render() { | |||
const { search, managed } = parseQuery(this.props.location.query); | |||
const { loading, paging, users, manageProvider } = this.state; | |||
// What if we have ONLY managed users? Should we not display the filter toggle? | |||
return ( | |||
<main className="page page-limited" id="users-page"> | |||
<Suggestions suggestions="users" /> | |||
<Helmet defer={false} title={translate('users.page')} /> | |||
<Header loading={loading} onUpdateUsers={this.fetchUsers} manageProvider={manageProvider} /> | |||
<Header onUpdateUsers={this.fetchUsers} manageProvider={manageProvider} /> | |||
<div className="display-flex-justify-start big-spacer-bottom big-spacer-top"> | |||
{manageProvider !== undefined && ( | |||
<div className="big-spacer-right"> | |||
<ButtonToggle | |||
value={managed === undefined ? 'all' : managed} | |||
disabled={loading} | |||
options={[ | |||
{ label: translate('all'), value: 'all' }, | |||
{ label: translate('users.managed'), value: true }, | |||
{ label: translate('users.local'), value: false }, | |||
{ label: translate('managed'), value: true }, | |||
{ label: translate('local'), value: false }, | |||
]} | |||
onCheck={(filterOption) => { | |||
if (filterOption === 'all') { |
@@ -51,7 +51,9 @@ export default function UsersList({ | |||
<th className="nowrap">{translate('users.last_connection')}</th> | |||
<th className="nowrap">{translate('my_profile.groups')}</th> | |||
<th className="nowrap">{translate('users.tokens')}</th> | |||
<th className="nowrap"> </th> | |||
{(manageProvider === undefined || users.some((u) => !u.managed)) && ( | |||
<th className="nowrap"> </th> | |||
)} | |||
</tr> | |||
</thead> | |||
<tbody> |
@@ -33,5 +33,5 @@ it('should open the user creation form', () => { | |||
}); | |||
function getWrapper(props = {}) { | |||
return shallow(<Header loading={true} onUpdateUsers={jest.fn()} {...props} />); | |||
return shallow(<Header onUpdateUsers={jest.fn()} {...props} />); | |||
} |
@@ -35,12 +35,17 @@ const ui = { | |||
infoManageMode: byText(/users\.page\.managed_description/), | |||
description: byText('users.page.description'), | |||
allFilter: byRole('button', { name: 'all' }), | |||
managedFilter: byRole('button', { name: 'users.managed' }), | |||
localFilter: byRole('button', { name: 'users.local' }), | |||
managedFilter: byRole('button', { name: 'managed' }), | |||
localFilter: byRole('button', { name: 'local' }), | |||
aliceRow: byRole('row', { name: 'AM Alice Merveille alice.merveille never' }), | |||
aliceRowWithLocalBadge: byRole('row', { | |||
name: 'AM Alice Merveille alice.merveille users.local never', | |||
name: 'AM Alice Merveille alice.merveille local never', | |||
}), | |||
aliceUpdateGroupButton: byRole('button', { name: 'users.update_users_groups.alice.merveille' }), | |||
aliceUpdateButton: byRole('button', { name: 'users.manage_user.alice.merveille' }), | |||
alicedDeactivateButton: byRole('button', { name: 'users.deactivate' }), | |||
bobUpdateGroupButton: byRole('button', { name: 'users.update_users_groups.bob.marley' }), | |||
bobUpdateButton: byRole('button', { name: 'users.manage_user.bob.marley' }), | |||
bobRow: byRole('row', { name: 'BM Bob Marley bob.marley never' }), | |||
}; | |||
@@ -56,6 +61,20 @@ describe('in non managed mode', () => { | |||
expect(ui.createUserButton.get()).toBeEnabled(); | |||
}); | |||
it("should be able to add/remove user's group", async () => { | |||
renderUsersApp(); | |||
expect(await ui.aliceUpdateGroupButton.find()).toBeInTheDocument(); | |||
expect(await ui.bobUpdateGroupButton.find()).toBeInTheDocument(); | |||
}); | |||
it('should be able to update / change password / deactivate a user', async () => { | |||
renderUsersApp(); | |||
expect(await ui.aliceUpdateButton.find()).toBeInTheDocument(); | |||
expect(await ui.bobUpdateButton.find()).toBeInTheDocument(); | |||
}); | |||
it('should render all users', async () => { | |||
renderUsersApp(); | |||
@@ -76,6 +95,32 @@ describe('in manage mode', () => { | |||
expect(await ui.infoManageMode.find()).toBeInTheDocument(); | |||
}); | |||
it("should not be able to add/remove a user's group", async () => { | |||
renderUsersApp(); | |||
expect(await ui.aliceRowWithLocalBadge.find()).toBeInTheDocument(); | |||
expect(ui.aliceUpdateGroupButton.query()).not.toBeInTheDocument(); | |||
expect(await ui.bobRow.find()).toBeInTheDocument(); | |||
expect(ui.bobUpdateGroupButton.query()).not.toBeInTheDocument(); | |||
}); | |||
it('should not be able to update / change password / deactivate a managed user', async () => { | |||
renderUsersApp(); | |||
expect(await ui.bobRow.find()).toBeInTheDocument(); | |||
expect(ui.bobUpdateButton.query()).not.toBeInTheDocument(); | |||
}); | |||
it('should ONLY be able to deactivate a local user', async () => { | |||
const user = userEvent.setup(); | |||
renderUsersApp(); | |||
expect(await ui.aliceRowWithLocalBadge.find()).toBeInTheDocument(); | |||
await user.click(ui.aliceUpdateButton.get()); | |||
expect(await ui.alicedDeactivateButton.get()).toBeInTheDocument(); | |||
}); | |||
it('should render list of all users', async () => { | |||
renderUsersApp(); | |||
@@ -89,9 +134,7 @@ describe('in manage mode', () => { | |||
const user = userEvent.setup(); | |||
renderUsersApp(); | |||
// The click downs't work without this line | |||
expect(await ui.managedFilter.find()).toBeInTheDocument(); | |||
await user.click(await ui.managedFilter.get()); | |||
await user.click(await ui.managedFilter.find()); | |||
expect(ui.aliceRowWithLocalBadge.query()).not.toBeInTheDocument(); | |||
expect(ui.bobRow.get()).toBeInTheDocument(); | |||
@@ -101,9 +144,7 @@ describe('in manage mode', () => { | |||
const user = userEvent.setup(); | |||
renderUsersApp(); | |||
// The click downs't work without this line | |||
expect(await ui.localFilter.find()).toBeInTheDocument(); | |||
await user.click(await ui.localFilter.get()); | |||
await user.click(await ui.localFilter.find()); | |||
expect(ui.aliceRowWithLocalBadge.get()).toBeInTheDocument(); | |||
expect(ui.bobRow.query()).not.toBeInTheDocument(); |
@@ -9,9 +9,6 @@ exports[`should render correctly 1`] = ` | |||
> | |||
users.page | |||
</h2> | |||
<DeferredSpinner | |||
loading={true} | |||
/> | |||
<div | |||
className="page-actions" | |||
> |
@@ -22,7 +22,7 @@ import ActionsDropdown, { | |||
ActionsDropdownDivider, | |||
ActionsDropdownItem, | |||
} from '../../../components/controls/ActionsDropdown'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { translate, translateWithParameters } from '../../../helpers/l10n'; | |||
import { isUserActive, User } from '../../../types/users'; | |||
import DeactivateForm from './DeactivateForm'; | |||
import PasswordForm from './PasswordForm'; | |||
@@ -32,6 +32,7 @@ interface Props { | |||
isCurrentUser: boolean; | |||
onUpdateUsers: () => void; | |||
user: User; | |||
manageProvider: string | undefined; | |||
} | |||
interface State { | |||
@@ -57,23 +58,41 @@ export default class UserActions extends React.PureComponent<Props, State> { | |||
this.setState({ openForm: undefined }); | |||
}; | |||
isInstanceManaged = () => { | |||
return this.props.manageProvider !== undefined; | |||
}; | |||
isUserLocal = () => { | |||
return this.isInstanceManaged() && !this.props.user.managed; | |||
}; | |||
isUserManaged = () => { | |||
return this.isInstanceManaged() && this.props.user.managed; | |||
}; | |||
renderActions = () => { | |||
const { user } = this.props; | |||
return ( | |||
<ActionsDropdown> | |||
<ActionsDropdownItem className="js-user-update" onClick={this.handleOpenUpdateForm}> | |||
{translate('update_details')} | |||
</ActionsDropdownItem> | |||
{user.local && ( | |||
<ActionsDropdownItem | |||
className="js-user-change-password" | |||
onClick={this.handleOpenPasswordForm} | |||
> | |||
{translate('my_profile.password.title')} | |||
</ActionsDropdownItem> | |||
<ActionsDropdown label={translateWithParameters('users.manage_user', user.login)}> | |||
{!this.isInstanceManaged() && ( | |||
<> | |||
<ActionsDropdownItem className="js-user-update" onClick={this.handleOpenUpdateForm}> | |||
{translate('update_details')} | |||
</ActionsDropdownItem> | |||
{user.local && ( | |||
<ActionsDropdownItem | |||
className="js-user-change-password" | |||
onClick={this.handleOpenPasswordForm} | |||
> | |||
{translate('my_profile.password.title')} | |||
</ActionsDropdownItem> | |||
)} | |||
</> | |||
)} | |||
<ActionsDropdownDivider /> | |||
{isUserActive(user) && ( | |||
{isUserActive(user) && !this.isInstanceManaged() && <ActionsDropdownDivider />} | |||
{isUserActive(user) && (!this.isInstanceManaged() || this.isUserLocal()) && ( | |||
<ActionsDropdownItem | |||
className="js-user-deactivate" | |||
destructive={true} | |||
@@ -90,6 +109,10 @@ export default class UserActions extends React.PureComponent<Props, State> { | |||
const { openForm } = this.state; | |||
const { isCurrentUser, onUpdateUsers, user } = this.props; | |||
if (this.isUserManaged()) { | |||
return null; | |||
} | |||
return ( | |||
<> | |||
{this.renderActions()} |
@@ -28,6 +28,7 @@ interface Props { | |||
groups: string[]; | |||
onUpdateUsers: () => void; | |||
user: User; | |||
manageProvider: string | undefined; | |||
} | |||
interface State { | |||
@@ -49,7 +50,8 @@ export default class UserGroups extends React.PureComponent<Props, State> { | |||
}; | |||
render() { | |||
const { groups } = this.props; | |||
const { groups, user, manageProvider } = this.props; | |||
const { showMore, openForm } = this.state; | |||
const limit = groups.length > GROUPS_LIMIT ? GROUPS_LIMIT - 1 : GROUPS_LIMIT; | |||
return ( | |||
<ul> | |||
@@ -59,31 +61,34 @@ export default class UserGroups extends React.PureComponent<Props, State> { | |||
</li> | |||
))} | |||
{groups.length > GROUPS_LIMIT && | |||
this.state.showMore && | |||
showMore && | |||
groups.slice(limit).map((group) => ( | |||
<li className="little-spacer-bottom" key={group}> | |||
{group} | |||
</li> | |||
))} | |||
<li className="little-spacer-bottom"> | |||
{groups.length > GROUPS_LIMIT && !this.state.showMore && ( | |||
{groups.length > GROUPS_LIMIT && !showMore && ( | |||
<a className="js-user-more-groups spacer-right" href="#" onClick={this.toggleShowMore}> | |||
{translateWithParameters('more_x', groups.length - limit)} | |||
</a> | |||
)} | |||
<ButtonIcon | |||
className="js-user-groups button-small" | |||
onClick={this.handleOpenForm} | |||
tooltip={translate('users.update_groups')} | |||
> | |||
<BulletListIcon /> | |||
</ButtonIcon> | |||
{manageProvider === undefined && ( | |||
<ButtonIcon | |||
aria-label={translateWithParameters('users.update_users_groups', user.login)} | |||
className="js-user-groups button-small" | |||
onClick={this.handleOpenForm} | |||
tooltip={translate('users.update_groups')} | |||
> | |||
<BulletListIcon /> | |||
</ButtonIcon> | |||
)} | |||
</li> | |||
{this.state.openForm && ( | |||
{openForm && ( | |||
<GroupsForm | |||
onClose={this.handleCloseForm} | |||
onUpdateUsers={this.props.onUpdateUsers} | |||
user={this.props.user} | |||
user={user} | |||
/> | |||
)} | |||
</ul> |
@@ -69,7 +69,12 @@ export default function UserListItem(props: UserListItemProps) { | |||
<DateFromNow date={user.lastConnectionDate} hourPrecision={true} /> | |||
</td> | |||
<td className="thin nowrap text-middle"> | |||
<UserGroups groups={user.groups || []} onUpdateUsers={onUpdateUsers} user={user} /> | |||
<UserGroups | |||
groups={user.groups || []} | |||
manageProvider={manageProvider} | |||
onUpdateUsers={onUpdateUsers} | |||
user={user} | |||
/> | |||
</td> | |||
<td className="thin nowrap text-middle"> | |||
{user.tokensCount} | |||
@@ -81,9 +86,18 @@ export default function UserListItem(props: UserListItemProps) { | |||
<BulletListIcon /> | |||
</ButtonIcon> | |||
</td> | |||
<td className="thin nowrap text-right text-middle"> | |||
<UserActions isCurrentUser={isCurrentUser} onUpdateUsers={onUpdateUsers} user={user} /> | |||
</td> | |||
{(manageProvider === undefined || !user.managed) && ( | |||
<td className="thin nowrap text-right text-middle"> | |||
<UserActions | |||
isCurrentUser={isCurrentUser} | |||
onUpdateUsers={onUpdateUsers} | |||
user={user} | |||
manageProvider={manageProvider} | |||
/> | |||
</td> | |||
)} | |||
{openTokenForm && ( | |||
<TokensFormModal | |||
onClose={() => setOpenTokenForm(false)} |
@@ -43,8 +43,8 @@ export default function UserListItemIdentity({ identityProvider, user, managePro | |||
{!user.local && user.externalProvider !== 'sonarqube' && ( | |||
<ExternalProvider identityProvider={identityProvider} user={user} /> | |||
)} | |||
{user.managed === false && manageProvider !== undefined && ( | |||
<span className="badge">{translate('users.local')}</span> | |||
{!user.managed && manageProvider !== undefined && ( | |||
<span className="badge">{translate('local')}</span> | |||
)} | |||
</td> | |||
); |
@@ -55,6 +55,12 @@ it('should open the deactivate form', () => { | |||
function getWrapper(props = {}) { | |||
return shallow( | |||
<UserActions isCurrentUser={false} onUpdateUsers={jest.fn()} user={user} {...props} /> | |||
<UserActions | |||
isCurrentUser={false} | |||
onUpdateUsers={jest.fn()} | |||
user={user} | |||
manageProvider={undefined} | |||
{...props} | |||
/> | |||
); | |||
} |
@@ -51,5 +51,13 @@ it('should open the groups form', () => { | |||
}); | |||
function getWrapper(props = {}) { | |||
return shallow(<UserGroups groups={groups} onUpdateUsers={jest.fn()} user={user} {...props} />); | |||
return shallow( | |||
<UserGroups | |||
groups={groups} | |||
onUpdateUsers={jest.fn()} | |||
user={user} | |||
manageProvider={undefined} | |||
{...props} | |||
/> | |||
); | |||
} |
@@ -2,7 +2,9 @@ | |||
exports[`should render correctly 1`] = ` | |||
<Fragment> | |||
<ActionsDropdown> | |||
<ActionsDropdown | |||
label="users.manage_user.obi" | |||
> | |||
<ActionsDropdownItem | |||
className="js-user-update" | |||
onClick={[Function]} |
@@ -25,6 +25,7 @@ exports[`should render correctly 1`] = ` | |||
more_x.2 | |||
</a> | |||
<ButtonIcon | |||
aria-label="users.update_users_groups.obi" | |||
className="js-user-groups button-small" | |||
onClick={[Function]} | |||
tooltip="users.update_groups" |
@@ -276,6 +276,7 @@ export function mockGroup(overrides: Partial<Group> = {}): Group { | |||
return { | |||
membersCount: 1, | |||
name: 'Foo', | |||
managed: false, | |||
...overrides, | |||
}; | |||
} |
@@ -219,6 +219,7 @@ export interface Group { | |||
description?: string; | |||
membersCount: number; | |||
name: string; | |||
managed: boolean; | |||
} | |||
export type HealthType = 'RED' | 'YELLOW' | 'GREEN'; |
@@ -271,8 +271,10 @@ false_positive=False positive | |||
go_back_to_homepage=Go back to the homepage | |||
last_analysis_before=Last analysis before | |||
less_than_1_hour_ago=< 1 hour ago | |||
local=Local | |||
logging_out=You're logging out, please wait... | |||
manage=Manage | |||
managed=Managed | |||
management=Management | |||
more_information=More information | |||
new_violations=New violations | |||
@@ -2149,15 +2151,6 @@ unauthorized.message=You're not authorized to access this page. Please contact t | |||
unauthorized.reason=Reason: | |||
#------------------------------------------------------------------------------ | |||
# | |||
# USERS & GROUPS PAGE | |||
# | |||
#------------------------------------------------------------------------------ | |||
groups.users.edit=Change group members | |||
#------------------------------------------------------------------------------ | |||
# | |||
# MY PROFILE & MY ACCOUNT | |||
@@ -4340,12 +4333,13 @@ users.cannot_update_delegated_user=You cannot update the name and email of this | |||
users.minimum_x_characters=Minimum {0} characters | |||
users.email=Email | |||
users.last_connection=Last connection | |||
users.update_users_groups=Update {0}'s group membership | |||
users.update_groups=Update Groups | |||
users.manage_user=Update {0} | |||
users.update_tokens=Update Tokens | |||
users.add=Add user | |||
users.remove=Remove user | |||
users.search_description=Search users by login or name | |||
users.update=Update users | |||
users.tokens=Tokens | |||
users.user_X_tokens=Tokens of {user} | |||
users.tokens.sure=Sure? | |||
@@ -4384,8 +4378,6 @@ users.change_admin_password.form.confirm=Confirm password for user 'admin' | |||
users.change_admin_password.form.cannot_use_default_password=You must choose a password that is different from the default password. | |||
users.change_admin_password.form.success=The admin user's password was successfully changed. | |||
users.change_admin_password.form.continue_to_app=Continue to SonarQube | |||
users.local=Local | |||
users.managed=Managed | |||
#------------------------------------------------------------------------------ | |||
# | |||
@@ -4400,7 +4392,9 @@ groups.delete_group=Delete Group | |||
groups.delete_group.confirmation=Are you sure you want to delete "{0}"? | |||
groups.create_group=Create Group | |||
groups.update_group=Update Group | |||
groups.users.edit=Change {0} members | |||
groups.anyone=Anyone | |||
groups.edit=Edit {0} | |||
#------------------------------------------------------------------------------ |