export default class UsersServiceMock {
isManaged = true;
users = cloneDeep(DEFAULT_USERS);
-
constructor() {
jest.mocked(getSystemInfo).mockImplementation(this.handleGetSystemInfo);
jest.mocked(getIdentityProviders).mockImplementation(this.handleGetIdentityProviders);
}
handleSearchUsers = (data: any): Promise<{ paging: Paging; users: User[] }> => {
- const paging = {
+ let paging = {
pageIndex: 1,
- pageSize: 100,
- total: 0,
+ pageSize: 2,
+ total: 6,
};
+ if (data.p !== undefined && data.p !== paging.pageIndex) {
+ paging = { pageIndex: 2, pageSize: 2, total: 6 };
+ const users = [
+ mockUser({ name: `local-user ${this.users.length + 4}` }),
+ mockUser({ name: `local-user ${this.users.length + 5}` }),
+ ];
+
+ return this.reply({ paging, users });
+ }
+
if (this.isManaged) {
if (data.managed === undefined) {
return this.reply({ paging, users: this.users });
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { noop } from 'lodash';
import * as React from 'react';
import { CurrentUser, HomePage, NoticeType } from '../../../types/users';
updateDismissedNotices: (key: NoticeType, value: boolean) => void;
}
-export const CurrentUserContext = React.createContext<CurrentUserContextInterface | undefined>(
- undefined
-);
+export const CurrentUserContext = React.createContext<CurrentUserContextInterface>({
+ currentUser: {
+ isLoggedIn: false,
+ dismissedNotices: {},
+ },
+ updateCurrentUserHomepage: noop,
+ updateDismissedNotices: noop,
+});
+++ /dev/null
-/*
- * 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 { omit } from 'lodash';
-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';
-import { translate } from '../../../helpers/l10n';
-import { omitNil } from '../../../helpers/request';
-import { Group, Paging, SysInfoCluster } from '../../../types/types';
-import '../groups.css';
-import DeleteForm from './DeleteForm';
-import Form from './Form';
-import Header from './Header';
-import List from './List';
-
-interface State {
- groups?: Group[];
- editedGroup?: Group;
- groupToBeDeleted?: Group;
- loading: boolean;
- paging?: Paging;
- query: string;
- manageProvider?: string;
- managed: boolean | undefined;
-}
-
-export default class App extends React.PureComponent<{}, State> {
- mounted = false;
- state: State = {
- loading: true,
- query: '',
- managed: undefined,
- paging: { pageIndex: 1, pageSize: 100, total: 1000 },
- };
-
- componentDidMount() {
- this.mounted = true;
- this.fetchGroups();
- 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;
- }
-
- async fetchManageInstance() {
- const info = (await getSystemInfo()) as SysInfoCluster;
- if (this.mounted) {
- this.setState({
- manageProvider: info.System['External Users and Groups Provisioning'],
- });
- }
- }
-
- stopLoading = () => {
- if (this.mounted) {
- this.setState({ loading: false });
- }
- };
-
- fetchGroups = async () => {
- const { query: q, managed } = this.state;
- this.setState({ loading: true });
- try {
- const { groups, paging } = await searchUsersGroups({
- q,
- managed,
- });
- if (this.mounted) {
- this.setState({ groups, loading: false, paging });
- }
- } catch {
- this.stopLoading();
- }
- };
-
- fetchMoreGroups = async () => {
- const { query: q, managed, paging: currentPaging } = this.state;
- if (currentPaging && currentPaging.total > currentPaging.pageIndex * currentPaging.pageSize) {
- try {
- const { groups, paging } = await searchUsersGroups({
- p: currentPaging.pageIndex,
- q,
- managed,
- });
- if (this.mounted) {
- this.setState(({ groups: existingGroups = [] }) => ({
- groups: [...existingGroups, ...groups],
- loading: false,
- paging,
- }));
- }
- } catch {
- this.stopLoading();
- }
- }
- };
-
- refresh = async () => {
- const { paging } = this.state;
-
- await this.fetchGroups();
-
- // reload all pages in order
- if (paging && paging.pageIndex > 1) {
- for (let p = 1; p < paging.pageIndex; p++) {
- // eslint-disable-next-line no-await-in-loop
- await this.fetchMoreGroups(); // This is a intentional promise chain
- }
- }
- };
-
- handleCreate = async (data: { description: string; name: string }) => {
- await createGroup({ ...data });
-
- await this.refresh();
- };
-
- handleDelete = async () => {
- const { groupToBeDeleted } = this.state;
-
- if (!groupToBeDeleted) {
- return;
- }
-
- await deleteGroup({ name: groupToBeDeleted.name });
-
- await this.refresh();
-
- if (this.mounted) {
- this.setState({ groupToBeDeleted: undefined });
- }
- };
-
- handleEdit = async ({ name, description }: { name?: string; description: string }) => {
- const { editedGroup } = this.state;
-
- if (!editedGroup) {
- return;
- }
-
- const data = {
- currentName: editedGroup.name,
- description,
- // pass `name` only if it has changed, otherwise the WS fails
- ...omitNil({ name: name !== editedGroup.name ? name : undefined }),
- };
-
- await updateGroup(data);
-
- if (this.mounted) {
- this.setState(({ groups = [] }: State) => ({
- editedGroup: undefined,
- groups: groups.map((group) =>
- group.name === editedGroup.name
- ? {
- ...group,
- ...omit(data, ['currentName']),
- }
- : group
- ),
- }));
- }
- };
-
- render() {
- const {
- editedGroup,
- groupToBeDeleted,
- groups,
- loading,
- paging,
- query,
- manageProvider,
- managed,
- } = this.state;
-
- return (
- <>
- <Suggestions suggestions="user_groups" />
- <Helmet defer={false} title={translate('user_groups.page')} />
- <main className="page page-limited" id="groups-page">
- <Header onCreate={this.handleCreate} 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('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={(groupToBeDeleted) => this.setState({ groupToBeDeleted })}
- onEdit={(editedGroup) => this.setState({ editedGroup })}
- onEditMembers={this.refresh}
- manageProvider={manageProvider}
- />
- )}
-
- {groups !== undefined && paging !== undefined && (
- <div id="groups-list-footer">
- <ListFooter
- count={groups.length}
- loading={loading}
- loadMore={() => {
- if (paging.total > paging.pageIndex * paging.pageSize) {
- this.setState({ paging: { ...paging, pageIndex: paging.pageIndex + 1 } });
- }
- }}
- ready={!loading}
- total={paging.total}
- />
- </div>
- )}
-
- {groupToBeDeleted && (
- <DeleteForm
- group={groupToBeDeleted}
- onClose={() => this.setState({ groupToBeDeleted: undefined })}
- onSubmit={this.handleDelete}
- />
- )}
-
- {editedGroup && (
- <Form
- confirmButtonText={translate('update_verb')}
- group={editedGroup}
- header={translate('groups.update_group')}
- onClose={() => this.setState({ editedGroup: undefined })}
- onSubmit={this.handleEdit}
- />
- )}
- </main>
- </>
- );
- }
-}
+++ /dev/null
-/*
- * 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 * as React from 'react';
-import { ResetButtonLink, SubmitButton } from '../../../components/controls/buttons';
-import SimpleModal from '../../../components/controls/SimpleModal';
-import DeferredSpinner from '../../../components/ui/DeferredSpinner';
-import { translate, translateWithParameters } from '../../../helpers/l10n';
-import { Group } from '../../../types/types';
-
-interface Props {
- group: Group;
- onClose: () => void;
- onSubmit: () => Promise<void>;
-}
-
-export default function DeleteForm({ group, onClose, onSubmit }: Props) {
- const header = translate('groups.delete_group');
-
- return (
- <SimpleModal header={header} onClose={onClose} onSubmit={onSubmit}>
- {({ onCloseClick, onFormSubmit, submitting }) => (
- <form onSubmit={onFormSubmit}>
- <header className="modal-head">
- <h2>{header}</h2>
- </header>
-
- <div className="modal-body">
- {translateWithParameters('groups.delete_group.confirmation', group.name)}
- </div>
-
- <footer className="modal-foot">
- <DeferredSpinner className="spacer-right" loading={submitting} />
- <SubmitButton className="button-red" disabled={submitting}>
- {translate('delete')}
- </SubmitButton>
- <ResetButtonLink disabled={submitting} onClick={onCloseClick}>
- {translate('cancel')}
- </ResetButtonLink>
- </footer>
- </form>
- )}
- </SimpleModal>
- );
-}
--- /dev/null
+/*
+ * 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 * as React from 'react';
+import { useCallback } from 'react';
+import { deleteGroup } from '../../../api/user_groups';
+import { ResetButtonLink, SubmitButton } from '../../../components/controls/buttons';
+import SimpleModal from '../../../components/controls/SimpleModal';
+import DeferredSpinner from '../../../components/ui/DeferredSpinner';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
+import { Group } from '../../../types/types';
+
+interface Props {
+ group: Group;
+ onClose: () => void;
+ reload: () => void;
+}
+
+export default function DeleteGroupForm(props: Props) {
+ const header = translate('groups.delete_group');
+ const { group, reload, onClose } = props;
+
+ const onSubmit = useCallback(async () => {
+ await deleteGroup({ name: group.name });
+ reload();
+ onClose();
+ }, [group, reload, onClose]);
+
+ return (
+ <SimpleModal header={header} onClose={props.onClose} onSubmit={onSubmit}>
+ {({ onCloseClick, onFormSubmit, submitting }) => (
+ <form onSubmit={onFormSubmit}>
+ <header className="modal-head">
+ <h2>{header}</h2>
+ </header>
+
+ <div className="modal-body">
+ {translateWithParameters('groups.delete_group.confirmation', group.name)}
+ </div>
+
+ <footer className="modal-foot">
+ <DeferredSpinner className="spacer-right" loading={submitting} />
+ <SubmitButton className="button-red" disabled={submitting}>
+ {translate('delete')}
+ </SubmitButton>
+ <ResetButtonLink disabled={submitting} onClick={onCloseClick}>
+ {translate('cancel')}
+ </ResetButtonLink>
+ </footer>
+ </form>
+ )}
+ </SimpleModal>
+ );
+}
+++ /dev/null
-/*
- * 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 * as React from 'react';
-import { ResetButtonLink, SubmitButton } from '../../../components/controls/buttons';
-import SimpleModal from '../../../components/controls/SimpleModal';
-import DeferredSpinner from '../../../components/ui/DeferredSpinner';
-import MandatoryFieldMarker from '../../../components/ui/MandatoryFieldMarker';
-import MandatoryFieldsExplanation from '../../../components/ui/MandatoryFieldsExplanation';
-import { translate } from '../../../helpers/l10n';
-import { Group } from '../../../types/types';
-
-interface Props {
- confirmButtonText: string;
- group?: Group;
- header: string;
- onClose: () => void;
- onSubmit: (data: { description: string; name: string }) => Promise<void>;
-}
-
-interface State {
- description: string;
- name: string;
-}
-
-export default class Form extends React.PureComponent<Props, State> {
- constructor(props: Props) {
- super(props);
- this.state = {
- description: (props.group && props.group.description) || '',
- name: (props.group && props.group.name) || '',
- };
- }
-
- handleSubmit = () => {
- return this.props
- .onSubmit({ description: this.state.description, name: this.state.name })
- .then(this.props.onClose);
- };
-
- handleDescriptionChange = (event: React.SyntheticEvent<HTMLTextAreaElement>) => {
- this.setState({ description: event.currentTarget.value });
- };
-
- handleNameChange = (event: React.SyntheticEvent<HTMLInputElement>) => {
- this.setState({ name: event.currentTarget.value });
- };
-
- render() {
- return (
- <SimpleModal
- header={this.props.header}
- onClose={this.props.onClose}
- onSubmit={this.handleSubmit}
- size="small"
- >
- {({ onCloseClick, onFormSubmit, submitting }) => (
- <form onSubmit={onFormSubmit}>
- <header className="modal-head">
- <h2>{this.props.header}</h2>
- </header>
-
- <div className="modal-body">
- <MandatoryFieldsExplanation className="modal-field" />
- <div className="modal-field">
- <label htmlFor="create-group-name">
- {translate('name')}
- <MandatoryFieldMarker />
- </label>
- <input
- autoFocus={true}
- id="create-group-name"
- maxLength={255}
- name="name"
- onChange={this.handleNameChange}
- required={true}
- size={50}
- type="text"
- value={this.state.name}
- />
- </div>
- <div className="modal-field">
- <label htmlFor="create-group-description">{translate('description')}</label>
- <textarea
- id="create-group-description"
- name="description"
- onChange={this.handleDescriptionChange}
- value={this.state.description}
- />
- </div>
- </div>
-
- <footer className="modal-foot">
- <DeferredSpinner className="spacer-right" loading={submitting} />
- <SubmitButton disabled={submitting}>{this.props.confirmButtonText}</SubmitButton>
- <ResetButtonLink onClick={onCloseClick}>{translate('cancel')}</ResetButtonLink>
- </footer>
- </form>
- )}
- </SimpleModal>
- );
- }
-}
--- /dev/null
+/*
+ * 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 * as React from 'react';
+import { useCallback, useEffect, useState } from 'react';
+import { createGroup, updateGroup } from '../../../api/user_groups';
+import { ResetButtonLink, SubmitButton } from '../../../components/controls/buttons';
+import SimpleModal from '../../../components/controls/SimpleModal';
+import DeferredSpinner from '../../../components/ui/DeferredSpinner';
+import MandatoryFieldMarker from '../../../components/ui/MandatoryFieldMarker';
+import MandatoryFieldsExplanation from '../../../components/ui/MandatoryFieldsExplanation';
+import { translate } from '../../../helpers/l10n';
+import { omitNil } from '../../../helpers/request';
+import { Group } from '../../../types/types';
+
+type Props =
+ | {
+ create: true;
+ group?: undefined;
+ onClose: () => void;
+ reload: () => void;
+ }
+ | {
+ create: false;
+ group: Group;
+ onClose: () => void;
+ reload: () => void;
+ };
+
+export default function GroupForm(props: Props) {
+ const { group, create, reload, onClose } = props;
+
+ const [name, setName] = useState<string>('');
+ const [description, setDescription] = useState<string>('');
+
+ const handleSubmit = useCallback(async () => {
+ try {
+ if (create) {
+ await createGroup({ name, description });
+ } else {
+ const data = {
+ currentName: group.name,
+ description,
+ // pass `name` only if it has changed, otherwise the WS fails
+ ...omitNil({ name: name !== group.name ? name : undefined }),
+ };
+ await updateGroup(data);
+ }
+ } finally {
+ reload();
+ onClose();
+ }
+ }, [name, description, group, create, reload, onClose]);
+
+ useEffect(() => {
+ if (!create) {
+ setDescription(group.description ?? '');
+ setName(group.name);
+ }
+ }, []);
+
+ return (
+ <SimpleModal
+ header={create ? translate('groups.create_group') : translate('groups.update_group')}
+ onClose={props.onClose}
+ onSubmit={handleSubmit}
+ size="small"
+ >
+ {({ onCloseClick, onFormSubmit, submitting }) => (
+ <form onSubmit={onFormSubmit}>
+ <header className="modal-head">
+ <h2>{create ? translate('groups.create_group') : translate('groups.update_group')}</h2>
+ </header>
+
+ <div className="modal-body">
+ <MandatoryFieldsExplanation className="modal-field" />
+ <div className="modal-field">
+ <label htmlFor="create-group-name">
+ {translate('name')}
+ <MandatoryFieldMarker />
+ </label>
+ <input
+ autoFocus={true}
+ id="create-group-name"
+ maxLength={255}
+ name="name"
+ onChange={(event: React.SyntheticEvent<HTMLInputElement>) => {
+ setName(event.currentTarget.value);
+ }}
+ required={true}
+ size={50}
+ type="text"
+ value={name}
+ />
+ </div>
+ <div className="modal-field">
+ <label htmlFor="create-group-description">{translate('description')}</label>
+ <textarea
+ id="create-group-description"
+ name="description"
+ onChange={(event: React.SyntheticEvent<HTMLTextAreaElement>) => {
+ setDescription(event.currentTarget.value);
+ }}
+ value={description}
+ />
+ </div>
+ </div>
+
+ <footer className="modal-foot">
+ <DeferredSpinner className="spacer-right" loading={submitting} />
+ <SubmitButton disabled={submitting}>
+ {create ? translate('create') : translate('update_verb')}
+ </SubmitButton>
+ <ResetButtonLink onClick={onCloseClick}>{translate('cancel')}</ResetButtonLink>
+ </footer>
+ </form>
+ )}
+ </SimpleModal>
+ );
+}
--- /dev/null
+/*
+ * 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 * as React from 'react';
+import { useCallback, useEffect, useState } from 'react';
+import { Helmet } from 'react-helmet-async';
+import { searchUsersGroups } from '../../../api/user_groups';
+import ListFooter from '../../../components/controls/ListFooter';
+import { ManagedFilter } from '../../../components/controls/ManagedFilter';
+import SearchBox from '../../../components/controls/SearchBox';
+import Suggestions from '../../../components/embed-docs-modal/Suggestions';
+import { useManageProvider } from '../../../components/hooks/useManageProvider';
+import { translate } from '../../../helpers/l10n';
+import { Group, Paging } from '../../../types/types';
+import '../groups.css';
+import Header from './Header';
+import List from './List';
+
+export default function App() {
+ const [loading, setLoading] = useState<boolean>(true);
+ const [paging, setPaging] = useState<Paging>();
+ const [search, setSearch] = useState<string>('');
+ const [groups, setGroups] = useState<Group[]>([]);
+ const [managed, setManaged] = useState<boolean | undefined>();
+ const manageProvider = useManageProvider();
+
+ const fetchGroups = useCallback(async () => {
+ setLoading(true);
+ try {
+ const { groups, paging } = await searchUsersGroups({
+ q: search,
+ managed,
+ });
+ setGroups(groups);
+ setPaging(paging);
+ } finally {
+ setLoading(false);
+ }
+ }, [search, managed]);
+
+ const fetchMoreGroups = useCallback(async () => {
+ if (!paging) {
+ return;
+ }
+ setLoading(true);
+ try {
+ const { groups: nextGroups, paging: nextPage } = await searchUsersGroups({
+ q: search,
+ managed,
+ p: paging.pageIndex + 1,
+ });
+ setPaging(nextPage);
+ setGroups([...groups, ...nextGroups]);
+ } finally {
+ setLoading(false);
+ }
+ }, [groups, search, managed, paging]);
+
+ useEffect(() => {
+ fetchGroups();
+ }, [search, managed]);
+
+ return (
+ <>
+ <Suggestions suggestions="user_groups" />
+ <Helmet defer={false} title={translate('user_groups.page')} />
+ <main className="page page-limited" id="groups-page">
+ <Header reload={fetchGroups} manageProvider={manageProvider} />
+
+ <div className="display-flex-justify-start big-spacer-bottom big-spacer-top">
+ <ManagedFilter
+ manageProvider={manageProvider}
+ loading={loading}
+ managed={managed}
+ setManaged={setManaged}
+ />
+ <SearchBox
+ id="groups-search"
+ minLength={2}
+ onChange={(q) => setSearch(q)}
+ placeholder={translate('search.search_by_name')}
+ value={search}
+ />
+ </div>
+
+ <List groups={groups} reload={fetchGroups} manageProvider={manageProvider} />
+
+ {paging !== undefined && (
+ <div id="groups-list-footer">
+ <ListFooter
+ count={groups.length}
+ loading={loading}
+ loadMore={fetchMoreGroups}
+ ready={!loading}
+ total={paging.total}
+ />
+ </div>
+ )}
+ </main>
+ </>
+ );
+}
import { Button } from '../../../components/controls/buttons';
import { Alert } from '../../../components/ui/Alert';
import { translate } from '../../../helpers/l10n';
-import Form from './Form';
+import GroupForm from './GroupForm';
interface HeaderProps {
- onCreate: (data: { description: string; name: string }) => Promise<void>;
+ reload: () => void;
manageProvider?: string;
}
)}
</div>
{createModal && (
- <Form
- confirmButtonText={translate('create')}
- header={translate('groups.create_group')}
- onClose={() => setCreateModal(false)}
- onSubmit={props.onCreate}
- />
+ <GroupForm onClose={() => setCreateModal(false)} create={true} reload={props.reload} />
)}
</>
);
interface Props {
groups: Group[];
- onDelete: (group: Group) => void;
- onEdit: (group: Group) => void;
- onEditMembers: () => void;
+ reload: () => void;
manageProvider: string | undefined;
}
<ListItem
group={group}
key={group.name}
- onDelete={props.onDelete}
- onEdit={props.onEdit}
- onEditMembers={props.onEditMembers}
+ reload={props.reload}
manageProvider={manageProvider}
/>
))}
*/
import classNames from 'classnames';
import * as React from 'react';
+import { useState } from 'react';
import ActionsDropdown, {
ActionsDropdownDivider,
ActionsDropdownItem,
} from '../../../components/controls/ActionsDropdown';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { Group } from '../../../types/types';
+import DeleteGroupForm from './DeleteGroupForm';
import EditMembers from './EditMembers';
+import GroupForm from './GroupForm';
export interface ListItemProps {
group: Group;
- onDelete: (group: Group) => void;
- onEdit: (group: Group) => void;
- onEditMembers: () => void;
+ reload: () => void;
manageProvider: string | undefined;
}
const { manageProvider, group } = props;
const { name, managed, membersCount, description } = group;
+ const [groupToDelete, setGroupToDelete] = useState<Group | undefined>();
+ const [groupToEdit, setGroupToEdit] = useState<Group | undefined>();
+
const isManaged = () => {
return manageProvider !== undefined;
};
>
{membersCount}
</span>
- {!group.default && !isManaged() && (
- <EditMembers group={group} onEdit={props.onEditMembers} />
- )}
+ {!group.default && !isManaged() && <EditMembers group={group} onEdit={props.reload} />}
</td>
<td className="width-40" headers="list-group-description">
<>
<ActionsDropdownItem
className="js-group-update"
- onClick={() => props.onEdit(group)}
+ onClick={() => setGroupToEdit(group)}
>
{translate('update_details')}
</ActionsDropdownItem>
<ActionsDropdownItem
className="js-group-delete"
destructive={true}
- onClick={() => props.onDelete(group)}
+ onClick={() => setGroupToDelete(group)}
>
{translate('delete')}
</ActionsDropdownItem>
)}
</ActionsDropdown>
)}
+ {groupToDelete && (
+ <DeleteGroupForm
+ group={groupToDelete}
+ reload={props.reload}
+ onClose={() => setGroupToDelete(undefined)}
+ />
+ )}
+ {groupToEdit && (
+ <GroupForm
+ create={false}
+ group={groupToEdit}
+ reload={props.reload}
+ onClose={() => setGroupToEdit(undefined)}
+ />
+ )}
</td>
</tr>
);
import { byRole, byText } from 'testing-library-selector';
import GroupsServiceMock from '../../../../api/mocks/GroupsServiceMock';
import { renderApp } from '../../../../helpers/testReactTestingUtils';
-import App from '../App';
+import App from '../GroupsApp';
jest.mock('../../../../api/users');
jest.mock('../../../../api/system');
renderGroupsApp();
expect(await ui.description.find()).toBeInTheDocument();
+ await act(async () => {
+ await user.click(ui.createGroupButton.get());
+ });
- 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!');
+ expect(await ui.createGroupDialog.find()).toBeInTheDocument();
await act(async () => {
+ await user.type(ui.nameInput.get(), 'local-group 2');
+ await user.type(ui.descriptionInput.get(), 'group 2 is loco!');
await user.click(ui.createGroupDialogButton.get());
});
const user = userEvent.setup();
renderGroupsApp();
- await user.click(await ui.localEditButton.find());
- await user.click(await ui.deleteButton.find());
+ await act(async () => {
+ await user.click(await ui.localEditButton.find());
+ await user.click(await ui.deleteButton.find());
+ });
expect(await ui.deleteDialog.find()).toBeInTheDocument();
await act(async () => {
const user = userEvent.setup();
renderGroupsApp();
- await user.click(await ui.localEditButton.find());
- await user.click(await ui.updateButton.find());
+ await act(async () => {
+ 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!');
+ await act(async () => {
+ 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();
expect(await ui.localGroupRow.find()).toBeInTheDocument();
expect(await ui.localGroupEditMembersButton.find()).toBeInTheDocument();
- await user.click(ui.localGroupEditMembersButton.get());
+ await act(async () => {
+ await user.click(ui.localGroupEditMembersButton.get());
+ });
+
expect(await ui.membersDialog.find()).toBeInTheDocument();
});
expect(await ui.localGroupRow.find()).toBeInTheDocument();
expect(ui.managedGroupRow.get()).toBeInTheDocument();
- await user.type(await ui.searchInput.find(), 'local');
+ await act(async () => {
+ await user.type(await ui.searchInput.find(), 'local');
+ });
expect(await ui.localGroupRow.find()).toBeInTheDocument();
expect(ui.managedGroupRow.query()).not.toBeInTheDocument();
const user = userEvent.setup();
renderGroupsApp();
+ expect(await ui.localGroupRow.find()).toBeInTheDocument();
expect(await screen.findAllByRole('row')).toHaveLength(3);
- await user.click(await ui.showMore.find());
+ await act(async () => {
+ await user.click(await ui.showMore.find());
+ });
expect(await screen.findAllByRole('row')).toHaveLength(5);
});
expect(await ui.localGroupRowWithLocalBadge.find()).toBeInTheDocument();
- await user.click(await ui.localFilter.find());
- await user.click(await ui.localEditButton.find());
+ await act(async () => {
+ 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());
+ await act(async () => {
+ await user.click(await ui.deleteButton.find());
+ });
expect(await ui.deleteDialog.find()).toBeInTheDocument();
await act(async () => {
const user = userEvent.setup();
renderGroupsApp();
- await user.click(await ui.managedFilter.find());
+ await act(async () => {
+ await user.click(await ui.managedFilter.find());
+ });
expect(ui.localGroupRow.query()).not.toBeInTheDocument();
expect(ui.managedGroupRow.get()).toBeInTheDocument();
const user = userEvent.setup();
renderGroupsApp();
- await user.click(await ui.localFilter.find());
+ await act(async () => {
+ await user.click(await ui.localFilter.find());
+ });
expect(ui.localGroupRowWithLocalBadge.get()).toBeInTheDocument();
expect(ui.managedGroupRow.query()).not.toBeInTheDocument();
mockGroup({ name: 'foo', description: 'foobar', membersCount: 0, default: false }),
mockGroup({ name: 'bar', description: 'barbar', membersCount: 1, default: false }),
];
- return shallow(
- <List
- groups={groups}
- onDelete={jest.fn()}
- onEdit={jest.fn()}
- onEditMembers={jest.fn()}
- manageProvider={undefined}
- />
- );
+ return shallow(<List groups={groups} manageProvider={undefined} reload={jest.fn()} />);
}
function shallowRender(overrides: Partial<ListItemProps> = {}) {
return shallow(
- <ListItem
- group={mockGroup()}
- onDelete={jest.fn()}
- onEdit={jest.fn()}
- onEditMembers={jest.fn()}
- manageProvider={undefined}
- {...overrides}
- />
+ <ListItem group={mockGroup()} reload={jest.fn()} manageProvider={undefined} {...overrides} />
);
}
}
}
key="bar"
- onDelete={[MockFunction]}
- onEdit={[MockFunction]}
- onEditMembers={[MockFunction]}
+ reload={[MockFunction]}
/>
<ListItem
group={
}
}
key="foo"
- onDelete={[MockFunction]}
- onEdit={[MockFunction]}
- onEditMembers={[MockFunction]}
+ reload={[MockFunction]}
/>
<ListItem
group={
}
}
key="sonar-users"
- onDelete={[MockFunction]}
- onEdit={[MockFunction]}
- onEditMembers={[MockFunction]}
+ reload={[MockFunction]}
/>
</tbody>
</table>
*/
import React from 'react';
import { Route } from 'react-router-dom';
-import App from './components/App';
+import App from './components/GroupsApp';
const routes = () => <Route path="groups" element={<App />} />;
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import * as React from 'react';
+import React, { useCallback, useEffect, useState } from 'react';
import { Helmet } from 'react-helmet-async';
-import { getSystemInfo } from '../../api/system';
import { getIdentityProviders, searchUsers } from '../../api/users';
-import withCurrentUserContext from '../../app/components/current-user/withCurrentUserContext';
-import ButtonToggle from '../../components/controls/ButtonToggle';
import ListFooter from '../../components/controls/ListFooter';
+import { ManagedFilter } from '../../components/controls/ManagedFilter';
import SearchBox from '../../components/controls/SearchBox';
import Suggestions from '../../components/embed-docs-modal/Suggestions';
-import { Location, Router, withRouter } from '../../components/hoc/withRouter';
+import { useManageProvider } from '../../components/hooks/useManageProvider';
+import DeferredSpinner from '../../components/ui/DeferredSpinner';
import { translate } from '../../helpers/l10n';
-import { IdentityProvider, Paging, SysInfoCluster } from '../../types/types';
-import { CurrentUser, User } from '../../types/users';
+import { IdentityProvider, Paging } from '../../types/types';
+import { User } from '../../types/users';
import Header from './Header';
import UsersList from './UsersList';
-import { parseQuery, Query, serializeQuery } from './utils';
-interface Props {
- currentUser: CurrentUser;
- location: Location;
- router: Router;
-}
-
-interface State {
- identityProviders: IdentityProvider[];
- manageProvider?: string;
- loading: boolean;
- paging?: Paging;
- users: User[];
-}
+export default function UsersApp() {
+ const [identityProviders, setIdentityProviders] = useState<IdentityProvider[]>([]);
-export class UsersApp extends React.PureComponent<Props, State> {
- mounted = false;
- state: State = { identityProviders: [], loading: true, users: [] };
+ const [loading, setLoading] = useState(true);
+ const [paging, setPaging] = useState<Paging>();
+ const [users, setUsers] = useState<User[]>([]);
- componentDidMount() {
- this.mounted = true;
- this.fetchIdentityProviders();
- this.fetchManageInstance();
- this.fetchUsers();
- }
+ const [search, setSearch] = useState('');
+ const [managed, setManaged] = useState<boolean | undefined>(undefined);
- componentDidUpdate(prevProps: Props) {
- if (
- prevProps.location.query.search !== this.props.location.query.search ||
- prevProps.location.query.managed !== this.props.location.query.managed
- ) {
- this.fetchUsers();
- }
- }
-
- componentWillUnmount() {
- this.mounted = false;
- }
+ const manageProvider = useManageProvider();
- finishLoading = () => {
- if (this.mounted) {
- this.setState({ loading: false });
+ const fetchUsers = useCallback(async () => {
+ setLoading(true);
+ try {
+ const { paging, users } = await searchUsers({ q: search, managed });
+ setPaging(paging);
+ setUsers(users);
+ } finally {
+ setLoading(false);
}
- };
+ }, [search, managed]);
- async fetchManageInstance() {
- const info = (await getSystemInfo()) as SysInfoCluster;
- if (this.mounted) {
- this.setState({
- manageProvider: info.System['External Users and Groups Provisioning'],
- });
+ const fetchMoreUsers = useCallback(async () => {
+ if (!paging) {
+ return;
}
- }
-
- fetchIdentityProviders = () =>
- getIdentityProviders().then(({ identityProviders }) => {
- if (this.mounted) {
- this.setState({ identityProviders });
- }
- });
-
- fetchUsers = () => {
- const { search, managed } = parseQuery(this.props.location.query);
- this.setState({ loading: true });
- searchUsers({
- q: search,
- managed,
- }).then(({ paging, users }) => {
- if (this.mounted) {
- this.setState({ loading: false, paging, users });
- }
- }, this.finishLoading);
- };
-
- fetchMoreUsers = () => {
- const { paging } = this.state;
- if (paging) {
- const { search, managed } = parseQuery(this.props.location.query);
- this.setState({ loading: true });
- searchUsers({
- p: paging.pageIndex + 1,
+ setLoading(true);
+ try {
+ const { paging: nextPage, users: nextUsers } = await searchUsers({
q: search,
managed,
- }).then(({ paging, users }) => {
- if (this.mounted) {
- this.setState((state) => ({ loading: false, users: [...state.users, ...users], paging }));
- }
- }, this.finishLoading);
+ p: paging.pageIndex + 1,
+ });
+ setPaging(nextPage);
+ setUsers([...users, ...nextUsers]);
+ } finally {
+ setLoading(false);
}
- };
-
- updateQuery = (newQuery: Partial<Query>) => {
- const query = serializeQuery({ ...parseQuery(this.props.location.query), ...newQuery });
- this.props.router.push({ ...this.props.location, query });
- };
-
- updateTokensCount = (login: string, tokensCount: number) => {
- this.setState((state) => ({
- users: state.users.map((user) => (user.login === login ? { ...user, tokensCount } : user)),
- }));
- };
-
- render() {
- const { search, managed } = parseQuery(this.props.location.query);
- const { loading, paging, users, manageProvider } = this.state;
-
- return (
- <main className="page page-limited" id="users-page">
- <Suggestions suggestions="users" />
- <Helmet defer={false} title={translate('users.page')} />
- <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('managed'), value: true },
- { label: translate('local'), value: false },
- ]}
- onCheck={(filterOption) => {
- if (filterOption === 'all') {
- this.updateQuery({ managed: undefined });
- } else {
- this.updateQuery({ managed: filterOption as boolean });
- }
- }}
- />
- </div>
- )}
- <SearchBox
- id="users-search"
- onChange={(search: string) => this.updateQuery({ search })}
- placeholder={translate('search.search_by_login_or_name')}
- value={search}
- />
- </div>
+ }, [search, managed, paging, users]);
+
+ useEffect(() => {
+ (async () => {
+ const { identityProviders } = await getIdentityProviders();
+ setIdentityProviders(identityProviders);
+ })();
+ }, []);
+
+ useEffect(() => {
+ fetchUsers();
+ }, [search, managed]);
+
+ return (
+ <main className="page page-limited" id="users-page">
+ <Suggestions suggestions="users" />
+ <Helmet defer={false} title={translate('users.page')} />
+ <Header onUpdateUsers={fetchUsers} manageProvider={manageProvider} />
+ <div className="display-flex-justify-start big-spacer-bottom big-spacer-top">
+ <ManagedFilter
+ manageProvider={manageProvider}
+ loading={loading}
+ managed={managed}
+ setManaged={setManaged}
+ />
+ <SearchBox
+ id="users-search"
+ minLength={2}
+ onChange={(search: string) => setSearch(search)}
+ placeholder={translate('search.search_by_login_or_name')}
+ value={search}
+ />
+ </div>
+ <DeferredSpinner loading={loading}>
<UsersList
- currentUser={this.props.currentUser}
- identityProviders={this.state.identityProviders}
- onUpdateUsers={this.fetchUsers}
- updateTokensCount={this.updateTokensCount}
+ identityProviders={identityProviders}
+ onUpdateUsers={fetchUsers}
+ updateTokensCount={fetchUsers}
users={users}
manageProvider={manageProvider}
/>
- {paging !== undefined && (
- <ListFooter
- count={users.length}
- loadMore={this.fetchMoreUsers}
- ready={!loading}
- total={paging.total}
- />
- )}
- </main>
- );
- }
+ </DeferredSpinner>
+ {paging !== undefined && (
+ <ListFooter
+ count={users.length}
+ loadMore={fetchMoreUsers}
+ ready={!loading}
+ total={paging.total}
+ />
+ )}
+ </main>
+ );
}
-
-export default withRouter(withCurrentUserContext(UsersApp));
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
+import { CurrentUserContext } from '../../app/components/current-user/CurrentUserContext';
import { translate } from '../../helpers/l10n';
import { IdentityProvider } from '../../types/types';
-import { User } from '../../types/users';
+import { isLoggedIn, User } from '../../types/users';
import UserListItem from './components/UserListItem';
interface Props {
- currentUser: { isLoggedIn: boolean; login?: string };
identityProviders: IdentityProvider[];
onUpdateUsers: () => void;
updateTokensCount: (login: string, tokensCount: number) => void;
}
export default function UsersList({
- currentUser,
identityProviders,
onUpdateUsers,
updateTokensCount,
users,
manageProvider,
}: Props) {
+ const userContext = React.useContext(CurrentUserContext);
+ const currentUser = userContext?.currentUser;
+
return (
<div className="boxed-group boxed-group-inner">
<table className="data zebra" id="users-list">
identityProvider={identityProviders.find(
(provider) => user.externalProvider === provider.key
)}
- isCurrentUser={currentUser.isLoggedIn && currentUser.login === user.login}
+ isCurrentUser={isLoggedIn(currentUser) && currentUser.login === user.login}
key={user.login}
onUpdateUsers={onUpdateUsers}
updateTokensCount={updateTokensCount}
+++ /dev/null
-/*
- * 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 render correctly', () => {
- expect(getWrapper()).toMatchSnapshot();
-});
-
-it('should open the user creation form', () => {
- const wrapper = getWrapper();
- click(wrapper.find('#users-create'));
- expect(wrapper.find('UserForm').exists()).toBe(true);
-});
-
-function getWrapper(props = {}) {
- return shallow(<Header onUpdateUsers={jest.fn()} {...props} />);
-}
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { act, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import * as React from 'react';
import { byLabelText, byRole, byText } from 'testing-library-selector';
allFilter: byRole('button', { name: 'all' }),
managedFilter: byRole('button', { name: 'managed' }),
localFilter: byRole('button', { name: 'local' }),
+ showMore: byRole('button', { name: 'show_more' }),
aliceRow: byRole('row', { name: 'AM Alice Merveille alice.merveille never' }),
aliceRowWithLocalBadge: byRole('row', {
name: 'AM Alice Merveille alice.merveille local never',
renderUsersApp();
expect(await ui.aliceUpdateGroupButton.find()).toBeInTheDocument();
- expect(await ui.bobUpdateGroupButton.find()).toBeInTheDocument();
+ expect(ui.bobUpdateGroupButton.get()).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();
+ expect(ui.bobUpdateButton.get()).toBeInTheDocument();
});
it('should render all users', async () => {
renderUsersApp();
+ expect(await ui.aliceRow.find()).toBeInTheDocument();
+ expect(ui.bobRow.get()).toBeInTheDocument();
expect(ui.aliceRowWithLocalBadge.query()).not.toBeInTheDocument();
+ });
+
+ it('should be able load more users', async () => {
+ const user = userEvent.setup();
+ renderUsersApp();
+
expect(await ui.aliceRow.find()).toBeInTheDocument();
- expect(await ui.bobRow.find()).toBeInTheDocument();
+ expect(ui.bobRow.get()).toBeInTheDocument();
+ expect(screen.getAllByRole('row')).toHaveLength(4);
+
+ await act(async () => {
+ await user.click(await ui.showMore.find());
+ });
+
+ expect(screen.getAllByRole('row')).toHaveLength(6);
});
});
it('should not be able to create a user"', async () => {
renderUsersApp();
- expect(await ui.createUserButton.get()).toBeDisabled();
+
expect(await ui.infoManageMode.find()).toBeInTheDocument();
+ expect(ui.createUserButton.get()).toBeDisabled();
});
it("should not be able to add/remove a user's group", async () => {
expect(await ui.aliceRowWithLocalBadge.find()).toBeInTheDocument();
expect(ui.aliceUpdateGroupButton.query()).not.toBeInTheDocument();
-
- expect(await ui.bobRow.find()).toBeInTheDocument();
+ expect(ui.bobRow.get()).toBeInTheDocument();
expect(ui.bobUpdateGroupButton.query()).not.toBeInTheDocument();
});
expect(await ui.aliceRowWithLocalBadge.find()).toBeInTheDocument();
await user.click(ui.aliceUpdateButton.get());
- expect(await ui.alicedDeactivateButton.get()).toBeInTheDocument();
+ expect(await ui.alicedDeactivateButton.find()).toBeInTheDocument();
});
it('should render list of all users', async () => {
const user = userEvent.setup();
renderUsersApp();
- await user.click(await ui.managedFilter.find());
+ expect(await ui.aliceRowWithLocalBadge.find()).toBeInTheDocument();
+
+ await act(async () => {
+ await user.click(await ui.managedFilter.find());
+ });
+ expect(await ui.bobRow.find()).toBeInTheDocument();
expect(ui.aliceRowWithLocalBadge.query()).not.toBeInTheDocument();
- expect(ui.bobRow.get()).toBeInTheDocument();
});
it('should render list of local users', async () => {
const user = userEvent.setup();
renderUsersApp();
- await user.click(await ui.localFilter.find());
+ await act(async () => {
+ await user.click(await ui.localFilter.find());
+ });
- expect(ui.aliceRowWithLocalBadge.get()).toBeInTheDocument();
+ expect(await ui.aliceRowWithLocalBadge.find()).toBeInTheDocument();
expect(ui.bobRow.query()).not.toBeInTheDocument();
});
});
+++ /dev/null
-/*
- * 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 UsersList from '../UsersList';
-
-const users = [
- {
- login: 'luke',
- name: 'Luke',
- active: true,
- scmAccounts: [],
- local: false,
- managed: false,
- },
- {
- login: 'obi',
- name: 'One',
- active: true,
- scmAccounts: [],
- local: false,
- managed: false,
- },
-];
-
-it('should render correctly', () => {
- expect(getWrapper()).toMatchSnapshot();
-});
-
-function getWrapper(props = {}) {
- return shallow(
- <UsersList
- currentUser={{ isLoggedIn: true, login: 'luke' }}
- identityProviders={[
- {
- backgroundColor: 'blue',
- iconPath: 'icon/path',
- key: 'foo',
- name: 'Foo Provider',
- },
- ]}
- onUpdateUsers={jest.fn()}
- updateTokensCount={jest.fn()}
- users={users}
- manageProvider={undefined}
- {...props}
- />
- );
-}
+++ /dev/null
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render correctly 1`] = `
-<div
- className="page-header null-spacer-bottom"
->
- <h2
- className="page-title"
- >
- users.page
- </h2>
- <div
- className="page-actions"
- >
- <Button
- disabled={false}
- id="users-create"
- onClick={[Function]}
- >
- users.create_user
- </Button>
- </div>
- <p
- className="page-description"
- >
- users.page.description
- </p>
-</div>
-`;
+++ /dev/null
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render correctly 1`] = `
-<div
- className="boxed-group boxed-group-inner"
->
- <table
- className="data zebra"
- id="users-list"
- >
- <thead>
- <tr>
- <th />
- <th
- className="nowrap"
- />
- <th
- className="nowrap"
- >
- my_profile.scm_accounts
- </th>
- <th
- className="nowrap"
- >
- users.last_connection
- </th>
- <th
- className="nowrap"
- >
- my_profile.groups
- </th>
- <th
- className="nowrap"
- >
- users.tokens
- </th>
- <th
- className="nowrap"
- >
-
- </th>
- </tr>
- </thead>
- <tbody>
- <UserListItem
- isCurrentUser={true}
- key="luke"
- onUpdateUsers={[MockFunction]}
- updateTokensCount={[MockFunction]}
- user={
- {
- "active": true,
- "local": false,
- "login": "luke",
- "managed": false,
- "name": "Luke",
- "scmAccounts": [],
- }
- }
- />
- <UserListItem
- isCurrentUser={false}
- key="obi"
- onUpdateUsers={[MockFunction]}
- updateTokensCount={[MockFunction]}
- user={
- {
- "active": true,
- "local": false,
- "login": "obi",
- "managed": false,
- "name": "One",
- "scmAccounts": [],
- }
- }
- />
- </tbody>
- </table>
-</div>
-`;
--- /dev/null
+/*
+ * 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 * as React from 'react';
+import { translate } from '../../helpers/l10n';
+import ButtonToggle from './ButtonToggle';
+
+interface ManagedFilterProps {
+ manageProvider: string | undefined;
+ loading: boolean;
+ managed: boolean | undefined;
+ setManaged: (managed: boolean | undefined) => void;
+}
+
+export function ManagedFilter(props: ManagedFilterProps) {
+ const { manageProvider, loading, managed } = props;
+
+ if (manageProvider === undefined) {
+ return null;
+ }
+
+ return (
+ <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') {
+ props.setManaged(undefined);
+ } else {
+ props.setManaged(filterOption as boolean);
+ }
+ }}
+ />
+ </div>
+ );
+}
--- /dev/null
+/*
+ * 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 * as React from 'react';
+import { useEffect } from 'react';
+import { getSystemInfo } from '../../api/system';
+import { SysInfoCluster } from '../../types/types';
+
+export function useManageProvider(): string | undefined {
+ const [manageProvider, setManageProvider] = React.useState<string | undefined>();
+
+ useEffect(() => {
+ (async () => {
+ const info = (await getSystemInfo()) as SysInfoCluster;
+ setManageProvider(info.System['External Users and Groups Provisioning']);
+ })();
+ }, []);
+
+ return manageProvider;
+}