postJSON,
RequestData
} from '../helpers/request';
+import { Paging } from '../app/types';
+import throwGlobalError from '../app/utils/throwGlobalError';
export interface Profile {
key: string;
projectCount?: number;
}
-export function searchQualityProfiles(data: {
+export interface SearchQualityProfilesParameters {
+ defaults?: boolean;
+ language?: string;
organization?: string;
- projectKey?: string;
-}): Promise<Profile[]> {
- return getJSON('/api/qualityprofiles/search', data).then(r => r.profiles);
+ project?: string;
+ qualityProfile?: string;
}
-export function getQualityProfiles(data: {
+export function searchQualityProfiles(
+ parameters: SearchQualityProfilesParameters
+): Promise<Profile[]> {
+ return getJSON('/api/qualityprofiles/search', parameters).then(r => r.profiles);
+}
+
+export function getQualityProfile(data: {
compareToSonarWay?: boolean;
profile: string;
}): Promise<any> {
export function dissociateProject(profileKey: string, projectKey: string): Promise<void> {
return post('/api/qualityprofiles/remove_project', { profileKey, projectKey });
}
+
+export interface SearchUsersGroupsParameters {
+ language: string;
+ organization?: string;
+ qualityProfile: string;
+ q?: string;
+ selected?: 'all' | 'selected' | 'deselected';
+}
+
+export interface SearchUsersResponse {
+ users: Array<{
+ avatar?: string;
+ login: string;
+ name: string;
+ selected?: boolean;
+ }>;
+ paging: Paging;
+}
+
+export function searchUsers(parameters: SearchUsersGroupsParameters): Promise<SearchUsersResponse> {
+ return getJSON('/api/qualityprofiles/search_users', parameters).catch(throwGlobalError);
+}
+
+export interface SearchGroupsResponse {
+ groups: Array<{ name: string }>;
+ paging: Paging;
+}
+
+export function searchGroups(
+ parameters: SearchUsersGroupsParameters
+): Promise<SearchGroupsResponse> {
+ return getJSON('/api/qualityprofiles/search_groups', parameters).catch(throwGlobalError);
+}
+
+export interface AddRemoveUserParameters {
+ language: string;
+ login: string;
+ organization?: string;
+ qualityProfile: string;
+}
+
+export function addUser(parameters: AddRemoveUserParameters): Promise<void | Response> {
+ return post('/api/qualityprofiles/add_user', parameters).catch(throwGlobalError);
+}
+
+export function removeUser(parameters: AddRemoveUserParameters): Promise<void | Response> {
+ return post('/api/qualityprofiles/remove_user', parameters).catch(throwGlobalError);
+}
+
+export interface AddRemoveGroupParameters {
+ group: string;
+ language: string;
+ organization?: string;
+ qualityProfile: string;
+}
+
+export function addGroup(parameters: AddRemoveGroupParameters): Promise<void | Response> {
+ return post('/api/qualityprofiles/add_group', parameters).catch(throwGlobalError);
+}
+
+export function removeGroup(parameters: AddRemoveGroupParameters): Promise<void | Response> {
+ return post('/api/qualityprofiles/remove_group', parameters).catch(throwGlobalError);
+}
*/
import React from 'react';
import PropTypes from 'prop-types';
-import GroupIcon from './GroupIcon';
+import GroupIcon from '../../../../components/icons-components/GroupIcon';
export default class GroupHolder extends React.PureComponent {
static propTypes = {
+++ /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';
-
-const GroupIcon = () => {
- /* eslint max-len: 0 */
- return (
- <div style={{ padding: '4px 3px 0' }}>
- <svg xmlns="http://www.w3.org/2000/svg" width="30" height="28" viewBox="0 0 480 448">
- <path
- fill="#aaa"
- d="M148.25 224q-40.5 1.25-66.25 32H48.5Q28 256 14 245.875T0 216.25Q0 128 31 128q1.5 0 10.875 5.25t24.375 10.625T96 149.25q16.75 0 33.25-5.75Q128 152.75 128 160q0 34.75 20.25 64zM416 383.25q0 30-18.25 47.375T349.25 448h-218.5q-30.25 0-48.5-17.375T64 383.25q0-13.25.875-25.875t3.5-27.25T75 303t10.75-24.375 15.5-20.25T122.625 245t27.875-5q2.5 0 10.75 5.375t18.25 12 26.75 12T240 274.75t33.75-5.375 26.75-12 18.25-12T329.5 240q15.25 0 27.875 5t21.375 13.375 15.5 20.25T405 303t6.625 27.125 3.5 27.25.875 25.875zM160 64q0 26.5-18.75 45.25T96 128t-45.25-18.75T32 64t18.75-45.25T96 0t45.25 18.75T160 64zm176 96q0 39.75-28.125 67.875T240 256t-67.875-28.125T144 160t28.125-67.875T240 64t67.875 28.125T336 160zm144 56.25q0 19.5-14 29.625T431.5 256H398q-25.75-30.75-66.25-32Q352 194.75 352 160q0-7.25-1.25-16.5 16.5 5.75 33.25 5.75 14.75 0 29.75-5.375t24.375-10.625T449 128q31 0 31 88.25zM448 64q0 26.5-18.75 45.25T384 128t-45.25-18.75T320 64t18.75-45.25T384 0t45.25 18.75T448 64z"
- />
- </svg>
- </div>
- );
-};
-
-export default GroupIcon;
const organization = this.props.customOrganizations ? component.organization : undefined;
Promise.all([
searchQualityProfiles({ organization }),
- searchQualityProfiles({ organization, projectKey: component.key })
+ searchQualityProfiles({ organization, project: component.key })
]).then(
([allProfiles, profiles]) => {
if (this.mounted) {
mount(<App component={component} customOrganizations={false} />);
expect(searchQualityProfiles.mock.calls).toHaveLength(2);
expect(searchQualityProfiles).toBeCalledWith({ organization: undefined });
- expect(searchQualityProfiles).toBeCalledWith({ organization: undefined, projectKey: 'foo' });
+ expect(searchQualityProfiles).toBeCalledWith({ organization: undefined, project: 'foo' });
});
it('fetches profiles with organization', () => {
mount(<App component={component} customOrganizations={true} />);
expect(searchQualityProfiles.mock.calls).toHaveLength(2);
expect(searchQualityProfiles).toBeCalledWith({ organization: 'org' });
- expect(searchQualityProfiles).toBeCalledWith({ organization: 'org', projectKey: 'foo' });
+ expect(searchQualityProfiles).toBeCalledWith({ organization: 'org', project: 'foo' });
});
it('changes profile', () => {
this.state.events.length < this.state.total;
return (
- <div className="quality-profile-box js-profile-changelog">
+ <div className="boxed-group boxed-group-inner js-profile-changelog">
<header className="spacer-bottom">
<ChangelogSearch
fromDate={query.since}
const { left, right, inLeft, inRight, modified } = this.state;
return (
- <div className="quality-profile-box js-profile-comparison">
- <header className="spacer-bottom">
+ <div className="boxed-group boxed-group-inner js-profile-comparison">
+ <header>
<ComparisonForm
withKey={withKey}
profile={profile}
right != null &&
inRight != null &&
modified != null && (
- <ComparisonResults
- left={left}
- right={right}
- inLeft={inLeft}
- inRight={inRight}
- modified={modified}
- organization={this.props.organization}
- />
+ <div className="spacer-top">
+ <ComparisonResults
+ left={left}
+ right={right}
+ inLeft={inLeft}
+ inRight={inRight}
+ modified={modified}
+ organization={this.props.organization}
+ />
+ </div>
)}
</div>
);
import ProfileProjects from './ProfileProjects';
import ProfileInheritance from './ProfileInheritance';
import ProfileExporters from './ProfileExporters';
+import ProfilePermissions from './ProfilePermissions';
import { Exporter, Profile } from '../types';
interface Props {
<div className="quality-profile-grid-left">
<ProfileRules {...props} />
<ProfileExporters {...props} />
+ {props.canAdmin &&
+ !props.profile.isBuiltIn && (
+ <ProfilePermissions
+ organization={props.organization || undefined}
+ profile={props.profile}
+ />
+ )}
</div>
<div className="quality-profile-grid-right">
<ProfileInheritance {...props} />
}
return (
- <div className="quality-profile-box quality-profile-exporters">
- <header className="big-spacer-bottom">
- <h2>{translate('quality_profiles.exporters')}</h2>
- </header>
- <ul>
- {exportersForLanguage.map(exporter => (
- <li key={exporter.key} data-key={exporter.key} className="spacer-top">
- <a href={this.getExportUrl(exporter)} target="_blank">
- {exporter.name}
- </a>
- </li>
- ))}
- </ul>
+ <div className="boxed-group quality-profile-exporters">
+ <h2>{translate('quality_profiles.exporters')}</h2>
+ <div className="boxed-group-inner">
+ <ul>
+ {exportersForLanguage.map((exporter, index) => (
+ <li
+ key={exporter.key}
+ data-key={exporter.key}
+ className={index > 0 ? 'spacer-top' : undefined}>
+ <a href={this.getExportUrl(exporter)} target="_blank">
+ {exporter.name}
+ </a>
+ </li>
+ ))}
+ </ul>
+ </div>
</div>
);
}
const extendsBuiltIn = ancestors != null && ancestors.some(profile => profile.isBuiltIn);
return (
- <div className="quality-profile-inheritance">
- <header className="big-spacer-bottom clearfix">
- <h2 className="pull-left">{translate('quality_profiles.profile_inheritance')}</h2>
- {this.props.canAdmin &&
- !this.props.profile.isBuiltIn && (
+ <div className="boxed-group quality-profile-inheritance">
+ {this.props.canAdmin &&
+ !this.props.profile.isBuiltIn && (
+ <div className="boxed-group-actions">
<button className="pull-right js-change-parent" onClick={this.handleChangeParentClick}>
{translate('quality_profiles.change_parent')}
</button>
- )}
+ </div>
+ )}
+
+ <header className="boxed-group-header">
+ <h2>{translate('quality_profiles.profile_inheritance')}</h2>
</header>
- {!this.state.loading && (
- <table className="data zebra">
- <tbody>
- {ancestors != null &&
- ancestors.map((ancestor, index) => (
+ <div className="boxed-group-inner">
+ {this.state.loading ? (
+ <i className="spinner" />
+ ) : (
+ <table className="data zebra">
+ <tbody>
+ {ancestors != null &&
+ ancestors.map((ancestor, index) => (
+ <ProfileInheritanceBox
+ className="js-inheritance-ancestor"
+ depth={index}
+ key={ancestor.key}
+ language={profile.language}
+ organization={this.props.organization}
+ profile={ancestor}
+ />
+ ))}
+
+ {this.state.profile != null && (
<ProfileInheritanceBox
- className="js-inheritance-ancestor"
- depth={index}
- key={ancestor.key}
+ className={currentClassName}
+ depth={ancestors ? ancestors.length : 0}
+ displayLink={false}
+ extendsBuiltIn={extendsBuiltIn}
language={profile.language}
organization={this.props.organization}
- profile={ancestor}
+ profile={this.state.profile}
/>
- ))}
-
- {this.state.profile != null && (
- <ProfileInheritanceBox
- className={currentClassName}
- depth={ancestors ? ancestors.length : 0}
- displayLink={false}
- extendsBuiltIn={extendsBuiltIn}
- language={profile.language}
- organization={this.props.organization}
- profile={this.state.profile}
- />
- )}
-
- {this.state.children != null &&
- this.state.children.map(child => (
- <ProfileInheritanceBox
- className="js-inheritance-child"
- depth={ancestors ? ancestors.length + 1 : 0}
- key={child.key}
- language={profile.language}
- organization={this.props.organization}
- profile={child}
- />
- ))}
- </tbody>
- </table>
- )}
+ )}
+
+ {this.state.children != null &&
+ this.state.children.map(child => (
+ <ProfileInheritanceBox
+ className="js-inheritance-child"
+ depth={ancestors ? ancestors.length + 1 : 0}
+ key={child.key}
+ language={profile.language}
+ organization={this.props.organization}
+ profile={child}
+ />
+ ))}
+ </tbody>
+ </table>
+ )}
+ </div>
{this.state.formOpen && (
<ChangeParentForm
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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 { sortBy, uniqBy } from 'lodash';
+import ProfilePermissionsUser from './ProfilePermissionsUser';
+import ProfilePermissionsGroup from './ProfilePermissionsGroup';
+import ProfilePermissionsForm from './ProfilePermissionsForm';
+import {
+ searchUsers,
+ searchGroups,
+ SearchUsersGroupsParameters
+} from '../../../api/quality-profiles';
+import { translate } from '../../../helpers/l10n';
+
+export interface User {
+ avatar?: string;
+ login: string;
+ name: string;
+}
+
+export interface Group {
+ name: string;
+}
+
+interface Props {
+ organization?: string;
+ profile: { language: string; name: string };
+}
+
+interface State {
+ addUserForm: boolean;
+ groups?: Group[];
+ loading: boolean;
+ users?: User[];
+}
+
+export default class ProfilePermissions extends React.PureComponent<Props, State> {
+ mounted: boolean;
+ state: State = { addUserForm: false, loading: true };
+
+ componentDidMount() {
+ this.mounted = true;
+ this.fetchUsersAndGroups();
+ }
+
+ componentDidUpdate(prevProps: Props) {
+ if (
+ prevProps.organization !== this.props.organization ||
+ prevProps.profile !== this.props.profile
+ ) {
+ this.fetchUsersAndGroups();
+ }
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ fetchUsersAndGroups() {
+ this.setState({ loading: true });
+ const { organization, profile } = this.props;
+ const parameters: SearchUsersGroupsParameters = {
+ language: profile.language,
+ organization,
+ qualityProfile: profile.name,
+ selected: 'selected'
+ };
+ Promise.all([searchUsers(parameters), searchGroups(parameters)]).then(
+ ([usersResponse, groupsResponse]) => {
+ if (this.mounted) {
+ this.setState({
+ groups: groupsResponse.groups,
+ loading: false,
+ users: usersResponse.users
+ });
+ }
+ },
+ () => {
+ if (this.mounted) {
+ this.setState({ loading: false });
+ }
+ }
+ );
+ }
+
+ handleAddUserButtonClick = (event: React.SyntheticEvent<HTMLButtonElement>) => {
+ event.preventDefault();
+ event.currentTarget.blur();
+ this.setState({ addUserForm: true });
+ };
+
+ handleAddUserFormClose = () => {
+ if (this.mounted) {
+ this.setState({ addUserForm: false });
+ }
+ };
+
+ handleUserAdd = (addedUser: User) => {
+ if (this.mounted) {
+ this.setState((state: State) => ({
+ addUserForm: false,
+ users: state.users && uniqBy([...state.users, addedUser], user => user.login)
+ }));
+ }
+ };
+
+ handleUserDelete = (removedUser: User) => {
+ if (this.mounted) {
+ this.setState((state: State) => ({
+ users: state.users && state.users.filter(user => user !== removedUser)
+ }));
+ }
+ };
+
+ handleGroupAdd = (addedGroup: Group) => {
+ if (this.mounted) {
+ this.setState((state: State) => ({
+ addUserForm: false,
+ groups: state.groups && uniqBy([...state.groups, addedGroup], group => group.name)
+ }));
+ }
+ };
+
+ handleGroupDelete = (removedGroup: Group) => {
+ if (this.mounted) {
+ this.setState((state: State) => ({
+ groups: state.groups && state.groups.filter(group => group !== removedGroup)
+ }));
+ }
+ };
+
+ render() {
+ return (
+ <div className="boxed-group">
+ <h2>{translate('permissions.page')}</h2>
+ <div className="boxed-group-inner">
+ <p className="note">{translate('quality_profiles.default_permissions')}</p>
+
+ {this.state.loading ? (
+ <div className="big-spacer-top">
+ <i className="spinner" />
+ </div>
+ ) : (
+ <div className="big-spacer-top">
+ {this.state.users &&
+ sortBy(this.state.users, 'name').map(user => (
+ <ProfilePermissionsUser
+ key={user.login}
+ onDelete={this.handleUserDelete}
+ organization={this.props.organization}
+ profile={this.props.profile}
+ user={user}
+ />
+ ))}
+ {this.state.groups &&
+ sortBy(this.state.groups, 'name').map(group => (
+ <ProfilePermissionsGroup
+ group={group}
+ key={group.name}
+ onDelete={this.handleGroupDelete}
+ organization={this.props.organization}
+ profile={this.props.profile}
+ />
+ ))}
+ <div className="text-right">
+ <button onClick={this.handleAddUserButtonClick}>
+ {translate('quality_profiles.grant_permissions_to_more_users')}
+ </button>
+ </div>
+ </div>
+ )}
+ </div>
+
+ {this.state.addUserForm && (
+ <ProfilePermissionsForm
+ onClose={this.handleAddUserFormClose}
+ onGroupAdd={this.handleGroupAdd}
+ onUserAdd={this.handleUserAdd}
+ profile={this.props.profile}
+ organization={this.props.organization}
+ />
+ )}
+ </div>
+ );
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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 'react-modal';
+import ProfilePermissionsFormSelect from './ProfilePermissionsFormSelect';
+import {
+ searchUsers,
+ searchGroups,
+ addUser,
+ addGroup,
+ SearchUsersGroupsParameters
+} from '../../../api/quality-profiles';
+import { translate } from '../../../helpers/l10n';
+import { User, Group } from './ProfilePermissions';
+
+interface Props {
+ onClose: () => void;
+ onGroupAdd: (group: Group) => void;
+ onUserAdd: (user: User) => void;
+ organization?: string;
+ profile: { language: string; name: string };
+}
+
+interface State {
+ selected?: User | Group;
+ submitting: boolean;
+}
+
+export default class ProfilePermissionsForm extends React.PureComponent<Props, State> {
+ mounted: boolean;
+ state: State = { submitting: false };
+
+ componentDidMount() {
+ this.mounted = true;
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ stopSubmitting = () => {
+ if (this.mounted) {
+ this.setState({ submitting: false });
+ }
+ };
+
+ handleUserAdd = (user: User) =>
+ addUser({
+ language: this.props.profile.language,
+ login: user.login,
+ organization: this.props.organization,
+ qualityProfile: this.props.profile.name
+ }).then(() => this.props.onUserAdd(user), this.stopSubmitting);
+
+ handleGroupAdd = (group: Group) =>
+ addGroup({
+ group: group.name,
+ language: this.props.profile.language,
+ organization: this.props.organization,
+ qualityProfile: this.props.profile.name
+ }).then(() => this.props.onGroupAdd(group), this.stopSubmitting);
+
+ handleFormSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => {
+ event.preventDefault();
+ const { selected } = this.state;
+ if (selected) {
+ this.setState({ submitting: true });
+ if ((selected as User).login != undefined) {
+ this.handleUserAdd(selected as User);
+ } else {
+ this.handleGroupAdd(selected as Group);
+ }
+ }
+ };
+
+ handleSearch = (q: string) => {
+ const { organization, profile } = this.props;
+ const parameters: SearchUsersGroupsParameters = {
+ language: profile.language,
+ organization,
+ q,
+ qualityProfile: profile.name,
+ selected: 'deselected'
+ };
+ return Promise.all([
+ searchUsers(parameters),
+ searchGroups(parameters)
+ ]).then(([usersResponse, groupsResponse]) => [
+ ...usersResponse.users,
+ ...groupsResponse.groups
+ ]);
+ };
+
+ handleValueChange = (selected: User | Group) => {
+ this.setState({ selected });
+ };
+
+ render() {
+ const header = translate('quality_profiles.grant_permissions_to_user_or_group');
+ const submitDisabled = !this.state.selected || this.state.submitting;
+ return (
+ <Modal
+ isOpen={true}
+ contentLabel={header}
+ className="modal"
+ overlayClassName="modal-overlay"
+ onRequestClose={this.props.onClose}>
+ <header className="modal-head">
+ <h2>{header}</h2>
+ </header>
+ <form onSubmit={this.handleFormSubmit}>
+ <div className="modal-body">
+ <div className="modal-large-field">
+ <label>{translate('quality_profiles.search_description')}</label>
+ <ProfilePermissionsFormSelect
+ selected={this.state.selected}
+ onChange={this.handleValueChange}
+ onSearch={this.handleSearch}
+ />
+ </div>
+ </div>
+ <footer className="modal-foot">
+ {this.state.submitting && <i className="spinner spacer-right" />}
+ <button disabled={submitDisabled} type="submit">
+ {translate('add_verb')}
+ </button>
+ <button className="button-link" onClick={this.props.onClose} type="reset">
+ {translate('cancel')}
+ </button>
+ </footer>
+ </form>
+ </Modal>
+ );
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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 Select from 'react-select';
+import { debounce, identity } from 'lodash';
+import { User, Group } from './ProfilePermissions';
+import Avatar from '../../../components/ui/Avatar';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
+import GroupIcon from '../../../components/icons-components/GroupIcon';
+
+type Option = User | Group;
+type OptionWithValue = Option & { value: string };
+
+interface Props {
+ onChange: (option: OptionWithValue) => void;
+ onSearch: (query: string) => Promise<Option[]>;
+ selected?: Option;
+}
+
+interface State {
+ loading: boolean;
+ query: string;
+ searchResults: Option[];
+}
+
+export default class ProfilePermissionsFormSelect extends React.PureComponent<Props, State> {
+ mounted: boolean;
+
+ constructor(props: Props) {
+ super(props);
+ this.handleSearch = debounce(this.handleSearch, 250);
+ this.state = { loading: false, query: '', searchResults: [] };
+ }
+
+ componentDidMount() {
+ this.mounted = true;
+ this.handleSearch(this.state.query);
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ handleSearch = (query: string) => {
+ this.setState({ loading: true });
+ this.props.onSearch(query).then(
+ searchResults => {
+ if (this.mounted) {
+ this.setState({ loading: false, searchResults });
+ }
+ },
+ () => {
+ if (this.mounted) {
+ this.setState({ loading: false });
+ }
+ }
+ );
+ };
+
+ handleInputChange = (query: string) => {
+ this.setState({ query });
+ if (query.length > 1) {
+ this.handleSearch(query);
+ }
+ };
+
+ render() {
+ const noResultsText =
+ this.state.query.length === 1
+ ? translateWithParameters('select2.tooShort', 2)
+ : translate('no_results');
+
+ // create a uniq string both for users and groups
+ const options = this.state.searchResults.map(r => ({ ...r, value: getStringValue(r) }));
+
+ return (
+ <Select
+ autofocus={true}
+ className="Select-big"
+ clearable={false}
+ isLoading={this.state.loading}
+ // disable default react-select filtering
+ filterOptions={identity}
+ noResultsText={noResultsText}
+ optionRenderer={optionRenderer}
+ options={options}
+ onChange={this.props.onChange}
+ onInputChange={this.handleInputChange}
+ placeholder=""
+ searchable={true}
+ value={this.props.selected && getStringValue(this.props.selected)}
+ valueRenderer={optionRenderer}
+ />
+ );
+ }
+}
+
+function isUser(option: Option): option is User {
+ return (option as User).login != undefined;
+}
+
+function getStringValue(option: Option) {
+ return isUser(option) ? `user:${option.login}` : `group:${option.name}`;
+}
+
+function optionRenderer(option: OptionWithValue) {
+ return isUser(option) ? (
+ <div>
+ <Avatar hash={option.avatar} name={option.name} size={16} />
+ <strong className="spacer-left">{option.name}</strong>
+ <span className="note little-spacer-left">{option.login}</span>
+ </div>
+ ) : (
+ <div>
+ <GroupIcon size={16} />
+ <strong className="spacer-left">{option.name}</strong>
+ </div>
+ );
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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 { FormattedMessage } from 'react-intl';
+import { Group } from './ProfilePermissions';
+import { removeGroup } from '../../../api/quality-profiles';
+import SimpleModal, { ChildrenProps } from '../../../components/controls/SimpleModal';
+import DeleteIcon from '../../../components/icons-components/DeleteIcon';
+import GroupIcon from '../../../components/icons-components/GroupIcon';
+import { translate } from '../../../helpers/l10n';
+
+interface Props {
+ group: Group;
+ onDelete: (group: Group) => void;
+ organization?: string;
+ profile: { language: string; name: string };
+}
+
+interface State {
+ deleteModal: boolean;
+}
+
+export default class ProfilePermissionsGroup extends React.PureComponent<Props, State> {
+ mounted: boolean;
+ state: State = { deleteModal: false };
+
+ componentDidMount() {
+ this.mounted = true;
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ handleDeleteClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
+ event.preventDefault();
+ event.currentTarget.blur();
+ this.setState({ deleteModal: true });
+ };
+
+ handleDeleteModalClose = () => {
+ if (this.mounted) {
+ this.setState({ deleteModal: false });
+ }
+ };
+
+ handleDelete = () => {
+ const { group, organization, profile } = this.props;
+
+ return removeGroup({
+ group: group.name,
+ language: profile.language,
+ organization,
+ qualityProfile: profile.name
+ }).then(() => {
+ this.handleDeleteModalClose();
+ this.props.onDelete(group);
+ });
+ };
+
+ renderDeleteModal = (props: ChildrenProps) => (
+ <div>
+ <header className="modal-head">
+ <h2>{translate('groups.remove')}</h2>
+ </header>
+
+ <div className="modal-body">
+ <FormattedMessage
+ defaultMessage={translate('groups.remove.confirmation')}
+ id="groups.remove.confirmation"
+ values={{
+ user: <strong>{this.props.group.name}</strong>
+ }}
+ />
+ </div>
+
+ <footer className="modal-foot">
+ {props.submitting && <i className="spinner spacer-right" />}
+ <button className="button-red" disabled={props.submitting} onClick={props.onSubmitClick}>
+ {translate('remove')}
+ </button>
+ <a href="#" onClick={props.onCloseClick}>
+ {translate('cancel')}
+ </a>
+ </footer>
+ </div>
+ );
+
+ render() {
+ const { group } = this.props;
+
+ return (
+ <div className="clearfix big-spacer-bottom">
+ <a
+ className="pull-right spacer-top spacer-left spacer-right button-icon"
+ href="#"
+ onClick={this.handleDeleteClick}>
+ <DeleteIcon />
+ </a>
+ <GroupIcon className="pull-left spacer-right" size={32} />
+ <div className="overflow-hidden" style={{ lineHeight: '32px' }}>
+ <strong>{group.name}</strong>
+ </div>
+
+ {this.state.deleteModal && (
+ <SimpleModal
+ header={translate('group.remove')}
+ onClose={this.handleDeleteModalClose}
+ onSubmit={this.handleDelete}>
+ {this.renderDeleteModal}
+ </SimpleModal>
+ )}
+ </div>
+ );
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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 { FormattedMessage } from 'react-intl';
+import { User } from './ProfilePermissions';
+import { removeUser } from '../../../api/quality-profiles';
+import SimpleModal, { ChildrenProps } from '../../../components/controls/SimpleModal';
+import DeleteIcon from '../../../components/icons-components/DeleteIcon';
+import Avatar from '../../../components/ui/Avatar';
+import { translate } from '../../../helpers/l10n';
+
+interface Props {
+ onDelete: (user: User) => void;
+ organization?: string;
+ profile: { language: string; name: string };
+ user: User;
+}
+
+interface State {
+ deleteModal: boolean;
+}
+
+export default class ProfilePermissionsUser extends React.PureComponent<Props, State> {
+ mounted: boolean;
+ state: State = { deleteModal: false };
+
+ componentDidMount() {
+ this.mounted = true;
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ handleDeleteClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
+ event.preventDefault();
+ event.currentTarget.blur();
+ this.setState({ deleteModal: true });
+ };
+
+ handleDeleteModalClose = () => {
+ if (this.mounted) {
+ this.setState({ deleteModal: false });
+ }
+ };
+
+ handleDelete = () => {
+ const { organization, profile, user } = this.props;
+
+ return removeUser({
+ language: profile.language,
+ login: user.login,
+ organization,
+ qualityProfile: profile.name
+ }).then(() => {
+ this.handleDeleteModalClose();
+ this.props.onDelete(user);
+ });
+ };
+
+ renderDeleteModal = (props: ChildrenProps) => (
+ <div>
+ <header className="modal-head">
+ <h2>{translate('users.remove')}</h2>
+ </header>
+
+ <div className="modal-body">
+ <FormattedMessage
+ defaultMessage={translate('users.remove.confirmation')}
+ id="users.remove.confirmation"
+ values={{
+ user: <strong>{this.props.user.name}</strong>
+ }}
+ />
+ </div>
+
+ <footer className="modal-foot">
+ {props.submitting && <i className="spinner spacer-right" />}
+ <button className="button-red" disabled={props.submitting} onClick={props.onSubmitClick}>
+ {translate('remove')}
+ </button>
+ <a href="#" onClick={props.onCloseClick}>
+ {translate('cancel')}
+ </a>
+ </footer>
+ </div>
+ );
+
+ render() {
+ const { user } = this.props;
+
+ return (
+ <div className="clearfix big-spacer-bottom">
+ <a
+ className="pull-right spacer-top spacer-left spacer-right button-icon"
+ href="#"
+ onClick={this.handleDeleteClick}>
+ <DeleteIcon />
+ </a>
+ <Avatar className="pull-left spacer-right" hash={user.avatar} name={user.name} size={32} />
+ <div className="overflow-hidden">
+ <strong>{user.name}</strong>
+ <div className="note">{user.login}</div>
+ </div>
+
+ {this.state.deleteModal && (
+ <SimpleModal
+ header={translate('users.remove')}
+ onClose={this.handleDeleteModalClose}
+ onSubmit={this.handleDelete}>
+ {this.renderDeleteModal}
+ </SimpleModal>
+ )}
+ </div>
+ );
+ }
+}
loadProjects() {
if (this.props.profile.isDefault) {
+ this.setState({ loading: false });
return;
}
render() {
return (
- <div className="quality-profile-projects">
- <header className="page-header">
- <h2 className="page-title">{translate('projects')}</h2>
-
- {this.props.canAdmin &&
- !this.props.profile.isDefault && (
- <div className="pull-right">
- <button className="js-change-projects" onClick={this.handleChangeClick}>
- {translate('quality_profiles.change_projects')}
- </button>
- </div>
- )}
+ <div className="boxed-group quality-profile-projects">
+ {this.props.canAdmin &&
+ !this.props.profile.isDefault && (
+ <div className="boxed-group-actions">
+ <button className="js-change-projects" onClick={this.handleChangeClick}>
+ {translate('quality_profiles.change_projects')}
+ </button>
+ </div>
+ )}
+
+ <header className="boxed-group-header">
+ <h2>{translate('projects')}</h2>
</header>
- {this.props.profile.isDefault ? this.renderDefault() : this.renderProjects()}
+ <div className="boxed-group-inner">
+ {this.state.loading ? (
+ <i className="spinner" />
+ ) : this.props.profile.isDefault ? (
+ this.renderDefault()
+ ) : (
+ this.renderProjects()
+ )}
+ </div>
{this.state.formOpen && (
<ChangeProjectsForm
import ProfileRulesDeprecatedWarning from './ProfileRulesDeprecatedWarning';
import ProfileRulesSonarWayComparison from './ProfileRulesSonarWayComparison';
import { searchRules, takeFacet } from '../../../api/rules';
-import { getQualityProfiles } from '../../../api/quality-profiles';
+import { getQualityProfile } from '../../../api/quality-profiles';
import { getRulesUrl } from '../../../helpers/urls';
import { translate } from '../../../helpers/l10n';
import { Profile } from '../types';
if (this.props.profile.isBuiltIn) {
return Promise.resolve(null);
}
- return getQualityProfiles({
+ return getQualityProfile({
compareToSonarWay: true,
profile: this.props.profile.key
});
);
return (
- <div className="quality-profile-rules">
+ <div className="boxed-group quality-profile-rules">
<div className="quality-profile-rules-distribution">
<table className="data condensed">
<thead>
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.
+ */
+jest.mock('../../../../api/quality-profiles', () => ({
+ searchUsers: jest.fn(() => Promise.resolve([])),
+ searchGroups: jest.fn(() => Promise.resolve([]))
+}));
+
+import * as React from 'react';
+import { mount, shallow } from 'enzyme';
+import ProfilePermissions from '../ProfilePermissions';
+import { click } from '../../../../helpers/testUtils';
+
+const searchUsers = require('../../../../api/quality-profiles').searchUsers as jest.Mock<any>;
+const searchGroups = require('../../../../api/quality-profiles').searchGroups as jest.Mock<any>;
+
+const profile = { name: 'Sonar way', language: 'js' };
+
+beforeEach(() => {
+ searchUsers.mockClear();
+ searchGroups.mockClear();
+});
+
+it('renders', () => {
+ const wrapper = shallow(<ProfilePermissions profile={profile} />);
+ expect(wrapper).toMatchSnapshot();
+
+ wrapper.setState({
+ groups: [{ name: 'Lambda' }],
+ loading: false,
+ users: [{ login: 'luke', name: 'Luke Skywalker' }]
+ });
+ expect(wrapper).toMatchSnapshot();
+});
+
+it('opens add users form', () => {
+ const wrapper = shallow(<ProfilePermissions profile={profile} />);
+ (wrapper.instance() as ProfilePermissions).mounted = true;
+ wrapper.setState({ loading: false, users: [{ login: 'luke', name: 'Luke Skywalker' }] });
+ expect(wrapper.find('ProfilePermissionsForm').exists()).toBeFalsy();
+
+ click(wrapper.find('button'));
+ expect(wrapper.find('ProfilePermissionsForm').exists()).toBeTruthy();
+
+ wrapper.find('ProfilePermissionsForm').prop<Function>('onClose')();
+ expect(wrapper.find('ProfilePermissionsForm').exists()).toBeFalsy();
+});
+
+it('removes user', () => {
+ const wrapper = shallow(<ProfilePermissions profile={profile} />);
+ (wrapper.instance() as ProfilePermissions).mounted = true;
+
+ const joda = { login: 'joda', name: 'Joda' };
+ wrapper.setState({ loading: false, users: [{ login: 'luke', name: 'Luke Skywalker' }, joda] });
+ expect(wrapper.find('ProfilePermissionsUser')).toHaveLength(2);
+
+ wrapper
+ .find('ProfilePermissionsUser')
+ .first()
+ .prop<Function>('onDelete')(joda);
+ wrapper.update();
+ expect(wrapper.find('ProfilePermissionsUser')).toHaveLength(1);
+});
+
+it('removes group', () => {
+ const wrapper = shallow(<ProfilePermissions profile={profile} />);
+ (wrapper.instance() as ProfilePermissions).mounted = true;
+
+ const lambda = { name: 'Lambda' };
+ wrapper.setState({ loading: false, groups: [{ name: 'Atlas' }, lambda] });
+ expect(wrapper.find('ProfilePermissionsGroup')).toHaveLength(2);
+
+ wrapper
+ .find('ProfilePermissionsGroup')
+ .first()
+ .prop<Function>('onDelete')(lambda);
+ wrapper.update();
+ expect(wrapper.find('ProfilePermissionsGroup')).toHaveLength(1);
+});
+
+it('fetches users and groups on mount', () => {
+ mount(<ProfilePermissions organization="org" profile={profile} />);
+ expect(searchUsers).toBeCalledWith({
+ language: 'js',
+ organization: 'org',
+ qualityProfile: 'Sonar way',
+ selected: 'selected'
+ });
+ expect(searchGroups).toBeCalledWith({
+ language: 'js',
+ organization: 'org',
+ qualityProfile: 'Sonar way',
+ selected: 'selected'
+ });
+});
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.
+ */
+jest.mock('../../../../api/quality-profiles', () => ({
+ addUser: jest.fn(() => Promise.resolve()),
+ addGroup: jest.fn(() => Promise.resolve())
+}));
+
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import ProfilePermissionsForm from '../ProfilePermissionsForm';
+import { submit } from '../../../../helpers/testUtils';
+
+const addUser = require('../../../../api/quality-profiles').addUser as jest.Mock<any>;
+const addGroup = require('../../../../api/quality-profiles').addGroup as jest.Mock<any>;
+
+const profile = { language: 'js', name: 'Sonar way' };
+
+it('adds user', async () => {
+ const onUserAdd = jest.fn();
+ const wrapper = shallow(
+ <ProfilePermissionsForm
+ onClose={jest.fn()}
+ onGroupAdd={jest.fn()}
+ onUserAdd={onUserAdd}
+ organization="org"
+ profile={profile}
+ />
+ );
+ expect(wrapper).toMatchSnapshot();
+
+ wrapper.setState({ selected: { login: 'luke' } });
+ expect(wrapper).toMatchSnapshot();
+
+ submit(wrapper.find('form'));
+ expect(wrapper).toMatchSnapshot();
+ expect(addUser).toBeCalledWith({
+ language: 'js',
+ login: 'luke',
+ organization: 'org',
+ qualityProfile: 'Sonar way'
+ });
+
+ await new Promise(setImmediate);
+ expect(onUserAdd).toBeCalledWith({ login: 'luke' });
+});
+
+it('adds group', async () => {
+ const onGroupAdd = jest.fn();
+ const wrapper = shallow(
+ <ProfilePermissionsForm
+ onClose={jest.fn()}
+ onGroupAdd={onGroupAdd}
+ onUserAdd={jest.fn()}
+ organization="org"
+ profile={profile}
+ />
+ );
+ expect(wrapper).toMatchSnapshot();
+
+ wrapper.setState({ selected: { name: 'lambda' } });
+ expect(wrapper).toMatchSnapshot();
+
+ submit(wrapper.find('form'));
+ expect(wrapper).toMatchSnapshot();
+ expect(addGroup).toBeCalledWith({
+ group: 'lambda',
+ language: 'js',
+ organization: 'org',
+ qualityProfile: 'Sonar way'
+ });
+
+ await new Promise(setImmediate);
+ expect(onGroupAdd).toBeCalledWith({ name: 'lambda' });
+});
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.
+ */
+jest.mock('lodash', () => {
+ const lodash = require.requireActual('lodash');
+ lodash.debounce = (fn: Function) => (...args: any[]) => fn(...args);
+ return lodash;
+});
+
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import ProfilePermissionsFormSelect from '../ProfilePermissionsFormSelect';
+
+it('renders', () => {
+ expect(
+ shallow(
+ <ProfilePermissionsFormSelect
+ onChange={jest.fn()}
+ onSearch={jest.fn()}
+ selected={{ name: 'lambda' }}
+ />
+ )
+ ).toMatchSnapshot();
+});
+
+it('searches', () => {
+ const onSearch = jest.fn(() => Promise.resolve([]));
+ const wrapper = shallow(
+ <ProfilePermissionsFormSelect
+ onChange={jest.fn()}
+ onSearch={onSearch}
+ selected={{ name: 'lambda' }}
+ />
+ );
+
+ wrapper.prop<Function>('onInputChange')('f');
+ expect(onSearch).not.toBeCalled();
+
+ wrapper.prop<Function>('onInputChange')('foo');
+ expect(onSearch).toBeCalledWith('foo');
+});
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.
+ */
+jest.mock('../../../../api/quality-profiles', () => ({
+ removeGroup: jest.fn(() => Promise.resolve())
+}));
+
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import ProfilePermissionsGroup from '../ProfilePermissionsGroup';
+import { click } from '../../../../helpers/testUtils';
+
+const removeGroup = require('../../../../api/quality-profiles').removeGroup as jest.Mock<any>;
+
+const profile = { language: 'js', name: 'Sonar way' };
+const group = { name: 'lambda' };
+
+beforeEach(() => {
+ removeGroup.mockClear();
+});
+
+it('renders', () => {
+ expect(
+ shallow(<ProfilePermissionsGroup group={group} onDelete={jest.fn()} profile={profile} />)
+ ).toMatchSnapshot();
+});
+
+it('removes user', async () => {
+ const onDelete = jest.fn();
+ const wrapper = shallow(
+ <ProfilePermissionsGroup
+ group={group}
+ onDelete={onDelete}
+ organization="org"
+ profile={profile}
+ />
+ );
+ (wrapper.instance() as ProfilePermissionsGroup).mounted = true;
+ expect(wrapper.find('SimpleModal').exists()).toBeFalsy();
+
+ click(wrapper.find('a'));
+ expect(wrapper.find('SimpleModal').exists()).toBeTruthy();
+
+ wrapper.find('SimpleModal').prop<Function>('onSubmit')();
+ expect(removeGroup).toBeCalledWith({
+ group: 'lambda',
+ language: 'js',
+ organization: 'org',
+ qualityProfile: 'Sonar way'
+ });
+
+ await new Promise(setImmediate);
+ expect(onDelete).toBeCalledWith(group);
+});
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.
+ */
+jest.mock('../../../../api/quality-profiles', () => ({
+ removeUser: jest.fn(() => Promise.resolve())
+}));
+
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import ProfilePermissionsUser from '../ProfilePermissionsUser';
+import { click } from '../../../../helpers/testUtils';
+
+const removeUser = require('../../../../api/quality-profiles').removeUser as jest.Mock<any>;
+
+const profile = { language: 'js', name: 'Sonar way' };
+const user = { login: 'luke', name: 'Luke Skywalker' };
+
+beforeEach(() => {
+ removeUser.mockClear();
+});
+
+it('renders', () => {
+ expect(
+ shallow(<ProfilePermissionsUser onDelete={jest.fn()} profile={profile} user={user} />)
+ ).toMatchSnapshot();
+});
+
+it('removes user', async () => {
+ const onDelete = jest.fn();
+ const wrapper = shallow(
+ <ProfilePermissionsUser onDelete={onDelete} organization="org" profile={profile} user={user} />
+ );
+ (wrapper.instance() as ProfilePermissionsUser).mounted = true;
+ expect(wrapper.find('SimpleModal').exists()).toBeFalsy();
+
+ click(wrapper.find('a'));
+ expect(wrapper.find('SimpleModal').exists()).toBeTruthy();
+
+ wrapper.find('SimpleModal').prop<Function>('onSubmit')();
+ expect(removeUser).toBeCalledWith({
+ language: 'js',
+ login: 'luke',
+ organization: 'org',
+ qualityProfile: 'Sonar way'
+ });
+
+ await new Promise(setImmediate);
+ expect(onDelete).toBeCalledWith(user);
+});
// Mock api some api functions
(apiRules as any).searchRules = (data: any) =>
Promise.resolve(data.activation === 'true' ? apiResponseActive : apiResponseAll);
-(apiQP as any).getQualityProfiles = () =>
+(apiQP as any).getQualityProfile = () =>
Promise.resolve({
compareToSonarWay: {
profile: 'sonarway',
});
it('should not show sonarway comparison for built in profiles', () => {
- (apiQP as any).getQualityProfiles = jest.fn(() => Promise.resolve());
+ (apiQP as any).getQualityProfile = jest.fn(() => Promise.resolve());
const wrapper = shallow(
<ProfileRules canAdmin={true} organization={null} profile={{ ...PROFILE, isBuiltIn: true }} />
);
instance.loadRules();
return doAsync(() => {
wrapper.update();
- expect(apiQP.getQualityProfiles).toHaveBeenCalledTimes(0);
+ expect(apiQP.getQualityProfile).toHaveBeenCalledTimes(0);
expect(wrapper.find('ProfileRulesSonarWayComparison')).toHaveLength(0);
});
});
it('should not show sonarway comparison if there is no missing rules', () => {
- (apiQP as any).getQualityProfiles = jest.fn(() =>
+ (apiQP as any).getQualityProfile = jest.fn(() =>
Promise.resolve({
compareToSonarWay: {
profile: 'sonarway',
instance.loadRules();
return doAsync(() => {
wrapper.update();
- expect(apiQP.getQualityProfiles).toHaveBeenCalledTimes(1);
+ expect(apiQP.getQualityProfile).toHaveBeenCalledTimes(1);
expect(wrapper.find('ProfileRulesSonarWayComparison')).toHaveLength(0);
});
});
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders 1`] = `
+<div
+ className="boxed-group"
+>
+ <h2>
+ permissions.page
+ </h2>
+ <div
+ className="boxed-group-inner"
+ >
+ <p
+ className="note"
+ >
+ quality_profiles.default_permissions
+ </p>
+ <div
+ className="big-spacer-top"
+ >
+ <i
+ className="spinner"
+ />
+ </div>
+ </div>
+</div>
+`;
+
+exports[`renders 2`] = `
+<div
+ className="boxed-group"
+>
+ <h2>
+ permissions.page
+ </h2>
+ <div
+ className="boxed-group-inner"
+ >
+ <p
+ className="note"
+ >
+ quality_profiles.default_permissions
+ </p>
+ <div
+ className="big-spacer-top"
+ >
+ <ProfilePermissionsUser
+ onDelete={[Function]}
+ profile={
+ Object {
+ "language": "js",
+ "name": "Sonar way",
+ }
+ }
+ user={
+ Object {
+ "login": "luke",
+ "name": "Luke Skywalker",
+ }
+ }
+ />
+ <ProfilePermissionsGroup
+ group={
+ Object {
+ "name": "Lambda",
+ }
+ }
+ onDelete={[Function]}
+ profile={
+ Object {
+ "language": "js",
+ "name": "Sonar way",
+ }
+ }
+ />
+ <div
+ className="text-right"
+ >
+ <button
+ onClick={[Function]}
+ >
+ quality_profiles.grant_permissions_to_more_users
+ </button>
+ </div>
+ </div>
+ </div>
+</div>
+`;
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`adds group 1`] = `
+<Modal
+ ariaHideApp={true}
+ bodyOpenClassName="ReactModal__Body--open"
+ className="modal"
+ closeTimeoutMS={0}
+ contentLabel="quality_profiles.grant_permissions_to_user_or_group"
+ isOpen={true}
+ onRequestClose={[Function]}
+ overlayClassName="modal-overlay"
+ parentSelector={[Function]}
+ portalClassName="ReactModalPortal"
+ shouldCloseOnOverlayClick={true}
+>
+ <header
+ className="modal-head"
+ >
+ <h2>
+ quality_profiles.grant_permissions_to_user_or_group
+ </h2>
+ </header>
+ <form
+ onSubmit={[Function]}
+ >
+ <div
+ className="modal-body"
+ >
+ <div
+ className="modal-large-field"
+ >
+ <label>
+ quality_profiles.search_description
+ </label>
+ <ProfilePermissionsFormSelect
+ onChange={[Function]}
+ onSearch={[Function]}
+ />
+ </div>
+ </div>
+ <footer
+ className="modal-foot"
+ >
+ <button
+ disabled={true}
+ type="submit"
+ >
+ add_verb
+ </button>
+ <button
+ className="button-link"
+ onClick={[Function]}
+ type="reset"
+ >
+ cancel
+ </button>
+ </footer>
+ </form>
+</Modal>
+`;
+
+exports[`adds group 2`] = `
+<Modal
+ ariaHideApp={true}
+ bodyOpenClassName="ReactModal__Body--open"
+ className="modal"
+ closeTimeoutMS={0}
+ contentLabel="quality_profiles.grant_permissions_to_user_or_group"
+ isOpen={true}
+ onRequestClose={[Function]}
+ overlayClassName="modal-overlay"
+ parentSelector={[Function]}
+ portalClassName="ReactModalPortal"
+ shouldCloseOnOverlayClick={true}
+>
+ <header
+ className="modal-head"
+ >
+ <h2>
+ quality_profiles.grant_permissions_to_user_or_group
+ </h2>
+ </header>
+ <form
+ onSubmit={[Function]}
+ >
+ <div
+ className="modal-body"
+ >
+ <div
+ className="modal-large-field"
+ >
+ <label>
+ quality_profiles.search_description
+ </label>
+ <ProfilePermissionsFormSelect
+ onChange={[Function]}
+ onSearch={[Function]}
+ selected={
+ Object {
+ "name": "lambda",
+ }
+ }
+ />
+ </div>
+ </div>
+ <footer
+ className="modal-foot"
+ >
+ <button
+ disabled={false}
+ type="submit"
+ >
+ add_verb
+ </button>
+ <button
+ className="button-link"
+ onClick={[Function]}
+ type="reset"
+ >
+ cancel
+ </button>
+ </footer>
+ </form>
+</Modal>
+`;
+
+exports[`adds group 3`] = `
+<Modal
+ ariaHideApp={true}
+ bodyOpenClassName="ReactModal__Body--open"
+ className="modal"
+ closeTimeoutMS={0}
+ contentLabel="quality_profiles.grant_permissions_to_user_or_group"
+ isOpen={true}
+ onRequestClose={[Function]}
+ overlayClassName="modal-overlay"
+ parentSelector={[Function]}
+ portalClassName="ReactModalPortal"
+ shouldCloseOnOverlayClick={true}
+>
+ <header
+ className="modal-head"
+ >
+ <h2>
+ quality_profiles.grant_permissions_to_user_or_group
+ </h2>
+ </header>
+ <form
+ onSubmit={[Function]}
+ >
+ <div
+ className="modal-body"
+ >
+ <div
+ className="modal-large-field"
+ >
+ <label>
+ quality_profiles.search_description
+ </label>
+ <ProfilePermissionsFormSelect
+ onChange={[Function]}
+ onSearch={[Function]}
+ selected={
+ Object {
+ "name": "lambda",
+ }
+ }
+ />
+ </div>
+ </div>
+ <footer
+ className="modal-foot"
+ >
+ <i
+ className="spinner spacer-right"
+ />
+ <button
+ disabled={true}
+ type="submit"
+ >
+ add_verb
+ </button>
+ <button
+ className="button-link"
+ onClick={[Function]}
+ type="reset"
+ >
+ cancel
+ </button>
+ </footer>
+ </form>
+</Modal>
+`;
+
+exports[`adds user 1`] = `
+<Modal
+ ariaHideApp={true}
+ bodyOpenClassName="ReactModal__Body--open"
+ className="modal"
+ closeTimeoutMS={0}
+ contentLabel="quality_profiles.grant_permissions_to_user_or_group"
+ isOpen={true}
+ onRequestClose={[Function]}
+ overlayClassName="modal-overlay"
+ parentSelector={[Function]}
+ portalClassName="ReactModalPortal"
+ shouldCloseOnOverlayClick={true}
+>
+ <header
+ className="modal-head"
+ >
+ <h2>
+ quality_profiles.grant_permissions_to_user_or_group
+ </h2>
+ </header>
+ <form
+ onSubmit={[Function]}
+ >
+ <div
+ className="modal-body"
+ >
+ <div
+ className="modal-large-field"
+ >
+ <label>
+ quality_profiles.search_description
+ </label>
+ <ProfilePermissionsFormSelect
+ onChange={[Function]}
+ onSearch={[Function]}
+ />
+ </div>
+ </div>
+ <footer
+ className="modal-foot"
+ >
+ <button
+ disabled={true}
+ type="submit"
+ >
+ add_verb
+ </button>
+ <button
+ className="button-link"
+ onClick={[Function]}
+ type="reset"
+ >
+ cancel
+ </button>
+ </footer>
+ </form>
+</Modal>
+`;
+
+exports[`adds user 2`] = `
+<Modal
+ ariaHideApp={true}
+ bodyOpenClassName="ReactModal__Body--open"
+ className="modal"
+ closeTimeoutMS={0}
+ contentLabel="quality_profiles.grant_permissions_to_user_or_group"
+ isOpen={true}
+ onRequestClose={[Function]}
+ overlayClassName="modal-overlay"
+ parentSelector={[Function]}
+ portalClassName="ReactModalPortal"
+ shouldCloseOnOverlayClick={true}
+>
+ <header
+ className="modal-head"
+ >
+ <h2>
+ quality_profiles.grant_permissions_to_user_or_group
+ </h2>
+ </header>
+ <form
+ onSubmit={[Function]}
+ >
+ <div
+ className="modal-body"
+ >
+ <div
+ className="modal-large-field"
+ >
+ <label>
+ quality_profiles.search_description
+ </label>
+ <ProfilePermissionsFormSelect
+ onChange={[Function]}
+ onSearch={[Function]}
+ selected={
+ Object {
+ "login": "luke",
+ }
+ }
+ />
+ </div>
+ </div>
+ <footer
+ className="modal-foot"
+ >
+ <button
+ disabled={false}
+ type="submit"
+ >
+ add_verb
+ </button>
+ <button
+ className="button-link"
+ onClick={[Function]}
+ type="reset"
+ >
+ cancel
+ </button>
+ </footer>
+ </form>
+</Modal>
+`;
+
+exports[`adds user 3`] = `
+<Modal
+ ariaHideApp={true}
+ bodyOpenClassName="ReactModal__Body--open"
+ className="modal"
+ closeTimeoutMS={0}
+ contentLabel="quality_profiles.grant_permissions_to_user_or_group"
+ isOpen={true}
+ onRequestClose={[Function]}
+ overlayClassName="modal-overlay"
+ parentSelector={[Function]}
+ portalClassName="ReactModalPortal"
+ shouldCloseOnOverlayClick={true}
+>
+ <header
+ className="modal-head"
+ >
+ <h2>
+ quality_profiles.grant_permissions_to_user_or_group
+ </h2>
+ </header>
+ <form
+ onSubmit={[Function]}
+ >
+ <div
+ className="modal-body"
+ >
+ <div
+ className="modal-large-field"
+ >
+ <label>
+ quality_profiles.search_description
+ </label>
+ <ProfilePermissionsFormSelect
+ onChange={[Function]}
+ onSearch={[Function]}
+ selected={
+ Object {
+ "login": "luke",
+ }
+ }
+ />
+ </div>
+ </div>
+ <footer
+ className="modal-foot"
+ >
+ <i
+ className="spinner spacer-right"
+ />
+ <button
+ disabled={true}
+ type="submit"
+ >
+ add_verb
+ </button>
+ <button
+ className="button-link"
+ onClick={[Function]}
+ type="reset"
+ >
+ cancel
+ </button>
+ </footer>
+ </form>
+</Modal>
+`;
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders 1`] = `
+<Select
+ addLabelText="Add \\"{label}\\"?"
+ arrowRenderer={[Function]}
+ autofocus={true}
+ autosize={true}
+ backspaceRemoves={true}
+ backspaceToRemoveMessage="Press backspace to remove {label}"
+ className="Select-big"
+ clearAllText="Clear all"
+ clearRenderer={[Function]}
+ clearValueText="Clear value"
+ clearable={false}
+ deleteRemoves={true}
+ delimiter=","
+ disabled={false}
+ escapeClearsValue={true}
+ filterOptions={[Function]}
+ ignoreAccents={true}
+ ignoreCase={true}
+ inputProps={Object {}}
+ isLoading={false}
+ joinValues={false}
+ labelKey="label"
+ matchPos="any"
+ matchProp="any"
+ menuBuffer={0}
+ menuRenderer={[Function]}
+ multi={false}
+ noResultsText="no_results"
+ onBlurResetsInput={true}
+ onChange={[Function]}
+ onCloseResetsInput={true}
+ onInputChange={[Function]}
+ optionComponent={[Function]}
+ optionRenderer={[Function]}
+ options={Array []}
+ pageSize={5}
+ placeholder=""
+ required={false}
+ scrollMenuIntoView={true}
+ searchable={true}
+ simpleValue={false}
+ tabSelectsValue={true}
+ value="group:lambda"
+ valueComponent={[Function]}
+ valueKey="value"
+ valueRenderer={[Function]}
+/>
+`;
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders 1`] = `
+<div
+ className="clearfix big-spacer-bottom"
+>
+ <a
+ className="pull-right spacer-top spacer-left spacer-right button-icon"
+ href="#"
+ onClick={[Function]}
+ >
+ <DeleteIcon />
+ </a>
+ <GroupIcon
+ className="pull-left spacer-right"
+ size={32}
+ />
+ <div
+ className="overflow-hidden"
+ style={
+ Object {
+ "lineHeight": "32px",
+ }
+ }
+ >
+ <strong>
+ lambda
+ </strong>
+ </div>
+</div>
+`;
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders 1`] = `
+<div
+ className="clearfix big-spacer-bottom"
+>
+ <a
+ className="pull-right spacer-top spacer-left spacer-right button-icon"
+ href="#"
+ onClick={[Function]}
+ >
+ <DeleteIcon />
+ </a>
+ <Connect(Avatar)
+ className="pull-left spacer-right"
+ name="Luke Skywalker"
+ size={32}
+ />
+ <div
+ className="overflow-hidden"
+ >
+ <strong>
+ Luke Skywalker
+ </strong>
+ <div
+ className="note"
+ >
+ luke
+ </div>
+ </div>
+</div>
+`;
exports[`should render the quality profiles rules with sonarway comparison 1`] = `
<div
- className="quality-profile-rules"
+ className="boxed-group quality-profile-rules"
>
<div
className="quality-profile-rules-distribution"
const sortedProfiles = sortBy(profilesWithDeprecations, p => -p.activeDeprecatedRuleCount);
return (
- <div className="quality-profile-box quality-profiles-evolution-deprecated">
+ <div className="boxed-group boxed-group-inner quality-profiles-evolution-deprecated">
<div className="spacer-bottom">
<strong>{translate('quality_profiles.deprecated_rules')}</strong>
</div>
);
return (
- <div className="quality-profile-box quality-profiles-evolution-rules">
+ <div className="boxed-group boxed-group-inner quality-profiles-evolution-rules">
<div className="clearfix">
<strong className="pull-left">{translate('quality_profiles.latest_new_rules')}</strong>
</div>
}
return (
- <div className="quality-profile-box quality-profiles-evolution-stagnant">
+ <div className="boxed-group boxed-group-inner quality-profiles-evolution-stagnant">
<div className="spacer-bottom">
<strong>{translate('quality_profiles.stagnant_profiles')}</strong>
</div>
)}
{languagesToShow.map(languageKey => (
- <div key={languageKey} className="quality-profile-box quality-profiles-table">
+ <div key={languageKey} className="boxed-group boxed-group-inner quality-profiles-table">
<table data-language={languageKey} className="data zebra zebra-hover">
{profilesToShow[languageKey] != null &&
this.renderHeader(languageKey, profilesToShow[languageKey].length)}
-.quality-profile-box {
- padding: 20px;
- border: 1px solid #e6e6e6;
- border-radius: 2px;
- background-color: #fff;
-}
-
.quality-profiles-table {
- margin-top: 20px;
- padding-top: 10px;
- padding-bottom: 10px;
+ padding-top: 7px;
}
.quality-profiles-table-name {
.quality-profiles-list-header {
line-height: 24px;
+ margin-bottom: 20px;
padding: 5px 10px;
border-bottom: 1px solid #e6e6e6;
}
margin-left: 20px;
}
-.quality-profile-rules,
-.quality-profile-projects,
-.quality-profile-inheritance,
-.quality-profile-evolution {
- border: 1px solid #e6e6e6;
- border-radius: 2px;
- background-color: #fff;
-}
-
-.quality-profile-evolution {
- padding: 20px;
-}
-
-.quality-profile-projects,
-.quality-profile-inheritance {
- padding: 15px 20px 20px;
-}
-
-.quality-profile-rules {
- min-height: 182px;
-}
-
-.quality-profile-rules > header {
- padding: 15px 20px;
-}
-
.quality-profile-rules-distribution {
- margin-bottom: 20px;
- padding: 5px 20px 0;
+ margin-bottom: 15px;
+ padding: 7px 20px 0;
}
.quality-profile-rules-deprecated {
+ margin-top: 20px;
padding: 15px 20px;
background-color: #f2dede;
}
.quality-profile-rules-sonarway-missing {
+ margin-top: 20px;
padding: 15px 20px;
background-color: #fcf8e3;
}
-.quality-profile-exporters {
- margin-top: 20px;
-}
-
-.quality-profile-evolution {
- display: flex;
- margin-top: 20px;
-}
-
-.quality-profile-evolution > div {
- width: 50%;
- text-align: center;
-}
-
-.quality-profile-projects {
- margin-top: 20px;
-}
-
-.quality-profile-inheritance {
-}
-
.quality-profile-not-found {
padding-top: 100px;
text-align: center;
}
.quality-profiles-evolution-deprecated {
- margin-bottom: 20px;
border-color: #ebccd1;
background-color: #f2dede;
}
.quality-profiles-evolution-stagnant {
- margin-bottom: 20px;
border-color: #faebcc;
background-color: #fcf8e3;
}
-.quality-profiles-evolution-rules {
- border: 1px solid #e6e6e6;
- background-color: #fff;
-}
-
.quality-profile-comparison-table {
table-layout: fixed;
}
};
*/
-const AVATAR_SIZE /*: number */ = 20;
+const AVATAR_SIZE /*: number */ = 16;
export default class UsersSelectSearchOption extends React.PureComponent {
/*:: props: Props; */
};
*/
-const AVATAR_SIZE /*: number */ = 20;
+const AVATAR_SIZE /*: number */ = 16;
export default class UsersSelectSearchValue extends React.PureComponent {
/*:: props: Props; */
<Connect(Avatar)
email="admin@admin.ch"
name="Administrator"
- size={20}
+ size={16}
/>
<strong
className="spacer-left"
<Connect(Avatar)
hash="7daf6c79d4802916d83f6266e24850af"
name="Administrator"
- size={20}
+ size={16}
/>
<strong
className="spacer-left"
<Connect(Avatar)
hash="7daf6c79d4802916d83f6266e24850af"
name="Administrator"
- size={20}
+ size={16}
/>
<strong
className="spacer-left"
<Connect(Avatar)
email="admin@admin.ch"
name="Administrator"
- size={20}
+ size={16}
/>
<strong
className="spacer-left"
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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 'react-modal';
+
+export interface ChildrenProps {
+ onCloseClick: (event: React.SyntheticEvent<HTMLElement>) => void;
+ onSubmitClick: (event: React.SyntheticEvent<HTMLElement>) => void;
+ submitting: boolean;
+}
+
+interface Props {
+ children: (props: ChildrenProps) => React.ReactNode;
+ header: string;
+ onClose: () => void;
+ onSubmit: () => void | Promise<void>;
+}
+
+interface State {
+ submitting: boolean;
+}
+
+export default class SimpleModal extends React.PureComponent<Props, State> {
+ mounted: boolean;
+ state: State = { submitting: false };
+
+ componentDidMount() {
+ this.mounted = true;
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ stopSubmitting = () => {
+ if (this.mounted) {
+ this.setState({ submitting: false });
+ }
+ };
+
+ handleCloseClick = (event: React.SyntheticEvent<HTMLElement>) => {
+ event.preventDefault();
+ event.currentTarget.blur();
+ this.props.onClose();
+ };
+
+ handleSubmitClick = (event: React.SyntheticEvent<HTMLElement>) => {
+ event.preventDefault();
+ event.currentTarget.blur();
+ const result = this.props.onSubmit();
+ if (result) {
+ this.setState({ submitting: true });
+ result.then(this.stopSubmitting, this.stopSubmitting);
+ }
+ };
+
+ render() {
+ return (
+ <Modal
+ isOpen={true}
+ contentLabel={this.props.header}
+ className="modal"
+ overlayClassName="modal-overlay"
+ onRequestClose={this.props.onClose}>
+ {this.props.children({
+ onCloseClick: this.handleCloseClick,
+ onSubmitClick: this.handleSubmitClick,
+ submitting: this.state.submitting
+ })}
+ </Modal>
+ );
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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 SimpleModal, { ChildrenProps } from '../SimpleModal';
+import { click } from '../../../helpers/testUtils';
+
+it('renders', () => {
+ const inner = () => <div />;
+ expect(
+ shallow(
+ <SimpleModal header="" onClose={jest.fn()} onSubmit={jest.fn()}>
+ {inner}
+ </SimpleModal>
+ )
+ ).toMatchSnapshot();
+});
+
+it('closes', () => {
+ const onClose = jest.fn();
+ const inner = ({ onCloseClick }: ChildrenProps) => <button onClick={onCloseClick}>close</button>;
+ const wrapper = shallow(
+ <SimpleModal header="" onClose={onClose} onSubmit={jest.fn()}>
+ {inner}
+ </SimpleModal>
+ );
+ click(wrapper.find('button'));
+ expect(onClose).toBeCalled();
+});
+
+it('submits', async () => {
+ const onSubmit = jest.fn(() => Promise.resolve());
+ const inner = ({ onSubmitClick, submitting }: ChildrenProps) => (
+ <button disabled={submitting} onClick={onSubmitClick}>
+ close
+ </button>
+ );
+ const wrapper = shallow(
+ <SimpleModal header="" onClose={jest.fn()} onSubmit={onSubmit}>
+ {inner}
+ </SimpleModal>
+ );
+ (wrapper.instance() as SimpleModal).mounted = true;
+ expect(wrapper).toMatchSnapshot();
+
+ click(wrapper.find('button'));
+ expect(onSubmit).toBeCalled();
+ expect(wrapper).toMatchSnapshot();
+
+ await new Promise(setImmediate);
+ wrapper.update();
+ expect(wrapper).toMatchSnapshot();
+});
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders 1`] = `
+<Modal
+ ariaHideApp={true}
+ bodyOpenClassName="ReactModal__Body--open"
+ className="modal"
+ closeTimeoutMS={0}
+ contentLabel=""
+ isOpen={true}
+ onRequestClose={[Function]}
+ overlayClassName="modal-overlay"
+ parentSelector={[Function]}
+ portalClassName="ReactModalPortal"
+ shouldCloseOnOverlayClick={true}
+>
+ <div />
+</Modal>
+`;
+
+exports[`submits 1`] = `
+<Modal
+ ariaHideApp={true}
+ bodyOpenClassName="ReactModal__Body--open"
+ className="modal"
+ closeTimeoutMS={0}
+ contentLabel=""
+ isOpen={true}
+ onRequestClose={[Function]}
+ overlayClassName="modal-overlay"
+ parentSelector={[Function]}
+ portalClassName="ReactModalPortal"
+ shouldCloseOnOverlayClick={true}
+>
+ <button
+ disabled={false}
+ onClick={[Function]}
+ >
+ close
+ </button>
+</Modal>
+`;
+
+exports[`submits 2`] = `
+<Modal
+ ariaHideApp={true}
+ bodyOpenClassName="ReactModal__Body--open"
+ className="modal"
+ closeTimeoutMS={0}
+ contentLabel=""
+ isOpen={true}
+ onRequestClose={[Function]}
+ overlayClassName="modal-overlay"
+ parentSelector={[Function]}
+ portalClassName="ReactModalPortal"
+ shouldCloseOnOverlayClick={true}
+>
+ <button
+ disabled={true}
+ onClick={[Function]}
+ >
+ close
+ </button>
+</Modal>
+`;
+
+exports[`submits 3`] = `
+<Modal
+ ariaHideApp={true}
+ bodyOpenClassName="ReactModal__Body--open"
+ className="modal"
+ closeTimeoutMS={0}
+ contentLabel=""
+ isOpen={true}
+ onRequestClose={[Function]}
+ overlayClassName="modal-overlay"
+ parentSelector={[Function]}
+ portalClassName="ReactModalPortal"
+ shouldCloseOnOverlayClick={true}
+>
+ <button
+ disabled={false}
+ onClick={[Function]}
+ >
+ close
+ </button>
+</Modal>
+`;
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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';
+
+interface Props {
+ className?: string;
+ fill?: string;
+ size?: number;
+}
+
+export default function GroupIcon({ className, fill = '#aaa', size = 36 }: Props) {
+ return (
+ <svg className={className} width={size} height={size} viewBox="0 0 36 36">
+ <g transform="matrix(0.0625,0,0,0.0625,3,4)">
+ <path
+ fill={fill}
+ d="M148.25,224C121.25,224.833 99.167,235.5 82,256L48.5,256C34.833,256 23.333,252.625 14,245.875C4.667,239.125 0,229.25 0,216.25C0,157.417 10.333,128 31,128C32,128 35.625,129.75 41.875,133.25C48.125,136.75 56.25,140.292 66.25,143.875C76.25,147.458 86.167,149.25 96,149.25C107.167,149.25 118.25,147.333 129.25,143.5C128.417,149.667 128,155.167 128,160C128,183.167 134.75,204.5 148.25,224ZM416,383.25C416,403.25 409.917,419.042 397.75,430.625C385.583,442.208 369.417,448 349.25,448L130.75,448C110.583,448 94.417,442.208 82.25,430.625C70.083,419.042 64,403.25 64,383.25C64,374.417 64.292,365.792 64.875,357.375C65.458,348.958 66.625,339.875 68.375,330.125C70.125,320.375 72.333,311.333 75,303C77.667,294.667 81.25,286.542 85.75,278.625C90.25,270.708 95.417,263.958 101.25,258.375C107.083,252.792 114.208,248.333 122.625,245C131.042,241.667 140.333,240 150.5,240C152.167,240 155.75,241.792 161.25,245.375C166.75,248.958 172.833,252.958 179.5,257.375C186.167,261.792 195.083,265.792 206.25,269.375C217.417,272.958 228.667,274.75 240,274.75C251.333,274.75 262.583,272.958 273.75,269.375C284.917,265.792 293.833,261.792 300.5,257.375C307.167,252.958 313.25,248.958 318.75,245.375C324.25,241.792 327.833,240 329.5,240C339.667,240 348.958,241.667 357.375,245C365.792,248.333 372.917,252.792 378.75,258.375C384.583,263.958 389.75,270.708 394.25,278.625C398.75,286.542 402.333,294.667 405,303C407.667,311.333 409.875,320.375 411.625,330.125C413.375,339.875 414.542,348.958 415.125,357.375C415.708,365.792 416,374.417 416,383.25ZM160,64C160,81.667 153.75,96.75 141.25,109.25C128.75,121.75 113.667,128 96,128C78.333,128 63.25,121.75 50.75,109.25C38.25,96.75 32,81.667 32,64C32,46.333 38.25,31.25 50.75,18.75C63.25,6.25 78.333,0 96,0C113.667,0 128.75,6.25 141.25,18.75C153.75,31.25 160,46.333 160,64ZM336,160C336,186.5 326.625,209.125 307.875,227.875C289.125,246.625 266.5,256 240,256C213.5,256 190.875,246.625 172.125,227.875C153.375,209.125 144,186.5 144,160C144,133.5 153.375,110.875 172.125,92.125C190.875,73.375 213.5,64 240,64C266.5,64 289.125,73.375 307.875,92.125C326.625,110.875 336,133.5 336,160ZM480,216.25C480,229.25 475.333,239.125 466,245.875C456.667,252.625 445.167,256 431.5,256L398,256C380.833,235.5 358.75,224.833 331.75,224C345.25,204.5 352,183.167 352,160C352,155.167 351.583,149.667 350.75,143.5C361.75,147.333 372.833,149.25 384,149.25C393.833,149.25 403.75,147.458 413.75,143.875C423.75,140.292 431.875,136.75 438.125,133.25C444.375,129.75 448,128 449,128C469.667,128 480,157.417 480,216.25ZM448,64C448,81.667 441.75,96.75 429.25,109.25C416.75,121.75 401.667,128 384,128C366.333,128 351.25,121.75 338.75,109.25C326.25,96.75 320,81.667 320,64C320,46.333 326.25,31.25 338.75,18.75C351.25,6.25 366.333,0 384,0C401.667,0 416.75,6.25 429.25,18.75C441.75,31.25 448,46.333 448,64Z"
+ />
+ </g>
+ </svg>
+ );
+}
.Select-big .Select-value-label {
display: inline-block;
- margin-top: 5px;
+ margin-top: 7px;
+ line-height: 16px;
}
.Select-big .Select-option {
- padding: 4px 8px;
+ padding: 7px 8px;
+ line-height: 16px;
}
-.Select-big img {
+.Select-big img,
+.Select-big svg {
padding-top: 0;
}
"strict": true,
"target": "es5",
"jsx": "react",
- "lib": ["es2017", "dom"],
+ // remove "es2015.promise", "es2015.iterable" when upgrading to TypeScript 2.5
+ // see https://github.com/Microsoft/TypeScript/issues/16017
+ "lib": ["es2015.promise", "es2015.iterable", "es2017", "dom"],
"module": "esnext",
"moduleResolution": "node",
"typeRoots": ["./src/main/js/typings", "./node_modules/@types"]
quality_profiles.built_in.description.1=This quality profile is provided by a plugin.
quality_profiles.built_in.description.2=It will automatically be updated when a new version of the supplying plugin changes its definition.
quality_profiles.extends_built_in=Because it inherits from a built-in quality profile, this quality profile can be automatically updated when a new version of the corresponding plugin is deployed.
+quality_profiles.default_permissions=Users with the global "Manage Quality Profile" permission can manage this quality profile.
+quality_profiles.grant_permissions_to_more_users=Grant permissions to more users
+quality_profiles.grant_permissions_to_user_or_group=Grant permissions to a user or a group
+quality_profiles.additional_user_groups=Additional users / groups:
+quality_profiles.search_description=Search users by login or name, and groups by name
#------------------------------------------------------------------------------
#
-# USERS PAGE
+# USERS & GROUPS PAGE
#
#------------------------------------------------------------------------------
users.add=Add user
users.remove=Remove user
+users.remove.confirmation=Are you sure you want to remove user "{user}"?
users.search_description=Search users by login or name
users.update=Update users
users.update_details=Update details
+groups.remove=Remove group
+groups.remove.confirmation=Are you sure you want to remove group "{user}"?
+
#------------------------------------------------------------------------------
#
# MY PROFILE & MY ACCOUNT