From: Grégoire Aubert Date: Mon, 6 Nov 2017 13:18:22 +0000 (+0100) Subject: Rewrite users page to TS and React X-Git-Tag: 7.0-RC1~282 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=1fccf4779bbf4d1c125fd65ed3972b57b3c1be6e;p=sonarqube.git Rewrite users page to TS and React --- diff --git a/server/sonar-web/package.json b/server/sonar-web/package.json index febc889e9b3..640c3f00eab 100644 --- a/server/sonar-web/package.json +++ b/server/sonar-web/package.json @@ -44,6 +44,7 @@ }, "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", diff --git a/server/sonar-web/src/main/js/api/components.ts b/server/sonar-web/src/main/js/api/components.ts index 2d03c4f0db4..6448333071f 100644 --- a/server/sonar-web/src/main/js/api/components.ts +++ b/server/sonar-web/src/main/js/api/components.ts @@ -154,12 +154,6 @@ export function getMyProjects(data: RequestData): Promise { return getJSON(url, data); } -export interface Paging { - pageIndex: number; - pageSize: number; - total: number; -} - export interface Component { organization: string; id: string; diff --git a/server/sonar-web/src/main/js/api/user-tokens.ts b/server/sonar-web/src/main/js/api/user-tokens.ts index 2b98e0407cd..8fa8b241575 100644 --- a/server/sonar-web/src/main/js/api/user-tokens.ts +++ b/server/sonar-web/src/main/js/api/user-tokens.ts @@ -17,37 +17,34 @@ * 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 { - return getJSON('/api/user_tokens/search', { login }).then(r => r.userTokens); +export function getTokens(login: string): Promise { + 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 { - const data: RequestData = { name: tokenName }; - if (userLogin) { - data.login = userLogin; - } +export function revokeToken(data: { name: string; login?: string }): Promise { return post('/api/user_tokens/revoke', data).catch(throwGlobalError); } diff --git a/server/sonar-web/src/main/js/api/users.ts b/server/sonar-web/src/main/js/api/users.ts index 2fb8213e139..f78343b5969 100644 --- a/server/sonar-web/src/main/js/api/users.ts +++ b/server/sonar-web/src/main/js/api/users.ts @@ -17,8 +17,9 @@ * 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; @@ -27,19 +28,29 @@ export interface IdentityProvider { 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 { return getJSON('/api/users/current'); } -export function changePassword( - login: string, - password: string, - previousPassword?: string -): Promise { - const data: RequestData = { login, password }; - if (previousPassword != null) { - data.previousPassword = previousPassword; - } +export function changePassword(data: { + login: string; + password: string; + previousPassword?: string; +}): Promise { return post('/api/users/change_password', data); } @@ -52,15 +63,40 @@ export function getUserGroups(login: string, organization?: string): Promise { - return getJSON('/api/users/identity_providers'); + return getJSON('/api/users/identity_providers').catch(throwGlobalError); } -export function searchUsers(query: string, pageSize?: number): Promise { - 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 { + return post('/api/users/create', data); +} + +export function updateUser(data: { + email?: string; + login: string; + name?: string; + scmAccount: string[]; +}): Promise { + return postJSON('/api/users/update', data); +} + +export function deactivateUser(data: { login: string }): Promise { + return postJSON('/api/users/deactivate', data).catch(throwGlobalError); } export function skipOnboarding(): Promise { diff --git a/server/sonar-web/src/main/js/app/styles/init/misc.css b/server/sonar-web/src/main/js/app/styles/init/misc.css index bdb9435384c..09aa7c6a906 100644 --- a/server/sonar-web/src/main/js/app/styles/init/misc.css +++ b/server/sonar-web/src/main/js/app/styles/init/misc.css @@ -316,6 +316,10 @@ td.big-spacer-top { cursor: not-allowed; } +.no-select { + user-select: none; +} + .no-outline, .no-outline:focus { outline: none; diff --git a/server/sonar-web/src/main/js/apps/account/components/Password.js b/server/sonar-web/src/main/js/apps/account/components/Password.js index f90aedc34cd..dec082bc69b 100644 --- a/server/sonar-web/src/main/js/apps/account/components/Password.js +++ b/server/sonar-web/src/main/js/apps/account/components/Password.js @@ -27,44 +27,44 @@ export default class Password extends Component { 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; @@ -73,7 +73,7 @@ export default class Password extends Component {

{translate('my_profile.password.title')}

-
+ {success && (
{translate('my_profile.password.changed')}
)} @@ -91,7 +91,7 @@ export default class Password extends Component { * (this.oldPassword = elem)} autoComplete="off" id="old_password" name="old_password" @@ -105,7 +105,7 @@ export default class Password extends Component { * (this.password = elem)} autoComplete="off" id="password" name="password" @@ -119,7 +119,7 @@ export default class Password extends Component { * (this.passwordConfirmation = elem)} autoComplete="off" id="password_confirmation" name="password_confirmation" diff --git a/server/sonar-web/src/main/js/apps/account/tokens-view.js b/server/sonar-web/src/main/js/apps/account/tokens-view.js index e63f1692804..68c74f3d6c8 100644 --- a/server/sonar-web/src/main/js/apps/account/tokens-view.js +++ b/server/sonar-web/src/main/js/apps/account/tokens-view.js @@ -53,7 +53,7 @@ export default Marionette.ItemView.extend({ 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(); @@ -68,7 +68,10 @@ export default Marionette.ItemView.extend({ 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(); diff --git a/server/sonar-web/src/main/js/apps/issues/utils.js b/server/sonar-web/src/main/js/apps/issues/utils.js index e0532e1e814..a044e880696 100644 --- a/server/sonar-web/src/main/js/apps/issues/utils.js +++ b/server/sonar-web/src/main/js/apps/issues/utils.js @@ -230,7 +230,7 @@ export const searchAssignees = (query /*: string */, organization /*: ?string */ 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, diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/TokenStep.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/TokenStep.js index dd72bd238e8..4a5609c3d9b 100644 --- a/server/sonar-web/src/main/js/apps/tutorials/onboarding/TokenStep.js +++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/TokenStep.js @@ -95,7 +95,7 @@ export default class TokenStep extends React.PureComponent { 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 }); @@ -114,7 +114,7 @@ export default class TokenStep extends React.PureComponent { 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 }); diff --git a/server/sonar-web/src/main/js/apps/users/Header.tsx b/server/sonar-web/src/main/js/apps/users/Header.tsx new file mode 100644 index 00000000000..60eeb835f18 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/users/Header.tsx @@ -0,0 +1,59 @@ +/* + * 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 { + state: State = { openUserForm: false }; + + handleOpenUserForm = () => this.setState({ openUserForm: true }); + handleCloseUserForm = () => this.setState({ openUserForm: false }); + + render() { + return ( +
+

{translate('users.page')}

+ + +
+ +
+ +

{translate('users.page.description')}

+ {this.state.openUserForm && ( + + )} +
+ ); + } +} diff --git a/server/sonar-web/src/main/js/apps/users/Search.tsx b/server/sonar-web/src/main/js/apps/users/Search.tsx new file mode 100644 index 00000000000..42f3684df90 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/users/Search.tsx @@ -0,0 +1,49 @@ +/* + * 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) => void; +} + +export default class Search extends React.PureComponent { + handleSearch = (search: string) => { + this.props.updateQuery({ search }); + }; + + render() { + const { query } = this.props; + + return ( + + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/users/UsersApp.tsx b/server/sonar-web/src/main/js/apps/users/UsersApp.tsx new file mode 100644 index 00000000000..24c638ca7b6 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/users/UsersApp.tsx @@ -0,0 +1,142 @@ +/* + * 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 { + 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) => { + 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 ( +
+ +
+ + + {paging !== undefined && ( + + )} +
+ ); + } +} diff --git a/server/sonar-web/src/main/js/apps/users/UsersAppContainer.tsx b/server/sonar-web/src/main/js/apps/users/UsersAppContainer.tsx new file mode 100644 index 00000000000..b5c14738d06 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/users/UsersAppContainer.tsx @@ -0,0 +1,39 @@ +/* + * 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(mapStateToProps)(UsersApp); diff --git a/server/sonar-web/src/main/js/apps/users/UsersList.tsx b/server/sonar-web/src/main/js/apps/users/UsersList.tsx new file mode 100644 index 00000000000..a81a9441af3 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/users/UsersList.tsx @@ -0,0 +1,68 @@ +/* + * 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 ( + + + + + {!organizationsEnabled && } + + + + + + {users.map(user => ( + user.externalProvider === provider.key + )} + isCurrentUser={currentUser.isLoggedIn && currentUser.login === user.login} + key={user.login} + onUpdateUsers={onUpdateUsers} + organizationsEnabled={organizationsEnabled} + user={user} + /> + ))} + +
+ + {translate('my_profile.scm_accounts')}{translate('my_profile.groups')}{translate('users.tokens')} 
+ ); +} diff --git a/server/sonar-web/src/main/js/apps/users/components/DeactivateForm.tsx b/server/sonar-web/src/main/js/apps/users/components/DeactivateForm.tsx new file mode 100644 index 00000000000..c9776ac8e72 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/users/components/DeactivateForm.tsx @@ -0,0 +1,95 @@ +/* + * 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 { + mounted: boolean; + state: State = { submitting: false }; + + componentDidMount() { + this.mounted = true; + } + + componentWillUnmount() { + this.mounted = false; + } + + handleCancelClick = (event: React.SyntheticEvent) => { + event.preventDefault(); + this.props.onClose(); + }; + + handleDeactivate = (event: React.SyntheticEvent) => { + 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 ( + + +
+

{header}

+
+
+ {translateWithParameters('users.deactivate_user.confirmation', user.name, user.login)} +
+ + +
+ ); + } +} diff --git a/server/sonar-web/src/main/js/apps/users/components/GroupsForm.tsx b/server/sonar-web/src/main/js/apps/users/components/GroupsForm.tsx new file mode 100644 index 00000000000..c7d616be792 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/users/components/GroupsForm.tsx @@ -0,0 +1,97 @@ +/* + * 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 { + container: HTMLDivElement | null; + + handleCloseClick = (event: React.SyntheticEvent) => { + 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)}
${escapeHtml(item.description)}`, + 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 ( + +
+

{header}

+
+ +
+
(this.container = node)} /> +
+ + + + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/users/components/PasswordForm.tsx b/server/sonar-web/src/main/js/apps/users/components/PasswordForm.tsx new file mode 100644 index 00000000000..b6864aa9faa --- /dev/null +++ b/server/sonar-web/src/main/js/apps/users/components/PasswordForm.tsx @@ -0,0 +1,182 @@ +/* + * 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 { + 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) => + this.setState({ confirmPassword: event.currentTarget.value }); + handleNewPasswordChange = (event: React.SyntheticEvent) => + this.setState({ newPassword: event.currentTarget.value }); + handleOldPasswordChange = (event: React.SyntheticEvent) => + this.setState({ oldPassword: event.currentTarget.value }); + + handleCancelClick = (event: React.SyntheticEvent) => { + event.preventDefault(); + this.props.onClose(); + }; + + handleChangePassword = (event: React.SyntheticEvent) => { + 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 ( + +
+
+

{header}

+
+
+ {error &&

{error}

} + {this.props.isCurrentUser && ( +
+ + {/* keep this fake field to hack browser autofill */} + + +
+ )} +
+ + {/* keep this fake field to hack browser autofill */} + + +
+
+ + {/* keep this fake field to hack browser autofill */} + + +
+
+ + +
+ ); + } +} diff --git a/server/sonar-web/src/main/js/apps/users/components/TokensForm.tsx b/server/sonar-web/src/main/js/apps/users/components/TokensForm.tsx new file mode 100644 index 00000000000..4d723aa723f --- /dev/null +++ b/server/sonar-web/src/main/js/apps/users/components/TokensForm.tsx @@ -0,0 +1,202 @@ +/* + * 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 { + 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) => { + evt.preventDefault(); + this.handleClose(); + }; + + handleClose = () => { + if (this.state.hasChanged) { + this.props.onUpdateUsers(); + } + this.props.onClose(); + }; + + handleGenerateToken = (evt: React.SyntheticEvent) => { + 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) => + this.setState({ newTokenName: evt.currentTarget.value }); + + renderItems() { + const { tokens } = this.state; + if (tokens.length <= 0) { + return ( + + + {translate('users.no_tokens')} + + + ); + } + return tokens.map(token => ( + + )); + } + + render() { + const { generating, loading, newToken, newTokenName, tokens } = this.state; + const header = translate('users.tokens'); + const customSpinner = ( + + + + + + ); + return ( + +
+

{header}

+
+
+

{translate('users.generate_tokens')}

+
+ + +
+ + {newToken && } + + + + + + + + + + + {this.renderItems()} + + +
{translate('name')}{translate('created')} +
+
+ +
+ ); + } +} diff --git a/server/sonar-web/src/main/js/apps/users/components/TokensFormItem.tsx b/server/sonar-web/src/main/js/apps/users/components/TokensFormItem.tsx new file mode 100644 index 00000000000..f94a65caf8c --- /dev/null +++ b/server/sonar-web/src/main/js/apps/users/components/TokensFormItem.tsx @@ -0,0 +1,97 @@ +/* + * 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 { + 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 ( + + + + {limitComponentName(token.name)} + + + + + + + + + + + + + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/users/components/TokensFormNewToken.tsx b/server/sonar-web/src/main/js/apps/users/components/TokensFormNewToken.tsx new file mode 100644 index 00000000000..2c3961196e6 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/users/components/TokensFormNewToken.tsx @@ -0,0 +1,101 @@ +/* + * 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 { + 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 = ( + + ); + return ( +
+

+ {translateWithParameters('users.tokens.new_token_created', name)} +

+ {this.state.tooltipShown ? ( + + {button} + + ) : ( + button + )} + {token} +
+ ); + } +} diff --git a/server/sonar-web/src/main/js/apps/users/components/UserActions.tsx b/server/sonar-web/src/main/js/apps/users/components/UserActions.tsx new file mode 100644 index 00000000000..9c5f64fccea --- /dev/null +++ b/server/sonar-web/src/main/js/apps/users/components/UserActions.tsx @@ -0,0 +1,113 @@ +/* + * 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 { + 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 ( + + + {translate('update_details')} + + {user.local && ( + + {translate('my_profile.password.title')} + + )} + + + {translate('users.deactivate')} + + + ); + }; + + render() { + const { openForm } = this.state; + const { isCurrentUser, onUpdateUsers, user } = this.props; + + if (openForm === 'deactivate') { + return [ + this.renderActions(), + + ]; + } + if (openForm === 'password') { + return [ + this.renderActions(), + + ]; + } + if (openForm === 'update') { + return [ + this.renderActions(), + + ]; + } + return this.renderActions(); + } +} diff --git a/server/sonar-web/src/main/js/apps/users/components/UserForm.tsx b/server/sonar-web/src/main/js/apps/users/components/UserForm.tsx new file mode 100644 index 00000000000..292d13f85c8 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/users/components/UserForm.tsx @@ -0,0 +1,270 @@ +/* + * 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 { + 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) => + this.setState({ email: event.currentTarget.value }); + handleLoginChange = (event: React.SyntheticEvent) => + this.setState({ login: event.currentTarget.value }); + handleNameChange = (event: React.SyntheticEvent) => + this.setState({ name: event.currentTarget.value }); + handlePasswordChange = (event: React.SyntheticEvent) => + this.setState({ password: event.currentTarget.value }); + + handleCancelClick = (event: React.SyntheticEvent) => { + event.preventDefault(); + this.props.onClose(); + }; + + handleCreateUser = (event: React.SyntheticEvent) => { + 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) => { + 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) => { + 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 ( + +
+
+

{header}

+
+ +
+ {error &&

{error}

} + + {!user && ( +
+ + {/* keep this fake field to hack browser autofill */} + + +

{translateWithParameters('users.minimum_x_characters', 3)}

+
+ )} +
+ + {/* keep this fake field to hack browser autofill */} + + +
+
+ + {/* keep this fake field to hack browser autofill */} + + +
+ {!user && ( +
+ + {/* keep this fake field to hack browser autofill */} + + +
+ )} +
+ + {this.state.scmAccounts.map((scm, idx) => ( + + ))} +
+ +
+

{translate('user.login_or_email_used_as_scm_account')}

+
+
+ + + +
+ ); + } +} diff --git a/server/sonar-web/src/main/js/apps/users/components/UserGroups.tsx b/server/sonar-web/src/main/js/apps/users/components/UserGroups.tsx new file mode 100644 index 00000000000..ee00341c44a --- /dev/null +++ b/server/sonar-web/src/main/js/apps/users/components/UserGroups.tsx @@ -0,0 +1,95 @@ +/* + * 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 { + state: State = { openForm: false, showMore: false }; + + handleOpenForm = () => this.setState({ openForm: true }); + handleCloseForm = () => this.setState({ openForm: false }); + + toggleShowMore = (evt: React.SyntheticEvent) => { + 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 ( +
    + {groups.slice(0, limit).map(group => ( +
  • + {group} +
  • + ))} + {groups.length > GROUPS_LIMIT && + this.state.showMore && + groups.slice(limit).map(group => ( +
  • + {group} +
  • + ))} +
  • + {groups.length > GROUPS_LIMIT && + !this.state.showMore && ( + + {translateWithParameters('more_x', groups.length - limit)} + + )} + + + +
  • + {this.state.openForm && ( + + )} +
+ ); + } +} diff --git a/server/sonar-web/src/main/js/apps/users/components/UserListItem.tsx b/server/sonar-web/src/main/js/apps/users/components/UserListItem.tsx new file mode 100644 index 00000000000..f6ebe464f4c --- /dev/null +++ b/server/sonar-web/src/main/js/apps/users/components/UserListItem.tsx @@ -0,0 +1,93 @@ +/* + * 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 { + state: State = { openTokenForm: false }; + + handleOpenTokensForm = () => this.setState({ openTokenForm: true }); + handleCloseTokensForm = () => this.setState({ openTokenForm: false }); + + render() { + const { identityProvider, onUpdateUsers, organizationsEnabled, user } = this.props; + + return ( + + + + + + + + + {!organizationsEnabled && ( + + + + )} + + {user.tokensCount} + + + + + + + + {this.state.openTokenForm && ( + + )} + + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/users/components/UserListItemIdentity.tsx b/server/sonar-web/src/main/js/apps/users/components/UserListItemIdentity.tsx new file mode 100644 index 00000000000..f3bcc9afa94 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/users/components/UserListItemIdentity.tsx @@ -0,0 +1,71 @@ +/* + * 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 ( + +
+ {user.name} + {user.login} +
+ {user.email &&
{user.email}
} + {!user.local && + user.externalProvider !== 'sonarqube' && ( + + )} + + ); +} + +export function ExternalProvider({ identityProvider, user }: Props) { + if (!identityProvider) { + return ( +
+ + {user.externalProvider}: {user.externalIdentity} + +
+ ); + } + + return ( +
+
+ {identityProvider.name} + {user.externalIdentity} +
+
+ ); +} diff --git a/server/sonar-web/src/main/js/apps/users/components/UserScmAccountInput.tsx b/server/sonar-web/src/main/js/apps/users/components/UserScmAccountInput.tsx new file mode 100644 index 00000000000..55a54a75217 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/users/components/UserScmAccountInput.tsx @@ -0,0 +1,48 @@ +/* + * 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 { + handleChange = (event: React.SyntheticEvent) => + this.props.onChange(this.props.idx, event.currentTarget.value); + handleRemove = () => this.props.onRemove(this.props.idx); + + render() { + return ( +
+ + +
+ ); + } +} diff --git a/server/sonar-web/src/main/js/apps/users/components/UserScmAccounts.tsx b/server/sonar-web/src/main/js/apps/users/components/UserScmAccounts.tsx new file mode 100644 index 00000000000..a5c4334e76b --- /dev/null +++ b/server/sonar-web/src/main/js/apps/users/components/UserScmAccounts.tsx @@ -0,0 +1,68 @@ +/* + * 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 { + state: State = { showMore: false }; + + toggleShowMore = (evt: React.SyntheticEvent) => { + 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 ( + + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/users/components/UsersAppContainer.js b/server/sonar-web/src/main/js/apps/users/components/UsersAppContainer.js deleted file mode 100644 index 3e6bbd28b32..00000000000 --- a/server/sonar-web/src/main/js/apps/users/components/UsersAppContainer.js +++ /dev/null @@ -1,54 +0,0 @@ -/* - * 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 ( -
- -
-
- ); - } -} - -const mapStateToProps = state => ({ - currentUser: getCurrentUser(state) -}); - -export default connect(mapStateToProps)(UsersAppContainer); diff --git a/server/sonar-web/src/main/js/apps/users/components/UsersAppContainerOld.js b/server/sonar-web/src/main/js/apps/users/components/UsersAppContainerOld.js new file mode 100644 index 00000000000..3e6bbd28b32 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/users/components/UsersAppContainerOld.js @@ -0,0 +1,54 @@ +/* + * 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 ( +
+ +
+
+ ); + } +} + +const mapStateToProps = state => ({ + currentUser: getCurrentUser(state) +}); + +export default connect(mapStateToProps)(UsersAppContainer); diff --git a/server/sonar-web/src/main/js/apps/users/components/UsersSelectSearch.js b/server/sonar-web/src/main/js/apps/users/components/UsersSelectSearch.js deleted file mode 100644 index b22877a618e..00000000000 --- a/server/sonar-web/src/main/js/apps/users/components/UsersSelectSearch.js +++ /dev/null @@ -1,131 +0,0 @@ -/* - * 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, - handleValueChange: Option => void, - searchUsers: (string, number) => Promise<*>, - selectedUser?: Option -}; -*/ - -/*:: -type State = { - isLoading: boolean, - search: string, - searchResult: Array