},
"devDependencies": {
"@types/classnames": "2.2.3",
+ "@types/clipboard": "1.5.35",
"@types/d3-array": "1.2.1",
"@types/d3-scale": "1.0.10",
"@types/enzyme": "3.1.1",
return getJSON(url, data);
}
-export interface Paging {
- pageIndex: number;
- pageSize: number;
- total: number;
-}
-
export interface Component {
organization: string;
id: string;
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { getJSON, postJSON, post, RequestData } from '../helpers/request';
+import { getJSON, postJSON, post } from '../helpers/request';
import throwGlobalError from '../app/utils/throwGlobalError';
+export interface UserToken {
+ name: string;
+ createdAt: string;
+}
+
/**
* List tokens for given user login
*/
-export function getTokens(login: string): Promise<any> {
- return getJSON('/api/user_tokens/search', { login }).then(r => r.userTokens);
+export function getTokens(login: string): Promise<UserToken[]> {
+ return getJSON('/api/user_tokens/search', { login }).then(r => r.userTokens, throwGlobalError);
}
/**
* Generate a user token
*/
-export function generateToken(
- tokenName: string,
- userLogin?: string
-): Promise<{ name: string; token: string }> {
- const data: RequestData = { name: tokenName };
- if (userLogin) {
- data.login = userLogin;
- }
+export function generateToken(data: {
+ name: string;
+ login?: string;
+}): Promise<{ login: string; name: string; token: string }> {
return postJSON('/api/user_tokens/generate', data).catch(throwGlobalError);
}
/**
* Revoke a user token
*/
-export function revokeToken(tokenName: string, userLogin?: string): Promise<void | Response> {
- const data: RequestData = { name: tokenName };
- if (userLogin) {
- data.login = userLogin;
- }
+export function revokeToken(data: { name: string; login?: string }): Promise<void | Response> {
return post('/api/user_tokens/revoke', data).catch(throwGlobalError);
}
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { getJSON, post, RequestData } from '../helpers/request';
+import { getJSON, post, postJSON, RequestData } from '../helpers/request';
import throwGlobalError from '../app/utils/throwGlobalError';
+import { Paging } from '../app/types';
export interface IdentityProvider {
backgroundColor: string;
name: string;
}
+export interface User {
+ login: string;
+ name: string;
+ active: boolean;
+ email?: string;
+ scmAccounts: string[];
+ groups?: string[];
+ tokensCount?: number;
+ local: boolean;
+ externalIdentity?: string;
+ externalProvider?: string;
+ avatar?: string;
+}
+
export function getCurrentUser(): Promise<any> {
return getJSON('/api/users/current');
}
-export function changePassword(
- login: string,
- password: string,
- previousPassword?: string
-): Promise<void> {
- const data: RequestData = { login, password };
- if (previousPassword != null) {
- data.previousPassword = previousPassword;
- }
+export function changePassword(data: {
+ login: string;
+ password: string;
+ previousPassword?: string;
+}): Promise<void> {
return post('/api/users/change_password', data);
}
}
export function getIdentityProviders(): Promise<{ identityProviders: IdentityProvider[] }> {
- return getJSON('/api/users/identity_providers');
+ return getJSON('/api/users/identity_providers').catch(throwGlobalError);
}
-export function searchUsers(query: string, pageSize?: number): Promise<any> {
- const data: RequestData = { q: query };
- if (pageSize != null) {
- data.ps = pageSize;
- }
- return getJSON('/api/users/search', data);
+export function searchUsers(data: {
+ p?: number;
+ ps?: number;
+ q?: string;
+}): Promise<{ paging: Paging; users: User[] }> {
+ data.q = data.q || undefined;
+ return getJSON('/api/users/search', data).catch(throwGlobalError);
+}
+
+export function createUser(data: {
+ email?: string;
+ local?: boolean;
+ login: string;
+ name: string;
+ password?: string;
+ scmAccount: string[];
+}): Promise<void | Response> {
+ return post('/api/users/create', data);
+}
+
+export function updateUser(data: {
+ email?: string;
+ login: string;
+ name?: string;
+ scmAccount: string[];
+}): Promise<User> {
+ return postJSON('/api/users/update', data);
+}
+
+export function deactivateUser(data: { login: string }): Promise<User> {
+ return postJSON('/api/users/deactivate', data).catch(throwGlobalError);
}
export function skipOnboarding(): Promise<void | Response> {
cursor: not-allowed;
}
+.no-select {
+ user-select: none;
+}
+
.no-outline,
.no-outline:focus {
outline: none;
errors: null
};
- handleSuccessfulChange() {
- this.refs.oldPassword.value = '';
- this.refs.password.value = '';
- this.refs.passwordConfirmation.value = '';
+ handleSuccessfulChange = () => {
+ this.oldPassword.value = '';
+ this.password.value = '';
+ this.passwordConfirmation.value = '';
this.setState({ success: true, errors: null });
- }
+ };
- handleFailedChange(e) {
+ handleFailedChange = e => {
e.response.json().then(r => {
- this.refs.oldPassword.focus();
+ this.oldPassword.focus();
this.setErrors(r.errors.map(e => e.msg));
});
- }
+ };
- setErrors(errors) {
+ setErrors = errors => {
this.setState({
success: false,
errors
});
- }
+ };
- handleChangePassword(e) {
+ handleChangePassword = e => {
e.preventDefault();
const { user } = this.props;
- const oldPassword = this.refs.oldPassword.value;
- const password = this.refs.password.value;
- const passwordConfirmation = this.refs.passwordConfirmation.value;
+ const previousPassword = this.oldPassword.value;
+ const password = this.password.value;
+ const passwordConfirmation = this.passwordConfirmation.value;
if (password !== passwordConfirmation) {
- this.refs.password.focus();
+ this.password.focus();
this.setErrors([translate('user.password_doesnt_match_confirmation')]);
} else {
- changePassword(user.login, password, oldPassword)
- .then(this.handleSuccessfulChange.bind(this))
- .catch(this.handleFailedChange.bind(this));
+ changePassword({ login: user.login, password, previousPassword })
+ .then(this.handleSuccessfulChange)
+ .catch(this.handleFailedChange);
}
- }
+ };
render() {
const { success, errors } = this.state;
<section>
<h2 className="spacer-bottom">{translate('my_profile.password.title')}</h2>
- <form onSubmit={this.handleChangePassword.bind(this)}>
+ <form onSubmit={this.handleChangePassword}>
{success && (
<div className="alert alert-success">{translate('my_profile.password.changed')}</div>
)}
<em className="mandatory">*</em>
</label>
<input
- ref="oldPassword"
+ ref={elem => (this.oldPassword = elem)}
autoComplete="off"
id="old_password"
name="old_password"
<em className="mandatory">*</em>
</label>
<input
- ref="password"
+ ref={elem => (this.password = elem)}
autoComplete="off"
id="password"
name="password"
<em className="mandatory">*</em>
</label>
<input
- ref="passwordConfirmation"
+ ref={elem => (this.passwordConfirmation = elem)}
autoComplete="off"
id="password_confirmation"
name="password_confirmation"
this.errors = [];
this.newToken = null;
const tokenName = this.$('.js-generate-token-form input').val();
- generateToken(tokenName, this.model.id).then(
+ generateToken({ name: tokenName, login: this.model.id }).then(
response => {
this.newToken = response;
this.requestTokens();
const token = this.tokens.find(token => token.name === `${tokenName}`);
if (token) {
if (token.deleting) {
- revokeToken(tokenName, this.model.id).then(this.requestTokens.bind(this), () => {});
+ revokeToken({ name: tokenName, login: this.model.id }).then(
+ () => this.requestTokens(),
+ () => {}
+ );
} else {
token.deleting = true;
this.render();
value: user.login
}))
)
- : searchUsers(query, 50).then(response =>
+ : searchUsers({ q: query }).then(response =>
response.users.map(user => ({
// TODO this WS returns no avatar
avatar: user.avatar,
const { tokenName } = this.state;
if (tokenName) {
this.setState({ loading: true });
- generateToken(tokenName).then(
+ generateToken({ name: tokenName }).then(
({ token }) => {
if (this.mounted) {
this.setState({ loading: false, token });
const { tokenName } = this.state;
if (tokenName) {
this.setState({ loading: true });
- revokeToken(tokenName).then(
+ revokeToken({ name: tokenName }).then(
() => {
if (this.mounted) {
this.setState({ loading: false, token: undefined, tokenName: undefined });
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 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 UserForm from './components/UserForm';
+import DeferredSpinner from '../../components/common/DeferredSpinner';
+import { translate } from '../../helpers/l10n';
+
+interface Props {
+ loading: boolean;
+ onUpdateUsers: () => void;
+}
+
+interface State {
+ openUserForm: boolean;
+}
+
+export default class Header extends React.PureComponent<Props, State> {
+ state: State = { openUserForm: false };
+
+ handleOpenUserForm = () => this.setState({ openUserForm: true });
+ handleCloseUserForm = () => this.setState({ openUserForm: false });
+
+ render() {
+ return (
+ <header id="users-header" className="page-header">
+ <h1 className="page-title">{translate('users.page')}</h1>
+ <DeferredSpinner loading={this.props.loading} />
+
+ <div className="page-actions">
+ <button id="users-create" onClick={this.handleOpenUserForm}>
+ {translate('users.create_user')}
+ </button>
+ </div>
+
+ <p className="page-description">{translate('users.page.description')}</p>
+ {this.state.openUserForm && (
+ <UserForm onClose={this.handleCloseUserForm} onUpdateUsers={this.props.onUpdateUsers} />
+ )}
+ </header>
+ );
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 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 { Query } from './utils';
+import SearchBox from '../../components/controls/SearchBox';
+import { translate } from '../../helpers/l10n';
+
+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 id="users-search" className="panel panel-vertical bordered-bottom spacer-bottom">
+ <SearchBox
+ minLength={2}
+ onChange={this.handleSearch}
+ placeholder={translate('search.search_by_login_or_name')}
+ value={query.search}
+ />
+ </div>
+ );
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 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 * as PropTypes from 'prop-types';
+import Helmet from 'react-helmet';
+import { Location } from 'history';
+import Header from './Header';
+import ListFooter from '../../components/controls/ListFooter';
+import Search from './Search';
+import UsersList from './UsersList';
+import { getIdentityProviders, IdentityProvider, searchUsers, User } from '../../api/users';
+import { Paging } from '../../app/types';
+import { translate } from '../../helpers/l10n';
+import { parseQuery, Query, serializeQuery } from './utils';
+
+interface Props {
+ currentUser: { isLoggedIn: boolean; login?: string };
+ location: Location;
+ organizationsEnabled: boolean;
+}
+
+interface State {
+ identityProviders: IdentityProvider[];
+ loading: boolean;
+ paging?: Paging;
+ users: User[];
+}
+
+export default class UsersApp extends React.PureComponent<Props, State> {
+ mounted: boolean;
+
+ static contextTypes = {
+ router: PropTypes.object.isRequired
+ };
+
+ state: State = { identityProviders: [], loading: true, users: [] };
+
+ componentDidMount() {
+ this.mounted = true;
+ this.fetchIdentityProviders();
+ this.fetchUsers();
+ }
+
+ componentWillReceiveProps(nextProps: Props) {
+ if (nextProps.location.query.search !== this.props.location.query.search) {
+ this.fetchUsers(nextProps);
+ }
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ finishLoading = () => {
+ if (this.mounted) {
+ this.setState({ loading: false });
+ }
+ };
+
+ fetchIdentityProviders = () =>
+ getIdentityProviders().then(
+ ({ identityProviders }) => {
+ if (this.mounted) {
+ this.setState({ identityProviders });
+ }
+ },
+ () => {}
+ );
+
+ fetchUsers = ({ location } = this.props) => {
+ this.setState({ loading: true });
+ searchUsers({ q: parseQuery(location.query).search }).then(({ paging, users }) => {
+ if (this.mounted) {
+ this.setState({ loading: false, paging, users });
+ }
+ }, this.finishLoading);
+ };
+
+ fetchMoreUsers = () => {
+ const { paging } = this.state;
+ if (paging) {
+ this.setState({ loading: true });
+ searchUsers({
+ p: paging.pageIndex + 1,
+ q: parseQuery(this.props.location.query).search
+ }).then(({ paging, users }) => {
+ if (this.mounted) {
+ this.setState(state => ({ loading: false, users: [...state.users, ...users], paging }));
+ }
+ }, this.finishLoading);
+ }
+ };
+
+ updateQuery = (newQuery: Partial<Query>) => {
+ const query = serializeQuery({ ...parseQuery(this.props.location.query), ...newQuery });
+ this.context.router.push({ ...this.props.location, query });
+ };
+
+ render() {
+ const query = parseQuery(this.props.location.query);
+ const { loading, paging, users } = this.state;
+ return (
+ <div id="users-page" className="page page-limited">
+ <Helmet title={translate('users.page')} />
+ <Header loading={loading} onUpdateUsers={this.fetchUsers} />
+ <Search query={query} updateQuery={this.updateQuery} />
+ <UsersList
+ currentUser={this.props.currentUser}
+ identityProviders={this.state.identityProviders}
+ onUpdateUsers={this.fetchUsers}
+ organizationsEnabled={this.props.organizationsEnabled}
+ users={users}
+ />
+ {paging !== undefined && (
+ <ListFooter
+ count={users.length}
+ total={paging.total}
+ ready={!loading}
+ loadMore={this.fetchMoreUsers}
+ />
+ )}
+ </div>
+ );
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 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 { connect } from 'react-redux';
+import { Location } from 'history';
+import UsersApp from './UsersApp';
+import { areThereCustomOrganizations, getCurrentUser } from '../../store/rootReducer';
+
+interface OwnProps {
+ location: Location;
+}
+
+interface StateToProps {
+ currentUser: { isLoggedIn: boolean; login?: string };
+ organizationsEnabled: boolean;
+}
+
+const mapStateToProps = (state: any) => ({
+ currentUser: getCurrentUser(state),
+ organizationsEnabled: areThereCustomOrganizations(state)
+});
+
+export default connect<StateToProps, {}, OwnProps>(mapStateToProps)(UsersApp);
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 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 UserListItem from './components/UserListItem';
+import { IdentityProvider, User } from '../../api/users';
+import { translate } from '../../helpers/l10n';
+
+interface Props {
+ currentUser: { isLoggedIn: boolean; login?: string };
+ identityProviders: IdentityProvider[];
+ onUpdateUsers: () => void;
+ organizationsEnabled: boolean;
+ users: User[];
+}
+
+export default function UsersList({
+ currentUser,
+ identityProviders,
+ onUpdateUsers,
+ organizationsEnabled,
+ users
+}: Props) {
+ return (
+ <table id="users-list" className="data zebra">
+ <thead>
+ <tr>
+ <th />
+ <th className="nowrap" />
+ <th className="nowrap">{translate('my_profile.scm_accounts')}</th>
+ {!organizationsEnabled && <th className="nowrap">{translate('my_profile.groups')}</th>}
+ <th className="nowrap">{translate('users.tokens')}</th>
+ <th className="nowrap"> </th>
+ </tr>
+ </thead>
+ <tbody>
+ {users.map(user => (
+ <UserListItem
+ identityProvider={identityProviders.find(
+ provider => user.externalProvider === provider.key
+ )}
+ isCurrentUser={currentUser.isLoggedIn && currentUser.login === user.login}
+ key={user.login}
+ onUpdateUsers={onUpdateUsers}
+ organizationsEnabled={organizationsEnabled}
+ user={user}
+ />
+ ))}
+ </tbody>
+ </table>
+ );
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 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 Modal from '../../../components/controls/Modal';
+import { deactivateUser, User } from '../../../api/users';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
+
+export interface Props {
+ onClose: () => void;
+ onUpdateUsers: () => void;
+ user: User;
+}
+
+interface State {
+ submitting: boolean;
+}
+
+export default class DeactivateForm extends React.PureComponent<Props, State> {
+ mounted: boolean;
+ state: State = { submitting: false };
+
+ componentDidMount() {
+ this.mounted = true;
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ handleCancelClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
+ event.preventDefault();
+ this.props.onClose();
+ };
+
+ handleDeactivate = (event: React.SyntheticEvent<HTMLFormElement>) => {
+ event.preventDefault();
+ this.setState({ submitting: true });
+ deactivateUser({ login: this.props.user.login }).then(
+ () => {
+ this.props.onUpdateUsers();
+ this.props.onClose();
+ },
+ () => {
+ if (this.mounted) {
+ this.setState({ submitting: false });
+ }
+ }
+ );
+ };
+
+ render() {
+ const { user } = this.props;
+ const { submitting } = this.state;
+
+ const header = translate('users.deactivate_user');
+ return (
+ <Modal contentLabel={header} onRequestClose={this.props.onClose}>
+ <form id="deactivate-user-form" onSubmit={this.handleDeactivate} autoComplete="off">
+ <header className="modal-head">
+ <h2>{header}</h2>
+ </header>
+ <div className="modal-body">
+ {translateWithParameters('users.deactivate_user.confirmation', user.name, user.login)}
+ </div>
+ <footer className="modal-foot">
+ {submitting && <i className="spinner spacer-right" />}
+ <button className="js-confirm button-red" disabled={submitting} type="submit">
+ {translate('users.deactivate')}
+ </button>
+ <a className="js-modal-close" href="#" onClick={this.handleCancelClick}>
+ {translate('cancel')}
+ </a>
+ </footer>
+ </form>
+ </Modal>
+ );
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 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 * as escapeHtml from 'escape-html';
+import Modal from '../../../components/controls/Modal';
+import SelectList from '../../../components/SelectList';
+import { User } from '../../../api/users';
+import { translate } from '../../../helpers/l10n';
+import { getBaseUrl } from '../../../helpers/urls';
+
+interface Props {
+ onClose: () => void;
+ onUpdateUsers: () => void;
+ user: User;
+}
+
+export default class GroupsForm extends React.PureComponent<Props> {
+ container: HTMLDivElement | null;
+
+ handleCloseClick = (event: React.SyntheticEvent<HTMLElement>) => {
+ event.preventDefault();
+ this.handleClose();
+ };
+
+ handleClose = () => {
+ this.props.onUpdateUsers();
+ this.props.onClose();
+ };
+
+ renderSelectList = () => {
+ const searchUrl = `${getBaseUrl()}/api/users/groups?ps=100&login=${encodeURIComponent(
+ this.props.user.login
+ )}`;
+
+ new (SelectList as any)({
+ el: this.container,
+ width: '100%',
+ readOnly: false,
+ focusSearch: false,
+ dangerouslyUnescapedHtmlFormat: (item: { name: string; description: string }) =>
+ `${escapeHtml(item.name)}<br><span class="note">${escapeHtml(item.description)}</span>`,
+ queryParam: 'q',
+ searchUrl,
+ selectUrl: getBaseUrl() + '/api/user_groups/add_user',
+ deselectUrl: getBaseUrl() + '/api/user_groups/remove_user',
+ extra: { login: this.props.user.login },
+ selectParameter: 'id',
+ selectParameterValue: 'id',
+ parse(r: any) {
+ this.more = false;
+ return r.groups;
+ }
+ });
+ };
+
+ render() {
+ const header = translate('users.update_groups');
+
+ return (
+ <Modal
+ contentLabel={header}
+ onAfterOpen={this.renderSelectList}
+ onRequestClose={this.handleClose}>
+ <div className="modal-head">
+ <h2>{header}</h2>
+ </div>
+
+ <div className="modal-body">
+ <div id="user-groups" ref={node => (this.container = node)} />
+ </div>
+
+ <footer className="modal-foot">
+ <a className="js-modal-close" href="#" onClick={this.handleCloseClick}>
+ {translate('Done')}
+ </a>
+ </footer>
+ </Modal>
+ );
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 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 Modal from '../../../components/controls/Modal';
+import addGlobalSuccessMessage from '../../../app/utils/addGlobalSuccessMessage';
+import throwGlobalError from '../../../app/utils/throwGlobalError';
+import { parseError } from '../../../helpers/request';
+import { changePassword, User } from '../../../api/users';
+import { translate } from '../../../helpers/l10n';
+
+interface Props {
+ isCurrentUser: boolean;
+ user: User;
+ onClose: () => void;
+}
+
+interface State {
+ confirmPassword: string;
+ error?: string;
+ newPassword: string;
+ oldPassword: string;
+ submitting: boolean;
+}
+
+export default class PasswordForm extends React.PureComponent<Props, State> {
+ mounted: boolean;
+ state: State = {
+ confirmPassword: '',
+ newPassword: '',
+ oldPassword: '',
+ submitting: false
+ };
+
+ componentDidMount() {
+ this.mounted = true;
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ handleError = (error: { response: Response }) => {
+ if (!this.mounted || error.response.status !== 400) {
+ return throwGlobalError(error);
+ } else {
+ return parseError(error).then(
+ errorMsg => this.setState({ error: errorMsg, submitting: false }),
+ throwGlobalError
+ );
+ }
+ };
+
+ handleConfirmPasswordChange = (event: React.SyntheticEvent<HTMLInputElement>) =>
+ this.setState({ confirmPassword: event.currentTarget.value });
+ handleNewPasswordChange = (event: React.SyntheticEvent<HTMLInputElement>) =>
+ this.setState({ newPassword: event.currentTarget.value });
+ handleOldPasswordChange = (event: React.SyntheticEvent<HTMLInputElement>) =>
+ this.setState({ oldPassword: event.currentTarget.value });
+
+ handleCancelClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
+ event.preventDefault();
+ this.props.onClose();
+ };
+
+ handleChangePassword = (event: React.SyntheticEvent<HTMLFormElement>) => {
+ event.preventDefault();
+ if (
+ this.state.newPassword.length > 0 &&
+ this.state.newPassword === this.state.confirmPassword
+ ) {
+ this.setState({ submitting: true });
+ changePassword({
+ login: this.props.user.login,
+ password: this.state.newPassword,
+ previousPassword: this.state.oldPassword
+ }).then(() => {
+ addGlobalSuccessMessage(translate('my_profile.password.changed'));
+ this.props.onClose();
+ }, this.handleError);
+ }
+ };
+
+ render() {
+ const { error, submitting, newPassword, confirmPassword } = this.state;
+
+ const header = translate('my_profile.password.title');
+ return (
+ <Modal contentLabel={header} onRequestClose={this.props.onClose}>
+ <form id="user-password-form" onSubmit={this.handleChangePassword} autoComplete="off">
+ <header className="modal-head">
+ <h2>{header}</h2>
+ </header>
+ <div className="modal-body">
+ {error && <p className="alert alert-danger">{error}</p>}
+ {this.props.isCurrentUser && (
+ <div className="modal-field">
+ <label htmlFor="old-user-password">
+ {translate('my_profile.password.old')}
+ <em className="mandatory">*</em>
+ </label>
+ {/* keep this fake field to hack browser autofill */}
+ <input name="old-password-fake" type="password" className="hidden" />
+ <input
+ id="old-user-password"
+ name="old-password"
+ type="password"
+ maxLength={50}
+ onChange={this.handleOldPasswordChange}
+ required={true}
+ value={this.state.oldPassword}
+ />
+ </div>
+ )}
+ <div className="modal-field">
+ <label htmlFor="user-password">
+ {translate('my_profile.password.new')}
+ <em className="mandatory">*</em>
+ </label>
+ {/* keep this fake field to hack browser autofill */}
+ <input name="password-fake" type="password" className="hidden" />
+ <input
+ id="user-password"
+ name="password"
+ type="password"
+ maxLength={50}
+ onChange={this.handleNewPasswordChange}
+ required={true}
+ value={this.state.newPassword}
+ />
+ </div>
+ <div className="modal-field">
+ <label htmlFor="confirm-user-password">
+ {translate('my_profile.password.confirm')}
+ <em className="mandatory">*</em>
+ </label>
+ {/* keep this fake field to hack browser autofill */}
+ <input name="confirm-password-fake" type="password" className="hidden" />
+ <input
+ id="confirm-user-password"
+ name="confirm-password"
+ type="password"
+ maxLength={50}
+ onChange={this.handleConfirmPasswordChange}
+ required={true}
+ value={this.state.confirmPassword}
+ />
+ </div>
+ </div>
+ <footer className="modal-foot">
+ {submitting && <i className="spinner spacer-right" />}
+ <button
+ className="js-confirm"
+ disabled={submitting || !newPassword || newPassword !== confirmPassword}
+ type="submit">
+ {translate('change_verb')}
+ </button>
+ <a className="js-modal-close" href="#" onClick={this.handleCancelClick}>
+ {translate('cancel')}
+ </a>
+ </footer>
+ </form>
+ </Modal>
+ );
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 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 Modal from '../../../components/controls/Modal';
+import TokensFormItem from './TokensFormItem';
+import TokensFormNewToken from './TokensFormNewToken';
+import DeferredSpinner from '../../../components/common/DeferredSpinner';
+import { User } from '../../../api/users';
+import { getTokens, generateToken, UserToken } from '../../../api/user-tokens';
+import { translate } from '../../../helpers/l10n';
+
+interface Props {
+ user: User;
+ onClose: () => void;
+ onUpdateUsers: () => void;
+}
+
+interface State {
+ generating: boolean;
+ hasChanged: boolean;
+ loading: boolean;
+ newToken?: { name: string; token: string };
+ newTokenName: string;
+ tokens: UserToken[];
+}
+
+export default class TokensForm extends React.PureComponent<Props, State> {
+ mounted: boolean;
+ state: State = {
+ generating: false,
+ hasChanged: false,
+ loading: true,
+ newTokenName: '',
+ tokens: []
+ };
+
+ componentDidMount() {
+ this.mounted = true;
+ this.fetchTokens();
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ fetchTokens = ({ user } = this.props) => {
+ this.setState({ loading: true });
+ getTokens(user.login).then(
+ tokens => {
+ if (this.mounted) {
+ this.setState({ loading: false, tokens });
+ }
+ },
+ () => {
+ if (this.mounted) {
+ this.setState({ loading: false });
+ }
+ }
+ );
+ };
+
+ handleCloseClick = (evt: React.SyntheticEvent<HTMLAnchorElement>) => {
+ evt.preventDefault();
+ this.handleClose();
+ };
+
+ handleClose = () => {
+ if (this.state.hasChanged) {
+ this.props.onUpdateUsers();
+ }
+ this.props.onClose();
+ };
+
+ handleGenerateToken = (evt: React.SyntheticEvent<HTMLFormElement>) => {
+ evt.preventDefault();
+ if (this.state.newTokenName.length > 0) {
+ this.setState({ generating: true });
+ generateToken({ name: this.state.newTokenName, login: this.props.user.login }).then(
+ newToken => {
+ if (this.mounted) {
+ this.fetchTokens();
+ this.setState({ generating: false, hasChanged: true, newToken, newTokenName: '' });
+ }
+ },
+ () => {
+ if (this.mounted) {
+ this.setState({ generating: false });
+ }
+ }
+ );
+ }
+ };
+
+ handleRevokeToken = () => {
+ this.setState({ hasChanged: true });
+ this.fetchTokens();
+ };
+
+ handleNewTokenChange = (evt: React.SyntheticEvent<HTMLInputElement>) =>
+ this.setState({ newTokenName: evt.currentTarget.value });
+
+ renderItems() {
+ const { tokens } = this.state;
+ if (tokens.length <= 0) {
+ return (
+ <tr>
+ <td colSpan={3} className="note">
+ {translate('users.no_tokens')}
+ </td>
+ </tr>
+ );
+ }
+ return tokens.map(token => (
+ <TokensFormItem
+ key={token.name}
+ token={token}
+ onRevokeToken={this.handleRevokeToken}
+ user={this.props.user}
+ />
+ ));
+ }
+
+ render() {
+ const { generating, loading, newToken, newTokenName, tokens } = this.state;
+ const header = translate('users.tokens');
+ const customSpinner = (
+ <tr>
+ <td>
+ <i className="spinner" />
+ </td>
+ </tr>
+ );
+ return (
+ <Modal contentLabel={header} onRequestClose={this.handleClose}>
+ <header className="modal-head">
+ <h2>{header}</h2>
+ </header>
+ <div className="modal-body modal-container">
+ <h3 className="spacer-bottom">{translate('users.generate_tokens')}</h3>
+ <form id="generate-token-form" onSubmit={this.handleGenerateToken} autoComplete="off">
+ <input
+ className="spacer-right"
+ type="text"
+ maxLength={100}
+ onChange={this.handleNewTokenChange}
+ placeholder={translate('users.enter_token_name')}
+ required={true}
+ value={newTokenName}
+ />
+ <button
+ className="js-generate-token"
+ disabled={generating || newTokenName.length <= 0}
+ type="submit">
+ {translate('users.generate')}
+ </button>
+ </form>
+
+ {newToken && <TokensFormNewToken token={newToken} />}
+
+ <table className="data zebra big-spacer-top ">
+ <thead>
+ <tr>
+ <th>{translate('name')}</th>
+ <th className="text-right">{translate('created')}</th>
+ <th />
+ </tr>
+ </thead>
+ <tbody>
+ <DeferredSpinner
+ customSpinner={customSpinner}
+ loading={loading && tokens.length <= 0}>
+ {this.renderItems()}
+ </DeferredSpinner>
+ </tbody>
+ </table>
+ </div>
+ <footer className="modal-foot">
+ <a className="js-modal-close" href="#" onClick={this.handleCloseClick}>
+ {translate('Done')}
+ </a>
+ </footer>
+ </Modal>
+ );
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 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 Tooltip from '../../../components/controls/Tooltip';
+import DateFormatter from '../../../components/intl/DateFormatter';
+import DeferredSpinner from '../../../components/common/DeferredSpinner';
+import { User } from '../../../api/users';
+import { revokeToken, UserToken } from '../../../api/user-tokens';
+import { limitComponentName } from '../../../helpers/path';
+import { translate } from '../../../helpers/l10n';
+
+interface Props {
+ token: UserToken;
+ user: User;
+ onRevokeToken: () => void;
+}
+
+interface State {
+ deleting: boolean;
+ loading: boolean;
+}
+
+export default class TokensFormItem extends React.PureComponent<Props, State> {
+ mounted: boolean;
+ state: State = { deleting: false, loading: false };
+
+ componentDidMount() {
+ this.mounted = true;
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ handleRevoke = () => {
+ if (this.state.deleting) {
+ this.setState({ loading: true });
+ revokeToken({ login: this.props.user.login, name: this.props.token.name }).then(
+ this.props.onRevokeToken,
+ () => {
+ if (this.mounted) {
+ this.setState({ loading: false, deleting: false });
+ }
+ }
+ );
+ } else {
+ this.setState({ deleting: true });
+ }
+ };
+
+ render() {
+ const { token } = this.props;
+ const { loading } = this.state;
+ return (
+ <tr>
+ <td>
+ <Tooltip overlay={token.name}>
+ <span>{limitComponentName(token.name)}</span>
+ </Tooltip>
+ </td>
+ <td className="thin nowrap text-right">
+ <DateFormatter date={token.createdAt} long={true} />
+ </td>
+ <td className="thin nowrap text-right">
+ <DeferredSpinner loading={loading}>
+ <i className="spinner-placeholder " />
+ </DeferredSpinner>
+ <button
+ className="button-red input-small spacer-left"
+ onClick={this.handleRevoke}
+ disabled={loading}>
+ {this.state.deleting
+ ? translate('users.tokens.sure')
+ : translate('users.tokens.revoke')}
+ </button>
+ </td>
+ </tr>
+ );
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 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 * as Clipboard from 'clipboard';
+import Tooltip from '../../../components/controls/Tooltip';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
+
+interface Props {
+ token: { name: string; token: string };
+}
+
+interface State {
+ tooltipShown: boolean;
+}
+
+export default class TokensFormNewToken extends React.PureComponent<Props, State> {
+ clipboard: Clipboard;
+ copyButton: HTMLButtonElement | null;
+ mounted: boolean;
+ state: State = { tooltipShown: false };
+
+ componentDidMount() {
+ this.mounted = true;
+ if (this.copyButton) {
+ this.clipboard = new Clipboard(this.copyButton);
+ this.clipboard.on('success', this.showTooltip);
+ }
+ }
+
+ componentDidUpdate() {
+ this.clipboard.destroy();
+ if (this.copyButton) {
+ this.clipboard = new Clipboard(this.copyButton);
+ this.clipboard.on('success', this.showTooltip);
+ }
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ this.clipboard.destroy();
+ }
+
+ showTooltip = () => {
+ if (this.mounted) {
+ this.setState({ tooltipShown: true });
+ setTimeout(() => {
+ if (this.mounted) {
+ this.setState({ tooltipShown: false });
+ }
+ }, 1000);
+ }
+ };
+
+ render() {
+ const { name, token } = this.props.token;
+ const button = (
+ <button
+ className="js-copy-to-clipboard no-select"
+ data-clipboard-text={token}
+ ref={node => (this.copyButton = node)}>
+ {translate('copy')}
+ </button>
+ );
+ return (
+ <div className="panel panel-white big-spacer-top">
+ <p className="alert alert-warning">
+ {translateWithParameters('users.tokens.new_token_created', name)}
+ </p>
+ {this.state.tooltipShown ? (
+ <Tooltip
+ defaultVisible={true}
+ placement="bottom"
+ overlay={translate('users.tokens.copied')}
+ trigger="manual">
+ {button}
+ </Tooltip>
+ ) : (
+ button
+ )}
+ <code className="big-spacer-left text-success">{token}</code>
+ </div>
+ );
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 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 ActionsDropdown, {
+ ActionsDropdownItem,
+ ActionsDropdownDivider
+} from '../../../components/controls/ActionsDropdown';
+import DeactivateForm from './DeactivateForm';
+import PasswordForm from './PasswordForm';
+import UserForm from './UserForm';
+import { User } from '../../../api/users';
+import { translate } from '../../../helpers/l10n';
+
+interface Props {
+ isCurrentUser: boolean;
+ onUpdateUsers: () => void;
+ user: User;
+}
+
+interface State {
+ openForm?: string;
+}
+
+export default class UserActions extends React.PureComponent<Props, State> {
+ state: State = {};
+
+ handleOpenDeactivateForm = () => this.setState({ openForm: 'deactivate' });
+ handleOpenPasswordForm = () => this.setState({ openForm: 'password' });
+ handleOpenUpdateForm = () => this.setState({ openForm: 'update' });
+ handleCloseForm = () => this.setState({ openForm: undefined });
+
+ renderActions = () => {
+ const { user } = this.props;
+ return (
+ <ActionsDropdown key="actions" menuClassName="dropdown-menu-right">
+ <ActionsDropdownItem className="js-user-update" onClick={this.handleOpenUpdateForm}>
+ {translate('update_details')}
+ </ActionsDropdownItem>
+ {user.local && (
+ <ActionsDropdownItem
+ className="js-user-change-password"
+ onClick={this.handleOpenPasswordForm}>
+ {translate('my_profile.password.title')}
+ </ActionsDropdownItem>
+ )}
+ <ActionsDropdownDivider />
+ <ActionsDropdownItem
+ className="js-user-deactivate"
+ destructive={true}
+ onClick={this.handleOpenDeactivateForm}>
+ {translate('users.deactivate')}
+ </ActionsDropdownItem>
+ </ActionsDropdown>
+ );
+ };
+
+ render() {
+ const { openForm } = this.state;
+ const { isCurrentUser, onUpdateUsers, user } = this.props;
+
+ if (openForm === 'deactivate') {
+ return [
+ this.renderActions(),
+ <DeactivateForm
+ key="form"
+ onClose={this.handleCloseForm}
+ onUpdateUsers={onUpdateUsers}
+ user={user}
+ />
+ ];
+ }
+ if (openForm === 'password') {
+ return [
+ this.renderActions(),
+ <PasswordForm
+ isCurrentUser={isCurrentUser}
+ key="form"
+ onClose={this.handleCloseForm}
+ user={user}
+ />
+ ];
+ }
+ if (openForm === 'update') {
+ return [
+ this.renderActions(),
+ <UserForm
+ key="form"
+ onClose={this.handleCloseForm}
+ onUpdateUsers={onUpdateUsers}
+ user={user}
+ />
+ ];
+ }
+ return this.renderActions();
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 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 { uniq } from 'lodash';
+import Modal from '../../../components/controls/Modal';
+import UserScmAccountInput from './UserScmAccountInput';
+import throwGlobalError from '../../../app/utils/throwGlobalError';
+import { parseError } from '../../../helpers/request';
+import { createUser, updateUser, User } from '../../../api/users';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
+
+export interface Props {
+ user?: User;
+ onClose: () => void;
+ onUpdateUsers: () => void;
+}
+
+interface State {
+ email: string;
+ error?: string;
+ login: string;
+ name: string;
+ password: string;
+ scmAccounts: string[];
+ submitting: boolean;
+}
+
+export default class UserForm extends React.PureComponent<Props, State> {
+ mounted: boolean;
+
+ constructor(props: Props) {
+ super(props);
+ const { user } = props;
+ if (user) {
+ this.state = {
+ email: user.email || '',
+ login: user.login,
+ name: user.name,
+ password: '',
+ scmAccounts: user.scmAccounts,
+ submitting: false
+ };
+ } else {
+ this.state = {
+ email: '',
+ login: '',
+ name: '',
+ password: '',
+ scmAccounts: [],
+ submitting: false
+ };
+ }
+ }
+
+ componentDidMount() {
+ this.mounted = true;
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ handleError = (error: { response: Response }) => {
+ if (!this.mounted || ![400, 500].includes(error.response.status)) {
+ return throwGlobalError(error);
+ } else {
+ return parseError(error).then(
+ errorMsg => this.setState({ error: errorMsg, submitting: false }),
+ throwGlobalError
+ );
+ }
+ };
+
+ handleEmailChange = (event: React.SyntheticEvent<HTMLInputElement>) =>
+ this.setState({ email: event.currentTarget.value });
+ handleLoginChange = (event: React.SyntheticEvent<HTMLInputElement>) =>
+ this.setState({ login: event.currentTarget.value });
+ handleNameChange = (event: React.SyntheticEvent<HTMLInputElement>) =>
+ this.setState({ name: event.currentTarget.value });
+ handlePasswordChange = (event: React.SyntheticEvent<HTMLInputElement>) =>
+ this.setState({ password: event.currentTarget.value });
+
+ handleCancelClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
+ event.preventDefault();
+ this.props.onClose();
+ };
+
+ handleCreateUser = (event: React.SyntheticEvent<HTMLFormElement>) => {
+ event.preventDefault();
+ this.setState({ submitting: true });
+ createUser({
+ email: this.state.email || undefined,
+ login: this.state.login,
+ name: this.state.name,
+ password: this.state.password,
+ scmAccount: uniq(this.state.scmAccounts)
+ }).then(() => {
+ this.props.onUpdateUsers();
+ this.props.onClose();
+ }, this.handleError);
+ };
+
+ handleUpdateUser = (event: React.SyntheticEvent<HTMLFormElement>) => {
+ event.preventDefault();
+ this.setState({ submitting: true });
+ updateUser({
+ email: this.state.email || undefined,
+ login: this.state.login,
+ name: this.state.name,
+ scmAccount: uniq(this.state.scmAccounts)
+ }).then(() => {
+ this.props.onUpdateUsers();
+ this.props.onClose();
+ }, this.handleError);
+ };
+
+ handleAddScmAccount = (evt: React.SyntheticEvent<HTMLButtonElement>) => {
+ evt.preventDefault();
+ this.setState(({ scmAccounts }) => ({ scmAccounts: scmAccounts.concat('') }));
+ };
+
+ handleUpdateScmAccount = (idx: number, scmAccount: string) =>
+ this.setState(({ scmAccounts: oldScmAccounts }) => {
+ const scmAccounts = oldScmAccounts.slice();
+ scmAccounts[idx] = scmAccount;
+ return { scmAccounts };
+ });
+
+ handleRemoveScmAccount = (idx: number) =>
+ this.setState(({ scmAccounts }) => ({
+ scmAccounts: scmAccounts.slice(0, idx).concat(scmAccounts.slice(idx + 1))
+ }));
+
+ render() {
+ const { user } = this.props;
+ const { error, submitting } = this.state;
+
+ const header = user ? translate('users.update_user') : translate('users.create_user');
+ return (
+ <Modal contentLabel={header} onRequestClose={this.props.onClose}>
+ <form
+ id="user-form"
+ onSubmit={this.props.user ? this.handleUpdateUser : this.handleCreateUser}
+ autoComplete="off">
+ <header className="modal-head">
+ <h2>{header}</h2>
+ </header>
+
+ <div className="modal-body">
+ {error && <p className="alert alert-danger">{error}</p>}
+
+ {!user && (
+ <div className="modal-field">
+ <label htmlFor="create-user-login">
+ {translate('login')}
+ <em className="mandatory">*</em>
+ </label>
+ {/* keep this fake field to hack browser autofill */}
+ <input name="login-fake" type="text" className="hidden" />
+ <input
+ id="create-user-login"
+ name="login"
+ type="text"
+ minLength={3}
+ maxLength={255}
+ onChange={this.handleLoginChange}
+ required={true}
+ value={this.state.login}
+ />
+ <p className="note">{translateWithParameters('users.minimum_x_characters', 3)}</p>
+ </div>
+ )}
+ <div className="modal-field">
+ <label htmlFor="create-user-name">
+ {translate('name')}
+ <em className="mandatory">*</em>
+ </label>
+ {/* keep this fake field to hack browser autofill */}
+ <input name="name-fake" type="text" className="hidden" />
+ <input
+ id="create-user-name"
+ name="name"
+ type="text"
+ maxLength={200}
+ onChange={this.handleNameChange}
+ required={true}
+ value={this.state.name}
+ />
+ </div>
+ <div className="modal-field">
+ <label htmlFor="create-user-email">{translate('users.email')}</label>
+ {/* keep this fake field to hack browser autofill */}
+ <input name="email-fake" type="email" className="hidden" />
+ <input
+ id="create-user-email"
+ name="email"
+ type="email"
+ maxLength={100}
+ onChange={this.handleEmailChange}
+ value={this.state.email}
+ />
+ </div>
+ {!user && (
+ <div className="modal-field">
+ <label htmlFor="create-user-password">
+ {translate('password')}
+ <em className="mandatory">*</em>
+ </label>
+ {/* keep this fake field to hack browser autofill */}
+ <input name="password-fake" type="password" className="hidden" />
+ <input
+ id="create-user-password"
+ name="password"
+ type="password"
+ maxLength={50}
+ onChange={this.handlePasswordChange}
+ required={true}
+ value={this.state.password}
+ />
+ </div>
+ )}
+ <div className="modal-field">
+ <label>{translate('my_profile.scm_accounts')}</label>
+ {this.state.scmAccounts.map((scm, idx) => (
+ <UserScmAccountInput
+ idx={idx}
+ key={idx}
+ onChange={this.handleUpdateScmAccount}
+ onRemove={this.handleRemoveScmAccount}
+ scmAccount={scm}
+ />
+ ))}
+ <div className="spacer-bottom">
+ <button onClick={this.handleAddScmAccount}>{translate('add_verb')}</button>
+ </div>
+ <p className="note">{translate('user.login_or_email_used_as_scm_account')}</p>
+ </div>
+ </div>
+
+ <footer className="modal-foot">
+ {submitting && <i className="spinner spacer-right" />}
+ <button className="js-confirm" disabled={submitting} type="submit">
+ {user ? translate('update_verb') : translate('create')}
+ </button>
+ <a className="js-modal-close" href="#" onClick={this.handleCancelClick}>
+ {translate('cancel')}
+ </a>
+ </footer>
+ </form>
+ </Modal>
+ );
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 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 BulletListIcon from '../../../components/icons-components/BulletListIcon';
+import GroupsForm from './GroupsForm';
+import { User } from '../../../api/users';
+import { ButtonIcon } from '../../../components/ui/buttons';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
+
+interface Props {
+ groups: string[];
+ onUpdateUsers: () => void;
+ user: User;
+}
+
+interface State {
+ openForm: boolean;
+ showMore: boolean;
+}
+
+const GROUPS_LIMIT = 3;
+
+export default class UserGroups extends React.PureComponent<Props, State> {
+ state: State = { openForm: false, showMore: false };
+
+ handleOpenForm = () => this.setState({ openForm: true });
+ handleCloseForm = () => this.setState({ openForm: false });
+
+ toggleShowMore = (evt: React.SyntheticEvent<HTMLAnchorElement>) => {
+ evt.preventDefault();
+ this.setState(state => ({ showMore: !state.showMore }));
+ };
+
+ render() {
+ const { groups } = this.props;
+ const limit = groups.length > GROUPS_LIMIT ? GROUPS_LIMIT - 1 : GROUPS_LIMIT;
+ return (
+ <ul>
+ {groups.slice(0, limit).map(group => (
+ <li key={group} className="little-spacer-bottom">
+ {group}
+ </li>
+ ))}
+ {groups.length > GROUPS_LIMIT &&
+ this.state.showMore &&
+ groups.slice(limit).map(group => (
+ <li key={group} className="little-spacer-bottom">
+ {group}
+ </li>
+ ))}
+ <li className="little-spacer-bottom">
+ {groups.length > GROUPS_LIMIT &&
+ !this.state.showMore && (
+ <a
+ className="js-user-more-groups spacer-right"
+ href="#"
+ onClick={this.toggleShowMore}>
+ {translateWithParameters('more_x', groups.length - limit)}
+ </a>
+ )}
+ <ButtonIcon
+ className="js-user-groups button-small"
+ onClick={this.handleOpenForm}
+ tooltip={translate('users.update_groups')}>
+ <BulletListIcon />
+ </ButtonIcon>
+ </li>
+ {this.state.openForm && (
+ <GroupsForm
+ onClose={this.handleCloseForm}
+ onUpdateUsers={this.props.onUpdateUsers}
+ user={this.props.user}
+ />
+ )}
+ </ul>
+ );
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 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 Avatar from '../../../components/ui/Avatar';
+import BulletListIcon from '../../../components/icons-components/BulletListIcon';
+import { ButtonIcon } from '../../../components/ui/buttons';
+import TokensForm from './TokensForm';
+import UserActions from './UserActions';
+import UserGroups from './UserGroups';
+import UserListItemIdentity from './UserListItemIdentity';
+import UserScmAccounts from './UserScmAccounts';
+import { IdentityProvider, User } from '../../../api/users';
+import { translate } from '../../../helpers/l10n';
+
+interface Props {
+ identityProvider?: IdentityProvider;
+ isCurrentUser: boolean;
+ onUpdateUsers: () => void;
+ organizationsEnabled: boolean;
+ user: User;
+}
+
+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 });
+
+ render() {
+ const { identityProvider, onUpdateUsers, organizationsEnabled, user } = this.props;
+
+ return (
+ <tr>
+ <td className="thin nowrap">
+ <Avatar hash={user.avatar} name={user.name} size={36} />
+ </td>
+ <UserListItemIdentity identityProvider={identityProvider} user={user} />
+ <td>
+ <UserScmAccounts scmAccounts={user.scmAccounts || []} />
+ </td>
+ {!organizationsEnabled && (
+ <td>
+ <UserGroups groups={user.groups || []} user={user} onUpdateUsers={onUpdateUsers} />
+ </td>
+ )}
+ <td>
+ {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">
+ <UserActions
+ isCurrentUser={this.props.isCurrentUser}
+ onUpdateUsers={onUpdateUsers}
+ user={user}
+ />
+ </td>
+ {this.state.openTokenForm && (
+ <TokensForm
+ user={user}
+ onClose={this.handleCloseTokensForm}
+ onUpdateUsers={onUpdateUsers}
+ />
+ )}
+ </tr>
+ );
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 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 { IdentityProvider, User } from '../../../api/users';
+import { getBaseUrl } from '../../../helpers/urls';
+
+interface Props {
+ identityProvider?: IdentityProvider;
+ user: User;
+}
+
+export default function UserListItemIdentity({ identityProvider, user }: Props) {
+ return (
+ <td>
+ <div>
+ <strong className="js-user-name">{user.name}</strong>
+ <span className="js-user-login note little-spacer-left">{user.login}</span>
+ </div>
+ {user.email && <div className="js-user-email little-spacer-top">{user.email}</div>}
+ {!user.local &&
+ user.externalProvider !== 'sonarqube' && (
+ <ExternalProvider identityProvider={identityProvider} user={user} />
+ )}
+ </td>
+ );
+}
+
+export function ExternalProvider({ identityProvider, user }: Props) {
+ if (!identityProvider) {
+ return (
+ <div className="js-user-identity-provider little-spacer-top">
+ <span>
+ {user.externalProvider}: {user.externalIdentity}
+ </span>
+ </div>
+ );
+ }
+
+ return (
+ <div className="js-user-identity-provider little-spacer-top">
+ <div
+ className="identity-provider"
+ style={{ 'background-color': identityProvider.backgroundColor }}>
+ <img
+ alt={identityProvider.name}
+ src={getBaseUrl() + identityProvider.iconPath}
+ width="14"
+ height="14"
+ />
+ {user.externalIdentity}
+ </div>
+ </div>
+ );
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 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 { DeleteButton } from '../../../components/ui/buttons';
+
+export interface Props {
+ idx: number;
+ scmAccount: string;
+ onChange: (idx: number, scmAccount: string) => void;
+ onRemove: (idx: number) => void;
+}
+
+export default class UserScmAccountInput extends React.PureComponent<Props> {
+ handleChange = (event: React.SyntheticEvent<HTMLInputElement>) =>
+ this.props.onChange(this.props.idx, event.currentTarget.value);
+ handleRemove = () => this.props.onRemove(this.props.idx);
+
+ render() {
+ return (
+ <div>
+ <input
+ maxLength={255}
+ onChange={this.handleChange}
+ type="text"
+ value={this.props.scmAccount}
+ />
+ <DeleteButton onClick={this.handleRemove} />
+ </div>
+ );
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 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 { translateWithParameters } from '../../../helpers/l10n';
+
+interface Props {
+ scmAccounts: string[];
+}
+
+interface State {
+ showMore: boolean;
+}
+
+const SCM_LIMIT = 3;
+
+export default class UserScmAccounts extends React.PureComponent<Props, State> {
+ state: State = { showMore: false };
+
+ toggleShowMore = (evt: React.SyntheticEvent<HTMLAnchorElement>) => {
+ evt.preventDefault();
+ this.setState(state => ({ showMore: !state.showMore }));
+ };
+
+ render() {
+ const { scmAccounts } = this.props;
+ const limit = scmAccounts.length > SCM_LIMIT ? SCM_LIMIT - 1 : SCM_LIMIT;
+ return (
+ <ul>
+ {scmAccounts.slice(0, limit).map((scmAccount, idx) => (
+ <li key={idx} className="little-spacer-bottom">
+ {scmAccount}
+ </li>
+ ))}
+ {scmAccounts.length > SCM_LIMIT &&
+ (this.state.showMore ? (
+ scmAccounts.slice(limit).map((scmAccount, idx) => (
+ <li key={idx + limit} className="little-spacer-bottom">
+ {scmAccount}
+ </li>
+ ))
+ ) : (
+ <li className="little-spacer-bottom">
+ <a className="js-user-more-scm" href="#" onClick={this.toggleShowMore}>
+ {translateWithParameters('more_x', scmAccounts.length - limit)}
+ </a>
+ </li>
+ ))}
+ </ul>
+ );
+ }
+}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2017 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 React from 'react';
-import PropTypes from 'prop-types';
-import Helmet from 'react-helmet';
-import { connect } from 'react-redux';
-import init from '../init';
-import { getCurrentUser } from '../../../store/rootReducer';
-import { translate } from '../../../helpers/l10n';
-// import styles to have the `.button-icon` styles
-import '../../../components/ui/buttons.css';
-import '../../../components/controls/SearchBox.css';
-
-class UsersAppContainer extends React.PureComponent {
- static propTypes = {
- currentUser: PropTypes.object.isRequired
- };
-
- componentDidMount() {
- init(this.refs.container, this.props.currentUser);
- }
-
- render() {
- return (
- <div>
- <Helmet title={translate('users.page')} />
- <div ref="container" />
- </div>
- );
- }
-}
-
-const mapStateToProps = state => ({
- currentUser: getCurrentUser(state)
-});
-
-export default connect(mapStateToProps)(UsersAppContainer);
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 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 React from 'react';
+import PropTypes from 'prop-types';
+import Helmet from 'react-helmet';
+import { connect } from 'react-redux';
+import init from '../init';
+import { getCurrentUser } from '../../../store/rootReducer';
+import { translate } from '../../../helpers/l10n';
+// import styles to have the `.button-icon` styles
+import '../../../components/ui/buttons.css';
+import '../../../components/controls/SearchBox.css';
+
+class UsersAppContainer extends React.PureComponent {
+ static propTypes = {
+ currentUser: PropTypes.object.isRequired
+ };
+
+ componentDidMount() {
+ init(this.refs.container, this.props.currentUser);
+ }
+
+ render() {
+ return (
+ <div>
+ <Helmet title={translate('users.page')} />
+ <div ref="container" />
+ </div>
+ );
+ }
+}
+
+const mapStateToProps = state => ({
+ currentUser: getCurrentUser(state)
+});
+
+export default connect(mapStateToProps)(UsersAppContainer);
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2017 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.
- */
-//@flow
-import React from 'react';
-import { debounce } from 'lodash';
-import Select from '../../../components/controls/Select';
-import { translate, translateWithParameters } from '../../../helpers/l10n';
-import UsersSelectSearchOption from './UsersSelectSearchOption';
-import UsersSelectSearchValue from './UsersSelectSearchValue';
-
-/*::
-export type Option = {
- login: string,
- name: string,
- email?: string,
- avatar?: string,
- groupCount?: number
-};
-*/
-
-/*::
-type Props = {
- autoFocus?: boolean,
- excludedUsers: Array<string>,
- handleValueChange: Option => void,
- searchUsers: (string, number) => Promise<*>,
- selectedUser?: Option
-};
-*/
-
-/*::
-type State = {
- isLoading: boolean,
- search: string,
- searchResult: Array<Option>
-};
-*/
-
-const LIST_SIZE = 10;
-
-export default class UsersSelectSearch extends React.PureComponent {
- /*:: mounted: boolean; */
- /*:: props: Props; */
- /*:: state: State; */
-
- constructor(props /*: Props */) {
- super(props);
- this.handleSearch = debounce(this.handleSearch, 250);
- this.state = { searchResult: [], isLoading: false, search: '' };
- }
- componentDidMount() {
- this.mounted = true;
- this.handleSearch(this.state.search);
- }
-
- componentWillReceiveProps(nextProps /*: Props */) {
- if (this.props.excludedUsers !== nextProps.excludedUsers) {
- this.handleSearch(this.state.search);
- }
- }
-
- componentWillUnmount() {
- this.mounted = false;
- }
-
- filterSearchResult = ({ users } /*: { users: Array<Option> } */) =>
- users.filter(user => !this.props.excludedUsers.includes(user.login)).slice(0, LIST_SIZE);
-
- handleSearch = (search /*: string */) => {
- this.props
- .searchUsers(search, Math.min(this.props.excludedUsers.length + LIST_SIZE, 500))
- .then(this.filterSearchResult)
- .then(searchResult => {
- if (this.mounted) {
- this.setState({ isLoading: false, searchResult });
- }
- });
- };
-
- handleInputChange = (search /*: string */) => {
- if (search == null || search.length === 1) {
- this.setState({ search });
- } else {
- this.setState({ isLoading: true, search });
- this.handleSearch(search);
- }
- };
-
- render() {
- const noResult =
- this.state.search.length === 1
- ? translateWithParameters('select2.tooShort', 2)
- : translate('no_results');
- return (
- <Select
- autofocus={this.props.autoFocus}
- className="Select-big"
- options={this.state.searchResult}
- isLoading={this.state.isLoading}
- optionComponent={UsersSelectSearchOption}
- valueComponent={UsersSelectSearchValue}
- onChange={this.props.handleValueChange}
- onInputChange={this.handleInputChange}
- value={this.props.selectedUser}
- placeholder=""
- noResultsText={noResult}
- labelKey="name"
- valueKey="login"
- clearable={false}
- searchable={true}
- />
- );
- }
-}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 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 { debounce } from 'lodash';
+import Avatar from '../../../components/ui/Avatar';
+import Select from '../../../components/controls/Select';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
+
+interface Option {
+ login: string;
+ name: string;
+ email?: string;
+ avatar?: string;
+}
+
+interface Props {
+ autoFocus?: boolean;
+ excludedUsers: string[];
+ handleValueChange: (option: Option) => void;
+ searchUsers: (query: string, ps: number) => Promise<{ users: Option[] }>;
+ selectedUser?: Option;
+}
+
+interface State {
+ isLoading: boolean;
+ search: string;
+ searchResult: Option[];
+}
+
+const LIST_SIZE = 10;
+const AVATAR_SIZE = 16;
+
+export default class UsersSelectSearch extends React.PureComponent<Props, State> {
+ mounted: boolean;
+
+ constructor(props: Props) {
+ super(props);
+ this.handleSearch = debounce(this.handleSearch, 250);
+ this.state = { searchResult: [], isLoading: false, search: '' };
+ }
+ componentDidMount() {
+ this.mounted = true;
+ this.handleSearch(this.state.search);
+ }
+
+ componentWillReceiveProps(nextProps: Props) {
+ if (this.props.excludedUsers !== nextProps.excludedUsers) {
+ this.handleSearch(this.state.search);
+ }
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ filterSearchResult = ({ users }: { users: Option[] }) =>
+ users.filter(user => !this.props.excludedUsers.includes(user.login)).slice(0, LIST_SIZE);
+
+ handleSearch = (search: string) => {
+ this.props
+ .searchUsers(search, Math.min(this.props.excludedUsers.length + LIST_SIZE, 500))
+ .then(this.filterSearchResult)
+ .then(searchResult => {
+ if (this.mounted) {
+ this.setState({ isLoading: false, searchResult });
+ }
+ });
+ };
+
+ handleInputChange = (search: string) => {
+ if (search == null || search.length === 1) {
+ this.setState({ search });
+ } else {
+ this.setState({ isLoading: true, search });
+ this.handleSearch(search);
+ }
+ };
+
+ render() {
+ const noResult =
+ this.state.search.length === 1
+ ? translateWithParameters('select2.tooShort', 2)
+ : translate('no_results');
+ return (
+ <Select
+ autofocus={this.props.autoFocus}
+ className="Select-big"
+ options={this.state.searchResult}
+ isLoading={this.state.isLoading}
+ optionComponent={UsersSelectSearchOption}
+ valueComponent={UsersSelectSearchValue}
+ onChange={this.props.handleValueChange}
+ onInputChange={this.handleInputChange}
+ value={this.props.selectedUser}
+ placeholder=""
+ noResultsText={noResult}
+ labelKey="name"
+ valueKey="login"
+ clearable={false}
+ searchable={true}
+ />
+ );
+ }
+}
+
+interface OptionProps {
+ children?: React.ReactNode;
+ className?: string;
+ isFocused?: boolean;
+ onFocus: (option: Option, evt: React.MouseEvent<HTMLDivElement>) => void;
+ onSelect: (option: Option, evt: React.MouseEvent<HTMLDivElement>) => void;
+ option: Option;
+}
+
+export class UsersSelectSearchOption extends React.PureComponent<OptionProps> {
+ handleMouseDown = (evt: React.MouseEvent<HTMLDivElement>) => {
+ evt.preventDefault();
+ evt.stopPropagation();
+ this.props.onSelect(this.props.option, evt);
+ };
+
+ handleMouseEnter = (evt: React.MouseEvent<HTMLDivElement>) => {
+ this.props.onFocus(this.props.option, evt);
+ };
+
+ handleMouseMove = (evt: React.MouseEvent<HTMLDivElement>) => {
+ if (this.props.isFocused) {
+ return;
+ }
+ this.props.onFocus(this.props.option, evt);
+ };
+
+ render() {
+ const { option } = this.props;
+ return (
+ <div
+ className={this.props.className}
+ onMouseDown={this.handleMouseDown}
+ onMouseEnter={this.handleMouseEnter}
+ onMouseMove={this.handleMouseMove}
+ title={option.name}>
+ <Avatar hash={option.avatar} name={option.name} size={AVATAR_SIZE} />
+ <strong className="spacer-left">{this.props.children}</strong>
+ <span className="note little-spacer-left">{option.login}</span>
+ </div>
+ );
+ }
+}
+
+interface ValueProps {
+ value?: Option;
+ children?: React.ReactNode;
+}
+
+export function UsersSelectSearchValue({ children, value }: ValueProps) {
+ return (
+ <div className="Select-value" title={value ? value.name : ''}>
+ {value &&
+ value.login && (
+ <div className="Select-value-label">
+ <Avatar hash={value.avatar} name={value.name} size={AVATAR_SIZE} />
+ <strong className="spacer-left">{children}</strong>
+ <span className="note little-spacer-left">{value.login}</span>
+ </div>
+ )}
+ </div>
+ );
+}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2017 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.
- */
-//@flow
-import React from 'react';
-import Avatar from '../../../components/ui/Avatar';
-/*:: import type { Option } from './UsersSelectSearch'; */
-
-/*::
-type Props = {
- option: Option,
- children?: Element | Text,
- className?: string,
- isFocused?: boolean,
- onFocus: (Option, MouseEvent) => void,
- onSelect: (Option, MouseEvent) => void
-};
-*/
-
-const AVATAR_SIZE /*: number */ = 16;
-
-export default class UsersSelectSearchOption extends React.PureComponent {
- /*:: props: Props; */
-
- handleMouseDown = (event /*: MouseEvent */) => {
- event.preventDefault();
- event.stopPropagation();
- this.props.onSelect(this.props.option, event);
- };
-
- handleMouseEnter = (event /*: MouseEvent */) => {
- this.props.onFocus(this.props.option, event);
- };
-
- handleMouseMove = (event /*: MouseEvent */) => {
- if (this.props.isFocused) {
- return;
- }
- this.props.onFocus(this.props.option, event);
- };
-
- render() {
- const user = this.props.option;
- return (
- <div
- className={this.props.className}
- onMouseDown={this.handleMouseDown}
- onMouseEnter={this.handleMouseEnter}
- onMouseMove={this.handleMouseMove}
- title={user.name}>
- <Avatar hash={user.avatar} name={user.name} size={AVATAR_SIZE} />
- <strong className="spacer-left">{this.props.children}</strong>
- <span className="note little-spacer-left">{user.login}</span>
- </div>
- );
- }
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2017 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.
- */
-//@flow
-import React from 'react';
-import Avatar from '../../../components/ui/Avatar';
-/*:: import type { Option } from './UsersSelectSearch'; */
-
-/*::
-type Props = {
- value: Option,
- children?: Element | Text
-};
-*/
-
-const AVATAR_SIZE /*: number */ = 16;
-
-export default class UsersSelectSearchValue extends React.PureComponent {
- /*:: props: Props; */
-
- render() {
- const user = this.props.value;
- return (
- <div className="Select-value" title={user ? user.name : ''}>
- {user &&
- user.login && (
- <div className="Select-value-label">
- <Avatar hash={user.avatar} name={user.name} size={AVATAR_SIZE} />
- <strong className="spacer-left">{this.props.children}</strong>
- <span className="note little-spacer-left">{user.login}</span>
- </div>
- )}
- </div>
- );
- }
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2017 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 React from 'react';
-import { shallow } from 'enzyme';
-import UsersSelectSearch from '../UsersSelectSearch';
-
-const selectedUser = {
- login: 'admin',
- name: 'Administrator',
- avatar: '7daf6c79d4802916d83f6266e24850af'
-};
-const users = [
- { login: 'admin', name: 'Administrator', email: 'admin@admin.ch' },
- { login: 'test', name: 'Tester', email: 'tester@testing.ch' },
- { login: 'foo', name: 'Foo Bar', email: 'foo@bar.ch' }
-];
-const excludedUsers = ['admin'];
-const onSearch = jest.fn(() => {
- return Promise.resolve(users);
-});
-const onChange = jest.fn();
-
-it('should render correctly', () => {
- const wrapper = shallow(
- <UsersSelectSearch
- selectedUser={selectedUser}
- excludedUsers={excludedUsers}
- isLoading={false}
- handleValueChange={onChange}
- searchUsers={onSearch}
- />
- );
- expect(wrapper).toMatchSnapshot();
- const searchResult = wrapper.instance().filterSearchResult({ users });
- expect(searchResult).toMatchSnapshot();
- expect(wrapper.setState({ searchResult })).toMatchSnapshot();
-});
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 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 { shallow } from 'enzyme';
+import UsersSelectSearch, {
+ UsersSelectSearchOption,
+ UsersSelectSearchValue
+} from '../UsersSelectSearch';
+
+const selectedUser = {
+ login: 'admin',
+ name: 'Administrator',
+ avatar: '7daf6c79d4802916d83f6266e24850af'
+};
+const users = [
+ { login: 'admin', name: 'Administrator', email: 'admin@admin.ch' },
+ { login: 'test', name: 'Tester', email: 'tester@testing.ch' },
+ { login: 'foo', name: 'Foo Bar', email: 'foo@bar.ch' }
+];
+const excludedUsers = ['admin'];
+
+describe('UsersSelectSearch', () => {
+ it('should render correctly', () => {
+ const onSearch = jest.fn(() => Promise.resolve(users));
+ const wrapper = shallow(
+ <UsersSelectSearch
+ selectedUser={selectedUser}
+ excludedUsers={excludedUsers}
+ handleValueChange={jest.fn()}
+ searchUsers={onSearch}
+ />
+ );
+ expect(wrapper).toMatchSnapshot();
+ const searchResult = (wrapper.instance() as UsersSelectSearch).filterSearchResult({ users });
+ expect(searchResult).toMatchSnapshot();
+ expect(wrapper.setState({ searchResult })).toMatchSnapshot();
+ });
+});
+
+describe('UsersSelectSearchOption', () => {
+ it('should render correctly without all parameters', () => {
+ const wrapper = shallow(
+ <UsersSelectSearchOption option={selectedUser} onFocus={jest.fn()} onSelect={jest.fn()}>
+ {selectedUser.name}
+ </UsersSelectSearchOption>
+ );
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ it('should render correctly with email instead of hash', () => {
+ const wrapper = shallow(
+ <UsersSelectSearchOption option={users[0]} onFocus={jest.fn()} onSelect={jest.fn()}>
+ {users[0].name}
+ </UsersSelectSearchOption>
+ );
+ expect(wrapper).toMatchSnapshot();
+ });
+});
+
+describe('UsersSelectSearchValue', () => {
+ it('should render correctly with a user', () => {
+ const wrapper = shallow(
+ <UsersSelectSearchValue value={selectedUser}>{selectedUser.name}</UsersSelectSearchValue>
+ );
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ it('should render correctly with email instead of hash', () => {
+ const wrapper = shallow(
+ <UsersSelectSearchValue value={users[0]}>{users[0].name}</UsersSelectSearchValue>
+ );
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ it('should render correctly without value', () => {
+ const wrapper = shallow(<UsersSelectSearchValue />);
+ expect(wrapper).toMatchSnapshot();
+ });
+});
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2017 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 React from 'react';
-import { shallow } from 'enzyme';
-import UsersSelectSearchOption from '../UsersSelectSearchOption';
-
-const user = {
- login: 'admin',
- name: 'Administrator',
- avatar: '7daf6c79d4802916d83f6266e24850af'
-};
-
-const user2 = {
- login: 'admin',
- name: 'Administrator',
- email: 'admin@admin.ch'
-};
-
-it('should render correctly without all parameters', () => {
- const wrapper = shallow(
- <UsersSelectSearchOption option={user}>{user.name}</UsersSelectSearchOption>
- );
- expect(wrapper).toMatchSnapshot();
-});
-
-it('should render correctly with email instead of hash', () => {
- const wrapper = shallow(
- <UsersSelectSearchOption option={user2}>{user.name}</UsersSelectSearchOption>
- );
- expect(wrapper).toMatchSnapshot();
-});
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2017 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 React from 'react';
-import { shallow } from 'enzyme';
-import UsersSelectSearchValue from '../UsersSelectSearchValue';
-
-const user = {
- login: 'admin',
- name: 'Administrator',
- avatar: '7daf6c79d4802916d83f6266e24850af'
-};
-
-const user2 = {
- login: 'admin',
- name: 'Administrator',
- email: 'admin@admin.ch'
-};
-
-it('should render correctly with a user', () => {
- const wrapper = shallow(
- <UsersSelectSearchValue value={user}>{user.name}</UsersSelectSearchValue>
- );
- expect(wrapper).toMatchSnapshot();
-});
-
-it('should render correctly with email instead of hash', () => {
- const wrapper = shallow(
- <UsersSelectSearchValue value={user2}>{user2.name}</UsersSelectSearchValue>
- );
- expect(wrapper).toMatchSnapshot();
-});
-
-it('should render correctly without value', () => {
- const wrapper = shallow(<UsersSelectSearchValue />);
- expect(wrapper).toMatchSnapshot();
-});
+++ /dev/null
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render correctly 1`] = `
-<Select
- className="Select-big"
- clearable={false}
- isLoading={false}
- labelKey="name"
- noResultsText="no_results"
- onChange={[Function]}
- onInputChange={[Function]}
- optionComponent={[Function]}
- options={Array []}
- placeholder=""
- searchable={true}
- value={
- Object {
- "avatar": "7daf6c79d4802916d83f6266e24850af",
- "login": "admin",
- "name": "Administrator",
- }
- }
- valueComponent={[Function]}
- valueKey="login"
-/>
-`;
-
-exports[`should render correctly 2`] = `
-Array [
- Object {
- "email": "tester@testing.ch",
- "login": "test",
- "name": "Tester",
- },
- Object {
- "email": "foo@bar.ch",
- "login": "foo",
- "name": "Foo Bar",
- },
-]
-`;
-
-exports[`should render correctly 3`] = `
-<Select
- className="Select-big"
- clearable={false}
- isLoading={false}
- labelKey="name"
- noResultsText="no_results"
- onChange={[Function]}
- onInputChange={[Function]}
- optionComponent={[Function]}
- options={
- Array [
- Object {
- "email": "tester@testing.ch",
- "login": "test",
- "name": "Tester",
- },
- Object {
- "email": "foo@bar.ch",
- "login": "foo",
- "name": "Foo Bar",
- },
- ]
- }
- placeholder=""
- searchable={true}
- value={
- Object {
- "avatar": "7daf6c79d4802916d83f6266e24850af",
- "login": "admin",
- "name": "Administrator",
- }
- }
- valueComponent={[Function]}
- valueKey="login"
-/>
-`;
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`UsersSelectSearch should render correctly 1`] = `
+<Select
+ className="Select-big"
+ clearable={false}
+ isLoading={false}
+ labelKey="name"
+ noResultsText="no_results"
+ onChange={[Function]}
+ onInputChange={[Function]}
+ optionComponent={[Function]}
+ options={Array []}
+ placeholder=""
+ searchable={true}
+ value={
+ Object {
+ "avatar": "7daf6c79d4802916d83f6266e24850af",
+ "login": "admin",
+ "name": "Administrator",
+ }
+ }
+ valueComponent={[Function]}
+ valueKey="login"
+/>
+`;
+
+exports[`UsersSelectSearch should render correctly 2`] = `
+Array [
+ Object {
+ "email": "tester@testing.ch",
+ "login": "test",
+ "name": "Tester",
+ },
+ Object {
+ "email": "foo@bar.ch",
+ "login": "foo",
+ "name": "Foo Bar",
+ },
+]
+`;
+
+exports[`UsersSelectSearch should render correctly 3`] = `
+<Select
+ className="Select-big"
+ clearable={false}
+ isLoading={false}
+ labelKey="name"
+ noResultsText="no_results"
+ onChange={[Function]}
+ onInputChange={[Function]}
+ optionComponent={[Function]}
+ options={
+ Array [
+ Object {
+ "email": "tester@testing.ch",
+ "login": "test",
+ "name": "Tester",
+ },
+ Object {
+ "email": "foo@bar.ch",
+ "login": "foo",
+ "name": "Foo Bar",
+ },
+ ]
+ }
+ placeholder=""
+ searchable={true}
+ value={
+ Object {
+ "avatar": "7daf6c79d4802916d83f6266e24850af",
+ "login": "admin",
+ "name": "Administrator",
+ }
+ }
+ valueComponent={[Function]}
+ valueKey="login"
+/>
+`;
+
+exports[`UsersSelectSearchOption should render correctly with email instead of hash 1`] = `
+<div
+ onMouseDown={[Function]}
+ onMouseEnter={[Function]}
+ onMouseMove={[Function]}
+ title="Administrator"
+>
+ <Connect(Avatar)
+ name="Administrator"
+ size={16}
+ />
+ <strong
+ className="spacer-left"
+ >
+ Administrator
+ </strong>
+ <span
+ className="note little-spacer-left"
+ >
+ admin
+ </span>
+</div>
+`;
+
+exports[`UsersSelectSearchOption should render correctly without all parameters 1`] = `
+<div
+ onMouseDown={[Function]}
+ onMouseEnter={[Function]}
+ onMouseMove={[Function]}
+ title="Administrator"
+>
+ <Connect(Avatar)
+ hash="7daf6c79d4802916d83f6266e24850af"
+ name="Administrator"
+ size={16}
+ />
+ <strong
+ className="spacer-left"
+ >
+ Administrator
+ </strong>
+ <span
+ className="note little-spacer-left"
+ >
+ admin
+ </span>
+</div>
+`;
+
+exports[`UsersSelectSearchValue should render correctly with a user 1`] = `
+<div
+ className="Select-value"
+ title="Administrator"
+>
+ <div
+ className="Select-value-label"
+ >
+ <Connect(Avatar)
+ hash="7daf6c79d4802916d83f6266e24850af"
+ name="Administrator"
+ size={16}
+ />
+ <strong
+ className="spacer-left"
+ >
+ Administrator
+ </strong>
+ <span
+ className="note little-spacer-left"
+ >
+ admin
+ </span>
+ </div>
+</div>
+`;
+
+exports[`UsersSelectSearchValue should render correctly with email instead of hash 1`] = `
+<div
+ className="Select-value"
+ title="Administrator"
+>
+ <div
+ className="Select-value-label"
+ >
+ <Connect(Avatar)
+ name="Administrator"
+ size={16}
+ />
+ <strong
+ className="spacer-left"
+ >
+ Administrator
+ </strong>
+ <span
+ className="note little-spacer-left"
+ >
+ admin
+ </span>
+ </div>
+</div>
+`;
+
+exports[`UsersSelectSearchValue should render correctly without value 1`] = `
+<div
+ className="Select-value"
+ title=""
+/>
+`;
+++ /dev/null
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render correctly with email instead of hash 1`] = `
-<div
- onMouseDown={[Function]}
- onMouseEnter={[Function]}
- onMouseMove={[Function]}
- title="Administrator"
->
- <Connect(Avatar)
- name="Administrator"
- size={16}
- />
- <strong
- className="spacer-left"
- >
- Administrator
- </strong>
- <span
- className="note little-spacer-left"
- >
- admin
- </span>
-</div>
-`;
-
-exports[`should render correctly without all parameters 1`] = `
-<div
- onMouseDown={[Function]}
- onMouseEnter={[Function]}
- onMouseMove={[Function]}
- title="Administrator"
->
- <Connect(Avatar)
- hash="7daf6c79d4802916d83f6266e24850af"
- name="Administrator"
- size={16}
- />
- <strong
- className="spacer-left"
- >
- Administrator
- </strong>
- <span
- className="note little-spacer-left"
- >
- admin
- </span>
-</div>
-`;
+++ /dev/null
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render correctly with a user 1`] = `
-<div
- className="Select-value"
- title="Administrator"
->
- <div
- className="Select-value-label"
- >
- <Connect(Avatar)
- hash="7daf6c79d4802916d83f6266e24850af"
- name="Administrator"
- size={16}
- />
- <strong
- className="spacer-left"
- >
- Administrator
- </strong>
- <span
- className="note little-spacer-left"
- >
- admin
- </span>
- </div>
-</div>
-`;
-
-exports[`should render correctly with email instead of hash 1`] = `
-<div
- className="Select-value"
- title="Administrator"
->
- <div
- className="Select-value-label"
- >
- <Connect(Avatar)
- name="Administrator"
- size={16}
- />
- <strong
- className="spacer-left"
- >
- Administrator
- </strong>
- <span
- className="note little-spacer-left"
- >
- admin
- </span>
- </div>
-</div>
-`;
-
-exports[`should render correctly without value 1`] = `
-<div
- className="Select-value"
- title=""
-/>
-`;
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { RouterState, IndexRouteProps } from 'react-router';
+import { RouterState, IndexRouteProps, RouteComponent } from 'react-router';
const routes = [
{
getIndexRoute(_: RouterState, callback: (err: any, route: IndexRouteProps) => any) {
- import('./components/UsersAppContainer').then(i => callback(null, { component: i.default }));
+ import('./UsersAppContainer').then(i => callback(null, { component: i.default }));
+ }
+ },
+ {
+ path: 'old',
+ getComponent(_: RouterState, callback: (err: any, component: RouteComponent) => any) {
+ import('./components/UsersAppContainerOld').then(i => callback(null, i.default));
}
}
];
this.errors = [];
this.newToken = null;
const tokenName = this.$('.js-generate-token-form input').val();
- generateToken(tokenName, this.model.id).then(
+ generateToken({ name: tokenName, login: this.model.id }).then(
response => {
this.newToken = response;
this.requestTokens();
const token = this.tokens.find(t => t.name === `${tokenName}`);
if (token) {
if (token.deleting) {
- revokeToken(tokenName, this.model.id).then(this.requestTokens.bind(this), () => {});
+ revokeToken({ name: tokenName, login: this.model.id }).then(
+ this.requestTokens.bind(this),
+ () => {}
+ );
} else {
token.deleting = true;
this.render();
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 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 { memoize } from 'lodash';
+import { cleanQuery, parseAsString, RawQuery, serializeString } from '../../helpers/query';
+
+export interface Query {
+ search: string;
+}
+
+export const parseQuery = memoize((urlQuery: RawQuery): Query => ({
+ search: parseAsString(urlQuery['search'])
+}));
+
+export const serializeQuery = memoize((query: Query): RawQuery =>
+ cleanQuery({
+ search: query.search ? serializeString(query.search) : undefined
+ })
+);
import * as classNames from 'classnames';
interface Props {
- children?: JSX.Element;
+ children?: JSX.Element | JSX.Element[];
className?: string;
loading?: boolean;
customSpinner?: JSX.Element;
interface Props {
className?: string;
children: React.ReactNode;
+ menuClassName?: string;
menuPosition?: 'left' | 'right';
small?: boolean;
toggleClassName?: string;
<i className="icon-dropdown little-spacer-left" />
</button>
<ul
- className={classNames('dropdown-menu', {
+ className={classNames('dropdown-menu', props.menuClassName, {
'dropdown-menu-right': menuPosition === 'right'
})}>
{props.children}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 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 { IconProps } from './types';
+
+export default function BulletListIcon({ className, fill = 'currentColor', size = 16 }: IconProps) {
+ return (
+ <svg
+ className={className}
+ width={size}
+ height={size}
+ viewBox="0 0 16 16"
+ version="1.1"
+ xmlnsXlink="http://www.w3.org/1999/xlink"
+ xmlSpace="preserve">
+ <path
+ style={{ fill }}
+ d="M2.968 11.274v1.51q0 0.102-0.075 0.177t-0.177 0.075h-1.51q-0.102 0-0.177-0.075t-0.075-0.177v-1.51q0-0.102 0.075-0.177t0.177-0.075h1.51q0.102 0 0.177 0.075t0.075 0.177zM2.968 8.255v1.51q0 0.102-0.075 0.177t-0.177 0.075h-1.51q-0.102 0-0.177-0.075t-0.075-0.177v-1.51q0-0.102 0.075-0.177t0.177-0.075h1.51q0.102 0 0.177 0.075t0.075 0.177zM2.968 5.235v1.51q0 0.102-0.075 0.177t-0.177 0.075h-1.51q-0.102 0-0.177-0.075t-0.075-0.177v-1.51q0-0.102 0.075-0.177t0.177-0.075h1.51q0.102 0 0.177 0.075t0.075 0.177zM15.045 11.274v1.51q0 0.102-0.075 0.177t-0.177 0.075h-10.568q-0.102 0-0.177-0.075t-0.075-0.177v-1.51q0-0.102 0.075-0.177t0.177-0.075h10.568q0.102 0 0.177 0.075t0.075 0.177zM2.968 2.216v1.51q0 0.102-0.075 0.177t-0.177 0.075h-1.51q-0.102 0-0.177-0.075t-0.075-0.177v-1.51q0-0.102 0.075-0.177t0.177-0.075h1.51q0.102 0 0.177 0.075t0.075 0.177zM15.045 8.255v1.51q0 0.102-0.075 0.177t-0.177 0.075h-10.568q-0.102 0-0.177-0.075t-0.075-0.177v-1.51q0-0.102 0.075-0.177t0.177-0.075h10.568q0.102 0 0.177 0.075t0.075 0.177zM15.045 5.235v1.51q0 0.102-0.075 0.177t-0.177 0.075h-10.568q-0.102 0-0.177-0.075t-0.075-0.177v-1.51q0-0.102 0.075-0.177t0.177-0.075h10.568q0.102 0 0.177 0.075t0.075 0.177zM15.045 2.216v1.51q0 0.102-0.075 0.177t-0.177 0.075h-10.568q-0.102 0-0.177-0.075t-0.075-0.177v-1.51q0-0.102 0.075-0.177t0.177-0.075h10.568q0.102 0 0.177 0.075t0.075 0.177z"
+ />
+ </svg>
+ );
+}
}).then(this.handleSearchResult, this.props.onFail);
};
- searchUsers = (query /*: string */) => {
- searchUsers(query, LIST_SIZE).then(this.handleSearchResult, this.props.onFail);
- };
+ searchUsers = (query /*: string */) =>
+ searchUsers({ q: query, ps: LIST_SIZE }).then(this.handleSearchResult, this.props.onFail);
handleSearchResult = (data /*: Object */) => {
this.setState({
import * as classNames from 'classnames';
import * as theme from '../../app/theme';
import ClearIcon from '../icons-components/ClearIcon';
-import './buttons.css';
import EditIcon from '../icons-components/EditIcon';
+import Tooltip from '../controls/Tooltip';
+import './buttons.css';
interface ButtonIconProps {
children: React.ReactNode;
className?: string;
color?: string;
onClick?: () => void;
+ tooltip?: string;
[x: string]: any;
}
};
render() {
- const { children, className, color = theme.darkBlue, onClick, ...props } = this.props;
- return (
+ const { children, className, color = theme.darkBlue, onClick, tooltip, ...props } = this.props;
+ const buttonComponent = (
<button
className={classNames(className, 'button-icon')}
onClick={this.handleClick}
{children}
</button>
);
+ if (tooltip) {
+ return (
+ <Tooltip overlay={tooltip} mouseEnterDelay={0.4}>
+ {buttonComponent}
+ </Tooltip>
+ );
+ }
+ return buttonComponent;
}
}
version "2.2.3"
resolved "https://registry.yarnpkg.com/@types/classnames/-/classnames-2.2.3.tgz#3f0ff6873da793870e20a260cada55982f38a9e5"
+"@types/clipboard@1.5.35":
+ version "1.5.35"
+ resolved "https://registry.yarnpkg.com/@types/clipboard/-/clipboard-1.5.35.tgz#bceb67a7e0aad3070468929b95a2d626405db464"
+
"@types/d3-array@1.2.1":
version "1.2.1"
resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-1.2.1.tgz#e489605208d46a1c9d980d2e5772fa9c75d9ec65"
min=Min
minor=Minor
more=More
+more_x={0} more
more_actions=More Actions
my_favorite=My Favorite
my_favorites=My Favorites