From 37add481610d383abdac8982537c8d6370463b84 Mon Sep 17 00:00:00 2001 From: guillaume-peoch-sonarsource Date: Tue, 14 Mar 2023 12:30:42 +0100 Subject: [PATCH] SONAR-18657 Add Filters for all, local and managed groups on groups list --- .../main/js/api/mocks/GroupsServiceMock.ts | 195 ++++++++++++++ .../sonar-web/src/main/js/api/user_groups.ts | 1 + .../main/js/apps/groups/components/App.tsx | 133 +++++---- .../js/apps/groups/components/EditMembers.tsx | 6 +- .../main/js/apps/groups/components/Header.tsx | 4 +- .../main/js/apps/groups/components/List.tsx | 8 +- .../js/apps/groups/components/ListItem.tsx | 62 +++-- .../groups/components/__tests__/App-test.tsx | 191 ------------- .../components/__tests__/DeleteForm-test.tsx | 29 -- .../components/__tests__/EditMembers-test.tsx | 3 +- .../__tests__/EditMembersModal-test.tsx | 3 +- .../groups/components/__tests__/Form-test.tsx | 49 ---- .../components/__tests__/GroupsApp-it.tsx | 255 ++++++++++++++++++ .../components/__tests__/Header-test.tsx | 35 --- .../groups/components/__tests__/List-test.tsx | 8 +- .../components/__tests__/ListItem-test.tsx | 1 + .../__tests__/__snapshots__/App-test.tsx.snap | 94 ------- .../__snapshots__/DeleteForm-test.tsx.snap | 45 ---- .../__snapshots__/EditMembers-test.tsx.snap | 14 +- .../__snapshots__/Form-test.tsx.snap | 82 ------ .../__snapshots__/Header-test.tsx.snap | 69 ----- .../__snapshots__/List-test.tsx.snap | 6 +- .../__snapshots__/ListItem-test.tsx.snap | 5 +- .../src/main/js/apps/users/Header.tsx | 5 +- .../src/main/js/apps/users/UsersApp.tsx | 9 +- .../src/main/js/apps/users/UsersList.tsx | 4 +- .../js/apps/users/__tests__/Header-test.tsx | 2 +- .../js/apps/users/__tests__/UsersApp-it.tsx | 59 +++- .../__snapshots__/Header-test.tsx.snap | 3 - .../js/apps/users/components/UserActions.tsx | 51 +++- .../js/apps/users/components/UserGroups.tsx | 29 +- .../js/apps/users/components/UserListItem.tsx | 22 +- .../users/components/UserListItemIdentity.tsx | 4 +- .../components/__tests__/UserActions-test.tsx | 8 +- .../components/__tests__/UserGroups-test.tsx | 10 +- .../__snapshots__/UserActions-test.tsx.snap | 4 +- .../__snapshots__/UserGroups-test.tsx.snap | 1 + .../src/main/js/helpers/testMocks.ts | 1 + server/sonar-web/src/main/js/types/types.ts | 1 + .../resources/org/sonar/l10n/core.properties | 18 +- 40 files changed, 771 insertions(+), 758 deletions(-) create mode 100644 server/sonar-web/src/main/js/api/mocks/GroupsServiceMock.ts delete mode 100644 server/sonar-web/src/main/js/apps/groups/components/__tests__/App-test.tsx delete mode 100644 server/sonar-web/src/main/js/apps/groups/components/__tests__/DeleteForm-test.tsx delete mode 100644 server/sonar-web/src/main/js/apps/groups/components/__tests__/Form-test.tsx create mode 100644 server/sonar-web/src/main/js/apps/groups/components/__tests__/GroupsApp-it.tsx delete mode 100644 server/sonar-web/src/main/js/apps/groups/components/__tests__/Header-test.tsx delete mode 100644 server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/App-test.tsx.snap delete mode 100644 server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/DeleteForm-test.tsx.snap delete mode 100644 server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/Form-test.tsx.snap delete mode 100644 server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/Header-test.tsx.snap diff --git a/server/sonar-web/src/main/js/api/mocks/GroupsServiceMock.ts b/server/sonar-web/src/main/js/api/mocks/GroupsServiceMock.ts new file mode 100644 index 00000000000..3965397c9d8 --- /dev/null +++ b/server/sonar-web/src/main/js/api/mocks/GroupsServiceMock.ts @@ -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) { + this.paging = { ...this.paging, ...paging }; + } + + handleCreateGroup = (group: { name: string; description?: string }): Promise => { + const newGroup = mockGroup(group); + this.groups.push(newGroup); + return this.reply(newGroup); + }; + + handleDeleteGroup = (group: { name: string }): Promise> => { + 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> => { + 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 => { + 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 => { + return this.reply( + mockClusterSysInfo( + this.isManaged + ? { + System: { + 'High Availability': true, + 'Server ID': 'asd564-asd54a-5dsfg45', + 'External Users and Groups Provisioning': 'Okta', + }, + } + : {} + ) + ); + }; + + reply(response: T): Promise { + return Promise.resolve(cloneDeep(response)); + } +} diff --git a/server/sonar-web/src/main/js/api/user_groups.ts b/server/sonar-web/src/main/js/api/user_groups.ts index 2093b210e7a..9b2ebe32352 100644 --- a/server/sonar-web/src/main/js/api/user_groups.ts +++ b/server/sonar-web/src/main/js/api/user_groups.ts @@ -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); } diff --git a/server/sonar-web/src/main/js/apps/groups/components/App.tsx b/server/sonar-web/src/main/js/apps/groups/components/App.tsx index 31cd060f6c9..847d29b9fb1 100644 --- a/server/sonar-web/src/main/js/apps/groups/components/App.tsx +++ b/server/sonar-web/src/main/js/apps/groups/components/App.tsx @@ -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> {
- +
+ {manageProvider !== undefined && ( +
+ { + if (filterOption === 'all') { + this.setState({ managed: undefined }); + } else { + this.setState({ managed: filterOption as boolean }); + } + }} + /> +
+ )} + this.setState({ query: q })} + placeholder={translate('search.search_by_name')} + value={query} + /> +
{groups !== undefined && ( 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> { { + 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 && ( 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} /> )} diff --git a/server/sonar-web/src/main/js/apps/groups/components/EditMembers.tsx b/server/sonar-web/src/main/js/apps/groups/components/EditMembers.tsx index d22a55ee014..ca86ea9fa52 100644 --- a/server/sonar-web/src/main/js/apps/groups/components/EditMembers.tsx +++ b/server/sonar-web/src/main/js/apps/groups/components/EditMembers.tsx @@ -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 { return ( <> diff --git a/server/sonar-web/src/main/js/apps/groups/components/Header.tsx b/server/sonar-web/src/main/js/apps/groups/components/Header.tsx index 81e71d6cd8d..8a681c42b5e 100644 --- a/server/sonar-web/src/main/js/apps/groups/components/Header.tsx +++ b/server/sonar-web/src/main/js/apps/groups/components/Header.tsx @@ -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; manageProvider?: string; } -export default function Header(props: Props) { +export default function Header(props: HeaderProps) { const { manageProvider } = props; const [createModal, setCreateModal] = React.useState(false); diff --git a/server/sonar-web/src/main/js/apps/groups/components/List.tsx b/server/sonar-web/src/main/js/apps/groups/components/List.tsx index 74bd3440f5e..06b658e5724 100644 --- a/server/sonar-web/src/main/js/apps/groups/components/List.tsx +++ b/server/sonar-web/src/main/js/apps/groups/components/List.tsx @@ -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 (
@@ -46,7 +49,7 @@ export default function List(props: Props) { - {props.showAnyone && ( + {showAnyone && ( )} - {sortBy(props.groups, (group) => group.name.toLowerCase()).map((group) => ( + {sortBy(groups, (group) => group.name.toLowerCase()).map((group) => ( ))} diff --git a/server/sonar-web/src/main/js/apps/groups/components/ListItem.tsx b/server/sonar-web/src/main/js/apps/groups/components/ListItem.tsx index f148746e94a..c631103f75a 100644 --- a/server/sonar-web/src/main/js/apps/groups/components/ListItem.tsx +++ b/server/sonar-web/src/main/js/apps/groups/components/ListItem.tsx @@ -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 ( - + - + diff --git a/server/sonar-web/src/main/js/apps/groups/components/__tests__/App-test.tsx b/server/sonar-web/src/main/js/apps/groups/components/__tests__/App-test.tsx deleted file mode 100644 index 8858be2fd5e..00000000000 --- a/server/sonar-web/src/main/js/apps/groups/components/__tests__/App-test.tsx +++ /dev/null @@ -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('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('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 = {}) { - return shallow(); -} diff --git a/server/sonar-web/src/main/js/apps/groups/components/__tests__/DeleteForm-test.tsx b/server/sonar-web/src/main/js/apps/groups/components/__tests__/DeleteForm-test.tsx deleted file mode 100644 index 0ae92dc74c6..00000000000 --- a/server/sonar-web/src/main/js/apps/groups/components/__tests__/DeleteForm-test.tsx +++ /dev/null @@ -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().dive() - ).toMatchSnapshot(); -}); diff --git a/server/sonar-web/src/main/js/apps/groups/components/__tests__/EditMembers-test.tsx b/server/sonar-web/src/main/js/apps/groups/components/__tests__/EditMembers-test.tsx index 5c0e6a440b5..cce45974640 100644 --- a/server/sonar-web/src/main/js/apps/groups/components/__tests__/EditMembers-test.tsx +++ b/server/sonar-web/src/main/js/apps/groups/components/__tests__/EditMembers-test.tsx @@ -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(); diff --git a/server/sonar-web/src/main/js/apps/groups/components/__tests__/EditMembersModal-test.tsx b/server/sonar-web/src/main/js/apps/groups/components/__tests__/EditMembersModal-test.tsx index a8b4cc49e0f..d0ba4a37412 100644 --- a/server/sonar-web/src/main/js/apps/groups/components/__tests__/EditMembersModal-test.tsx +++ b/server/sonar-web/src/main/js/apps/groups/components/__tests__/EditMembersModal-test.tsx @@ -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({ diff --git a/server/sonar-web/src/main/js/apps/groups/components/__tests__/Form-test.tsx b/server/sonar-web/src/main/js/apps/groups/components/__tests__/Form-test.tsx deleted file mode 100644 index 31992d1d140..00000000000 --- a/server/sonar-web/src/main/js/apps/groups/components/__tests__/Form-test.tsx +++ /dev/null @@ -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( -
- ).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(); -}); diff --git a/server/sonar-web/src/main/js/apps/groups/components/__tests__/GroupsApp-it.tsx b/server/sonar-web/src/main/js/apps/groups/components/__tests__/GroupsApp-it.tsx new file mode 100644 index 00000000000..d705163afe5 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/groups/components/__tests__/GroupsApp-it.tsx @@ -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', ); +} diff --git a/server/sonar-web/src/main/js/apps/groups/components/__tests__/Header-test.tsx b/server/sonar-web/src/main/js/apps/groups/components/__tests__/Header-test.tsx deleted file mode 100644 index d0e25d51f7c..00000000000 --- a/server/sonar-web/src/main/js/apps/groups/components/__tests__/Header-test.tsx +++ /dev/null @@ -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(
); - expect(wrapper).toMatchSnapshot(); - - click(wrapper.find('[id="groups-create"]')); - expect(wrapper).toMatchSnapshot(); - - wrapper.find('Form').prop('onSubmit')({ name: 'foo', description: 'bar' }); - expect(onCreate).toHaveBeenCalledWith({ name: 'foo', description: 'bar' }); -}); diff --git a/server/sonar-web/src/main/js/apps/groups/components/__tests__/List-test.tsx b/server/sonar-web/src/main/js/apps/groups/components/__tests__/List-test.tsx index 625a816334a..a003332f77b 100644 --- a/server/sonar-web/src/main/js/apps/groups/components/__tests__/List-test.tsx +++ b/server/sonar-web/src/main/js/apps/groups/components/__tests__/List-test.tsx @@ -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( ); } diff --git a/server/sonar-web/src/main/js/apps/groups/components/__tests__/ListItem-test.tsx b/server/sonar-web/src/main/js/apps/groups/components/__tests__/ListItem-test.tsx index 7bcfafa6968..206257c15cd 100644 --- a/server/sonar-web/src/main/js/apps/groups/components/__tests__/ListItem-test.tsx +++ b/server/sonar-web/src/main/js/apps/groups/components/__tests__/ListItem-test.tsx @@ -34,6 +34,7 @@ function shallowRender(overrides: Partial = {}) { onDelete={jest.fn()} onEdit={jest.fn()} onEditMembers={jest.fn()} + manageProvider={undefined} {...overrides} /> ); diff --git a/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/App-test.tsx.snap b/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/App-test.tsx.snap deleted file mode 100644 index a06d0810832..00000000000 --- a/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/App-test.tsx.snap +++ /dev/null @@ -1,94 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render correctly 1`] = ` - - - -
-
- -
-
-`; - -exports[`should render correctly 2`] = ` - - - -
-
- - - -
-
-`; diff --git a/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/DeleteForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/DeleteForm-test.tsx.snap deleted file mode 100644 index 6f7b6125eff..00000000000 --- a/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/DeleteForm-test.tsx.snap +++ /dev/null @@ -1,45 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render 1`] = ` - - -
-

- groups.delete_group -

-
-
- groups.delete_group.confirmation.Foo -
-
- - - delete - - - cancel - -
- -
-`; diff --git a/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/EditMembers-test.tsx.snap b/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/EditMembers-test.tsx.snap index b074ac3da9b..044ea201e66 100644 --- a/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/EditMembers-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/EditMembers-test.tsx.snap @@ -3,10 +3,10 @@ exports[`should edit members 1`] = ` @@ -16,17 +16,17 @@ exports[`should edit members 1`] = ` exports[`should edit members 2`] = ` diff --git a/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/Form-test.tsx.snap b/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/Form-test.tsx.snap deleted file mode 100644 index 1f5ac9699d7..00000000000 --- a/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/Form-test.tsx.snap +++ /dev/null @@ -1,82 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render form 1`] = ` - -
-
-

- header -

-
-
- -
- - -
-
- -
{translate('groups.anyone')} @@ -61,13 +64,14 @@ export default function List(props: Props) {
- {group.name} + {name} {group.default && ({translate('default')})} + {isGroupLocal() && {translate('local')}} {group.membersCount}{membersCount} - {!group.default && } + {!group.default && !isManaged() && ( + + )} - {group.description} + {description} - {!group.default && ( - - props.onEdit(group)}> - {translate('update_details')} - - - props.onDelete(group)} - > - {translate('delete')} - + {!group.default && (!isManaged() || isGroupLocal()) && ( + + {!isManaged() && ( + <> + props.onEdit(group)} + > + {translate('update_details')} + + + + )} + {(!isManaged() || isGroupLocal()) && ( + props.onDelete(group)} + > + {translate('delete')} + + )} )}