*/
import { cloneDeep } from 'lodash';
-import { mockClusterSysInfo, mockIdentityProvider } from '../../helpers/testMocks';
+import { mockClusterSysInfo, mockIdentityProvider, mockUser } from '../../helpers/testMocks';
import { IdentityProvider, Paging, SysInfoCluster } from '../../types/types';
import { User } from '../../types/users';
import { getSystemInfo } from '../system';
export default class UsersServiceMock {
isManaged = true;
+ users = [
+ mockUser({
+ managed: true,
+ login: 'bob.marley',
+ name: 'Bob Marley',
+ }),
+ mockUser({
+ managed: false,
+ login: 'alice.merveille',
+ name: 'Alice Merveille',
+ }),
+ ];
constructor() {
jest.mocked(getSystemInfo).mockImplementation(this.handleGetSystemInfo);
jest.mocked(getIdentityProviders).mockImplementation(this.handleGetIdentityProviders);
- jest.mocked(searchUsers).mockImplementation(this.handleSearchUsers);
+ jest.mocked(searchUsers).mockImplementation((p) => this.handleSearchUsers(p));
}
setIsManaged(managed: boolean) {
this.isManaged = managed;
}
- handleSearchUsers = (): Promise<{ paging: Paging; users: User[] }> => {
- return this.reply({
- paging: {
- pageIndex: 1,
- pageSize: 100,
- total: 0,
- },
- users: [],
- });
+ handleSearchUsers = (data: any): Promise<{ paging: Paging; users: User[] }> => {
+ const paging = {
+ pageIndex: 1,
+ pageSize: 100,
+ total: 0,
+ };
+
+ if (this.isManaged) {
+ if (data.managed === undefined) {
+ return this.reply({ paging, users: this.users });
+ }
+ const users = this.users.filter((user) => user.managed === data.managed);
+ return this.reply({ paging, users });
+ }
+ return this.reply({ paging, users: this.users });
};
handleGetIdentityProviders = (): Promise<{ identityProviders: IdentityProvider[] }> => {
p?: number;
ps?: number;
q?: string;
+ managed?: boolean;
}): Promise<{ paging: Paging; users: User[] }> {
data.q = data.q || undefined;
return getJSON('/api/users/search', data).catch(throwGlobalError);
+++ /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 SearchBox from '../../components/controls/SearchBox';
-import { translate } from '../../helpers/l10n';
-import { Query } from './utils';
-
-interface Props {
- query: Query;
- updateQuery: (newQuery: Partial<Query>) => void;
-}
-
-export default class Search extends React.PureComponent<Props> {
- handleSearch = (search: string) => {
- this.props.updateQuery({ search });
- };
-
- render() {
- const { query } = this.props;
-
- return (
- <div className="panel panel-vertical bordered-bottom spacer-bottom" id="users-search">
- <SearchBox
- minLength={2}
- onChange={this.handleSearch}
- placeholder={translate('search.search_by_login_or_name')}
- value={query.search}
- />
- </div>
- );
- }
-}
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 SearchBox from '../../components/controls/SearchBox';
import Suggestions from '../../components/embed-docs-modal/Suggestions';
import { Location, Router, withRouter } from '../../components/hoc/withRouter';
import { translate } from '../../helpers/l10n';
import { IdentityProvider, Paging, SysInfoCluster } from '../../types/types';
import { CurrentUser, User } from '../../types/users';
import Header from './Header';
-import Search from './Search';
import UsersList from './UsersList';
import { parseQuery, Query, serializeQuery } from './utils';
}
componentDidUpdate(prevProps: Props) {
- if (prevProps.location.query.search !== this.props.location.query.search) {
+ if (
+ prevProps.location.query.search !== this.props.location.query.search ||
+ prevProps.location.query.managed !== this.props.location.query.managed
+ ) {
this.fetchUsers();
}
}
});
fetchUsers = () => {
- const { location } = this.props;
+ const { search, managed } = parseQuery(this.props.location.query);
this.setState({ loading: true });
- searchUsers({ q: parseQuery(location.query).search }).then(({ paging, users }) => {
+ searchUsers({
+ q: search,
+ managed,
+ }).then(({ paging, users }) => {
if (this.mounted) {
this.setState({ loading: false, paging, users });
}
fetchMoreUsers = () => {
const { paging } = this.state;
if (paging) {
+ const { search, managed } = parseQuery(this.props.location.query);
this.setState({ loading: true });
searchUsers({
p: paging.pageIndex + 1,
- q: parseQuery(this.props.location.query).search,
+ q: search,
+ managed,
}).then(({ paging, users }) => {
if (this.mounted) {
this.setState((state) => ({ loading: false, users: [...state.users, ...users], paging }));
};
render() {
- const query = parseQuery(this.props.location.query);
+ const { search, managed } = parseQuery(this.props.location.query);
const { loading, paging, users, manageProvider } = this.state;
+ // What if we have ONLY managed users? Should we not display the filter toggle?
return (
<main className="page page-limited" id="users-page">
<Suggestions suggestions="users" />
<Helmet defer={false} title={translate('users.page')} />
<Header loading={loading} onUpdateUsers={this.fetchUsers} manageProvider={manageProvider} />
- <Search query={query} updateQuery={this.updateQuery} />
+ <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}
+ options={[
+ { label: translate('all'), value: 'all' },
+ { label: translate('users.managed'), value: true },
+ { label: translate('users.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>
<UsersList
currentUser={this.props.currentUser}
identityProviders={this.state.identityProviders}
onUpdateUsers={this.fetchUsers}
updateTokensCount={this.updateTokensCount}
users={users}
+ manageProvider={manageProvider}
/>
{paging !== undefined && (
<ListFooter
onUpdateUsers: () => void;
updateTokensCount: (login: string, tokensCount: number) => void;
users: User[];
+ manageProvider: string | undefined;
}
export default function UsersList({
onUpdateUsers,
updateTokensCount,
users,
+ manageProvider,
}: Props) {
return (
<div className="boxed-group boxed-group-inner">
onUpdateUsers={onUpdateUsers}
updateTokensCount={updateTokensCount}
user={user}
+ manageProvider={manageProvider}
/>
))}
</tbody>
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import userEvent from '@testing-library/user-event';
import * as React from 'react';
import { byRole, byText } from 'testing-library-selector';
import UsersServiceMock from '../../../api/mocks/UsersServiceMock';
createUserButton: byRole('button', { name: 'users.create_user' }),
infoManageMode: byText(/users\.page\.managed_description/),
description: byText('users.page.description'),
+ allFilter: byRole('button', { name: 'all' }),
+ managedFilter: byRole('button', { name: 'users.managed' }),
+ localFilter: byRole('button', { name: 'users.local' }),
+ aliceRow: byRole('row', { name: 'AM Alice Merveille alice.merveille never' }),
+ aliceRowWithLocalBadge: byRole('row', {
+ name: 'AM Alice Merveille alice.merveille users.local never',
+ }),
+ bobRow: byRole('row', { name: 'BM Bob Marley bob.marley never' }),
};
-it('should render list of user in non manage mode', async () => {
- handler.setIsManaged(false);
- renderUsersApp();
+describe('in non managed mode', () => {
+ beforeEach(() => {
+ handler.setIsManaged(false);
+ });
- expect(await ui.description.find()).toBeInTheDocument();
- expect(ui.createUserButton.get()).toBeEnabled();
+ it('should allow the creation of user', async () => {
+ renderUsersApp();
+
+ expect(await ui.description.find()).toBeInTheDocument();
+ expect(ui.createUserButton.get()).toBeEnabled();
+ });
+
+ it('should render all users', async () => {
+ renderUsersApp();
+
+ expect(ui.aliceRowWithLocalBadge.query()).not.toBeInTheDocument();
+ expect(await ui.aliceRow.find()).toBeInTheDocument();
+ expect(await ui.bobRow.find()).toBeInTheDocument();
+ });
});
-it('should render list of user in manage mode', async () => {
- handler.setIsManaged(true);
- renderUsersApp();
+describe('in manage mode', () => {
+ beforeEach(() => {
+ handler.setIsManaged(true);
+ });
+
+ it('should not be able to create a user"', async () => {
+ renderUsersApp();
+ expect(await ui.createUserButton.get()).toBeDisabled();
+ expect(await ui.infoManageMode.find()).toBeInTheDocument();
+ });
+
+ it('should render list of all users', async () => {
+ renderUsersApp();
+
+ expect(await ui.allFilter.find()).toBeInTheDocument();
+
+ expect(ui.aliceRowWithLocalBadge.get()).toBeInTheDocument();
+ expect(ui.bobRow.get()).toBeInTheDocument();
+ });
+
+ it('should render list of managed users', async () => {
+ const user = userEvent.setup();
+ renderUsersApp();
+
+ // The click downs't work without this line
+ expect(await ui.managedFilter.find()).toBeInTheDocument();
+ await user.click(await ui.managedFilter.get());
+
+ expect(ui.aliceRowWithLocalBadge.query()).not.toBeInTheDocument();
+ expect(ui.bobRow.get()).toBeInTheDocument();
+ });
+
+ it('should render list of local users', async () => {
+ const user = userEvent.setup();
+ renderUsersApp();
+
+ // The click downs't work without this line
+ expect(await ui.localFilter.find()).toBeInTheDocument();
+ await user.click(await ui.localFilter.get());
- expect(await ui.infoManageMode.find()).toBeInTheDocument();
- expect(ui.createUserButton.get()).toBeDisabled();
+ expect(ui.aliceRowWithLocalBadge.get()).toBeInTheDocument();
+ expect(ui.bobRow.query()).not.toBeInTheDocument();
+ });
});
function renderUsersApp() {
- renderApp('admin/users', <UsersApp />);
+ return renderApp('admin/users', <UsersApp />);
}
+++ /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 { Location } from '../../../components/hoc/withRouter';
-import { mockRouter } from '../../../helpers/testMocks';
-import { waitAndUpdate } from '../../../helpers/testUtils';
-import { UsersApp } from '../UsersApp';
-
-jest.mock('../../../api/users', () => ({
- getIdentityProviders: jest.fn(() =>
- Promise.resolve({
- identityProviders: [
- {
- backgroundColor: 'blue',
- iconPath: 'icon/path',
- key: 'foo',
- name: 'Foo Provider',
- },
- ],
- })
- ),
- searchUsers: jest.fn(() =>
- Promise.resolve({
- paging: {
- pageIndex: 1,
- pageSize: 1,
- total: 2,
- },
- users: [
- {
- login: 'luke',
- name: 'Luke',
- active: true,
- scmAccounts: [],
- local: false,
- },
- ],
- })
- ),
-}));
-
-const getIdentityProviders = require('../../../api/users').getIdentityProviders as jest.Mock<any>;
-const searchUsers = require('../../../api/users').searchUsers as jest.Mock<any>;
-
-const currentUser = { isLoggedIn: true, login: 'luke', dismissedNotices: {} };
-const location = { pathname: '', query: {} } as Location;
-
-beforeEach(() => {
- getIdentityProviders.mockClear();
- searchUsers.mockClear();
-});
-
-it('should render correctly', async () => {
- const wrapper = getWrapper();
- expect(wrapper).toMatchSnapshot();
- expect(getIdentityProviders).toHaveBeenCalled();
- expect(searchUsers).toHaveBeenCalled();
- await waitAndUpdate(wrapper);
- expect(wrapper).toMatchSnapshot();
-});
-
-function getWrapper(props: Partial<UsersApp['props']> = {}) {
- return shallow(
- <UsersApp currentUser={currentUser} location={location} router={mockRouter()} {...props} />
- );
-}
active: true,
scmAccounts: [],
local: false,
+ managed: false,
},
{
login: 'obi',
active: true,
scmAccounts: [],
local: false,
+ managed: false,
},
];
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`] = `
-<main
- className="page page-limited"
- id="users-page"
->
- <Suggestions
- suggestions="users"
- />
- <Helmet
- defer={false}
- encodeSpecialCharacters={true}
- prioritizeSeoTags={false}
- title="users.page"
- />
- <Header
- loading={true}
- onUpdateUsers={[Function]}
- />
- <Search
- query={
- {
- "search": "",
- }
- }
- updateQuery={[Function]}
- />
- <UsersList
- currentUser={
- {
- "dismissedNotices": {},
- "isLoggedIn": true,
- "login": "luke",
- }
- }
- identityProviders={[]}
- onUpdateUsers={[Function]}
- updateTokensCount={[Function]}
- users={[]}
- />
-</main>
-`;
-
-exports[`should render correctly 2`] = `
-<main
- className="page page-limited"
- id="users-page"
->
- <Suggestions
- suggestions="users"
- />
- <Helmet
- defer={false}
- encodeSpecialCharacters={true}
- prioritizeSeoTags={false}
- title="users.page"
- />
- <Header
- loading={false}
- onUpdateUsers={[Function]}
- />
- <Search
- query={
- {
- "search": "",
- }
- }
- updateQuery={[Function]}
- />
- <UsersList
- currentUser={
- {
- "dismissedNotices": {},
- "isLoggedIn": true,
- "login": "luke",
- }
- }
- identityProviders={
- [
- {
- "backgroundColor": "blue",
- "iconPath": "icon/path",
- "key": "foo",
- "name": "Foo Provider",
- },
- ]
- }
- onUpdateUsers={[Function]}
- updateTokensCount={[Function]}
- users={
- [
- {
- "active": true,
- "local": false,
- "login": "luke",
- "name": "Luke",
- "scmAccounts": [],
- },
- ]
- }
- />
- <ListFooter
- count={1}
- loadMore={[Function]}
- ready={true}
- total={2}
- />
-</main>
-`;
"active": true,
"local": false,
"login": "luke",
+ "managed": false,
"name": "Luke",
"scmAccounts": [],
}
"active": true,
"local": false,
"login": "obi",
+ "managed": false,
"name": "One",
"scmAccounts": [],
}
import UserListItemIdentity from './UserListItemIdentity';
import UserScmAccounts from './UserScmAccounts';
-interface Props {
+export interface UserListItemProps {
identityProvider?: IdentityProvider;
isCurrentUser: boolean;
onUpdateUsers: () => void;
updateTokensCount: (login: string, tokensCount: number) => void;
user: User;
+ manageProvider: string | undefined;
}
-interface State {
- openTokenForm: boolean;
-}
-
-export default class UserListItem extends React.PureComponent<Props, State> {
- state: State = { openTokenForm: false };
-
- handleOpenTokensForm = () => this.setState({ openTokenForm: true });
- handleCloseTokensForm = () => this.setState({ openTokenForm: false });
+export default function UserListItem(props: UserListItemProps) {
+ const [openTokenForm, setOpenTokenForm] = React.useState(false);
- render() {
- const { identityProvider, onUpdateUsers, user } = this.props;
+ const {
+ identityProvider,
+ onUpdateUsers,
+ user,
+ manageProvider,
+ isCurrentUser,
+ updateTokensCount,
+ } = props;
- return (
- <tr>
- <td className="thin nowrap text-middle">
- <Avatar hash={user.avatar} name={user.name} size={36} />
- </td>
- <UserListItemIdentity identityProvider={identityProvider} user={user} />
- <td className="thin nowrap text-middle">
- <UserScmAccounts scmAccounts={user.scmAccounts || []} />
- </td>
- <td className="thin nowrap text-middle">
- <DateFromNow date={user.lastConnectionDate} hourPrecision={true} />
- </td>
- <td className="thin nowrap text-middle">
- <UserGroups groups={user.groups || []} onUpdateUsers={onUpdateUsers} user={user} />
- </td>
- <td className="thin nowrap text-middle">
- {user.tokensCount}
- <ButtonIcon
- className="js-user-tokens spacer-left button-small"
- onClick={this.handleOpenTokensForm}
- tooltip={translate('users.update_tokens')}
- >
- <BulletListIcon />
- </ButtonIcon>
- </td>
- <td className="thin nowrap text-right text-middle">
- <UserActions
- isCurrentUser={this.props.isCurrentUser}
- onUpdateUsers={onUpdateUsers}
- user={user}
- />
- </td>
- {this.state.openTokenForm && (
- <TokensFormModal
- onClose={this.handleCloseTokensForm}
- updateTokensCount={this.props.updateTokensCount}
- user={user}
- />
- )}
- </tr>
- );
- }
+ return (
+ <tr>
+ <td className="thin nowrap text-middle">
+ <Avatar hash={user.avatar} name={user.name} size={36} />
+ </td>
+ <UserListItemIdentity
+ identityProvider={identityProvider}
+ user={user}
+ manageProvider={manageProvider}
+ />
+ <td className="thin nowrap text-middle">
+ <UserScmAccounts scmAccounts={user.scmAccounts || []} />
+ </td>
+ <td className="thin nowrap text-middle">
+ <DateFromNow date={user.lastConnectionDate} hourPrecision={true} />
+ </td>
+ <td className="thin nowrap text-middle">
+ <UserGroups groups={user.groups || []} onUpdateUsers={onUpdateUsers} user={user} />
+ </td>
+ <td className="thin nowrap text-middle">
+ {user.tokensCount}
+ <ButtonIcon
+ className="js-user-tokens spacer-left button-small"
+ onClick={() => setOpenTokenForm(true)}
+ tooltip={translate('users.update_tokens')}
+ >
+ <BulletListIcon />
+ </ButtonIcon>
+ </td>
+ <td className="thin nowrap text-right text-middle">
+ <UserActions isCurrentUser={isCurrentUser} onUpdateUsers={onUpdateUsers} user={user} />
+ </td>
+ {openTokenForm && (
+ <TokensFormModal
+ onClose={() => setOpenTokenForm(false)}
+ updateTokensCount={updateTokensCount}
+ user={user}
+ />
+ )}
+ </tr>
+ );
}
import { getTextColor } from 'design-system';
import * as React from 'react';
import { colors } from '../../../app/theme';
+import { translate } from '../../../helpers/l10n';
import { getBaseUrl } from '../../../helpers/system';
import { IdentityProvider } from '../../../types/types';
import { User } from '../../../types/users';
export interface Props {
identityProvider?: IdentityProvider;
user: User;
+ manageProvider?: string;
}
-export default function UserListItemIdentity({ identityProvider, user }: Props) {
+export default function UserListItemIdentity({ identityProvider, user, manageProvider }: Props) {
return (
<td className="text-middle">
<div>
{!user.local && user.externalProvider !== 'sonarqube' && (
<ExternalProvider identityProvider={identityProvider} user={user} />
)}
+ {user.managed === false && manageProvider !== undefined && (
+ <span className="badge">{translate('users.local')}</span>
+ )}
</td>
);
}
-export function ExternalProvider({ identityProvider, user }: Props) {
+export function ExternalProvider({ identityProvider, user }: Omit<Props, 'manageProvider'>) {
if (!identityProvider) {
return (
<div className="js-user-identity-provider little-spacer-top">
active: true,
scmAccounts: [],
local: false,
+ managed: false,
};
it('should render correctly', () => {
active: true,
scmAccounts: [],
local: false,
+ managed: false,
};
const groups = ['foo', 'bar', 'baz', 'plop'];
import * as React from 'react';
import { click } from '../../../../helpers/testUtils';
import { User } from '../../../../types/users';
-import UserListItem from '../UserListItem';
+import UserListItem, { UserListItemProps } from '../UserListItem';
jest.mock('../../../../components/intl/DateFromNow');
jest.mock('../../../../components/intl/DateTimeFormatter');
login: 'obi',
name: 'One',
scmAccounts: [],
+ managed: false,
};
it('should render correctly', () => {
expect(wrapper.find('TokensFormModal').exists()).toBe(true);
});
-function shallowRender(props: Partial<UserListItem['props']> = {}) {
+function shallowRender(props: Partial<UserListItemProps> = {}) {
return shallow(
<UserListItem
isCurrentUser={false}
onUpdateUsers={jest.fn()}
updateTokensCount={jest.fn()}
user={user}
+ manageProvider={undefined}
{...props}
/>
);
login: 'obi',
name: 'One',
scmAccounts: [],
+ managed: false,
}}
{...props}
/>
login: 'obi',
name: 'One',
scmAccounts: [],
+ managed: false,
}}
{...props}
/>
"lastConnectionDate": "2019-01-18T15:06:33+0100",
"local": false,
"login": "obi",
+ "managed": false,
"name": "One",
"scmAccounts": [],
}
"lastConnectionDate": "2019-01-18T15:06:33+0100",
"local": false,
"login": "obi",
+ "managed": false,
"name": "One",
"scmAccounts": [],
}
"lastConnectionDate": "2019-01-18T15:06:33+0100",
"local": false,
"login": "obi",
+ "managed": false,
"name": "One",
"scmAccounts": [],
}
"lastConnectionDate": "2019-01-18T15:06:33+0100",
"local": false,
"login": "obi",
+ "managed": false,
"name": "One",
"scmAccounts": [],
}
"lastConnectionDate": "2019-01-18T15:06:33+0100",
"local": false,
"login": "obi",
+ "managed": false,
"name": "One",
"scmAccounts": [],
}
"lastConnectionDate": "2019-01-18T15:06:33+0100",
"local": false,
"login": "obi",
+ "managed": false,
"name": "One",
"scmAccounts": [],
}
"lastConnectionDate": "2019-01-18T15:06:33+0100",
"local": false,
"login": "obi",
+ "managed": false,
"name": "One",
"scmAccounts": [],
}
export interface Query {
search: string;
+ managed?: boolean;
}
export const parseQuery = memoize(
(urlQuery: RawQuery): Query => ({
search: parseAsString(urlQuery['search']),
+ managed: urlQuery['managed'] !== undefined ? urlQuery['managed'] === 'true' : undefined,
})
);
(query: Query): RawQuery =>
cleanQuery({
search: query.search ? serializeString(query.search) : undefined,
+ managed: query.managed,
})
);
local: true,
login: 'john.doe',
name: 'John Doe',
+ managed: false,
...overrides,
};
}
groups?: string[];
lastConnectionDate?: string;
local: boolean;
+ managed: boolean;
scmAccounts?: string[];
tokensCount?: number;
}
users.change_admin_password.form.cannot_use_default_password=You must choose a password that is different from the default password.
users.change_admin_password.form.success=The admin user's password was successfully changed.
users.change_admin_password.form.continue_to_app=Continue to SonarQube
+users.local=Local
+users.managed=Managed
#------------------------------------------------------------------------------
#