aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src/main/js/apps/projectQualityProfiles
diff options
context:
space:
mode:
authorStas Vilchik <stas.vilchik@sonarsource.com>2017-09-01 16:33:48 +0200
committerJanos Gyerik <janos.gyerik@sonarsource.com>2017-09-12 11:34:58 +0200
commitc2954e35714c703c84b0e4c2af4e0249751516b6 (patch)
tree2b41a712c1f161815fc936eb520ee04da49b2a07 /server/sonar-web/src/main/js/apps/projectQualityProfiles
parent081f3bce1dfa04d2445178045885bd8a50494f1f (diff)
downloadsonarqube-c2954e35714c703c84b0e4c2af4e0249751516b6.tar.gz
sonarqube-c2954e35714c703c84b0e4c2af4e0249751516b6.zip
SONAR-9736 fix access to project admin pages
Diffstat (limited to 'server/sonar-web/src/main/js/apps/projectQualityProfiles')
-rw-r--r--server/sonar-web/src/main/js/apps/projectQualityProfiles/App.tsx139
-rw-r--r--server/sonar-web/src/main/js/apps/projectQualityProfiles/Header.tsx34
-rw-r--r--server/sonar-web/src/main/js/apps/projectQualityProfiles/ProfileRow.tsx122
-rw-r--r--server/sonar-web/src/main/js/apps/projectQualityProfiles/Table.tsx65
-rw-r--r--server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/App-test.tsx122
-rw-r--r--server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/Header-test.tsx26
-rw-r--r--server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/ProfileRow-test.tsx65
-rw-r--r--server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/Table-test.tsx49
-rw-r--r--server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/__snapshots__/Header-test.tsx.snap18
-rw-r--r--server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/__snapshots__/ProfileRow-test.tsx.snap81
-rw-r--r--server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/__snapshots__/Table-test.tsx.snap99
-rw-r--r--server/sonar-web/src/main/js/apps/projectQualityProfiles/routes.ts30
12 files changed, 850 insertions, 0 deletions
diff --git a/server/sonar-web/src/main/js/apps/projectQualityProfiles/App.tsx b/server/sonar-web/src/main/js/apps/projectQualityProfiles/App.tsx
new file mode 100644
index 00000000000..ccc85320a62
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectQualityProfiles/App.tsx
@@ -0,0 +1,139 @@
+/*
+ * 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 Helmet from 'react-helmet';
+import Header from './Header';
+import Table from './Table';
+import {
+ associateProject,
+ dissociateProject,
+ searchQualityProfiles,
+ Profile
+} from '../../api/quality-profiles';
+import { Component } from '../../app/types';
+import addGlobalSuccessMessage from '../../app/utils/addGlobalSuccessMessage';
+import handleRequiredAuthorization from '../../app/utils/handleRequiredAuthorization';
+import { translate, translateWithParameters } from '../../helpers/l10n';
+
+interface Props {
+ component: Component;
+ customOrganizations: boolean;
+}
+
+interface State {
+ allProfiles?: Profile[];
+ loading: boolean;
+ profiles?: Profile[];
+}
+
+export default class QualityProfiles extends React.PureComponent<Props, State> {
+ mounted: boolean;
+ state: State = { loading: true };
+
+ componentDidMount() {
+ this.mounted = true;
+ if (this.checkPermissions()) {
+ this.fetchProfiles();
+ } else {
+ handleRequiredAuthorization();
+ }
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ checkPermissions() {
+ const { configuration } = this.props.component;
+ const hasPermission = configuration && configuration.showQualityProfiles;
+ return !!hasPermission;
+ }
+
+ fetchProfiles() {
+ const { component } = this.props;
+ const organization = this.props.customOrganizations ? component.organization : undefined;
+ Promise.all([
+ searchQualityProfiles({ organization }),
+ searchQualityProfiles({ organization, projectKey: component.key })
+ ]).then(
+ ([allProfiles, profiles]) => {
+ if (this.mounted) {
+ this.setState({ loading: false, allProfiles, profiles });
+ }
+ },
+ () => {
+ if (this.mounted) {
+ this.setState({ loading: false });
+ }
+ }
+ );
+ }
+
+ handleChangeProfile = (oldKey: string, newKey: string) => {
+ const { component } = this.props;
+ const { allProfiles, profiles } = this.state;
+ const newProfile = allProfiles && allProfiles.find(profile => profile.key === newKey);
+ const request =
+ newProfile && newProfile.isDefault
+ ? dissociateProject(oldKey, component.key)
+ : associateProject(newKey, component.key);
+
+ return request.then(() => {
+ if (this.mounted && profiles && newProfile) {
+ // remove old profile, add new one
+ const nextProfiles = [...profiles.filter(profile => profile.key !== oldKey), newProfile];
+ this.setState({ profiles: nextProfiles });
+
+ addGlobalSuccessMessage(
+ translateWithParameters(
+ 'project_quality_profile.successfully_updated',
+ newProfile.languageName
+ )
+ );
+ }
+ });
+ };
+
+ render() {
+ if (!this.checkPermissions()) {
+ return null;
+ }
+
+ const { allProfiles, loading, profiles } = this.state;
+
+ return (
+ <div className="page page-limited">
+ <Helmet title={translate('project_quality_profiles.page')} />
+
+ <Header />
+
+ {loading
+ ? <i className="spinner" />
+ : allProfiles &&
+ profiles &&
+ <Table
+ allProfiles={allProfiles}
+ profiles={profiles}
+ onChangeProfile={this.handleChangeProfile}
+ />}
+ </div>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/projectQualityProfiles/Header.tsx b/server/sonar-web/src/main/js/apps/projectQualityProfiles/Header.tsx
new file mode 100644
index 00000000000..a758189099d
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectQualityProfiles/Header.tsx
@@ -0,0 +1,34 @@
+/*
+ * 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 { translate } from '../../helpers/l10n';
+
+export default function Header() {
+ return (
+ <header className="page-header">
+ <h1 className="page-title">
+ {translate('project_quality_profiles.page')}
+ </h1>
+ <div className="page-description">
+ {translate('project_quality_profiles.page.description')}
+ </div>
+ </header>
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/projectQualityProfiles/ProfileRow.tsx b/server/sonar-web/src/main/js/apps/projectQualityProfiles/ProfileRow.tsx
new file mode 100644
index 00000000000..0679b4463b4
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectQualityProfiles/ProfileRow.tsx
@@ -0,0 +1,122 @@
+/*
+ * 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 Select from 'react-select';
+import { Profile } from '../../api/quality-profiles';
+import { translate } from '../../helpers/l10n';
+
+interface Props {
+ onChangeProfile: (oldProfile: string, newProfile: string) => Promise<void>;
+ possibleProfiles: Profile[];
+ profile: Profile;
+}
+
+interface State {
+ loading: boolean;
+}
+
+export default class ProfileRow extends React.PureComponent<Props, State> {
+ mounted: boolean;
+ state: State = { loading: false };
+
+ componentDidMount() {
+ this.mounted = true;
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ stopLoading = () => {
+ if (this.mounted) {
+ this.setState({ loading: false });
+ }
+ };
+
+ handleChange = (option: { value: string }) => {
+ if (this.props.profile.key !== option.value) {
+ this.setState({ loading: true });
+ this.props
+ .onChangeProfile(this.props.profile.key, option.value)
+ .then(this.stopLoading, this.stopLoading);
+ }
+ };
+
+ renderProfileName = (profileOption: { isDefault: boolean; label: string }) => {
+ if (profileOption.isDefault) {
+ return (
+ <span>
+ <strong>
+ {translate('default')}
+ </strong>
+ {': '}
+ {profileOption.label}
+ </span>
+ );
+ }
+
+ return (
+ <span>
+ {profileOption.label}
+ </span>
+ );
+ };
+
+ renderProfileSelect() {
+ const { profile, possibleProfiles } = this.props;
+
+ const options = possibleProfiles.map(profile => ({
+ value: profile.key,
+ label: profile.name,
+ isDefault: profile.isDefault
+ }));
+
+ return (
+ <Select
+ clearable={false}
+ disabled={this.state.loading}
+ onChange={this.handleChange}
+ optionRenderer={this.renderProfileName}
+ options={options}
+ style={{ width: 300 }}
+ valueRenderer={this.renderProfileName}
+ value={profile.key}
+ />
+ );
+ }
+
+ render() {
+ const { profile } = this.props;
+
+ return (
+ <tr data-key={profile.language}>
+ <td className="thin nowrap">
+ {profile.languageName}
+ </td>
+ <td className="thin nowrap">
+ {this.renderProfileSelect()}
+ </td>
+ <td>
+ {this.state.loading && <i className="spinner" />}
+ </td>
+ </tr>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/projectQualityProfiles/Table.tsx b/server/sonar-web/src/main/js/apps/projectQualityProfiles/Table.tsx
new file mode 100644
index 00000000000..43fca05f7ae
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectQualityProfiles/Table.tsx
@@ -0,0 +1,65 @@
+/*
+ * 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 { groupBy, orderBy } from 'lodash';
+import ProfileRow from './ProfileRow';
+import { Profile } from '../../api/quality-profiles';
+import { translate } from '../../helpers/l10n';
+
+interface Props {
+ allProfiles: Profile[];
+ profiles: Profile[];
+ onChangeProfile: (oldProfile: string, newProfile: string) => Promise<void>;
+}
+
+export default function Table(props: Props) {
+ const profilesByLanguage = groupBy(props.allProfiles, 'language');
+ const orderedProfiles = orderBy(props.profiles, 'languageName');
+
+ // set key to language to avoid destroying of component
+ const profileRows = orderedProfiles.map(profile =>
+ <ProfileRow
+ key={profile.language}
+ profile={profile}
+ possibleProfiles={profilesByLanguage[profile.language]}
+ onChangeProfile={props.onChangeProfile}
+ />
+ );
+
+ return (
+ <table className="data zebra">
+ <thead>
+ <tr>
+ <th className="thin nowrap">
+ {translate('language')}
+ </th>
+ <th className="thin nowrap">
+ {translate('quality_profile')}
+ </th>
+ {/* keep one empty cell for the spinner */}
+ <th>&nbsp;</th>
+ </tr>
+ </thead>
+ <tbody>
+ {profileRows}
+ </tbody>
+ </table>
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/App-test.tsx b/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/App-test.tsx
new file mode 100644
index 00000000000..ab1f2ec7071
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/App-test.tsx
@@ -0,0 +1,122 @@
+/*
+ * 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', () => ({
+ associateProject: jest.fn(() => Promise.resolve()),
+ dissociateProject: jest.fn(() => Promise.resolve()),
+ searchQualityProfiles: jest.fn()
+}));
+
+jest.mock('../../../app/utils/addGlobalSuccessMessage', () => ({
+ default: jest.fn()
+}));
+
+jest.mock('../../../app/utils/handleRequiredAuthorization', () => ({
+ default: jest.fn()
+}));
+
+import * as React from 'react';
+import { mount } from 'enzyme';
+import App from '../App';
+
+const associateProject = require('../../../api/quality-profiles').associateProject as jest.Mock<
+ any
+>;
+
+const dissociateProject = require('../../../api/quality-profiles')
+ .dissociateProject as jest.Mock<any>;
+
+const searchQualityProfiles = require('../../../api/quality-profiles')
+ .searchQualityProfiles as jest.Mock<any>;
+
+const addGlobalSuccessMessage = require('../../../app/utils/addGlobalSuccessMessage')
+ .default as jest.Mock<any>;
+
+const handleRequiredAuthorization = require('../../../app/utils/handleRequiredAuthorization')
+ .default as jest.Mock<any>;
+
+const component = {
+ analysisDate: '',
+ breadcrumbs: [],
+ configuration: { showQualityProfiles: true },
+ key: 'foo',
+ name: 'foo',
+ organization: 'org',
+ qualifier: 'TRK',
+ version: '0.0.1'
+};
+
+it('checks permissions', () => {
+ handleRequiredAuthorization.mockClear();
+ mount(<App component={{ ...component, configuration: undefined }} customOrganizations={false} />);
+ expect(handleRequiredAuthorization).toBeCalled();
+});
+
+it('fetches profiles', () => {
+ searchQualityProfiles.mockClear();
+ mount(<App component={component} customOrganizations={false} />);
+ expect(searchQualityProfiles.mock.calls).toHaveLength(2);
+ expect(searchQualityProfiles).toBeCalledWith({ organization: undefined });
+ expect(searchQualityProfiles).toBeCalledWith({ organization: undefined, projectKey: 'foo' });
+});
+
+it('fetches profiles with organization', () => {
+ searchQualityProfiles.mockClear();
+ mount(<App component={component} customOrganizations={true} />);
+ expect(searchQualityProfiles.mock.calls).toHaveLength(2);
+ expect(searchQualityProfiles).toBeCalledWith({ organization: 'org' });
+ expect(searchQualityProfiles).toBeCalledWith({ organization: 'org', projectKey: 'foo' });
+});
+
+it('changes profile', () => {
+ associateProject.mockClear();
+ dissociateProject.mockClear();
+ addGlobalSuccessMessage.mockClear();
+ const wrapper = mount(<App component={component} customOrganizations={false} />);
+
+ const fooJava = randomProfile('foo-java', 'java');
+ const fooJs = randomProfile('foo-js', 'js');
+ const allProfiles = [
+ fooJava,
+ randomProfile('bar-java', 'java'),
+ randomProfile('baz-java', 'java', true),
+ fooJs
+ ];
+ const profiles = [fooJava, fooJs];
+ wrapper.setState({ allProfiles, loading: false, profiles });
+
+ wrapper.find('Table').prop<Function>('onChangeProfile')('foo-java', 'bar-java');
+ expect(associateProject).toBeCalledWith('bar-java', 'foo');
+
+ wrapper.find('Table').prop<Function>('onChangeProfile')('foo-java', 'baz-java');
+ expect(dissociateProject).toBeCalledWith('foo-java', 'foo');
+});
+
+function randomProfile(key: string, language: string, isDefault = false) {
+ return {
+ activeRuleCount: 17,
+ activeDeprecatedRuleCount: 0,
+ isDefault,
+ key,
+ name: key,
+ language: language,
+ languageName: language,
+ organization: 'org'
+ };
+}
diff --git a/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/Header-test.tsx b/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/Header-test.tsx
new file mode 100644
index 00000000000..adeee211e56
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/Header-test.tsx
@@ -0,0 +1,26 @@
+/*
+ * 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 Header from '../Header';
+
+it('renders', () => {
+ expect(shallow(<Header />)).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/ProfileRow-test.tsx b/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/ProfileRow-test.tsx
new file mode 100644
index 00000000000..cec351e32db
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/ProfileRow-test.tsx
@@ -0,0 +1,65 @@
+/*
+ * 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 ProfileRow from '../ProfileRow';
+import { doAsync } from '../../../helpers/testUtils';
+
+it('renders', () => {
+ expect(
+ shallow(
+ <ProfileRow
+ onChangeProfile={jest.fn()}
+ possibleProfiles={[randomProfile('bar'), randomProfile('baz')]}
+ profile={randomProfile('foo')}
+ />
+ )
+ ).toMatchSnapshot();
+});
+
+it('changes profile', () => {
+ const onChangeProfile = jest.fn(() => Promise.resolve());
+ const wrapper = shallow(
+ <ProfileRow
+ onChangeProfile={onChangeProfile}
+ possibleProfiles={[randomProfile('bar'), randomProfile('baz')]}
+ profile={randomProfile('foo')}
+ />
+ );
+ (wrapper.instance() as ProfileRow).mounted = true;
+ wrapper.find('Select').prop<Function>('onChange')({ value: 'baz' });
+ expect(onChangeProfile).toBeCalledWith('foo', 'baz');
+ expect(wrapper.state().loading).toBeTruthy();
+ return doAsync().then(() => {
+ expect(wrapper.state().loading).toBeFalsy();
+ });
+});
+
+function randomProfile(key: string) {
+ return {
+ activeRuleCount: 17,
+ activeDeprecatedRuleCount: 0,
+ key,
+ name: key,
+ language: 'xoo',
+ languageName: 'xoo',
+ organization: 'org'
+ };
+}
diff --git a/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/Table-test.tsx b/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/Table-test.tsx
new file mode 100644
index 00000000000..84e70359fab
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/Table-test.tsx
@@ -0,0 +1,49 @@
+/*
+ * 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 Table from '../Table';
+
+it('renders', () => {
+ const fooJava = randomProfile('foo-java', 'java');
+ const fooJs = randomProfile('foo-js', 'js');
+ const allProfiles = [
+ fooJava,
+ randomProfile('bar-java', 'java'),
+ randomProfile('baz-java', 'java'),
+ fooJs
+ ];
+ const profiles = [fooJava, fooJs];
+ expect(
+ shallow(<Table allProfiles={allProfiles} onChangeProfile={jest.fn()} profiles={profiles} />)
+ ).toMatchSnapshot();
+});
+
+function randomProfile(key: string, language: string) {
+ return {
+ activeRuleCount: 17,
+ activeDeprecatedRuleCount: 0,
+ key,
+ name: key,
+ language: language,
+ languageName: language,
+ organization: 'org'
+ };
+}
diff --git a/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/__snapshots__/Header-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/__snapshots__/Header-test.tsx.snap
new file mode 100644
index 00000000000..4f0e35f4a2f
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/__snapshots__/Header-test.tsx.snap
@@ -0,0 +1,18 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders 1`] = `
+<header
+ className="page-header"
+>
+ <h1
+ className="page-title"
+ >
+ project_quality_profiles.page
+ </h1>
+ <div
+ className="page-description"
+ >
+ project_quality_profiles.page.description
+ </div>
+</header>
+`;
diff --git a/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/__snapshots__/ProfileRow-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/__snapshots__/ProfileRow-test.tsx.snap
new file mode 100644
index 00000000000..541a117c889
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/__snapshots__/ProfileRow-test.tsx.snap
@@ -0,0 +1,81 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders 1`] = `
+<tr
+ data-key="xoo"
+>
+ <td
+ className="thin nowrap"
+ >
+ xoo
+ </td>
+ <td
+ className="thin nowrap"
+ >
+ <Select
+ addLabelText="Add \\"{label}\\"?"
+ arrowRenderer={[Function]}
+ autosize={true}
+ backspaceRemoves={true}
+ backspaceToRemoveMessage="Press backspace to remove {label}"
+ 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 found"
+ onBlurResetsInput={true}
+ onChange={[Function]}
+ onCloseResetsInput={true}
+ optionComponent={[Function]}
+ optionRenderer={[Function]}
+ options={
+ Array [
+ Object {
+ "isDefault": undefined,
+ "label": "bar",
+ "value": "bar",
+ },
+ Object {
+ "isDefault": undefined,
+ "label": "baz",
+ "value": "baz",
+ },
+ ]
+ }
+ pageSize={5}
+ placeholder="Select..."
+ required={false}
+ scrollMenuIntoView={true}
+ searchable={true}
+ simpleValue={false}
+ style={
+ Object {
+ "width": 300,
+ }
+ }
+ tabSelectsValue={true}
+ value="foo"
+ valueComponent={[Function]}
+ valueKey="value"
+ valueRenderer={[Function]}
+ />
+ </td>
+ <td />
+</tr>
+`;
diff --git a/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/__snapshots__/Table-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/__snapshots__/Table-test.tsx.snap
new file mode 100644
index 00000000000..b4b991157eb
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/__snapshots__/Table-test.tsx.snap
@@ -0,0 +1,99 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders 1`] = `
+<table
+ className="data zebra"
+>
+ <thead>
+ <tr>
+ <th
+ className="thin nowrap"
+ >
+ language
+ </th>
+ <th
+ className="thin nowrap"
+ >
+ quality_profile
+ </th>
+ <th>
+  
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ <ProfileRow
+ onChangeProfile={[Function]}
+ possibleProfiles={
+ Array [
+ Object {
+ "activeDeprecatedRuleCount": 0,
+ "activeRuleCount": 17,
+ "key": "foo-java",
+ "language": "java",
+ "languageName": "java",
+ "name": "foo-java",
+ "organization": "org",
+ },
+ Object {
+ "activeDeprecatedRuleCount": 0,
+ "activeRuleCount": 17,
+ "key": "bar-java",
+ "language": "java",
+ "languageName": "java",
+ "name": "bar-java",
+ "organization": "org",
+ },
+ Object {
+ "activeDeprecatedRuleCount": 0,
+ "activeRuleCount": 17,
+ "key": "baz-java",
+ "language": "java",
+ "languageName": "java",
+ "name": "baz-java",
+ "organization": "org",
+ },
+ ]
+ }
+ profile={
+ Object {
+ "activeDeprecatedRuleCount": 0,
+ "activeRuleCount": 17,
+ "key": "foo-java",
+ "language": "java",
+ "languageName": "java",
+ "name": "foo-java",
+ "organization": "org",
+ }
+ }
+ />
+ <ProfileRow
+ onChangeProfile={[Function]}
+ possibleProfiles={
+ Array [
+ Object {
+ "activeDeprecatedRuleCount": 0,
+ "activeRuleCount": 17,
+ "key": "foo-js",
+ "language": "js",
+ "languageName": "js",
+ "name": "foo-js",
+ "organization": "org",
+ },
+ ]
+ }
+ profile={
+ Object {
+ "activeDeprecatedRuleCount": 0,
+ "activeRuleCount": 17,
+ "key": "foo-js",
+ "language": "js",
+ "languageName": "js",
+ "name": "foo-js",
+ "organization": "org",
+ }
+ }
+ />
+ </tbody>
+</table>
+`;
diff --git a/server/sonar-web/src/main/js/apps/projectQualityProfiles/routes.ts b/server/sonar-web/src/main/js/apps/projectQualityProfiles/routes.ts
new file mode 100644
index 00000000000..e342e0f4070
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectQualityProfiles/routes.ts
@@ -0,0 +1,30 @@
+/*
+ * 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 { RouterState, IndexRouteProps } from 'react-router';
+
+const routes = [
+ {
+ getIndexRoute(_: RouterState, callback: (err: any, route: IndexRouteProps) => any) {
+ import('./App').then(i => callback(null, { component: i.default }));
+ }
+ }
+];
+
+export default routes;