aboutsummaryrefslogtreecommitdiffstats
path: root/server
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
parent081f3bce1dfa04d2445178045885bd8a50494f1f (diff)
downloadsonarqube-c2954e35714c703c84b0e4c2af4e0249751516b6.tar.gz
sonarqube-c2954e35714c703c84b0e4c2af4e0249751516b6.zip
SONAR-9736 fix access to project admin pages
Diffstat (limited to 'server')
-rw-r--r--server/sonar-web/src/main/js/api/quality-gates.ts25
-rw-r--r--server/sonar-web/src/main/js/api/quality-profiles.ts21
-rw-r--r--server/sonar-web/src/main/js/app/components/ProjectAdminContainer.js5
-rw-r--r--server/sonar-web/src/main/js/app/utils/addGlobalSuccessMessage.ts26
-rw-r--r--server/sonar-web/src/main/js/app/utils/startReactApp.js41
-rw-r--r--server/sonar-web/src/main/js/apps/project-admin/quality-gate/QualityGate.js65
-rw-r--r--server/sonar-web/src/main/js/apps/project-admin/quality-profiles/QualityProfiles.js89
-rw-r--r--server/sonar-web/src/main/js/apps/project-admin/routes.js10
-rw-r--r--server/sonar-web/src/main/js/apps/project-admin/store/actions.js108
-rw-r--r--server/sonar-web/src/main/js/apps/project-admin/store/gateByProject.js37
-rw-r--r--server/sonar-web/src/main/js/apps/project-admin/store/profilesByProject.js40
-rw-r--r--server/sonar-web/src/main/js/apps/project-admin/store/rootReducer.js24
-rw-r--r--server/sonar-web/src/main/js/apps/projectQualityGate/App.tsx128
-rw-r--r--server/sonar-web/src/main/js/apps/projectQualityGate/Form.tsx (renamed from server/sonar-web/src/main/js/apps/project-admin/quality-gate/Form.js)93
-rw-r--r--server/sonar-web/src/main/js/apps/projectQualityGate/Header.tsx (renamed from server/sonar-web/src/main/js/apps/project-admin/quality-gate/Header.js)4
-rw-r--r--server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/App-test.tsx123
-rw-r--r--server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/Form-test.tsx48
-rw-r--r--server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/Header-test.tsx26
-rw-r--r--server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/__snapshots__/Form-test.tsx.snap73
-rw-r--r--server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/__snapshots__/Header-test.tsx.snap18
-rw-r--r--server/sonar-web/src/main/js/apps/projectQualityGate/routes.ts (renamed from server/sonar-web/src/main/js/apps/project-admin/store/gates.js)22
-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.tsx (renamed from server/sonar-web/src/main/js/apps/project-admin/quality-profiles/Header.js)4
-rw-r--r--server/sonar-web/src/main/js/apps/projectQualityProfiles/ProfileRow.tsx (renamed from server/sonar-web/src/main/js/apps/project-admin/quality-profiles/ProfileRow.js)71
-rw-r--r--server/sonar-web/src/main/js/apps/projectQualityProfiles/Table.tsx (renamed from server/sonar-web/src/main/js/apps/project-admin/quality-profiles/Table.js)68
-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.ts (renamed from server/sonar-web/src/main/js/apps/project-admin/store/profiles.js)22
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/ProfileRules-test.tsx15
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/home/EvolutionStagnant.tsx21
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/types.ts19
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/utils.ts7
-rw-r--r--server/sonar-web/src/main/js/store/rootReducer.js18
38 files changed, 1281 insertions, 589 deletions
diff --git a/server/sonar-web/src/main/js/api/quality-gates.ts b/server/sonar-web/src/main/js/api/quality-gates.ts
index b07f1a89104..2839607c23c 100644
--- a/server/sonar-web/src/main/js/api/quality-gates.ts
+++ b/server/sonar-web/src/main/js/api/quality-gates.ts
@@ -24,11 +24,21 @@ export function fetchQualityGatesAppDetails(): Promise<any> {
return getJSON('/api/qualitygates/app').catch(throwGlobalError);
}
-export function fetchQualityGates(): Promise<any> {
+export interface QualityGate {
+ isDefault?: boolean;
+ id: string;
+ name: string;
+}
+
+export function fetchQualityGates(): Promise<QualityGate[]> {
return getJSON('/api/qualitygates/list').then(
r =>
r.qualitygates.map((qualityGate: any) => {
- return { ...qualityGate, isDefault: qualityGate.id === r.default };
+ return {
+ ...qualityGate,
+ id: String(qualityGate.id),
+ isDefault: qualityGate.id === r.default
+ };
}),
throwGlobalError
);
@@ -74,8 +84,15 @@ export function deleteCondition(id: string): Promise<void> {
return post('/api/qualitygates/delete_condition', { id });
}
-export function getGateForProject(projectKey: string): Promise<any> {
- return getJSON('/api/qualitygates/get_by_project', { projectKey }).then(r => r.qualityGate);
+export function getGateForProject(projectKey: string): Promise<QualityGate | undefined> {
+ return getJSON('/api/qualitygates/get_by_project', { projectKey }).then(
+ r =>
+ r.qualityGate && {
+ id: r.qualityGate.id,
+ isDefault: r.qualityGate.default,
+ name: r.qualityGate.name
+ }
+ );
}
export function associateGateWithProject(
diff --git a/server/sonar-web/src/main/js/api/quality-profiles.ts b/server/sonar-web/src/main/js/api/quality-profiles.ts
index 630effa1831..9ee43f3eabf 100644
--- a/server/sonar-web/src/main/js/api/quality-profiles.ts
+++ b/server/sonar-web/src/main/js/api/quality-profiles.ts
@@ -27,10 +27,29 @@ import {
RequestData
} from '../helpers/request';
+export interface Profile {
+ key: string;
+ name: string;
+ language: string;
+ languageName: string;
+ isInherited?: boolean;
+ parentKey?: string;
+ parentName?: string;
+ isDefault?: boolean;
+ activeRuleCount: number;
+ activeDeprecatedRuleCount: number;
+ rulesUpdatedAt?: string;
+ lastUsed?: string;
+ userUpdatedAt?: string;
+ organization: string;
+ isBuiltIn?: boolean;
+ projectCount?: number;
+}
+
export function searchQualityProfiles(data: {
organization?: string;
projectKey?: string;
-}): Promise<any> {
+}): Promise<Profile[]> {
return getJSON('/api/qualityprofiles/search', data).then(r => r.profiles);
}
diff --git a/server/sonar-web/src/main/js/app/components/ProjectAdminContainer.js b/server/sonar-web/src/main/js/app/components/ProjectAdminContainer.js
index fed3232e2c2..52292826872 100644
--- a/server/sonar-web/src/main/js/app/components/ProjectAdminContainer.js
+++ b/server/sonar-web/src/main/js/app/components/ProjectAdminContainer.js
@@ -55,8 +55,7 @@ export default class ProjectAdminContainer extends React.PureComponent {
return null;
}
- return React.cloneElement(this.props.children, {
- component: this.props.component
- });
+ const { children, ...props } = this.props;
+ return React.cloneElement(children, props);
}
}
diff --git a/server/sonar-web/src/main/js/app/utils/addGlobalSuccessMessage.ts b/server/sonar-web/src/main/js/app/utils/addGlobalSuccessMessage.ts
new file mode 100644
index 00000000000..3563651a54c
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/utils/addGlobalSuccessMessage.ts
@@ -0,0 +1,26 @@
+/*
+ * 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 getStore from './getStore';
+import * as globalMessages from '../../store/globalMessages/duck';
+
+export default function addGlobalSuccessMessage(message: string): void {
+ const store = getStore();
+ store.dispatch(globalMessages.addGlobalSuccessMessage(message));
+}
diff --git a/server/sonar-web/src/main/js/app/utils/startReactApp.js b/server/sonar-web/src/main/js/app/utils/startReactApp.js
index cab8a95008e..53e2290fbaa 100644
--- a/server/sonar-web/src/main/js/app/utils/startReactApp.js
+++ b/server/sonar-web/src/main/js/app/utils/startReactApp.js
@@ -56,6 +56,8 @@ import permissionTemplatesRoutes from '../../apps/permission-templates/routes';
import projectActivityRoutes from '../../apps/projectActivity/routes';
import projectAdminRoutes from '../../apps/project-admin/routes';
import projectBranchesRoutes from '../../apps/projectBranches/routes';
+import projectQualityGateRoutes from '../../apps/projectQualityGate/routes';
+import projectQualityProfilesRoutes from '../../apps/projectQualityProfiles/routes';
import projectsRoutes from '../../apps/projects/routes';
import projectsManagementRoutes from '../../apps/projectsManagement/routes';
import qualityGatesRoutes from '../../apps/quality-gates/routes';
@@ -173,28 +175,31 @@ const startReactApp = () => {
import('../components/ComponentContainer').then(i => i.default)}>
<Route path="code" childRoutes={codeRoutes} />
<Route path="component_measures" childRoutes={componentMeasuresRoutes} />
- <Route path="custom_measures" childRoutes={customMeasuresRoutes} />
<Route path="dashboard" childRoutes={overviewRoutes} />
- <Route path="project">
- <Route path="activity" childRoutes={projectActivityRoutes} />
- <Route path="admin" component={ProjectAdminContainer}>
- <Route
- path="extension/:pluginKey/:extensionKey"
- component={ProjectAdminPageExtension}
- />
- </Route>
+ <Route path="portfolio" component={PortfolioDashboard} />
+ <Route path="project/activity" childRoutes={projectActivityRoutes} />
+ <Route
+ path="project/extension/:pluginKey/:extensionKey"
+ component={ProjectPageExtension}
+ />
+ <Route path="project/issues" childRoutes={issuesRoutes} />
+ <Route path="project/quality_gate" childRoutes={projectQualityGateRoutes} />
+ <Route
+ path="project/quality_profiles"
+ childRoutes={projectQualityProfilesRoutes}
+ />
+ <Route component={ProjectAdminContainer}>
+ <Route path="custom_measures" childRoutes={customMeasuresRoutes} />
<Route
- path="extension/:pluginKey/:extensionKey"
- component={ProjectPageExtension}
+ path="project/admin/extension/:pluginKey/:extensionKey"
+ component={ProjectAdminPageExtension}
/>
- <Route path="background_tasks" childRoutes={backgroundTasksRoutes} />
- <Route path="branches" childRoutes={projectBranchesRoutes} />
- <Route path="issues" childRoutes={issuesRoutes} />
- <Route path="settings" childRoutes={settingsRoutes} />
- {projectAdminRoutes}
+ <Route path="project/background_tasks" childRoutes={backgroundTasksRoutes} />
+ <Route path="project/branches" childRoutes={projectBranchesRoutes} />
+ <Route path="project/settings" childRoutes={settingsRoutes} />
+ <Route path="project_roles" childRoutes={projectPermissionsRoutes} />
</Route>
- <Route path="project_roles" childRoutes={projectPermissionsRoutes} />
- <Route path="portfolio" component={PortfolioDashboard} />
+ {projectAdminRoutes}
</Route>
<Route component={AdminContainer}>
diff --git a/server/sonar-web/src/main/js/apps/project-admin/quality-gate/QualityGate.js b/server/sonar-web/src/main/js/apps/project-admin/quality-gate/QualityGate.js
deleted file mode 100644
index 6194c04371a..00000000000
--- a/server/sonar-web/src/main/js/apps/project-admin/quality-gate/QualityGate.js
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import React from 'react';
-import PropTypes from 'prop-types';
-import Helmet from 'react-helmet';
-import { connect } from 'react-redux';
-import Header from './Header';
-import Form from './Form';
-import { fetchProjectGate, setProjectGate } from '../store/actions';
-import { getProjectAdminAllGates, getProjectAdminProjectGate } from '../../../store/rootReducer';
-import { translate } from '../../../helpers/l10n';
-
-class QualityGate extends React.PureComponent {
- static propTypes = {
- component: PropTypes.object,
- allGates: PropTypes.array,
- gate: PropTypes.object
- };
-
- componentDidMount() {
- this.props.fetchProjectGate(this.props.component.key);
- }
-
- handleChangeGate(oldId, newId) {
- this.props.setProjectGate(this.props.component.key, oldId, newId);
- }
-
- render() {
- return (
- <div id="project-quality-gate" className="page page-limited">
- <Helmet title={translate('project_quality_gate.page')} />
- <Header />
- <Form
- allGates={this.props.allGates}
- gate={this.props.gate}
- onChange={this.handleChangeGate.bind(this)}
- />
- </div>
- );
- }
-}
-
-const mapStateToProps = (state, ownProps) => ({
- allGates: getProjectAdminAllGates(state),
- gate: getProjectAdminProjectGate(state, ownProps.location.query.id)
-});
-
-export default connect(mapStateToProps, { fetchProjectGate, setProjectGate })(QualityGate);
diff --git a/server/sonar-web/src/main/js/apps/project-admin/quality-profiles/QualityProfiles.js b/server/sonar-web/src/main/js/apps/project-admin/quality-profiles/QualityProfiles.js
deleted file mode 100644
index 02ff3c3eac5..00000000000
--- a/server/sonar-web/src/main/js/apps/project-admin/quality-profiles/QualityProfiles.js
+++ /dev/null
@@ -1,89 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-// @flow
-import React from 'react';
-import Helmet from 'react-helmet';
-import { connect } from 'react-redux';
-import Header from './Header';
-import Table from './Table';
-import { fetchProjectProfiles, setProjectProfile } from '../store/actions';
-import {
- areThereCustomOrganizations,
- getProjectAdminAllProfiles,
- getProjectAdminProjectProfiles
-} from '../../../store/rootReducer';
-import { translate } from '../../../helpers/l10n';
-
-/*::
-type Props = {
- allProfiles: Array<{}>,
- component: { key: string, organization: string },
- customOrganizations: boolean,
- fetchProjectProfiles: (componentKey: string, organization?: string) => void,
- profiles: Array<{}>,
- setProjectProfile: (string, string, string) => void
-};
-*/
-
-class QualityProfiles extends React.PureComponent {
- /*:: props: Props; */
-
- componentDidMount() {
- if (this.props.customOrganizations) {
- this.props.fetchProjectProfiles(this.props.component.key, this.props.component.organization);
- } else {
- this.props.fetchProjectProfiles(this.props.component.key);
- }
- }
-
- handleChangeProfile = (oldKey, newKey) => {
- this.props.setProjectProfile(this.props.component.key, oldKey, newKey);
- };
-
- render() {
- const { allProfiles, profiles } = this.props;
-
- return (
- <div className="page page-limited">
- <Helmet title={translate('project_quality_profiles.page')} />
-
- <Header />
-
- {profiles.length > 0
- ? <Table
- allProfiles={allProfiles}
- profiles={profiles}
- onChangeProfile={this.handleChangeProfile}
- />
- : <i className="spinner" />}
- </div>
- );
- }
-}
-
-const mapStateToProps = (state, ownProps) => ({
- customOrganizations: areThereCustomOrganizations(state),
- allProfiles: getProjectAdminAllProfiles(state),
- profiles: getProjectAdminProjectProfiles(state, ownProps.location.query.id)
-});
-
-const mapDispatchToProps = { fetchProjectProfiles, setProjectProfile };
-
-export default connect(mapStateToProps, mapDispatchToProps)(QualityProfiles);
diff --git a/server/sonar-web/src/main/js/apps/project-admin/routes.js b/server/sonar-web/src/main/js/apps/project-admin/routes.js
index a7b363c2c7b..f5efaa8e722 100644
--- a/server/sonar-web/src/main/js/apps/project-admin/routes.js
+++ b/server/sonar-web/src/main/js/apps/project-admin/routes.js
@@ -20,15 +20,11 @@
import React from 'react';
import { Route } from 'react-router';
import Deletion from './deletion/Deletion';
-import QualityProfiles from './quality-profiles/QualityProfiles';
-import QualityGate from './quality-gate/QualityGate';
import Links from './links/Links';
import Key from './key/Key';
export default [
- <Route key="deletion" path="deletion" component={Deletion} />,
- <Route key="quality_profiles" path="quality_profiles" component={QualityProfiles} />,
- <Route key="quality_gate" path="quality_gate" component={QualityGate} />,
- <Route key="links" path="links" component={Links} />,
- <Route key="key" path="key" component={Key} />
+ <Route key="deletion" path="project/deletion" component={Deletion} />,
+ <Route key="links" path="project/links" component={Links} />,
+ <Route key="key" path="project/key" component={Key} />
];
diff --git a/server/sonar-web/src/main/js/apps/project-admin/store/actions.js b/server/sonar-web/src/main/js/apps/project-admin/store/actions.js
index 6701e8f07db..28f7473e260 100644
--- a/server/sonar-web/src/main/js/apps/project-admin/store/actions.js
+++ b/server/sonar-web/src/main/js/apps/project-admin/store/actions.js
@@ -17,116 +17,8 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import {
- searchQualityProfiles,
- associateProject,
- dissociateProject
-} from '../../../api/quality-profiles';
-import {
- fetchQualityGates,
- getGateForProject,
- associateGateWithProject,
- dissociateGateWithProject
-} from '../../../api/quality-gates';
import { getProjectLinks, createLink } from '../../../api/projectLinks';
import { getTree, changeKey as changeKeyApi } from '../../../api/components';
-import { addGlobalSuccessMessage } from '../../../store/globalMessages/duck';
-import { translate, translateWithParameters } from '../../../helpers/l10n';
-import { getProjectAdminProfileByKey } from '../../../store/rootReducer';
-
-export const RECEIVE_PROFILES = 'projectAdmin/RECEIVE_PROFILES';
-export const receiveProfiles = profiles => ({
- type: RECEIVE_PROFILES,
- profiles
-});
-
-export const RECEIVE_PROJECT_PROFILES = 'projectAdmin/RECEIVE_PROJECT_PROFILES';
-export const receiveProjectProfiles = (projectKey, profiles) => ({
- type: RECEIVE_PROJECT_PROFILES,
- projectKey,
- profiles
-});
-
-export const fetchProjectProfiles = (projectKey, organization) => dispatch => {
- Promise.all([
- organization ? searchQualityProfiles({ organization }) : searchQualityProfiles(),
- organization
- ? searchQualityProfiles({ organization, projectKey })
- : searchQualityProfiles({ projectKey })
- ]).then(responses => {
- const [allProfiles, projectProfiles] = responses;
- dispatch(receiveProfiles(allProfiles));
- dispatch(receiveProjectProfiles(projectKey, projectProfiles));
- });
-};
-
-export const SET_PROJECT_PROFILE = 'projectAdmin/SET_PROJECT_PROFILE';
-const setProjectProfileAction = (projectKey, oldProfileKey, newProfileKey) => ({
- type: SET_PROJECT_PROFILE,
- projectKey,
- oldProfileKey,
- newProfileKey
-});
-
-export const setProjectProfile = (projectKey, oldKey, newKey) => (dispatch, getState) => {
- const state = getState();
- const newProfile = getProjectAdminProfileByKey(state, newKey);
- const request = newProfile.isDefault
- ? dissociateProject(oldKey, projectKey)
- : associateProject(newKey, projectKey);
-
- request.then(() => {
- dispatch(setProjectProfileAction(projectKey, oldKey, newKey));
- dispatch(
- addGlobalSuccessMessage(
- translateWithParameters(
- 'project_quality_profile.successfully_updated',
- newProfile.languageName
- )
- )
- );
- });
-};
-
-export const RECEIVE_GATES = 'projectAdmin/RECEIVE_GATES';
-export const receiveGates = gates => ({
- type: RECEIVE_GATES,
- gates
-});
-
-export const RECEIVE_PROJECT_GATE = 'projectAdmin/RECEIVE_PROJECT_GATE';
-export const receiveProjectGate = (projectKey, gate) => ({
- type: RECEIVE_PROJECT_GATE,
- projectKey,
- gate
-});
-
-export const fetchProjectGate = projectKey => dispatch => {
- Promise.all([fetchQualityGates(), getGateForProject(projectKey)]).then(responses => {
- const [allGates, projectGate] = responses;
- dispatch(receiveGates(allGates));
- dispatch(receiveProjectGate(projectKey, projectGate));
- });
-};
-
-export const SET_PROJECT_GATE = 'projectAdmin/SET_PROJECT_GATE';
-const setProjectGateAction = (projectKey, gateId) => ({
- type: SET_PROJECT_GATE,
- projectKey,
- gateId
-});
-
-export const setProjectGate = (projectKey, oldId, newId) => dispatch => {
- const request =
- newId != null
- ? associateGateWithProject(newId, projectKey)
- : dissociateGateWithProject(oldId, projectKey);
-
- request.then(() => {
- dispatch(setProjectGateAction(projectKey, newId));
- dispatch(addGlobalSuccessMessage(translate('project_quality_gate.successfully_updated')));
- });
-};
export const RECEIVE_PROJECT_LINKS = 'projectAdmin/RECEIVE_PROJECT_LINKS';
export const receiveProjectLinks = (projectKey, links) => ({
diff --git a/server/sonar-web/src/main/js/apps/project-admin/store/gateByProject.js b/server/sonar-web/src/main/js/apps/project-admin/store/gateByProject.js
deleted file mode 100644
index 451f531d1ca..00000000000
--- a/server/sonar-web/src/main/js/apps/project-admin/store/gateByProject.js
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import { RECEIVE_PROJECT_GATE, SET_PROJECT_GATE } from './actions';
-
-const gateByProject = (state = {}, action = {}) => {
- if (action.type === RECEIVE_PROJECT_GATE) {
- const gateId = action.gate ? action.gate.id : null;
- return { ...state, [action.projectKey]: gateId };
- }
-
- if (action.type === SET_PROJECT_GATE) {
- return { ...state, [action.projectKey]: action.gateId };
- }
-
- return state;
-};
-
-export default gateByProject;
-
-export const getProjectGate = (state, projectKey) => state[projectKey];
diff --git a/server/sonar-web/src/main/js/apps/project-admin/store/profilesByProject.js b/server/sonar-web/src/main/js/apps/project-admin/store/profilesByProject.js
deleted file mode 100644
index afc7ea0143b..00000000000
--- a/server/sonar-web/src/main/js/apps/project-admin/store/profilesByProject.js
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import { without } from 'lodash';
-import { RECEIVE_PROJECT_PROFILES, SET_PROJECT_PROFILE } from './actions';
-
-const profilesByProject = (state = {}, action = {}) => {
- if (action.type === RECEIVE_PROJECT_PROFILES) {
- const profileKeys = action.profiles.map(profile => profile.key);
- return { ...state, [action.projectKey]: profileKeys };
- }
-
- if (action.type === SET_PROJECT_PROFILE) {
- const profileKeys = state[action.projectKey];
- const nextProfileKeys = [...without(profileKeys, action.oldProfileKey), action.newProfileKey];
- return { ...state, [action.projectKey]: nextProfileKeys };
- }
-
- return state;
-};
-
-export default profilesByProject;
-
-export const getProfiles = (state, projectKey) => state[projectKey] || [];
diff --git a/server/sonar-web/src/main/js/apps/project-admin/store/rootReducer.js b/server/sonar-web/src/main/js/apps/project-admin/store/rootReducer.js
index 4eb6dec8e0e..43ab5760cfd 100644
--- a/server/sonar-web/src/main/js/apps/project-admin/store/rootReducer.js
+++ b/server/sonar-web/src/main/js/apps/project-admin/store/rootReducer.js
@@ -18,10 +18,6 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { combineReducers } from 'redux';
-import profiles, { getProfile, getAllProfiles as nextGetAllProfiles } from './profiles';
-import profilesByProject, { getProfiles } from './profilesByProject';
-import gates, { getAllGates as nextGetAllGates, getGate } from './gates';
-import gateByProject, { getProjectGate as nextGetProjectGate } from './gateByProject';
import links, { getLink } from './links';
import linksByProject, { getLinks } from './linksByProject';
import components, { getComponentByKey as nextGetComponentByKey } from './components';
@@ -31,10 +27,6 @@ import globalMessages, {
} from '../../../store/globalMessages/duck';
const rootReducer = combineReducers({
- profiles,
- profilesByProject,
- gates,
- gateByProject,
links,
linksByProject,
components,
@@ -44,22 +36,6 @@ const rootReducer = combineReducers({
export default rootReducer;
-export const getProfileByKey = (state, profileKey) => getProfile(state.profiles, profileKey);
-
-export const getAllProfiles = state => nextGetAllProfiles(state.profiles);
-
-export const getProjectProfiles = (state, projectKey) =>
- getProfiles(state.profilesByProject, projectKey).map(profileKey =>
- getProfileByKey(state, profileKey)
- );
-
-export const getGateById = (state, gateId) => getGate(state.gates, gateId);
-
-export const getAllGates = state => nextGetAllGates(state.gates);
-
-export const getProjectGate = (state, projectKey) =>
- getGateById(state, nextGetProjectGate(state.gateByProject, projectKey));
-
export const getLinkById = (state, linkId) => getLink(state.links, linkId);
export const getProjectLinks = (state, projectKey) =>
diff --git a/server/sonar-web/src/main/js/apps/projectQualityGate/App.tsx b/server/sonar-web/src/main/js/apps/projectQualityGate/App.tsx
new file mode 100644
index 00000000000..51d42a94526
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectQualityGate/App.tsx
@@ -0,0 +1,128 @@
+/*
+ * 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 Form from './Form';
+import {
+ fetchQualityGates,
+ getGateForProject,
+ associateGateWithProject,
+ dissociateGateWithProject,
+ QualityGate
+} from '../../api/quality-gates';
+import addGlobalSuccessMessage from '../../app/utils/addGlobalSuccessMessage';
+import handleRequiredAuthorization from '../../app/utils/handleRequiredAuthorization';
+import { Component } from '../../app/types';
+import { translate } from '../../helpers/l10n';
+
+interface Props {
+ component: Component;
+}
+
+interface State {
+ allGates?: QualityGate[];
+ gate?: QualityGate;
+ loading: boolean;
+}
+
+export default class App extends React.PureComponent<Props> {
+ mounted: boolean;
+ state: State = { loading: true };
+
+ componentDidMount() {
+ this.mounted = true;
+ if (this.checkPermissions()) {
+ this.fetchQualityGates();
+ } else {
+ handleRequiredAuthorization();
+ }
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ checkPermissions() {
+ const { configuration } = this.props.component;
+ const hasPermission = configuration && configuration.showQualityGates;
+ return !!hasPermission;
+ }
+
+ fetchQualityGates() {
+ this.setState({ loading: true });
+ Promise.all([fetchQualityGates(), getGateForProject(this.props.component.key)]).then(
+ ([allGates, gate]) => {
+ if (this.mounted) {
+ this.setState({ allGates, gate, loading: false });
+ }
+ },
+ () => {
+ if (this.mounted) {
+ this.setState({ loading: false });
+ }
+ }
+ );
+ }
+
+ handleChangeGate = (oldId: string | undefined, newId: string | undefined) => {
+ const { allGates } = this.state;
+
+ if ((!oldId && !newId) || !allGates) {
+ return Promise.resolve();
+ }
+
+ const request = newId
+ ? associateGateWithProject(newId, this.props.component.key)
+ : dissociateGateWithProject(oldId!, this.props.component.key);
+
+ return request.then(() => {
+ if (this.mounted) {
+ addGlobalSuccessMessage(translate('project_quality_gate.successfully_updated'));
+ if (newId) {
+ const newGate = allGates.find(gate => gate.id === newId);
+ if (newGate) {
+ this.setState({ gate: newGate });
+ }
+ } else {
+ this.setState({ gate: undefined });
+ }
+ }
+ });
+ };
+
+ render() {
+ if (!this.checkPermissions()) {
+ return null;
+ }
+
+ const { allGates, gate, loading } = this.state;
+
+ return (
+ <div id="project-quality-gate" className="page page-limited">
+ <Helmet title={translate('project_quality_gate.page')} />
+ <Header />
+ {loading
+ ? <i className="spinner" />
+ : allGates && <Form allGates={allGates} gate={gate} onChange={this.handleChangeGate} />}
+ </div>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/project-admin/quality-gate/Form.js b/server/sonar-web/src/main/js/apps/projectQualityGate/Form.tsx
index 69b5fdc2126..2e0e502db14 100644
--- a/server/sonar-web/src/main/js/apps/project-admin/quality-gate/Form.js
+++ b/server/sonar-web/src/main/js/apps/projectQualityGate/Form.tsx
@@ -17,39 +17,47 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import React from 'react';
-import PropTypes from 'prop-types';
-import Select from 'react-select';
+import * as React from 'react';
+import * as Select from 'react-select';
import { some } from 'lodash';
-import { translate } from '../../../helpers/l10n';
+import { QualityGate } from '../../api/quality-gates';
+import { translate } from '../../helpers/l10n';
-export default class Form extends React.PureComponent {
- static propTypes = {
- allGates: PropTypes.array.isRequired,
- gate: PropTypes.object,
- onChange: PropTypes.func.isRequired
- };
+interface Props {
+ allGates: QualityGate[];
+ gate?: QualityGate;
+ onChange: (oldGate: string | undefined, newGate: string) => Promise<void>;
+}
- state = {
- loading: false
- };
+interface State {
+ loading: boolean;
+}
- componentWillMount() {
- this.handleChange = this.handleChange.bind(this);
- this.renderGateName = this.renderGateName.bind(this);
- }
+interface Option {
+ isDefault?: boolean;
+ label: string;
+ value: string;
+}
- componentDidUpdate(prevProps) {
- if (prevProps.gate !== this.props.gate) {
- this.stopLoading();
- }
+export default class Form extends React.PureComponent<Props, State> {
+ mounted: boolean;
+ state: State = { loading: false };
+
+ componentDidMount() {
+ this.mounted = true;
}
- stopLoading() {
- this.setState({ loading: false });
+ componentWillUnmount() {
+ this.mounted = false;
}
- handleChange(option) {
+ stopLoading = () => {
+ if (this.mounted) {
+ this.setState({ loading: false });
+ }
+ };
+
+ handleChange = (option: { value: string }) => {
const { gate } = this.props;
const isSet = gate == null && option.value != null;
@@ -59,30 +67,34 @@ export default class Form extends React.PureComponent {
if (hasChanged) {
this.setState({ loading: true });
- this.props.onChange(gate && gate.id, option.value);
+ this.props.onChange(gate && gate.id, option.value).then(this.stopLoading, this.stopLoading);
}
- }
+ };
- renderGateName(gateOption) {
- if (gateOption.isDefault) {
+ renderGateName = (option: { isDefault?: boolean; label: string }) => {
+ if (option.isDefault) {
return (
<span>
<strong>
{translate('default')}
</strong>
{': '}
- {gateOption.label}
+ {option.label}
</span>
);
}
- return gateOption.label;
- }
+ return (
+ <span>
+ {option.label}
+ </span>
+ );
+ };
renderSelect() {
const { gate, allGates } = this.props;
- const options = allGates.map(gate => ({
+ const options: Option[] = allGates.map(gate => ({
value: gate.id,
label: gate.name,
isDefault: gate.isDefault
@@ -90,23 +102,20 @@ export default class Form extends React.PureComponent {
const hasDefault = some(allGates, gate => gate.isDefault);
if (!hasDefault) {
- options.unshift({
- value: null,
- label: translate('none')
- });
+ options.unshift({ value: '', label: translate('none') });
}
return (
<Select
- options={options}
- valueRenderer={this.renderGateName}
- optionRenderer={this.renderGateName}
- value={gate && gate.id}
clearable={false}
+ disabled={this.state.loading}
+ onChange={this.handleChange}
+ optionRenderer={this.renderGateName}
+ options={options}
placeholder={translate('none')}
style={{ width: 300 }}
- disabled={this.state.loading}
- onChange={this.handleChange.bind(this)}
+ value={gate && gate.id}
+ valueRenderer={this.renderGateName}
/>
);
}
diff --git a/server/sonar-web/src/main/js/apps/project-admin/quality-gate/Header.js b/server/sonar-web/src/main/js/apps/projectQualityGate/Header.tsx
index fb3dd5ff053..4570192a235 100644
--- a/server/sonar-web/src/main/js/apps/project-admin/quality-gate/Header.js
+++ b/server/sonar-web/src/main/js/apps/projectQualityGate/Header.tsx
@@ -17,8 +17,8 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import React from 'react';
-import { translate } from '../../../helpers/l10n';
+import * as React from 'react';
+import { translate } from '../../helpers/l10n';
export default function Header() {
return (
diff --git a/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/App-test.tsx b/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/App-test.tsx
new file mode 100644
index 00000000000..ac0d97d33d7
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/App-test.tsx
@@ -0,0 +1,123 @@
+/*
+ * 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-gates', () => ({
+ associateGateWithProject: jest.fn(() => Promise.resolve()),
+ dissociateGateWithProject: jest.fn(() => Promise.resolve()),
+ fetchQualityGates: jest.fn(),
+ getGateForProject: 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 associateGateWithProject = require('../../../api/quality-gates')
+ .associateGateWithProject as jest.Mock<any>;
+
+const dissociateGateWithProject = require('../../../api/quality-gates')
+ .dissociateGateWithProject as jest.Mock<any>;
+
+const fetchQualityGates = require('../../../api/quality-gates').fetchQualityGates as jest.Mock<any>;
+
+const getGateForProject = require('../../../api/quality-gates').getGateForProject 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: { showQualityGates: true },
+ key: 'component',
+ name: 'component',
+ organization: 'org',
+ qualifier: 'TRK',
+ version: '0.0.1'
+};
+
+beforeEach(() => {
+ associateGateWithProject.mockClear();
+ dissociateGateWithProject.mockClear();
+ addGlobalSuccessMessage.mockClear();
+});
+
+it('checks permissions', () => {
+ handleRequiredAuthorization.mockClear();
+ mount(<App component={{ ...component, configuration: undefined }} />);
+ expect(handleRequiredAuthorization).toBeCalled();
+});
+
+it('fetches quality gates', () => {
+ fetchQualityGates.mockClear();
+ getGateForProject.mockClear();
+ mount(<App component={component} />);
+ expect(fetchQualityGates).toBeCalledWith();
+ expect(getGateForProject).toBeCalledWith('component');
+});
+
+it('changes quality gate from custom to default', () => {
+ const gate = randomGate('foo');
+ const allGates = [gate, randomGate('bar', true), randomGate('baz')];
+ const wrapper = mountRender(allGates, gate);
+ wrapper.find('Form').prop<Function>('onChange')('foo', 'bar');
+ expect(associateGateWithProject).toBeCalledWith('bar', 'component');
+});
+
+it('changes quality gate from custom to custom', () => {
+ const allGates = [randomGate('foo'), randomGate('bar', true), randomGate('baz')];
+ const wrapper = mountRender(allGates, randomGate('foo'));
+ wrapper.find('Form').prop<Function>('onChange')('foo', 'baz');
+ expect(associateGateWithProject).toBeCalledWith('baz', 'component');
+});
+
+it('changes quality gate from custom to none', () => {
+ const allGates = [randomGate('foo'), randomGate('bar'), randomGate('baz')];
+ const wrapper = mountRender(allGates, randomGate('foo'));
+ wrapper.find('Form').prop<Function>('onChange')('foo', undefined);
+ expect(dissociateGateWithProject).toBeCalledWith('foo', 'component');
+});
+
+it('changes quality gate from none to custom', () => {
+ const allGates = [randomGate('foo'), randomGate('bar'), randomGate('baz')];
+ const wrapper = mountRender(allGates);
+ wrapper.find('Form').prop<Function>('onChange')(undefined, 'baz');
+ expect(associateGateWithProject).toBeCalledWith('baz', 'component');
+});
+
+function randomGate(id: string, isDefault = false) {
+ return { id, isDefault, name: id };
+}
+
+function mountRender(allGates: any[], gate?: any) {
+ const wrapper = mount(<App component={component} />);
+ wrapper.setState({ allGates, loading: false, gate });
+ return wrapper;
+}
diff --git a/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/Form-test.tsx b/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/Form-test.tsx
new file mode 100644
index 00000000000..f06a73eea6d
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/Form-test.tsx
@@ -0,0 +1,48 @@
+/*
+ * 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 Form from '../Form';
+
+it('renders', () => {
+ const foo = randomGate('foo');
+ const allGates = [foo, randomGate('bar')];
+ expect(shallow(<Form allGates={allGates} gate={foo} onChange={jest.fn()} />)).toMatchSnapshot();
+});
+
+it('changes quality gate', () => {
+ const allGates = [randomGate('foo'), randomGate('bar')];
+ const onChange = jest.fn(() => Promise.resolve());
+ const wrapper = shallow(<Form allGates={allGates} onChange={onChange} />);
+
+ wrapper.find('Select').prop<Function>('onChange')({ value: 'bar' });
+ expect(onChange).lastCalledWith(undefined, 'bar');
+
+ wrapper.setProps({ gate: randomGate('foo') });
+ wrapper.find('Select').prop<Function>('onChange')({ value: 'bar' });
+ expect(onChange).lastCalledWith('foo', 'bar');
+});
+
+function randomGate(id: string) {
+ return {
+ id,
+ name: id
+ };
+}
diff --git a/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/Header-test.tsx b/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/Header-test.tsx
new file mode 100644
index 00000000000..adeee211e56
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectQualityGate/__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/projectQualityGate/__tests__/__snapshots__/Form-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/__snapshots__/Form-test.tsx.snap
new file mode 100644
index 00000000000..fcbb0ecfff8
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/__snapshots__/Form-test.tsx.snap
@@ -0,0 +1,73 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders 1`] = `
+<div>
+ <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 {
+ "label": "none",
+ "value": "",
+ },
+ Object {
+ "isDefault": undefined,
+ "label": "foo",
+ "value": "foo",
+ },
+ Object {
+ "isDefault": undefined,
+ "label": "bar",
+ "value": "bar",
+ },
+ ]
+ }
+ pageSize={5}
+ placeholder="none"
+ required={false}
+ scrollMenuIntoView={true}
+ searchable={true}
+ simpleValue={false}
+ style={
+ Object {
+ "width": 300,
+ }
+ }
+ tabSelectsValue={true}
+ value="foo"
+ valueComponent={[Function]}
+ valueKey="value"
+ valueRenderer={[Function]}
+ />
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/__snapshots__/Header-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/__snapshots__/Header-test.tsx.snap
new file mode 100644
index 00000000000..eaade8b5468
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectQualityGate/__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_gate.page
+ </h1>
+ <div
+ className="page-description"
+ >
+ project_quality_gate.page.description
+ </div>
+</header>
+`;
diff --git a/server/sonar-web/src/main/js/apps/project-admin/store/gates.js b/server/sonar-web/src/main/js/apps/projectQualityGate/routes.ts
index d68aae11508..e342e0f4070 100644
--- a/server/sonar-web/src/main/js/apps/project-admin/store/gates.js
+++ b/server/sonar-web/src/main/js/apps/projectQualityGate/routes.ts
@@ -17,20 +17,14 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { keyBy, values } from 'lodash';
-import { RECEIVE_GATES } from './actions';
+import { RouterState, IndexRouteProps } from 'react-router';
-const gates = (state = {}, action = {}) => {
- if (action.type === RECEIVE_GATES) {
- const newGatesById = keyBy(action.gates, 'id');
- return { ...state, ...newGatesById };
+const routes = [
+ {
+ getIndexRoute(_: RouterState, callback: (err: any, route: IndexRouteProps) => any) {
+ import('./App').then(i => callback(null, { component: i.default }));
+ }
}
+];
- return state;
-};
-
-export default gates;
-
-export const getAllGates = state => values(state);
-
-export const getGate = (state, id) => state[id];
+export default routes;
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/project-admin/quality-profiles/Header.js b/server/sonar-web/src/main/js/apps/projectQualityProfiles/Header.tsx
index 0ecebfdd8cb..a758189099d 100644
--- a/server/sonar-web/src/main/js/apps/project-admin/quality-profiles/Header.js
+++ b/server/sonar-web/src/main/js/apps/projectQualityProfiles/Header.tsx
@@ -17,8 +17,8 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import React from 'react';
-import { translate } from '../../../helpers/l10n';
+import * as React from 'react';
+import { translate } from '../../helpers/l10n';
export default function Header() {
return (
diff --git a/server/sonar-web/src/main/js/apps/project-admin/quality-profiles/ProfileRow.js b/server/sonar-web/src/main/js/apps/projectQualityProfiles/ProfileRow.tsx
index 8a9e02e717f..0679b4463b4 100644
--- a/server/sonar-web/src/main/js/apps/project-admin/quality-profiles/ProfileRow.js
+++ b/server/sonar-web/src/main/js/apps/projectQualityProfiles/ProfileRow.tsx
@@ -17,36 +17,49 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import React from 'react';
-import PropTypes from 'prop-types';
-import Select from 'react-select';
-import { translate } from '../../../helpers/l10n';
+import * as React from 'react';
+import * as Select from 'react-select';
+import { Profile } from '../../api/quality-profiles';
+import { translate } from '../../helpers/l10n';
-export default class ProfileRow extends React.PureComponent {
- static propTypes = {
- profile: PropTypes.object.isRequired,
- possibleProfiles: PropTypes.array.isRequired,
- onChangeProfile: PropTypes.func.isRequired
- };
+interface Props {
+ onChangeProfile: (oldProfile: string, newProfile: string) => Promise<void>;
+ possibleProfiles: Profile[];
+ profile: Profile;
+}
- state = {
- loading: false
- };
+interface State {
+ loading: boolean;
+}
+
+export default class ProfileRow extends React.PureComponent<Props, State> {
+ mounted: boolean;
+ state: State = { loading: false };
+
+ componentDidMount() {
+ this.mounted = true;
+ }
- componentWillUpdate(nextProps) {
- if (nextProps.profile !== this.props.profile) {
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ stopLoading = () => {
+ if (this.mounted) {
this.setState({ loading: false });
}
- }
+ };
- handleChange(option) {
+ handleChange = (option: { value: string }) => {
if (this.props.profile.key !== option.value) {
this.setState({ loading: true });
- this.props.onChangeProfile(this.props.profile.key, option.value);
+ this.props
+ .onChangeProfile(this.props.profile.key, option.value)
+ .then(this.stopLoading, this.stopLoading);
}
- }
+ };
- renderProfileName(profileOption) {
+ renderProfileName = (profileOption: { isDefault: boolean; label: string }) => {
if (profileOption.isDefault) {
return (
<span>
@@ -59,8 +72,12 @@ export default class ProfileRow extends React.PureComponent {
);
}
- return profileOption.label;
- }
+ return (
+ <span>
+ {profileOption.label}
+ </span>
+ );
+ };
renderProfileSelect() {
const { profile, possibleProfiles } = this.props;
@@ -73,14 +90,14 @@ export default class ProfileRow extends React.PureComponent {
return (
<Select
+ clearable={false}
+ disabled={this.state.loading}
+ onChange={this.handleChange}
+ optionRenderer={this.renderProfileName}
options={options}
+ style={{ width: 300 }}
valueRenderer={this.renderProfileName}
- optionRenderer={this.renderProfileName}
value={profile.key}
- clearable={false}
- style={{ width: 300 }}
- disabled={this.state.loading}
- onChange={this.handleChange.bind(this)}
/>
);
}
diff --git a/server/sonar-web/src/main/js/apps/project-admin/quality-profiles/Table.js b/server/sonar-web/src/main/js/apps/projectQualityProfiles/Table.tsx
index 24a099134b2..43fca05f7ae 100644
--- a/server/sonar-web/src/main/js/apps/project-admin/quality-profiles/Table.js
+++ b/server/sonar-web/src/main/js/apps/projectQualityProfiles/Table.tsx
@@ -17,22 +17,34 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import React from 'react';
-import PropTypes from 'prop-types';
+import * as React from 'react';
import { groupBy, orderBy } from 'lodash';
import ProfileRow from './ProfileRow';
-import { translate } from '../../../helpers/l10n';
+import { Profile } from '../../api/quality-profiles';
+import { translate } from '../../helpers/l10n';
-export default class Table extends React.PureComponent {
- static propTypes = {
- allProfiles: PropTypes.array.isRequired,
- profiles: PropTypes.array.isRequired,
- onChangeProfile: PropTypes.func.isRequired
- };
+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}
+ />
+ );
- renderHeader() {
- // keep one empty cell for the spinner
- return (
+ return (
+ <table className="data zebra">
<thead>
<tr>
<th className="thin nowrap">
@@ -41,33 +53,13 @@ export default class Table extends React.PureComponent {
<th className="thin nowrap">
{translate('quality_profile')}
</th>
+ {/* keep one empty cell for the spinner */}
<th>&nbsp;</th>
</tr>
</thead>
- );
- }
-
- render() {
- const profilesByLanguage = groupBy(this.props.allProfiles, 'language');
- const orderedProfiles = orderBy(this.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={this.props.onChangeProfile}
- />
- );
-
- return (
- <table className="data zebra">
- {this.renderHeader()}
- <tbody>
- {profileRows}
- </tbody>
- </table>
- );
- }
+ <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/project-admin/store/profiles.js b/server/sonar-web/src/main/js/apps/projectQualityProfiles/routes.ts
index ed6a8345d0e..e342e0f4070 100644
--- a/server/sonar-web/src/main/js/apps/project-admin/store/profiles.js
+++ b/server/sonar-web/src/main/js/apps/projectQualityProfiles/routes.ts
@@ -17,20 +17,14 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { keyBy, values } from 'lodash';
-import { RECEIVE_PROFILES } from './actions';
+import { RouterState, IndexRouteProps } from 'react-router';
-const profiles = (state = {}, action = {}) => {
- if (action.type === RECEIVE_PROFILES) {
- const newProfilesByKey = keyBy(action.profiles, 'key');
- return { ...state, ...newProfilesByKey };
+const routes = [
+ {
+ getIndexRoute(_: RouterState, callback: (err: any, route: IndexRouteProps) => any) {
+ import('./App').then(i => callback(null, { component: i.default }));
+ }
}
+];
- return state;
-};
-
-export default profiles;
-
-export const getAllProfiles = state => values(state);
-
-export const getProfile = (state, key) => state[key];
+export default routes;
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/ProfileRules-test.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/ProfileRules-test.tsx
index 91e5ed96cfc..df6462d30b0 100644
--- a/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/ProfileRules-test.tsx
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/ProfileRules-test.tsx
@@ -25,18 +25,19 @@ import * as apiRules from '../../../../api/rules';
import * as apiQP from '../../../../api/quality-profiles';
const PROFILE = {
- key: 'foo',
- name: 'Foo',
+ activeRuleCount: 68,
+ activeDeprecatedRuleCount: 0,
+ childrenCount: 0,
+ depth: 0,
isBuiltIn: false,
isDefault: false,
isInherited: false,
+ key: 'foo',
language: 'java',
languageName: 'Java',
- activeRuleCount: 68,
- activeDeprecatedRuleCount: 0,
- rulesUpdatedAt: '2017-06-28T12:58:44+0000',
- depth: 0,
- childrenCount: 0
+ name: 'Foo',
+ organization: 'org',
+ rulesUpdatedAt: '2017-06-28T12:58:44+0000'
};
const apiResponseAll = {
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/home/EvolutionStagnant.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/home/EvolutionStagnant.tsx
index 5252d1c1840..957fa4790a2 100644
--- a/server/sonar-web/src/main/js/apps/quality-profiles/home/EvolutionStagnant.tsx
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/home/EvolutionStagnant.tsx
@@ -60,16 +60,17 @@ export default function EvolutionStagnant(props: Props) {
{profile.name}
</ProfileLink>
</div>
- <DateFormatter date={profile.rulesUpdatedAt} long={true}>
- {formattedDate =>
- <div className="note">
- {translateWithParameters(
- 'quality_profiles.x_updated_on_y',
- profile.languageName,
- formattedDate
- )}
- </div>}
- </DateFormatter>
+ {profile.rulesUpdatedAt &&
+ <DateFormatter date={profile.rulesUpdatedAt} long={true}>
+ {formattedDate =>
+ <div className="note">
+ {translateWithParameters(
+ 'quality_profiles.x_updated_on_y',
+ profile.languageName,
+ formattedDate
+ )}
+ </div>}
+ </DateFormatter>}
</li>
)}
</ul>
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/types.ts b/server/sonar-web/src/main/js/apps/quality-profiles/types.ts
index 415ab3c03bb..88758d46591 100644
--- a/server/sonar-web/src/main/js/apps/quality-profiles/types.ts
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/types.ts
@@ -17,22 +17,9 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-export interface Profile {
- key: string;
- name: string;
- isBuiltIn: boolean;
- isDefault: boolean;
- isInherited: boolean;
- language: string;
- languageName: string;
- activeRuleCount: number;
- activeDeprecatedRuleCount: number;
- projectCount?: number;
- parentKey?: string;
- parentName?: string;
- userUpdatedAt?: string;
- lastUsed?: string;
- rulesUpdatedAt: string;
+import { Profile as BaseProfile } from '../../api/quality-profiles';
+
+export interface Profile extends BaseProfile {
depth: number;
childrenCount: number;
}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/utils.ts b/server/sonar-web/src/main/js/apps/quality-profiles/utils.ts
index 6d5c9db9d9f..a23ab59a6c6 100644
--- a/server/sonar-web/src/main/js/apps/quality-profiles/utils.ts
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/utils.ts
@@ -18,20 +18,21 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { sortBy } from 'lodash';
+import { Profile as BaseProfile } from '../../api/quality-profiles';
import { differenceInYears, isValidDate, parseDate } from '../../helpers/dates';
import { Profile } from './types';
-export function sortProfiles(profiles: Profile[]) {
+export function sortProfiles(profiles: BaseProfile[]): Profile[] {
const result: Profile[] = [];
const sorted = sortBy(profiles, 'name');
- function retrieveChildren(parent: Profile | null) {
+ function retrieveChildren(parent: BaseProfile | null) {
return sorted.filter(
p => (parent == null && p.parentKey == null) || (parent != null && p.parentKey === parent.key)
);
}
- function putProfile(profile: Profile | null = null, depth: number = 1) {
+ function putProfile(profile: BaseProfile | null = null, depth: number = 1) {
const children = retrieveChildren(profile);
if (profile != null) {
diff --git a/server/sonar-web/src/main/js/store/rootReducer.js b/server/sonar-web/src/main/js/store/rootReducer.js
index 9cdd198e860..ef3e95d1931 100644
--- a/server/sonar-web/src/main/js/store/rootReducer.js
+++ b/server/sonar-web/src/main/js/store/rootReducer.js
@@ -189,24 +189,6 @@ export const getSettingsAppEncryptionState = state =>
export const getSettingsAppGlobalMessages = state =>
fromSettingsApp.getGlobalMessages(state.settingsApp);
-export const getProjectAdminProfileByKey = (state, profileKey) =>
- fromProjectAdminApp.getProfileByKey(state.projectAdminApp, profileKey);
-
-export const getProjectAdminAllProfiles = state =>
- fromProjectAdminApp.getAllProfiles(state.projectAdminApp);
-
-export const getProjectAdminProjectProfiles = (state, projectKey) =>
- fromProjectAdminApp.getProjectProfiles(state.projectAdminApp, projectKey);
-
-export const getProjectAdminGateById = (state, gateId) =>
- fromProjectAdminApp.getGateById(state.projectAdminApp, gateId);
-
-export const getProjectAdminAllGates = state =>
- fromProjectAdminApp.getAllGates(state.projectAdminApp);
-
-export const getProjectAdminProjectGate = (state, projectKey) =>
- fromProjectAdminApp.getProjectGate(state.projectAdminApp, projectKey);
-
export const getProjectAdminLinkById = (state, linkId) =>
fromProjectAdminApp.getLinkById(state.projectAdminApp, linkId);