]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-9736 fix access to project admin pages
authorStas Vilchik <stas.vilchik@sonarsource.com>
Fri, 1 Sep 2017 14:33:48 +0000 (16:33 +0200)
committerJanos Gyerik <janos.gyerik@sonarsource.com>
Tue, 12 Sep 2017 09:34:58 +0000 (11:34 +0200)
45 files changed:
server/sonar-web/src/main/js/api/quality-gates.ts
server/sonar-web/src/main/js/api/quality-profiles.ts
server/sonar-web/src/main/js/app/components/ProjectAdminContainer.js
server/sonar-web/src/main/js/app/utils/addGlobalSuccessMessage.ts [new file with mode: 0644]
server/sonar-web/src/main/js/app/utils/startReactApp.js
server/sonar-web/src/main/js/apps/project-admin/quality-gate/Form.js [deleted file]
server/sonar-web/src/main/js/apps/project-admin/quality-gate/Header.js [deleted file]
server/sonar-web/src/main/js/apps/project-admin/quality-gate/QualityGate.js [deleted file]
server/sonar-web/src/main/js/apps/project-admin/quality-profiles/Header.js [deleted file]
server/sonar-web/src/main/js/apps/project-admin/quality-profiles/ProfileRow.js [deleted file]
server/sonar-web/src/main/js/apps/project-admin/quality-profiles/QualityProfiles.js [deleted file]
server/sonar-web/src/main/js/apps/project-admin/quality-profiles/Table.js [deleted file]
server/sonar-web/src/main/js/apps/project-admin/routes.js
server/sonar-web/src/main/js/apps/project-admin/store/actions.js
server/sonar-web/src/main/js/apps/project-admin/store/gateByProject.js [deleted file]
server/sonar-web/src/main/js/apps/project-admin/store/gates.js [deleted file]
server/sonar-web/src/main/js/apps/project-admin/store/profiles.js [deleted file]
server/sonar-web/src/main/js/apps/project-admin/store/profilesByProject.js [deleted file]
server/sonar-web/src/main/js/apps/project-admin/store/rootReducer.js
server/sonar-web/src/main/js/apps/projectQualityGate/App.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectQualityGate/Form.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectQualityGate/Header.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/App-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/Form-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/Header-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/__snapshots__/Form-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/__snapshots__/Header-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectQualityGate/routes.ts [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectQualityProfiles/App.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectQualityProfiles/Header.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectQualityProfiles/ProfileRow.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectQualityProfiles/Table.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/App-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/Header-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/ProfileRow-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/Table-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/__snapshots__/Header-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/__snapshots__/ProfileRow-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/__snapshots__/Table-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectQualityProfiles/routes.ts [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/ProfileRules-test.tsx
server/sonar-web/src/main/js/apps/quality-profiles/home/EvolutionStagnant.tsx
server/sonar-web/src/main/js/apps/quality-profiles/types.ts
server/sonar-web/src/main/js/apps/quality-profiles/utils.ts
server/sonar-web/src/main/js/store/rootReducer.js

index b07f1a891049311cfcc09f8dbbd82056a2c737f9..2839607c23cec577e0c15c1575a35047b341e5d7 100644 (file)
@@ -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(
index 630effa1831c84c7cc17b753de267c16d09dfe9c..9ee43f3eabfdb9418b354a1312fce4838832fdb4 100644 (file)
@@ -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);
 }
 
index fed3232e2c2424f5f0b9855354ada98680d09ba4..5229282687297f3c4ebf6c5d26cd15d7fb5cd661 100644 (file)
@@ -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 (file)
index 0000000..3563651
--- /dev/null
@@ -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));
+}
index cab8a95008e158137f6bdc741b5a83866acfbfa5..53e2290fbaa7abaecb7826c4dd48fb1de6d7a69e 100644 (file)
@@ -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/Form.js b/server/sonar-web/src/main/js/apps/project-admin/quality-gate/Form.js
deleted file mode 100644 (file)
index 69b5fdc..0000000
+++ /dev/null
@@ -1,122 +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 Select from 'react-select';
-import { some } from 'lodash';
-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
-  };
-
-  state = {
-    loading: false
-  };
-
-  componentWillMount() {
-    this.handleChange = this.handleChange.bind(this);
-    this.renderGateName = this.renderGateName.bind(this);
-  }
-
-  componentDidUpdate(prevProps) {
-    if (prevProps.gate !== this.props.gate) {
-      this.stopLoading();
-    }
-  }
-
-  stopLoading() {
-    this.setState({ loading: false });
-  }
-
-  handleChange(option) {
-    const { gate } = this.props;
-
-    const isSet = gate == null && option.value != null;
-    const isUnset = gate != null && option.value == null;
-    const isChanged = gate != null && gate.id !== option.value;
-    const hasChanged = isSet || isUnset || isChanged;
-
-    if (hasChanged) {
-      this.setState({ loading: true });
-      this.props.onChange(gate && gate.id, option.value);
-    }
-  }
-
-  renderGateName(gateOption) {
-    if (gateOption.isDefault) {
-      return (
-        <span>
-          <strong>
-            {translate('default')}
-          </strong>
-          {': '}
-          {gateOption.label}
-        </span>
-      );
-    }
-
-    return gateOption.label;
-  }
-
-  renderSelect() {
-    const { gate, allGates } = this.props;
-
-    const options = allGates.map(gate => ({
-      value: gate.id,
-      label: gate.name,
-      isDefault: gate.isDefault
-    }));
-
-    const hasDefault = some(allGates, gate => gate.isDefault);
-    if (!hasDefault) {
-      options.unshift({
-        value: null,
-        label: translate('none')
-      });
-    }
-
-    return (
-      <Select
-        options={options}
-        valueRenderer={this.renderGateName}
-        optionRenderer={this.renderGateName}
-        value={gate && gate.id}
-        clearable={false}
-        placeholder={translate('none')}
-        style={{ width: 300 }}
-        disabled={this.state.loading}
-        onChange={this.handleChange.bind(this)}
-      />
-    );
-  }
-
-  render() {
-    return (
-      <div>
-        {this.renderSelect()}
-        {this.state.loading && <i className="spinner spacer-left" />}
-      </div>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/project-admin/quality-gate/Header.js b/server/sonar-web/src/main/js/apps/project-admin/quality-gate/Header.js
deleted file mode 100644 (file)
index fb3dd5f..0000000
+++ /dev/null
@@ -1,34 +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 { translate } from '../../../helpers/l10n';
-
-export default function Header() {
-  return (
-    <header className="page-header">
-      <h1 className="page-title">
-        {translate('project_quality_gate.page')}
-      </h1>
-      <div className="page-description">
-        {translate('project_quality_gate.page.description')}
-      </div>
-    </header>
-  );
-}
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 (file)
index 6194c04..0000000
+++ /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/Header.js b/server/sonar-web/src/main/js/apps/project-admin/quality-profiles/Header.js
deleted file mode 100644 (file)
index 0ecebfd..0000000
+++ /dev/null
@@ -1,34 +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 { 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/project-admin/quality-profiles/ProfileRow.js b/server/sonar-web/src/main/js/apps/project-admin/quality-profiles/ProfileRow.js
deleted file mode 100644 (file)
index 8a9e02e..0000000
+++ /dev/null
@@ -1,105 +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 Select from 'react-select';
-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
-  };
-
-  state = {
-    loading: false
-  };
-
-  componentWillUpdate(nextProps) {
-    if (nextProps.profile !== this.props.profile) {
-      this.setState({ loading: false });
-    }
-  }
-
-  handleChange(option) {
-    if (this.props.profile.key !== option.value) {
-      this.setState({ loading: true });
-      this.props.onChangeProfile(this.props.profile.key, option.value);
-    }
-  }
-
-  renderProfileName(profileOption) {
-    if (profileOption.isDefault) {
-      return (
-        <span>
-          <strong>
-            {translate('default')}
-          </strong>
-          {': '}
-          {profileOption.label}
-        </span>
-      );
-    }
-
-    return profileOption.label;
-  }
-
-  renderProfileSelect() {
-    const { profile, possibleProfiles } = this.props;
-
-    const options = possibleProfiles.map(profile => ({
-      value: profile.key,
-      label: profile.name,
-      isDefault: profile.isDefault
-    }));
-
-    return (
-      <Select
-        options={options}
-        valueRenderer={this.renderProfileName}
-        optionRenderer={this.renderProfileName}
-        value={profile.key}
-        clearable={false}
-        style={{ width: 300 }}
-        disabled={this.state.loading}
-        onChange={this.handleChange.bind(this)}
-      />
-    );
-  }
-
-  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/project-admin/quality-profiles/QualityProfiles.js b/server/sonar-web/src/main/js/apps/project-admin/quality-profiles/QualityProfiles.js
deleted file mode 100644 (file)
index 02ff3c3..0000000
+++ /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/quality-profiles/Table.js b/server/sonar-web/src/main/js/apps/project-admin/quality-profiles/Table.js
deleted file mode 100644 (file)
index 24a0991..0000000
+++ /dev/null
@@ -1,73 +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 { groupBy, orderBy } from 'lodash';
-import ProfileRow from './ProfileRow';
-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
-  };
-
-  renderHeader() {
-    // keep one empty cell for the spinner
-    return (
-      <thead>
-        <tr>
-          <th className="thin nowrap">
-            {translate('language')}
-          </th>
-          <th className="thin nowrap">
-            {translate('quality_profile')}
-          </th>
-          <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>
-    );
-  }
-}
index a7b363c2c7b9f25a1d7f0b8e093110b1a52eba10..f5efaa8e7223dbdfe591c2541444a3ed2fd5c56b 100644 (file)
 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} />
 ];
index 6701e8f07db24dbc7408167b5197aa456e31962a..28f7473e260a00b42ff19394e1b46ed2e5bfb5f8 100644 (file)
  * 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 (file)
index 451f531..0000000
+++ /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/gates.js b/server/sonar-web/src/main/js/apps/project-admin/store/gates.js
deleted file mode 100644 (file)
index d68aae1..0000000
+++ /dev/null
@@ -1,36 +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 { keyBy, values } from 'lodash';
-import { RECEIVE_GATES } from './actions';
-
-const gates = (state = {}, action = {}) => {
-  if (action.type === RECEIVE_GATES) {
-    const newGatesById = keyBy(action.gates, 'id');
-    return { ...state, ...newGatesById };
-  }
-
-  return state;
-};
-
-export default gates;
-
-export const getAllGates = state => values(state);
-
-export const getGate = (state, id) => state[id];
diff --git a/server/sonar-web/src/main/js/apps/project-admin/store/profiles.js b/server/sonar-web/src/main/js/apps/project-admin/store/profiles.js
deleted file mode 100644 (file)
index ed6a834..0000000
+++ /dev/null
@@ -1,36 +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 { keyBy, values } from 'lodash';
-import { RECEIVE_PROFILES } from './actions';
-
-const profiles = (state = {}, action = {}) => {
-  if (action.type === RECEIVE_PROFILES) {
-    const newProfilesByKey = keyBy(action.profiles, 'key');
-    return { ...state, ...newProfilesByKey };
-  }
-
-  return state;
-};
-
-export default profiles;
-
-export const getAllProfiles = state => values(state);
-
-export const getProfile = (state, key) => state[key];
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 (file)
index afc7ea0..0000000
+++ /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] || [];
index 4eb6dec8e0e898f8718255f6648a1de6eda8438f..43ab5760cfdf8ad4ea90c5420802fc4c79fced08 100644 (file)
  * 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 (file)
index 0000000..51d42a9
--- /dev/null
@@ -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/projectQualityGate/Form.tsx b/server/sonar-web/src/main/js/apps/projectQualityGate/Form.tsx
new file mode 100644 (file)
index 0000000..2e0e502
--- /dev/null
@@ -0,0 +1,131 @@
+/*
+ * 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 { some } from 'lodash';
+import { QualityGate } from '../../api/quality-gates';
+import { translate } from '../../helpers/l10n';
+
+interface Props {
+  allGates: QualityGate[];
+  gate?: QualityGate;
+  onChange: (oldGate: string | undefined, newGate: string) => Promise<void>;
+}
+
+interface State {
+  loading: boolean;
+}
+
+interface Option {
+  isDefault?: boolean;
+  label: string;
+  value: string;
+}
+
+export default class Form 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 }) => {
+    const { gate } = this.props;
+
+    const isSet = gate == null && option.value != null;
+    const isUnset = gate != null && option.value == null;
+    const isChanged = gate != null && gate.id !== option.value;
+    const hasChanged = isSet || isUnset || isChanged;
+
+    if (hasChanged) {
+      this.setState({ loading: true });
+      this.props.onChange(gate && gate.id, option.value).then(this.stopLoading, this.stopLoading);
+    }
+  };
+
+  renderGateName = (option: { isDefault?: boolean; label: string }) => {
+    if (option.isDefault) {
+      return (
+        <span>
+          <strong>
+            {translate('default')}
+          </strong>
+          {': '}
+          {option.label}
+        </span>
+      );
+    }
+
+    return (
+      <span>
+        {option.label}
+      </span>
+    );
+  };
+
+  renderSelect() {
+    const { gate, allGates } = this.props;
+
+    const options: Option[] = allGates.map(gate => ({
+      value: gate.id,
+      label: gate.name,
+      isDefault: gate.isDefault
+    }));
+
+    const hasDefault = some(allGates, gate => gate.isDefault);
+    if (!hasDefault) {
+      options.unshift({ value: '', label: translate('none') });
+    }
+
+    return (
+      <Select
+        clearable={false}
+        disabled={this.state.loading}
+        onChange={this.handleChange}
+        optionRenderer={this.renderGateName}
+        options={options}
+        placeholder={translate('none')}
+        style={{ width: 300 }}
+        value={gate && gate.id}
+        valueRenderer={this.renderGateName}
+      />
+    );
+  }
+
+  render() {
+    return (
+      <div>
+        {this.renderSelect()}
+        {this.state.loading && <i className="spinner spacer-left" />}
+      </div>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/projectQualityGate/Header.tsx b/server/sonar-web/src/main/js/apps/projectQualityGate/Header.tsx
new file mode 100644 (file)
index 0000000..4570192
--- /dev/null
@@ -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_gate.page')}
+      </h1>
+      <div className="page-description">
+        {translate('project_quality_gate.page.description')}
+      </div>
+    </header>
+  );
+}
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 (file)
index 0000000..ac0d97d
--- /dev/null
@@ -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 (file)
index 0000000..f06a73e
--- /dev/null
@@ -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 (file)
index 0000000..adeee21
--- /dev/null
@@ -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 (file)
index 0000000..fcbb0ec
--- /dev/null
@@ -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 (file)
index 0000000..eaade8b
--- /dev/null
@@ -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/projectQualityGate/routes.ts b/server/sonar-web/src/main/js/apps/projectQualityGate/routes.ts
new file mode 100644 (file)
index 0000000..e342e0f
--- /dev/null
@@ -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;
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 (file)
index 0000000..ccc8532
--- /dev/null
@@ -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 (file)
index 0000000..a758189
--- /dev/null
@@ -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 (file)
index 0000000..0679b44
--- /dev/null
@@ -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 (file)
index 0000000..43fca05
--- /dev/null
@@ -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 (file)
index 0000000..ab1f2ec
--- /dev/null
@@ -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 (file)
index 0000000..adeee21
--- /dev/null
@@ -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 (file)
index 0000000..cec351e
--- /dev/null
@@ -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 (file)
index 0000000..84e7035
--- /dev/null
@@ -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 (file)
index 0000000..4f0e35f
--- /dev/null
@@ -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 (file)
index 0000000..541a117
--- /dev/null
@@ -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 (file)
index 0000000..b4b9911
--- /dev/null
@@ -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 (file)
index 0000000..e342e0f
--- /dev/null
@@ -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;
index 91e5ed96cfc0d907d1f28a9d7d30a01d7c08a293..df6462d30b0163fa1fccdb1f18827ade7fc15e71 100644 (file)
@@ -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 = {
index 5252d1c184004235c9a4331223c4a684ba1c4bbe..957fa4790a2d817497d40f6d75a72a5f03153a4c 100644 (file)
@@ -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>
index 415ab3c03bb315a58cbeadb7e9a2ef5264200d86..88758d4659105f10436a07599bdb1a38e8b409a9 100644 (file)
  * 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;
 }
index 6d5c9db9d9fe7d327588a62c8cfd4529c40b2827..a23ab59a6c6f8acd47c89c83882e4ca7773568d2 100644 (file)
  * 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) {
index 9cdd198e860860799ce5856bc87b248896e34518..ef3e95d19311509a43e26b29d99bc3dcaaec6d70 100644 (file)
@@ -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);