]> source.dussan.org Git - sonarqube.git/commitdiff
rewrite custom measures app in react (#3052)
authorStas Vilchik <stas.vilchik@sonarsource.com>
Tue, 13 Feb 2018 08:19:41 +0000 (09:19 +0100)
committerGitHub <noreply@github.com>
Tue, 13 Feb 2018 08:19:41 +0000 (09:19 +0100)
47 files changed:
server/sonar-web/src/main/js/api/measures.ts
server/sonar-web/src/main/js/app/types.ts
server/sonar-web/src/main/js/apps/custom-measures/components/App.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/custom-measures/components/CreateButton.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/custom-measures/components/CustomMeasuresAppContainer.js [deleted file]
server/sonar-web/src/main/js/apps/custom-measures/components/DeleteButton.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/custom-measures/components/EditButton.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/custom-measures/components/Form.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/custom-measures/components/Header.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/custom-measures/components/List.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/App-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/CreateButton-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/DeleteButton-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/EditButton-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/Form-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/Header-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/List-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/App-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/CreateButton-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/DeleteButton-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/EditButton-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/Form-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/Header-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/List-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/custom-measures/create-view.js [deleted file]
server/sonar-web/src/main/js/apps/custom-measures/custom-measure.js [deleted file]
server/sonar-web/src/main/js/apps/custom-measures/custom-measures.js [deleted file]
server/sonar-web/src/main/js/apps/custom-measures/delete-view.js [deleted file]
server/sonar-web/src/main/js/apps/custom-measures/form-view.js [deleted file]
server/sonar-web/src/main/js/apps/custom-measures/header-view.js [deleted file]
server/sonar-web/src/main/js/apps/custom-measures/init.js [deleted file]
server/sonar-web/src/main/js/apps/custom-measures/layout.js [deleted file]
server/sonar-web/src/main/js/apps/custom-measures/list-footer-view.js [deleted file]
server/sonar-web/src/main/js/apps/custom-measures/list-item-view.js [deleted file]
server/sonar-web/src/main/js/apps/custom-measures/list-view.js [deleted file]
server/sonar-web/src/main/js/apps/custom-measures/metric.js [deleted file]
server/sonar-web/src/main/js/apps/custom-measures/metrics.js [deleted file]
server/sonar-web/src/main/js/apps/custom-measures/routes.ts
server/sonar-web/src/main/js/apps/custom-measures/templates/custom-measures-delete.hbs [deleted file]
server/sonar-web/src/main/js/apps/custom-measures/templates/custom-measures-form.hbs [deleted file]
server/sonar-web/src/main/js/apps/custom-measures/templates/custom-measures-header.hbs [deleted file]
server/sonar-web/src/main/js/apps/custom-measures/templates/custom-measures-layout.hbs [deleted file]
server/sonar-web/src/main/js/apps/custom-measures/templates/custom-measures-list-footer.hbs [deleted file]
server/sonar-web/src/main/js/apps/custom-measures/templates/custom-measures-list-item.hbs [deleted file]
server/sonar-web/src/main/js/apps/custom-measures/templates/custom-measures-list.hbs [deleted file]
server/sonar-web/src/main/js/apps/custom-measures/update-view.js [deleted file]
server/sonar-web/src/main/js/apps/custom-metrics/components/App.tsx

index 2f5ae506503e3a42aef0d1d9ca24adc17556813c..bf6f8712090be0206a1360d76f8ae083e35939e8 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 { getJSON, RequestData } from '../helpers/request';
+import { getJSON, RequestData, postJSON, post } from '../helpers/request';
 import throwGlobalError from '../app/utils/throwGlobalError';
 import { Measure, MeasurePeriod } from '../helpers/measures';
-import { Metric } from '../app/types';
+import { Metric, CustomMeasure, Paging } from '../app/types';
 import { Period } from '../helpers/periods';
 
 export function getMeasures(
@@ -66,3 +66,36 @@ export function getMeasuresForProjects(
     metricKeys: metricKeys.join()
   }).then(r => r.measures);
 }
+
+export function getCustomMeasures(data: {
+  f?: string;
+  p?: number;
+  projectKey: string;
+  ps?: number;
+}): Promise<{ customMeasures: CustomMeasure[]; paging: Paging }> {
+  return getJSON('/api/custom_measures/search', data).then(
+    r =>
+      ({
+        customMeasures: r.customMeasures,
+        paging: { pageIndex: r.p, pageSize: r.ps, total: r.total }
+      } as any),
+    throwGlobalError
+  );
+}
+
+export function createCustomMeasure(data: {
+  description?: string;
+  metricKey: string;
+  projectKey: string;
+  value: string;
+}): Promise<CustomMeasure> {
+  return postJSON('/api/custom_measures/create', data).catch(throwGlobalError);
+}
+
+export function updateCustomMeasure(data: { description?: string; id: string; value?: string }) {
+  return post('/api/custom_measures/update', data).catch(throwGlobalError);
+}
+
+export function deleteCustomMeasure(data: { id: string }) {
+  return post('/api/custom_measures/delete', data).catch(throwGlobalError);
+}
index 1eb6b2b5350cffda8b2a462d29936de61129842a..504e86547be5689ac9bae628a2a72ec3263bdfcc 100644 (file)
@@ -292,3 +292,25 @@ export interface User {
   scmAccounts?: string[];
   tokensCount?: number;
 }
+
+export interface CustomMeasure {
+  createdAt?: string;
+  description?: string;
+  id: string;
+  metric: {
+    key: string;
+    name: string;
+    domain?: string;
+    type: string;
+  };
+  projectKey: string;
+  pending?: boolean;
+  user: {
+    active?: boolean;
+    email?: string;
+    login: string;
+    name: string;
+  };
+  value: string;
+  updatedAt?: string;
+}
diff --git a/server/sonar-web/src/main/js/apps/custom-measures/components/App.tsx b/server/sonar-web/src/main/js/apps/custom-measures/components/App.tsx
new file mode 100644 (file)
index 0000000..10ad59e
--- /dev/null
@@ -0,0 +1,159 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 List from './List';
+import {
+  getCustomMeasures,
+  createCustomMeasure,
+  updateCustomMeasure,
+  deleteCustomMeasure
+} from '../../../api/measures';
+import { Paging, CustomMeasure } from '../../../app/types';
+import ListFooter from '../../../components/controls/ListFooter';
+import { translate } from '../../../helpers/l10n';
+
+interface Props {
+  component: { key: string };
+}
+
+interface State {
+  loading: boolean;
+  measures?: CustomMeasure[];
+  paging?: Paging;
+}
+
+const PAGE_SIZE = 50;
+
+export default class App extends React.PureComponent<Props, State> {
+  mounted: boolean;
+  state: State = { loading: true };
+
+  componentDidMount() {
+    this.mounted = true;
+    this.fetchMeasures();
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+  }
+
+  fetchMeasures = () => {
+    this.setState({ loading: true });
+    getCustomMeasures({ projectKey: this.props.component.key, ps: PAGE_SIZE }).then(
+      ({ customMeasures, paging }) => {
+        if (this.mounted) {
+          this.setState({ loading: false, measures: customMeasures, paging });
+        }
+      },
+      this.stopLoading
+    );
+  };
+
+  fetchMore = () => {
+    const { paging } = this.state;
+    if (paging) {
+      this.setState({ loading: true });
+      getCustomMeasures({
+        projectKey: this.props.component.key,
+        p: paging.pageIndex + 1,
+        ps: PAGE_SIZE
+      }).then(({ customMeasures, paging }) => {
+        if (this.mounted) {
+          this.setState(({ measures = [] }: State) => ({
+            loading: false,
+            measures: [...measures, ...customMeasures],
+            paging
+          }));
+        }
+      }, this.stopLoading);
+    }
+  };
+
+  stopLoading = () => {
+    if (this.mounted) {
+      this.setState({ loading: false });
+    }
+  };
+
+  handleCreate = (data: { description: string; metricKey: string; value: string }) => {
+    return createCustomMeasure({ ...data, projectKey: this.props.component.key }).then(measure => {
+      if (this.mounted) {
+        this.setState(({ measures = [], paging }: State) => ({
+          measures: [...measures, measure],
+          paging: paging && { ...paging, total: paging.total + 1 }
+        }));
+      }
+    });
+  };
+
+  handleEdit = (data: { description: string; id: string; value: string }) => {
+    return updateCustomMeasure(data).then(() => {
+      if (this.mounted) {
+        this.setState(({ measures = [] }: State) => ({
+          measures: measures.map(
+            measure => (measure.id === data.id ? { ...measure, ...data } : measure)
+          )
+        }));
+      }
+    });
+  };
+
+  handleDelete = (measureId: string) => {
+    return deleteCustomMeasure({ id: measureId }).then(() => {
+      if (this.mounted) {
+        this.setState(({ measures = [], paging }: State) => ({
+          measures: measures.filter(measure => measure.id !== measureId),
+          paging: paging && { ...paging, total: paging.total - 1 }
+        }));
+      }
+    });
+  };
+
+  render() {
+    const { loading, measures, paging } = this.state;
+
+    return (
+      <>
+        <Helmet title={translate('custom_measures.page')} />
+        <div className="page page-limited">
+          <Header
+            loading={loading}
+            onCreate={this.handleCreate}
+            skipMetrics={measures && measures.map(measure => measure.metric.key)}
+          />
+          {measures && (
+            <List measures={measures} onDelete={this.handleDelete} onEdit={this.handleEdit} />
+          )}
+          {measures &&
+            paging && (
+              <ListFooter
+                count={measures.length}
+                loadMore={this.fetchMore}
+                ready={!loading}
+                total={paging.total}
+              />
+            )}
+        </div>
+      </>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/custom-measures/components/CreateButton.tsx b/server/sonar-web/src/main/js/apps/custom-measures/components/CreateButton.tsx
new file mode 100644 (file)
index 0000000..ed9e536
--- /dev/null
@@ -0,0 +1,73 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 Form from './Form';
+import { translate } from '../../../helpers/l10n';
+
+interface Props {
+  onCreate: (data: { description: string; metricKey: string; value: string }) => Promise<void>;
+  skipMetrics: string[] | undefined;
+}
+
+interface State {
+  modal: boolean;
+}
+
+export default class CreateButton extends React.PureComponent<Props, State> {
+  mounted: boolean;
+  state: State = { modal: false };
+
+  componentDidMount() {
+    this.mounted = true;
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+  }
+
+  handleClick = () => {
+    this.setState({ modal: true });
+  };
+
+  handleClose = () => {
+    if (this.mounted) {
+      this.setState({ modal: false });
+    }
+  };
+
+  render() {
+    return (
+      <>
+        <button id="custom-measures-create" onClick={this.handleClick} type="button">
+          {translate('create')}
+        </button>
+        {this.state.modal && (
+          <Form
+            confirmButtonText={translate('create')}
+            header={translate('custom_measures.create_custom_measure')}
+            onClose={this.handleClose}
+            onSubmit={this.props.onCreate}
+            skipMetrics={this.props.skipMetrics}
+          />
+        )}
+      </>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/custom-measures/components/CustomMeasuresAppContainer.js b/server/sonar-web/src/main/js/apps/custom-measures/components/CustomMeasuresAppContainer.js
deleted file mode 100644 (file)
index 3decb58..0000000
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 Helmet from 'react-helmet';
-import init from '../init';
-import { translate } from '../../../helpers/l10n';
-
-export default class CustomMeasuresAppContainer extends React.PureComponent {
-  componentDidMount() {
-    init(this.refs.container, this.props.component);
-  }
-
-  render() {
-    return (
-      <div>
-        <Helmet title={translate('custom_measures.page')} />
-        <div ref="container" />
-      </div>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/custom-measures/components/DeleteButton.tsx b/server/sonar-web/src/main/js/apps/custom-measures/components/DeleteButton.tsx
new file mode 100644 (file)
index 0000000..f4f2ff1
--- /dev/null
@@ -0,0 +1,53 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 { CustomMeasure } from '../../../app/types';
+import ConfirmButton from '../../../components/controls/ConfirmButton';
+import { ActionsDropdownItem } from '../../../components/controls/ActionsDropdown';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
+
+interface Props {
+  measure: CustomMeasure;
+  onDelete: (measureId: string) => Promise<void>;
+}
+
+export default function DeleteButton({ measure, onDelete }: Props) {
+  return (
+    <ConfirmButton
+      confirmButtonText={translate('delete')}
+      confirmData={measure.id}
+      isDestructive={true}
+      modalBody={translateWithParameters(
+        'custom_measures.delete_custom_measure.confirmation',
+        measure.metric.name
+      )}
+      modalHeader={translate('custom_measures.delete_custom_measure')}
+      onConfirm={onDelete}>
+      {({ onClick }) => (
+        <ActionsDropdownItem
+          className="js-custom-measure-delete"
+          destructive={true}
+          onClick={onClick}>
+          {translate('delete')}
+        </ActionsDropdownItem>
+      )}
+    </ConfirmButton>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/custom-measures/components/EditButton.tsx b/server/sonar-web/src/main/js/apps/custom-measures/components/EditButton.tsx
new file mode 100644 (file)
index 0000000..ac0bef4
--- /dev/null
@@ -0,0 +1,79 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 Form from './Form';
+import { CustomMeasure } from '../../../app/types';
+import { translate } from '../../../helpers/l10n';
+import { ActionsDropdownItem } from '../../../components/controls/ActionsDropdown';
+
+interface Props {
+  measure: CustomMeasure;
+  onEdit: (data: { description: string; id: string; value: string }) => Promise<void>;
+}
+
+interface State {
+  modal: boolean;
+}
+
+export default class EditButton extends React.PureComponent<Props, State> {
+  mounted: boolean;
+  state: State = { modal: false };
+
+  componentDidMount() {
+    this.mounted = true;
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+  }
+
+  handleClick = () => {
+    this.setState({ modal: true });
+  };
+
+  handleClose = () => {
+    if (this.mounted) {
+      this.setState({ modal: false });
+    }
+  };
+
+  handleSubmit = (data: { description: string; value: string }) => {
+    return this.props.onEdit({ id: this.props.measure.id, ...data });
+  };
+
+  render() {
+    return (
+      <>
+        <ActionsDropdownItem className="js-custom-measure-update" onClick={this.handleClick}>
+          {translate('update_verb')}
+        </ActionsDropdownItem>
+        {this.state.modal && (
+          <Form
+            confirmButtonText={translate('update_verb')}
+            header={translate('custom_measures.update_custom_measure')}
+            measure={this.props.measure}
+            onClose={this.handleClose}
+            onSubmit={this.handleSubmit}
+          />
+        )}
+      </>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/custom-measures/components/Form.tsx b/server/sonar-web/src/main/js/apps/custom-measures/components/Form.tsx
new file mode 100644 (file)
index 0000000..97aceef
--- /dev/null
@@ -0,0 +1,209 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 { getAllMetrics } from '../../../api/metrics';
+import { CustomMeasure, Metric } from '../../../app/types';
+import DeferredSpinner from '../../../components/common/DeferredSpinner';
+import Select from '../../../components/controls/Select';
+import SimpleModal from '../../../components/controls/SimpleModal';
+import { translate } from '../../../helpers/l10n';
+
+interface Props {
+  confirmButtonText: string;
+  header: string;
+  measure?: CustomMeasure;
+  onClose: () => void;
+  onSubmit: (data: { description: string; metricKey: string; value: string }) => Promise<void>;
+  skipMetrics?: string[];
+}
+
+interface State {
+  description: string;
+  loading: boolean;
+  metricKey?: string;
+  metrics?: Metric[];
+  value: string;
+}
+
+export default class Form extends React.PureComponent<Props, State> {
+  mounted: boolean;
+
+  constructor(props: Props) {
+    super(props);
+    this.state = {
+      description: (props.measure && props.measure.description) || '',
+      loading: false,
+      metricKey: props.measure && props.measure.metric.key,
+      value: (props.measure && props.measure.value) || ''
+    };
+  }
+
+  componentDidMount() {
+    this.mounted = true;
+    if (!this.props.measure) {
+      this.fetchCustomMetrics();
+    }
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+  }
+
+  handleSubmit = () => {
+    return this.state.metricKey
+      ? this.props
+          .onSubmit({
+            description: this.state.description,
+            metricKey: this.state.metricKey,
+            value: this.state.value
+          })
+          .then(this.props.onClose)
+      : Promise.reject(undefined);
+  };
+
+  fetchCustomMetrics = () => {
+    this.setState({ loading: true });
+    getAllMetrics({ isCustom: true }).then(
+      metrics => {
+        if (this.mounted) {
+          this.setState({ loading: false, metrics });
+        }
+      },
+      () => {
+        if (this.mounted) {
+          this.setState({ loading: false });
+        }
+      }
+    );
+  };
+
+  handleMetricSelect = ({ value }: { value: string }) => {
+    this.setState({ metricKey: value });
+  };
+
+  handleDescriptionChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
+    this.setState({ description: event.currentTarget.value });
+  };
+
+  handleValueChange = (event: React.ChangeEvent<HTMLInputElement>) => {
+    this.setState({ value: event.currentTarget.value });
+  };
+
+  renderMetricSelect = (options: { label: string; value: string }[]) => {
+    if (!options.length && !this.state.loading) {
+      return (
+        <div className="alert alert-warning">{translate('custom_measures.all_metrics_taken')}</div>
+      );
+    }
+    return (
+      <div className="modal-field">
+        <label htmlFor="create-custom-measure-metric">
+          {translate('custom_measures.metric')}
+          <em className="mandatory">*</em>
+        </label>
+        {this.state.loading ? (
+          <i className="spinner" />
+        ) : (
+          <Select
+            autofocus={true}
+            clearable={false}
+            onChange={this.handleMetricSelect}
+            options={options}
+            value={this.state.metricKey}
+          />
+        )}
+      </div>
+    );
+  };
+
+  render() {
+    const { skipMetrics = [] } = this.props;
+    const { metrics = [] } = this.state;
+    const options = metrics
+      .filter(metric => !skipMetrics.includes(metric.key))
+      .map(metric => ({ label: metric.name, value: metric.key }));
+    const forbidSubmitting = !this.props.measure && !options.length;
+
+    return (
+      <SimpleModal
+        header={this.props.header}
+        onClose={this.props.onClose}
+        onSubmit={this.handleSubmit}>
+        {({ onCloseClick, onFormSubmit, submitting }) => (
+          <form onSubmit={onFormSubmit}>
+            <header className="modal-head">
+              <h2>{this.props.header}</h2>
+            </header>
+
+            <div className="modal-body">
+              {!this.props.measure && this.renderMetricSelect(options)}
+
+              <div className="modal-field">
+                <label htmlFor="create-custom-measure-value">
+                  {translate('value')}
+                  <em className="mandatory">*</em>
+                </label>
+                <input
+                  autoFocus={this.props.measure !== undefined}
+                  id="create-custom-measure-value"
+                  maxLength={200}
+                  name="value"
+                  onChange={this.handleValueChange}
+                  required={true}
+                  type="text"
+                  value={this.state.value}
+                />
+              </div>
+              <div className="modal-field">
+                <label htmlFor="create-custom-measure-description">
+                  {translate('description')}
+                </label>
+                <textarea
+                  id="create-custom-measure-description"
+                  name="description"
+                  onChange={this.handleDescriptionChange}
+                  value={this.state.description}
+                />
+              </div>
+            </div>
+
+            <footer className="modal-foot">
+              <DeferredSpinner className="spacer-right" loading={submitting} />
+              <button
+                disabled={forbidSubmitting || submitting}
+                id="create-custom-measure-submit"
+                type="submit">
+                {this.props.confirmButtonText}
+              </button>
+              <button
+                className="button-link"
+                disabled={submitting}
+                id="create-custom-measure-cancel"
+                onClick={onCloseClick}
+                type="reset">
+                {translate('cancel')}
+              </button>
+            </footer>
+          </form>
+        )}
+      </SimpleModal>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/custom-measures/components/Header.tsx b/server/sonar-web/src/main/js/apps/custom-measures/components/Header.tsx
new file mode 100644 (file)
index 0000000..ce71ae6
--- /dev/null
@@ -0,0 +1,42 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 CreateButton from './CreateButton';
+import DeferredSpinner from '../../../components/common/DeferredSpinner';
+import { translate } from '../../../helpers/l10n';
+
+interface Props {
+  loading: boolean;
+  onCreate: (data: { description: string; metricKey: string; value: string }) => Promise<void>;
+  skipMetrics: string[] | undefined;
+}
+
+export default function Header({ loading, onCreate, skipMetrics }: Props) {
+  return (
+    <header className="page-header" id="custom-measures-header">
+      <h1 className="page-title">{translate('custom_measures.page')}</h1>
+      <DeferredSpinner loading={loading} />
+      <div className="page-actions">
+        <CreateButton onCreate={onCreate} skipMetrics={skipMetrics} />
+      </div>
+      <p className="page-description">{translate('custom_measures.page.description')}</p>
+    </header>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/custom-measures/components/List.tsx b/server/sonar-web/src/main/js/apps/custom-measures/components/List.tsx
new file mode 100644 (file)
index 0000000..239b4ee
--- /dev/null
@@ -0,0 +1,125 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 { sortBy } from 'lodash';
+import DeleteButton from './DeleteButton';
+import EditButton from './EditButton';
+import { CustomMeasure } from '../../../app/types';
+import ActionsDropdown, {
+  ActionsDropdownDivider
+} from '../../../components/controls/ActionsDropdown';
+import { translate } from '../../../helpers/l10n';
+import Tooltip from '../../../components/controls/Tooltip';
+import { formatMeasure } from '../../../helpers/measures';
+import DateFormatter from '../../../components/intl/DateFormatter';
+
+interface Props {
+  measures: CustomMeasure[];
+  onDelete: (measureId: string) => Promise<void>;
+  onEdit: (data: { description: string; id: string; value: string }) => Promise<void>;
+}
+
+export default function List({ measures, onDelete, onEdit }: Props) {
+  return (
+    <div className="boxed-group boxed-group-inner" id="custom-measures-list">
+      {measures.length > 0 ? (
+        <table className="data zebra zebra-hover">
+          <thead>
+            <tr>
+              <th>{translate('custom_measures.metric')}</th>
+              <th>{translate('value')}</th>
+              <th>{translate('description')}</th>
+              <th>{translate('date')}</th>
+              <th />
+            </tr>
+          </thead>
+          <tbody>
+            {sortBy(measures, measure => measure.metric.name.toLowerCase()).map(measure => (
+              <tr data-metric={measure.metric.key} key={measure.id}>
+                <td className="nowrap">
+                  <div>
+                    <span className="js-custom-measure-metric-name">{measure.metric.name}</span>
+                    {measure.pending && (
+                      <Tooltip overlay={translate('custom_measures.pending_tooltip')}>
+                        <span className="js-custom-measure-pending badge badge-warning spacer-left">
+                          {translate('custom_measures.pending')}
+                        </span>
+                      </Tooltip>
+                    )}
+                  </div>
+                  <span className="js-custom-measure-domain note">{measure.metric.domain}</span>
+                </td>
+
+                <td className="nowrap">
+                  <strong className="js-custom-measure-value">
+                    {formatMeasure(measure.value, measure.metric.type)}
+                  </strong>
+                </td>
+
+                <td>
+                  <span className="js-custom-measure-description">{measure.description}</span>
+                </td>
+
+                <td>
+                  <MeasureDate measure={measure} /> {translate('by_')}{' '}
+                  <span className="js-custom-measure-user">{measure.user.name}</span>
+                </td>
+
+                <td className="thin nowrap">
+                  <ActionsDropdown>
+                    <EditButton measure={measure} onEdit={onEdit} />
+                    <ActionsDropdownDivider />
+                    <DeleteButton measure={measure} onDelete={onDelete} />
+                  </ActionsDropdown>
+                </td>
+              </tr>
+            ))}
+          </tbody>
+        </table>
+      ) : (
+        <p>{translate('no_results')}</p>
+      )}
+    </div>
+  );
+}
+
+function MeasureDate({ measure }: { measure: CustomMeasure }) {
+  if (measure.updatedAt) {
+    return (
+      <>
+        {translate('updated_on')}{' '}
+        <span className="js-custom-measure-created-at">
+          <DateFormatter date={measure.updatedAt} />
+        </span>
+      </>
+    );
+  } else if (measure.createdAt) {
+    return (
+      <>
+        {translate('created_on')}{' '}
+        <span className="js-custom-measure-created-at">
+          <DateFormatter date={measure.createdAt} />
+        </span>
+      </>
+    );
+  } else {
+    return <>{translate('created')}</>;
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/App-test.tsx b/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/App-test.tsx
new file mode 100644 (file)
index 0000000..a264af8
--- /dev/null
@@ -0,0 +1,90 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 { shallow } from 'enzyme';
+import App from '../App';
+
+jest.mock('../../../../api/measures', () => ({
+  getCustomMeasures: () =>
+    Promise.resolve({
+      customMeasures: [
+        {
+          createdAt: '2017-01-01',
+          description: 'my custom measure',
+          id: '1',
+          metric: { key: 'custom', name: 'custom-metric', type: 'STRING' },
+          projectKey: 'foo',
+          user: { active: true, login: 'user', name: 'user' },
+          value: 'custom-value'
+        }
+      ],
+      paging: { pageIndex: 1, pageSize: 1, total: 1 }
+    }),
+  createCustomMeasure: () =>
+    Promise.resolve({
+      createdAt: '2018-01-01',
+      description: 'description',
+      id: '2',
+      metric: { key: 'metricKey', name: 'Metric Name', type: 'STRING' },
+      projectKey: 'foo',
+      user: { active: true, login: 'user', name: 'user' },
+      value: 'value'
+    }),
+  updateCustomMeasure: () => Promise.resolve(),
+  deleteCustomMeasure: () => Promise.resolve()
+}));
+
+it('should work', async () => {
+  const wrapper = shallow(<App component={{ key: 'foo' }} />);
+  expect(wrapper).toMatchSnapshot();
+
+  await new Promise(setImmediate);
+  wrapper.update();
+  expect(wrapper).toMatchSnapshot();
+
+  // create
+  wrapper.find('Header').prop<Function>('onCreate')({
+    description: 'description',
+    metricKey: 'metricKey',
+    value: 'value'
+  });
+  await new Promise(setImmediate);
+  wrapper.update();
+  expect(wrapper.state().measures).toMatchSnapshot();
+  expect(wrapper.state().paging.total).toBe(2);
+
+  // edit
+  wrapper.find('List').prop<Function>('onEdit')({
+    description: 'another',
+    id: '2',
+    value: 'other'
+  });
+  await new Promise(setImmediate);
+  wrapper.update();
+  expect(wrapper.state().measures).toMatchSnapshot();
+  expect(wrapper.state().paging.total).toBe(2);
+
+  // delete
+  wrapper.find('List').prop<Function>('onDelete')('2');
+  await new Promise(setImmediate);
+  wrapper.update();
+  expect(wrapper.state().measures).toMatchSnapshot();
+  expect(wrapper.state().paging.total).toBe(1);
+});
diff --git a/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/CreateButton-test.tsx b/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/CreateButton-test.tsx
new file mode 100644 (file)
index 0000000..f939e14
--- /dev/null
@@ -0,0 +1,43 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 { shallow } from 'enzyme';
+import CreateButton from '../CreateButton';
+import { click } from '../../../../helpers/testUtils';
+
+it('should create new custom measure', () => {
+  const onCreate = jest.fn(() => Promise.resolve());
+  const wrapper = shallow(<CreateButton onCreate={onCreate} skipMetrics={[]} />);
+  expect(wrapper).toMatchSnapshot();
+
+  click(wrapper.find('#custom-measures-create'));
+  expect(wrapper).toMatchSnapshot();
+
+  wrapper.find('Form').prop<Function>('onSubmit')({
+    description: 'description',
+    metricKey: 'metricKey',
+    value: 'value'
+  });
+  expect(onCreate).toBeCalledWith({
+    description: 'description',
+    metricKey: 'metricKey',
+    value: 'value'
+  });
+});
diff --git a/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/DeleteButton-test.tsx b/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/DeleteButton-test.tsx
new file mode 100644 (file)
index 0000000..3c032da
--- /dev/null
@@ -0,0 +1,40 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 { shallow } from 'enzyme';
+import DeleteButton from '../DeleteButton';
+
+it('should delete custom measure', () => {
+  const measure = {
+    createdAt: '2017-01-01',
+    description: 'my custom measure',
+    id: '1',
+    metric: { key: 'custom', name: 'custom-metric', type: 'STRING' },
+    projectKey: 'foo',
+    user: { active: true, login: 'user', name: 'user' },
+    value: 'custom-value'
+  };
+  const onDelete = jest.fn();
+  const wrapper = shallow(<DeleteButton measure={measure} onDelete={onDelete} />);
+  expect(wrapper).toMatchSnapshot();
+
+  wrapper.find('ConfirmButton').prop<Function>('onConfirm')('1');
+  expect(onDelete).toBeCalledWith('1');
+});
diff --git a/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/EditButton-test.tsx b/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/EditButton-test.tsx
new file mode 100644 (file)
index 0000000..f1efbc3
--- /dev/null
@@ -0,0 +1,50 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 { shallow } from 'enzyme';
+import EditButton from '../EditButton';
+import { click } from '../../../../helpers/testUtils';
+
+it('should edit metric', () => {
+  const measure = {
+    createdAt: '2017-01-01',
+    description: 'my custom measure',
+    id: '1',
+    metric: { key: 'custom', name: 'custom-metric', type: 'STRING' },
+    projectKey: 'foo',
+    user: { active: true, login: 'user', name: 'user' },
+    value: 'custom-value'
+  };
+  const onEdit = jest.fn();
+
+  const wrapper = shallow(<EditButton measure={measure} onEdit={onEdit} />);
+  expect(wrapper).toMatchSnapshot();
+
+  click(wrapper.find('.js-custom-measure-update'));
+  wrapper.update();
+  expect(wrapper).toMatchSnapshot();
+
+  wrapper.find('Form').prop<Function>('onSubmit')({
+    ...measure,
+    description: 'new-description',
+    value: 'new-value'
+  });
+  expect(onEdit).toBeCalledWith({ ...measure, description: 'new-description', value: 'new-value' });
+});
diff --git a/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/Form-test.tsx b/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/Form-test.tsx
new file mode 100644 (file)
index 0000000..3af7a3c
--- /dev/null
@@ -0,0 +1,68 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 { shallow } from 'enzyme';
+import Form from '../Form';
+import { change, submit, click } from '../../../../helpers/testUtils';
+
+jest.mock('../../../../api/metrics', () => ({
+  getAllMetrics: () =>
+    Promise.resolve([
+      { id: '1', key: 'custom-metric', name: 'Custom Metric', type: 'STRING' },
+      { id: '2', key: 'skipped-metric', name: 'Skipped Metric', type: 'FLOAT' }
+    ])
+}));
+
+it('should render form', async () => {
+  const onClose = jest.fn();
+  const onSubmit = jest.fn(() => Promise.resolve());
+  const wrapper = shallow(
+    <Form
+      confirmButtonText="confirmButtonText"
+      header="header"
+      onClose={onClose}
+      onSubmit={onSubmit}
+      skipMetrics={['skipped-metric']}
+    />
+  );
+  expect(wrapper.dive()).toMatchSnapshot();
+
+  await new Promise(setImmediate);
+  wrapper.update();
+  const form = wrapper.dive();
+  expect(form).toMatchSnapshot();
+
+  form.find('Select').prop<Function>('onChange')({ value: 'custom-metric' });
+  change(form.find('[name="value"]'), 'Foo');
+  change(form.find('[name="description"]'), 'bar');
+  submit(form.find('form'));
+  expect(onSubmit).toBeCalledWith({
+    description: 'bar',
+    metricKey: 'custom-metric',
+    value: 'Foo'
+  });
+
+  await new Promise(setImmediate);
+  expect(onClose).toBeCalled();
+
+  onClose.mockClear();
+  click(form.find('button[type="reset"]'));
+  expect(onClose).toBeCalled();
+});
diff --git a/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/Header-test.tsx b/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/Header-test.tsx
new file mode 100644 (file)
index 0000000..f4b9a77
--- /dev/null
@@ -0,0 +1,35 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 { shallow } from 'enzyme';
+import Header from '../Header';
+
+it('should create new custom measure', () => {
+  const onCreate = jest.fn(() => Promise.resolve());
+  const wrapper = shallow(<Header loading={false} onCreate={onCreate} skipMetrics={[]} />);
+  expect(wrapper).toMatchSnapshot();
+
+  wrapper.find('CreateButton').prop<Function>('onCreate')({
+    description: 'bla',
+    metricKey: 'custom-metric',
+    name: 'Foo'
+  });
+  expect(onCreate).toBeCalledWith({ description: 'bla', metricKey: 'custom-metric', name: 'Foo' });
+});
diff --git a/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/List-test.tsx b/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/List-test.tsx
new file mode 100644 (file)
index 0000000..adf6858
--- /dev/null
@@ -0,0 +1,51 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 { shallow } from 'enzyme';
+import List from '../List';
+
+it('should render', () => {
+  const measures = [
+    {
+      createdAt: '2017-01-01',
+      description: 'my custom measure',
+      id: '1',
+      metric: { key: 'custom', name: 'custom-metric', type: 'STRING' },
+      projectKey: 'foo',
+      user: { active: true, login: 'user', name: 'user' },
+      value: 'custom-value'
+    },
+    {
+      createdAt: '2017-01-01',
+      id: '2',
+      metric: { key: 'another', name: 'another-metric', type: 'STRING' },
+      projectKey: 'foo',
+      user: { active: true, login: 'user', name: 'user' },
+      value: 'another-value'
+    }
+  ];
+  expect(
+    shallow(<List measures={measures} onDelete={jest.fn()} onEdit={jest.fn()} />)
+  ).toMatchSnapshot();
+});
+
+it('should render no results', () => {
+  expect(shallow(<List measures={[]} onDelete={jest.fn()} onEdit={jest.fn()} />)).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/App-test.tsx.snap b/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/App-test.tsx.snap
new file mode 100644 (file)
index 0000000..cb97413
--- /dev/null
@@ -0,0 +1,173 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should work 1`] = `
+<React.Fragment>
+  <HelmetWrapper
+    defer={true}
+    encodeSpecialCharacters={true}
+    title="custom_measures.page"
+  />
+  <div
+    className="page page-limited"
+  >
+    <Header
+      loading={true}
+      onCreate={[Function]}
+    />
+  </div>
+</React.Fragment>
+`;
+
+exports[`should work 2`] = `
+<React.Fragment>
+  <HelmetWrapper
+    defer={true}
+    encodeSpecialCharacters={true}
+    title="custom_measures.page"
+  />
+  <div
+    className="page page-limited"
+  >
+    <Header
+      loading={false}
+      onCreate={[Function]}
+      skipMetrics={
+        Array [
+          "custom",
+        ]
+      }
+    />
+    <List
+      measures={
+        Array [
+          Object {
+            "createdAt": "2017-01-01",
+            "description": "my custom measure",
+            "id": "1",
+            "metric": Object {
+              "key": "custom",
+              "name": "custom-metric",
+              "type": "STRING",
+            },
+            "projectKey": "foo",
+            "user": Object {
+              "active": true,
+              "login": "user",
+              "name": "user",
+            },
+            "value": "custom-value",
+          },
+        ]
+      }
+      onDelete={[Function]}
+      onEdit={[Function]}
+    />
+    <ListFooter
+      count={1}
+      loadMore={[Function]}
+      ready={true}
+      total={1}
+    />
+  </div>
+</React.Fragment>
+`;
+
+exports[`should work 3`] = `
+Array [
+  Object {
+    "createdAt": "2017-01-01",
+    "description": "my custom measure",
+    "id": "1",
+    "metric": Object {
+      "key": "custom",
+      "name": "custom-metric",
+      "type": "STRING",
+    },
+    "projectKey": "foo",
+    "user": Object {
+      "active": true,
+      "login": "user",
+      "name": "user",
+    },
+    "value": "custom-value",
+  },
+  Object {
+    "createdAt": "2018-01-01",
+    "description": "description",
+    "id": "2",
+    "metric": Object {
+      "key": "metricKey",
+      "name": "Metric Name",
+      "type": "STRING",
+    },
+    "projectKey": "foo",
+    "user": Object {
+      "active": true,
+      "login": "user",
+      "name": "user",
+    },
+    "value": "value",
+  },
+]
+`;
+
+exports[`should work 4`] = `
+Array [
+  Object {
+    "createdAt": "2017-01-01",
+    "description": "my custom measure",
+    "id": "1",
+    "metric": Object {
+      "key": "custom",
+      "name": "custom-metric",
+      "type": "STRING",
+    },
+    "projectKey": "foo",
+    "user": Object {
+      "active": true,
+      "login": "user",
+      "name": "user",
+    },
+    "value": "custom-value",
+  },
+  Object {
+    "createdAt": "2018-01-01",
+    "description": "another",
+    "id": "2",
+    "metric": Object {
+      "key": "metricKey",
+      "name": "Metric Name",
+      "type": "STRING",
+    },
+    "projectKey": "foo",
+    "user": Object {
+      "active": true,
+      "login": "user",
+      "name": "user",
+    },
+    "value": "other",
+  },
+]
+`;
+
+exports[`should work 5`] = `
+Array [
+  Object {
+    "createdAt": "2017-01-01",
+    "description": "my custom measure",
+    "id": "1",
+    "metric": Object {
+      "key": "custom",
+      "name": "custom-metric",
+      "type": "STRING",
+    },
+    "projectKey": "foo",
+    "user": Object {
+      "active": true,
+      "login": "user",
+      "name": "user",
+    },
+    "value": "custom-value",
+  },
+]
+`;
diff --git a/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/CreateButton-test.tsx.snap b/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/CreateButton-test.tsx.snap
new file mode 100644 (file)
index 0000000..78c55c6
--- /dev/null
@@ -0,0 +1,32 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should create new custom measure 1`] = `
+<React.Fragment>
+  <button
+    id="custom-measures-create"
+    onClick={[Function]}
+    type="button"
+  >
+    create
+  </button>
+</React.Fragment>
+`;
+
+exports[`should create new custom measure 2`] = `
+<React.Fragment>
+  <button
+    id="custom-measures-create"
+    onClick={[Function]}
+    type="button"
+  >
+    create
+  </button>
+  <Form
+    confirmButtonText="create"
+    header="custom_measures.create_custom_measure"
+    onClose={[Function]}
+    onSubmit={[MockFunction]}
+    skipMetrics={Array []}
+  />
+</React.Fragment>
+`;
diff --git a/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/DeleteButton-test.tsx.snap b/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/DeleteButton-test.tsx.snap
new file mode 100644 (file)
index 0000000..c9fc543
--- /dev/null
@@ -0,0 +1,12 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should delete custom measure 1`] = `
+<ConfirmButton
+  confirmButtonText="delete"
+  confirmData="1"
+  isDestructive={true}
+  modalBody="custom_measures.delete_custom_measure.confirmation.custom-metric"
+  modalHeader="custom_measures.delete_custom_measure"
+  onConfirm={[MockFunction]}
+/>
+`;
diff --git a/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/EditButton-test.tsx.snap b/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/EditButton-test.tsx.snap
new file mode 100644 (file)
index 0000000..bd0a803
--- /dev/null
@@ -0,0 +1,48 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should edit metric 1`] = `
+<React.Fragment>
+  <ActionsDropdownItem
+    className="js-custom-measure-update"
+    onClick={[Function]}
+  >
+    update_verb
+  </ActionsDropdownItem>
+</React.Fragment>
+`;
+
+exports[`should edit metric 2`] = `
+<React.Fragment>
+  <ActionsDropdownItem
+    className="js-custom-measure-update"
+    onClick={[Function]}
+  >
+    update_verb
+  </ActionsDropdownItem>
+  <Form
+    confirmButtonText="update_verb"
+    header="custom_measures.update_custom_measure"
+    measure={
+      Object {
+        "createdAt": "2017-01-01",
+        "description": "my custom measure",
+        "id": "1",
+        "metric": Object {
+          "key": "custom",
+          "name": "custom-metric",
+          "type": "STRING",
+        },
+        "projectKey": "foo",
+        "user": Object {
+          "active": true,
+          "login": "user",
+          "name": "user",
+        },
+        "value": "custom-value",
+      }
+    }
+    onClose={[Function]}
+    onSubmit={[Function]}
+  />
+</React.Fragment>
+`;
diff --git a/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/Form-test.tsx.snap b/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/Form-test.tsx.snap
new file mode 100644 (file)
index 0000000..17057ba
--- /dev/null
@@ -0,0 +1,219 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render form 1`] = `
+<Modal
+  contentLabel="header"
+  onRequestClose={[MockFunction]}
+>
+  <form
+    onSubmit={[Function]}
+  >
+    <header
+      className="modal-head"
+    >
+      <h2>
+        header
+      </h2>
+    </header>
+    <div
+      className="modal-body"
+    >
+      <div
+        className="modal-field"
+      >
+        <label
+          htmlFor="create-custom-measure-metric"
+        >
+          custom_measures.metric
+          <em
+            className="mandatory"
+          >
+            *
+          </em>
+        </label>
+        <i
+          className="spinner"
+        />
+      </div>
+      <div
+        className="modal-field"
+      >
+        <label
+          htmlFor="create-custom-measure-value"
+        >
+          value
+          <em
+            className="mandatory"
+          >
+            *
+          </em>
+        </label>
+        <input
+          autoFocus={false}
+          id="create-custom-measure-value"
+          maxLength={200}
+          name="value"
+          onChange={[Function]}
+          required={true}
+          type="text"
+          value=""
+        />
+      </div>
+      <div
+        className="modal-field"
+      >
+        <label
+          htmlFor="create-custom-measure-description"
+        >
+          description
+        </label>
+        <textarea
+          id="create-custom-measure-description"
+          name="description"
+          onChange={[Function]}
+          value=""
+        />
+      </div>
+    </div>
+    <footer
+      className="modal-foot"
+    >
+      <DeferredSpinner
+        className="spacer-right"
+        loading={false}
+        timeout={100}
+      />
+      <button
+        disabled={true}
+        id="create-custom-measure-submit"
+        type="submit"
+      >
+        confirmButtonText
+      </button>
+      <button
+        className="button-link"
+        disabled={false}
+        id="create-custom-measure-cancel"
+        onClick={[Function]}
+        type="reset"
+      >
+        cancel
+      </button>
+    </footer>
+  </form>
+</Modal>
+`;
+
+exports[`should render form 2`] = `
+<Modal
+  contentLabel="header"
+  onRequestClose={[MockFunction]}
+>
+  <form
+    onSubmit={[Function]}
+  >
+    <header
+      className="modal-head"
+    >
+      <h2>
+        header
+      </h2>
+    </header>
+    <div
+      className="modal-body"
+    >
+      <div
+        className="modal-field"
+      >
+        <label
+          htmlFor="create-custom-measure-metric"
+        >
+          custom_measures.metric
+          <em
+            className="mandatory"
+          >
+            *
+          </em>
+        </label>
+        <Select
+          autofocus={true}
+          clearable={false}
+          onChange={[Function]}
+          options={
+            Array [
+              Object {
+                "label": "Custom Metric",
+                "value": "custom-metric",
+              },
+            ]
+          }
+        />
+      </div>
+      <div
+        className="modal-field"
+      >
+        <label
+          htmlFor="create-custom-measure-value"
+        >
+          value
+          <em
+            className="mandatory"
+          >
+            *
+          </em>
+        </label>
+        <input
+          autoFocus={false}
+          id="create-custom-measure-value"
+          maxLength={200}
+          name="value"
+          onChange={[Function]}
+          required={true}
+          type="text"
+          value=""
+        />
+      </div>
+      <div
+        className="modal-field"
+      >
+        <label
+          htmlFor="create-custom-measure-description"
+        >
+          description
+        </label>
+        <textarea
+          id="create-custom-measure-description"
+          name="description"
+          onChange={[Function]}
+          value=""
+        />
+      </div>
+    </div>
+    <footer
+      className="modal-foot"
+    >
+      <DeferredSpinner
+        className="spacer-right"
+        loading={false}
+        timeout={100}
+      />
+      <button
+        disabled={false}
+        id="create-custom-measure-submit"
+        type="submit"
+      >
+        confirmButtonText
+      </button>
+      <button
+        className="button-link"
+        disabled={false}
+        id="create-custom-measure-cancel"
+        onClick={[Function]}
+        type="reset"
+      >
+        cancel
+      </button>
+    </footer>
+  </form>
+</Modal>
+`;
diff --git a/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/Header-test.tsx.snap b/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/Header-test.tsx.snap
new file mode 100644 (file)
index 0000000..0ab1902
--- /dev/null
@@ -0,0 +1,31 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should create new custom measure 1`] = `
+<header
+  className="page-header"
+  id="custom-measures-header"
+>
+  <h1
+    className="page-title"
+  >
+    custom_measures.page
+  </h1>
+  <DeferredSpinner
+    loading={false}
+    timeout={100}
+  />
+  <div
+    className="page-actions"
+  >
+    <CreateButton
+      onCreate={[MockFunction]}
+      skipMetrics={Array []}
+    />
+  </div>
+  <p
+    className="page-description"
+  >
+    custom_measures.page.description
+  </p>
+</header>
+`;
diff --git a/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/List-test.tsx.snap b/server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/List-test.tsx.snap
new file mode 100644 (file)
index 0000000..005bedf
--- /dev/null
@@ -0,0 +1,272 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render 1`] = `
+<div
+  className="boxed-group boxed-group-inner"
+  id="custom-measures-list"
+>
+  <table
+    className="data zebra zebra-hover"
+  >
+    <thead>
+      <tr>
+        <th>
+          custom_measures.metric
+        </th>
+        <th>
+          value
+        </th>
+        <th>
+          description
+        </th>
+        <th>
+          date
+        </th>
+        <th />
+      </tr>
+    </thead>
+    <tbody>
+      <tr
+        data-metric="another"
+        key="2"
+      >
+        <td
+          className="nowrap"
+        >
+          <div>
+            <span
+              className="js-custom-measure-metric-name"
+            >
+              another-metric
+            </span>
+          </div>
+          <span
+            className="js-custom-measure-domain note"
+          />
+        </td>
+        <td
+          className="nowrap"
+        >
+          <strong
+            className="js-custom-measure-value"
+          >
+            another-value
+          </strong>
+        </td>
+        <td>
+          <span
+            className="js-custom-measure-description"
+          />
+        </td>
+        <td>
+          <MeasureDate
+            measure={
+              Object {
+                "createdAt": "2017-01-01",
+                "id": "2",
+                "metric": Object {
+                  "key": "another",
+                  "name": "another-metric",
+                  "type": "STRING",
+                },
+                "projectKey": "foo",
+                "user": Object {
+                  "active": true,
+                  "login": "user",
+                  "name": "user",
+                },
+                "value": "another-value",
+              }
+            }
+          />
+           
+          by_
+           
+          <span
+            className="js-custom-measure-user"
+          >
+            user
+          </span>
+        </td>
+        <td
+          className="thin nowrap"
+        >
+          <ActionsDropdown>
+            <EditButton
+              measure={
+                Object {
+                  "createdAt": "2017-01-01",
+                  "id": "2",
+                  "metric": Object {
+                    "key": "another",
+                    "name": "another-metric",
+                    "type": "STRING",
+                  },
+                  "projectKey": "foo",
+                  "user": Object {
+                    "active": true,
+                    "login": "user",
+                    "name": "user",
+                  },
+                  "value": "another-value",
+                }
+              }
+              onEdit={[MockFunction]}
+            />
+            <ActionsDropdownDivider />
+            <DeleteButton
+              measure={
+                Object {
+                  "createdAt": "2017-01-01",
+                  "id": "2",
+                  "metric": Object {
+                    "key": "another",
+                    "name": "another-metric",
+                    "type": "STRING",
+                  },
+                  "projectKey": "foo",
+                  "user": Object {
+                    "active": true,
+                    "login": "user",
+                    "name": "user",
+                  },
+                  "value": "another-value",
+                }
+              }
+              onDelete={[MockFunction]}
+            />
+          </ActionsDropdown>
+        </td>
+      </tr>
+      <tr
+        data-metric="custom"
+        key="1"
+      >
+        <td
+          className="nowrap"
+        >
+          <div>
+            <span
+              className="js-custom-measure-metric-name"
+            >
+              custom-metric
+            </span>
+          </div>
+          <span
+            className="js-custom-measure-domain note"
+          />
+        </td>
+        <td
+          className="nowrap"
+        >
+          <strong
+            className="js-custom-measure-value"
+          >
+            custom-value
+          </strong>
+        </td>
+        <td>
+          <span
+            className="js-custom-measure-description"
+          >
+            my custom measure
+          </span>
+        </td>
+        <td>
+          <MeasureDate
+            measure={
+              Object {
+                "createdAt": "2017-01-01",
+                "description": "my custom measure",
+                "id": "1",
+                "metric": Object {
+                  "key": "custom",
+                  "name": "custom-metric",
+                  "type": "STRING",
+                },
+                "projectKey": "foo",
+                "user": Object {
+                  "active": true,
+                  "login": "user",
+                  "name": "user",
+                },
+                "value": "custom-value",
+              }
+            }
+          />
+           
+          by_
+           
+          <span
+            className="js-custom-measure-user"
+          >
+            user
+          </span>
+        </td>
+        <td
+          className="thin nowrap"
+        >
+          <ActionsDropdown>
+            <EditButton
+              measure={
+                Object {
+                  "createdAt": "2017-01-01",
+                  "description": "my custom measure",
+                  "id": "1",
+                  "metric": Object {
+                    "key": "custom",
+                    "name": "custom-metric",
+                    "type": "STRING",
+                  },
+                  "projectKey": "foo",
+                  "user": Object {
+                    "active": true,
+                    "login": "user",
+                    "name": "user",
+                  },
+                  "value": "custom-value",
+                }
+              }
+              onEdit={[MockFunction]}
+            />
+            <ActionsDropdownDivider />
+            <DeleteButton
+              measure={
+                Object {
+                  "createdAt": "2017-01-01",
+                  "description": "my custom measure",
+                  "id": "1",
+                  "metric": Object {
+                    "key": "custom",
+                    "name": "custom-metric",
+                    "type": "STRING",
+                  },
+                  "projectKey": "foo",
+                  "user": Object {
+                    "active": true,
+                    "login": "user",
+                    "name": "user",
+                  },
+                  "value": "custom-value",
+                }
+              }
+              onDelete={[MockFunction]}
+            />
+          </ActionsDropdown>
+        </td>
+      </tr>
+    </tbody>
+  </table>
+</div>
+`;
+
+exports[`should render no results 1`] = `
+<div
+  className="boxed-group boxed-group-inner"
+  id="custom-measures-list"
+>
+  <p>
+    no_results
+  </p>
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/apps/custom-measures/create-view.js b/server/sonar-web/src/main/js/apps/custom-measures/create-view.js
deleted file mode 100644 (file)
index 3d36638..0000000
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 CustomMeasure from './custom-measure';
-import FormView from './form-view';
-
-export default FormView.extend({
-  sendRequest() {
-    const that = this;
-    const customMeasure = new CustomMeasure({
-      metricId: this.$('#create-custom-measure-metric').val(),
-      value: this.$('#create-custom-measure-value').val(),
-      description: this.$('#create-custom-measure-description').val(),
-      projectId: this.options.projectId
-    });
-    this.disableForm();
-    return customMeasure
-      .save(null, {
-        statusCode: {
-          // do not show global error
-          400: null
-        }
-      })
-      .done(() => {
-        that.collection.refresh();
-        that.destroy();
-      })
-      .fail(jqXHR => {
-        that.enableForm();
-        that.showErrors(jqXHR.responseJSON.errors, jqXHR.responseJSON.warnings);
-      });
-  }
-});
diff --git a/server/sonar-web/src/main/js/apps/custom-measures/custom-measure.js b/server/sonar-web/src/main/js/apps/custom-measures/custom-measure.js
deleted file mode 100644 (file)
index b6b3691..0000000
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 { defaults, pick } from 'lodash';
-import Backbone from 'backbone';
-
-export default Backbone.Model.extend({
-  idAttribute: 'id',
-
-  urlRoot() {
-    return window.baseUrl + '/api/custom_measures';
-  },
-
-  sync(method, model, options) {
-    const opts = options || {};
-    if (method === 'create') {
-      defaults(opts, {
-        url: this.urlRoot() + '/create',
-        type: 'POST',
-        data: pick(model.toJSON(), 'metricId', 'value', 'description', 'projectId')
-      });
-    }
-    if (method === 'update') {
-      defaults(opts, {
-        url: this.urlRoot() + '/update',
-        type: 'POST',
-        data: pick(model.toJSON(), 'id', 'value', 'description')
-      });
-    }
-    if (method === 'delete') {
-      defaults(opts, {
-        url: this.urlRoot() + '/delete',
-        type: 'POST',
-        data: { id: this.id }
-      });
-    }
-    return Backbone.ajax(opts);
-  }
-});
diff --git a/server/sonar-web/src/main/js/apps/custom-measures/custom-measures.js b/server/sonar-web/src/main/js/apps/custom-measures/custom-measures.js
deleted file mode 100644 (file)
index 98f31eb..0000000
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 { uniq } from 'lodash';
-import Backbone from 'backbone';
-import CustomMeasure from './custom-measure';
-
-export default Backbone.Collection.extend({
-  model: CustomMeasure,
-
-  initialize(options) {
-    this.projectId = options.projectId;
-  },
-
-  url() {
-    return window.baseUrl + '/api/custom_measures/search';
-  },
-
-  parse(r) {
-    this.total = r.total;
-    this.p = r.p;
-    this.ps = r.ps;
-    return r.customMeasures;
-  },
-
-  fetch(options) {
-    const opts = { data: {}, ...options };
-    this.q = opts.data.q;
-    opts.data.projectId = this.projectId;
-    return Backbone.Collection.prototype.fetch.call(this, opts);
-  },
-
-  fetchMore() {
-    const p = this.p + 1;
-    return this.fetch({ add: true, remove: false, data: { p, ps: this.ps, q: this.q } });
-  },
-
-  refresh() {
-    return this.fetch({ reset: true, data: { q: this.q } });
-  },
-
-  hasMore() {
-    return this.total > this.p * this.ps;
-  },
-
-  getTakenMetrics() {
-    const metrics = this.map(model => model.get('metric').id);
-    return uniq(metrics);
-  }
-});
diff --git a/server/sonar-web/src/main/js/apps/custom-measures/delete-view.js b/server/sonar-web/src/main/js/apps/custom-measures/delete-view.js
deleted file mode 100644 (file)
index e910f28..0000000
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 Template from './templates/custom-measures-delete.hbs';
-import ModalForm from '../../components/common/modal-form';
-
-export default ModalForm.extend({
-  template: Template,
-
-  onFormSubmit() {
-    ModalForm.prototype.onFormSubmit.apply(this, arguments);
-    this.sendRequest();
-  },
-
-  sendRequest() {
-    const that = this;
-    const collection = this.model.collection;
-    return this.model
-      .destroy({
-        wait: true,
-        statusCode: {
-          // do not show global error
-          400: null
-        }
-      })
-      .done(() => {
-        collection.refresh();
-        that.destroy();
-      })
-      .fail(jqXHR => {
-        that.showErrors(jqXHR.responseJSON.errors, jqXHR.responseJSON.warnings);
-      });
-  }
-});
diff --git a/server/sonar-web/src/main/js/apps/custom-measures/form-view.js b/server/sonar-web/src/main/js/apps/custom-measures/form-view.js
deleted file mode 100644 (file)
index 35e2e05..0000000
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 Metrics from './metrics';
-import Template from './templates/custom-measures-form.hbs';
-import ModalForm from '../../components/common/modal-form';
-
-export default ModalForm.extend({
-  template: Template,
-
-  initialize() {
-    this.metrics = new Metrics();
-    this.listenTo(this.metrics, 'reset', this.render);
-    this.metrics.fetch({ reset: true });
-  },
-
-  onRender() {
-    ModalForm.prototype.onRender.apply(this, arguments);
-    this.$('[data-toggle="tooltip"]').tooltip({ container: 'body', placement: 'bottom' });
-    this.$('#create-custom-measure-metric').select2({
-      width: '250px',
-      minimumResultsForSearch: 20
-    });
-  },
-
-  onDestroy() {
-    ModalForm.prototype.onDestroy.apply(this, arguments);
-    this.$('[data-toggle="tooltip"]').tooltip('destroy');
-  },
-
-  onFormSubmit() {
-    ModalForm.prototype.onFormSubmit.apply(this, arguments);
-    this.sendRequest();
-  },
-
-  getAvailableMetrics() {
-    const takenMetrics = this.collection.getTakenMetrics();
-    return this.metrics.toJSON().filter(metric => takenMetrics.indexOf(metric.id) === -1);
-  },
-
-  serializeData() {
-    const metrics = this.getAvailableMetrics();
-    const isNew = !this.model;
-    return {
-      ...ModalForm.prototype.serializeData.apply(this, arguments),
-      metrics,
-      canCreateMetric: !isNew || metrics.length > 0
-    };
-  }
-});
diff --git a/server/sonar-web/src/main/js/apps/custom-measures/header-view.js b/server/sonar-web/src/main/js/apps/custom-measures/header-view.js
deleted file mode 100644 (file)
index bfe9ee8..0000000
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 Marionette from 'backbone.marionette';
-import CreateView from './create-view';
-import Template from './templates/custom-measures-header.hbs';
-
-export default Marionette.ItemView.extend({
-  template: Template,
-
-  events: {
-    'click #custom-measures-create': 'onCreateClick'
-  },
-
-  onCreateClick(e) {
-    e.preventDefault();
-    this.createCustomMeasure();
-  },
-
-  createCustomMeasure() {
-    new CreateView({
-      collection: this.collection,
-      projectId: this.options.projectId
-    }).render();
-  }
-});
diff --git a/server/sonar-web/src/main/js/apps/custom-measures/init.js b/server/sonar-web/src/main/js/apps/custom-measures/init.js
deleted file mode 100644 (file)
index a1dc8d2..0000000
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 Marionette from 'backbone.marionette';
-import Layout from './layout';
-import CustomMeasures from './custom-measures';
-import HeaderView from './header-view';
-import ListView from './list-view';
-import ListFooterView from './list-footer-view';
-
-const App = new Marionette.Application();
-const init = function(el, component) {
-  // Layout
-  this.layout = new Layout({ el });
-  this.layout.render();
-
-  // Collection
-  this.customMeasures = new CustomMeasures({
-    projectId: component.id
-  });
-
-  // Header View
-  this.headerView = new HeaderView({
-    collection: this.customMeasures,
-    projectId: component.id
-  });
-  this.layout.headerRegion.show(this.headerView);
-
-  // List View
-  this.listView = new ListView({
-    collection: this.customMeasures
-  });
-  this.layout.listRegion.show(this.listView);
-
-  // List Footer View
-  this.listFooterView = new ListFooterView({
-    collection: this.customMeasures
-  });
-  this.layout.listFooterRegion.show(this.listFooterView);
-
-  // Go!
-  this.customMeasures.fetch();
-};
-
-App.on('start', options => {
-  init.call(App, options.el, options.component);
-});
-
-export default function(el, component) {
-  App.start({ el, component });
-}
diff --git a/server/sonar-web/src/main/js/apps/custom-measures/layout.js b/server/sonar-web/src/main/js/apps/custom-measures/layout.js
deleted file mode 100644 (file)
index 73f5ee6..0000000
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 Marionette from 'backbone.marionette';
-import Template from './templates/custom-measures-layout.hbs';
-
-export default Marionette.LayoutView.extend({
-  template: Template,
-
-  regions: {
-    headerRegion: '#custom-measures-header',
-    listRegion: '#custom-measures-list',
-    listFooterRegion: '#custom-measures-list-footer'
-  }
-});
diff --git a/server/sonar-web/src/main/js/apps/custom-measures/list-footer-view.js b/server/sonar-web/src/main/js/apps/custom-measures/list-footer-view.js
deleted file mode 100644 (file)
index 0b66b16..0000000
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 Marionette from 'backbone.marionette';
-import Template from './templates/custom-measures-list-footer.hbs';
-
-export default Marionette.ItemView.extend({
-  template: Template,
-
-  collectionEvents: {
-    all: 'render'
-  },
-
-  events: {
-    'click #custom-measures-fetch-more': 'onMoreClick'
-  },
-
-  onMoreClick(e) {
-    e.preventDefault();
-    this.fetchMore();
-  },
-
-  fetchMore() {
-    this.collection.fetchMore();
-  },
-
-  serializeData() {
-    return {
-      ...Marionette.ItemView.prototype.serializeData.apply(this, arguments),
-      total: this.collection.total,
-      count: this.collection.length,
-      more: this.collection.hasMore()
-    };
-  }
-});
diff --git a/server/sonar-web/src/main/js/apps/custom-measures/list-item-view.js b/server/sonar-web/src/main/js/apps/custom-measures/list-item-view.js
deleted file mode 100644 (file)
index e8a43f8..0000000
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 Marionette from 'backbone.marionette';
-import UpdateView from './update-view';
-import DeleteView from './delete-view';
-import Template from './templates/custom-measures-list-item.hbs';
-
-export default Marionette.ItemView.extend({
-  tagName: 'tr',
-  template: Template,
-
-  events: {
-    'click .js-custom-measure-update': 'onUpdateClick',
-    'click .js-custom-measure-delete': 'onDeleteClick'
-  },
-
-  onRender() {
-    this.$el.attr('data-id', this.model.id);
-    this.$('[data-toggle="tooltip"]').tooltip({ container: 'body', placement: 'bottom' });
-  },
-
-  onDestroy() {
-    this.$('[data-toggle="tooltip"]').tooltip('destroy');
-  },
-
-  onUpdateClick(e) {
-    e.preventDefault();
-    this.updateCustomMeasure();
-  },
-
-  onDeleteClick(e) {
-    e.preventDefault();
-    this.deleteCustomMeasure();
-  },
-
-  updateCustomMeasure() {
-    new UpdateView({
-      model: this.model,
-      collection: this.model.collection
-    }).render();
-  },
-
-  deleteCustomMeasure() {
-    new DeleteView({ model: this.model }).render();
-  }
-});
diff --git a/server/sonar-web/src/main/js/apps/custom-measures/list-view.js b/server/sonar-web/src/main/js/apps/custom-measures/list-view.js
deleted file mode 100644 (file)
index 211eb14..0000000
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 Marionette from 'backbone.marionette';
-import ListItemView from './list-item-view';
-import Template from './templates/custom-measures-list.hbs';
-
-export default Marionette.CompositeView.extend({
-  template: Template,
-  childView: ListItemView,
-  childViewContainer: 'tbody'
-});
diff --git a/server/sonar-web/src/main/js/apps/custom-measures/metric.js b/server/sonar-web/src/main/js/apps/custom-measures/metric.js
deleted file mode 100644 (file)
index 29fdb20..0000000
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 { defaults, pick } from 'lodash';
-import Backbone from 'backbone';
-
-export default Backbone.Model.extend({
-  idAttribute: 'id',
-
-  urlRoot() {
-    return window.baseUrl + '/api/metrics';
-  },
-
-  sync(method, model, options) {
-    const opts = options || {};
-    if (method === 'create') {
-      defaults(opts, {
-        url: this.urlRoot() + '/create',
-        type: 'POST',
-        data: pick(model.toJSON(), 'key', 'name', 'description', 'domain', 'type')
-      });
-    }
-    if (method === 'update') {
-      defaults(opts, {
-        url: this.urlRoot() + '/update',
-        type: 'POST',
-        data: pick(model.toJSON(), 'id', 'key', 'name', 'description', 'domain', 'type')
-      });
-    }
-    if (method === 'delete') {
-      defaults(opts, {
-        url: this.urlRoot() + '/delete',
-        type: 'POST',
-        data: { ids: this.id }
-      });
-    }
-    return Backbone.ajax(opts);
-  }
-});
diff --git a/server/sonar-web/src/main/js/apps/custom-measures/metrics.js b/server/sonar-web/src/main/js/apps/custom-measures/metrics.js
deleted file mode 100644 (file)
index 0fb8c5e..0000000
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 Backbone from 'backbone';
-import Metric from './metric';
-
-export default Backbone.Collection.extend({
-  model: Metric,
-
-  url() {
-    return window.baseUrl + '/api/metrics/search';
-  },
-
-  parse(r) {
-    this.total = r.total;
-    this.p = r.p;
-    this.ps = r.ps;
-    return r.metrics;
-  },
-
-  fetch(options) {
-    const opts = { data: {}, ...options };
-    this.q = opts.data.q;
-    opts.data.isCustom = true;
-    return Backbone.Collection.prototype.fetch.call(this, opts);
-  },
-
-  fetchMore() {
-    const p = this.p + 1;
-    return this.fetch({ add: true, remove: false, data: { p, ps: this.ps, q: this.q } });
-  },
-
-  refresh() {
-    return this.fetch({ reset: true, data: { q: this.q } });
-  },
-
-  hasMore() {
-    return this.total > this.p * this.ps;
-  }
-});
index 5651d247031c6a39203d140b40b7db85f207c164..ad7784f990670d20aca71c24d4bc28f8c57111d2 100644 (file)
@@ -22,9 +22,7 @@ import { RouterState, IndexRouteProps } from 'react-router';
 const routes = [
   {
     getIndexRoute(_: RouterState, callback: (err: any, route: IndexRouteProps) => any) {
-      import('./components/CustomMeasuresAppContainer').then(i =>
-        callback(null, { component: (i as any).default })
-      );
+      import('./components/App').then(i => callback(null, { component: (i as any).default }));
     }
   }
 ];
diff --git a/server/sonar-web/src/main/js/apps/custom-measures/templates/custom-measures-delete.hbs b/server/sonar-web/src/main/js/apps/custom-measures/templates/custom-measures-delete.hbs
deleted file mode 100644 (file)
index c216329..0000000
+++ /dev/null
@@ -1,13 +0,0 @@
-<form id="delete-custom-measure-form">
-  <div class="modal-head">
-    <h2>{{t 'custom_measures.delete_custom_measure'}}</h2>
-  </div>
-  <div class="modal-body">
-    <div class="js-modal-messages"></div>
-    {{tp 'custom_measures.delete_custom_measure.confirmation' metric.name}}
-  </div>
-  <div class="modal-foot">
-    <button id="delete-custom-measure-submit" class="button-red">{{t 'delete'}}</button>
-    <a href="#" class="js-modal-close" id="delete-custom-measure-cancel">{{t 'cancel'}}</a>
-  </div>
-</form>
diff --git a/server/sonar-web/src/main/js/apps/custom-measures/templates/custom-measures-form.hbs b/server/sonar-web/src/main/js/apps/custom-measures/templates/custom-measures-form.hbs
deleted file mode 100644 (file)
index 48f4200..0000000
+++ /dev/null
@@ -1,39 +0,0 @@
-<form id="create-custom-measure-form" autocomplete="off">
-  <div class="modal-head">
-    <h2>{{#if id}}{{t 'custom_measures.update_custom_measure'}}{{else}}{{t 'custom_measures.create_custom_measure'}}{{/if}}</h2>
-  </div>
-  <div class="modal-body">
-    <div class="js-modal-messages"></div>
-
-    {{#unless id}}
-      {{#if canCreateMetric}}
-        <div class="modal-field">
-          <label for="create-custom-measure-metric">{{t 'custom_measures.metric'}}<em class="mandatory">*</em></label>
-          <select id="create-custom-measure-metric" name="metric" required>
-            {{#each metrics}}
-              <option value="{{id}}" {{#eq id ../metric.id}}selected{{/eq}}>{{name}}</option>
-            {{/each}}
-          </select>
-        </div>
-      {{else}}
-        <div class="alert alert-warning">{{t 'custom_measures.all_metrics_taken'}}</div>
-      {{/if}}
-    {{/unless}}
-
-    <div class="modal-field">
-      <label for="create-custom-measure-value">{{t 'value'}}<em class="mandatory">*</em></label>
-      <input id="create-custom-measure-value" name="value" type="text" maxlength="200" required value="{{value}}">
-    </div>
-
-    <div class="modal-field">
-      <label for="create-custom-measure-description">{{t 'description'}}</label>
-      <textarea id="create-custom-measure-description" name="description">{{description}}</textarea>
-    </div>
-  </div>
-  <div class="modal-foot">
-    <button id="create-custom-measure-submit" {{#unless canCreateMetric}}disabled{{/unless}}>
-      {{#if id}}{{t 'update_verb'}}{{else}}{{t 'create'}}{{/if}}
-    </button>
-    <a href="#" class="js-modal-close" id="create-custom-measure-cancel">{{t 'cancel'}}</a>
-  </div>
-</form>
diff --git a/server/sonar-web/src/main/js/apps/custom-measures/templates/custom-measures-header.hbs b/server/sonar-web/src/main/js/apps/custom-measures/templates/custom-measures-header.hbs
deleted file mode 100644 (file)
index 840706f..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-<header class="page-header">
-  <h1 class="page-title">{{t 'custom_measures.page'}}</h1>
-  <div class="page-actions">
-    <button id="custom-measures-create">{{t 'create'}}</button>
-  </div>
-  <p class="page-description">{{t 'custom_measures.page.description'}}</p>
-</header>
diff --git a/server/sonar-web/src/main/js/apps/custom-measures/templates/custom-measures-layout.hbs b/server/sonar-web/src/main/js/apps/custom-measures/templates/custom-measures-layout.hbs
deleted file mode 100644 (file)
index 14efdfa..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-<div class="page page-limited">
-  <div id="custom-measures-header"></div>
-  <div id="custom-measures-list"></div>
-  <div id="custom-measures-list-footer"></div>
-</div>
diff --git a/server/sonar-web/src/main/js/apps/custom-measures/templates/custom-measures-list-footer.hbs b/server/sonar-web/src/main/js/apps/custom-measures/templates/custom-measures-list-footer.hbs
deleted file mode 100644 (file)
index 91d1dbc..0000000
+++ /dev/null
@@ -1,6 +0,0 @@
-<footer class="spacer-top note text-center">
-  {{tp 'x_of_y_shown' count total}}
-  {{#if more}}
-    <a id="custom-measures-fetch-more" class="spacer-left" href="#">{{t 'show_more'}}</a>
-  {{/if}}
-</footer>
diff --git a/server/sonar-web/src/main/js/apps/custom-measures/templates/custom-measures-list-item.hbs b/server/sonar-web/src/main/js/apps/custom-measures/templates/custom-measures-list-item.hbs
deleted file mode 100644 (file)
index 0d40e51..0000000
+++ /dev/null
@@ -1,49 +0,0 @@
-<td class="nowrap">
-  <div>
-    <span class="js-custom-measure-metric-name">{{metric.name}}</span>
-    {{#if pending}}
-      <span class="js-custom-measure-pending badge badge-warning spacer-left"
-            title="{{t 'custom_measures.pending_tooltip'}}"
-            data-toggle="tooltip" data-placement="bottom">{{t 'custom_measures.pending'}}</span>
-    {{/if}}
-  </div>
-  <span class="js-custom-measure-domain note">{{metric.domain}}</span>
-</td>
-
-<td class="nowrap">
-  <strong class="js-custom-measure-value">{{formatMeasure value metric.type}}</strong>
-</td>
-
-<td class="">
-  <span class="js-custom-measure-description">{{description}}</span>
-</td>
-
-<td class="">
-  {{#if updatedAt }}
-    {{t 'updated_on'}} <span class="js-custom-measure-created-at">{{d updatedAt}}</span>
-  {{else}}
-    {{#if createdAt }}
-      {{t 'created_on'}} <span class="js-custom-measure-created-at">{{d createdAt}}</span>
-    {{else}}
-      {{t 'created'}} 
-    {{/if}}
-  {{/if}}
-  {{t 'by_'}} <span class="js-custom-measure-user">{{user.name}}</span>
-</td>
-
-<td class="thin nowrap">
-  <div class="dropdown">
-    <button class="dropdown-toggle" data-toggle="dropdown">
-      {{settingsIcon}}<i class="icon-dropdown little-spacer-left" />
-    </button>
-    <ul class="dropdown-menu dropdown-menu-right">
-      <li>
-        <a class="js-custom-measure-update" href="#">{{t 'update_verb'}}</a>
-      </li>
-      <li class="divider" />
-      <li>
-        <a class="js-custom-measure-delete text-danger" href="#">{{t 'delete'}}</a>
-      </li>
-    </ul>
-  </div>
-</td>
diff --git a/server/sonar-web/src/main/js/apps/custom-measures/templates/custom-measures-list.hbs b/server/sonar-web/src/main/js/apps/custom-measures/templates/custom-measures-list.hbs
deleted file mode 100644 (file)
index 97c5132..0000000
+++ /dev/null
@@ -1,14 +0,0 @@
-<div class="boxed-group boxed-group-inner">
-  <table class="data zebra">
-    <thead>
-    <tr>
-      <th>{{t 'custom_measures.metric'}}</th>
-      <th>{{t 'value'}}</th>
-      <th>{{t 'description'}}</th>
-      <th>{{t 'date'}}</th>
-      <th>&nbsp;</th>
-    </tr>
-    </thead>
-    <tbody></tbody>
-  </table>
-</div>
diff --git a/server/sonar-web/src/main/js/apps/custom-measures/update-view.js b/server/sonar-web/src/main/js/apps/custom-measures/update-view.js
deleted file mode 100644 (file)
index 26716f5..0000000
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 FormView from './form-view';
-
-export default FormView.extend({
-  sendRequest() {
-    const that = this;
-    this.model.set({
-      value: this.$('#create-custom-measure-value').val(),
-      description: this.$('#create-custom-measure-description').val()
-    });
-    this.disableForm();
-    return this.model
-      .save(null, {
-        statusCode: {
-          // do not show global error
-          400: null
-        }
-      })
-      .done(() => {
-        that.collection.refresh();
-        that.destroy();
-      })
-      .fail(jqXHR => {
-        that.enableForm();
-        that.showErrors(jqXHR.responseJSON.errors, jqXHR.responseJSON.warnings);
-      });
-  }
-});
index 09dffecab9370f74827644beb195d5ae5cfbe36b..a616a996a858dc2234a08b906698ae2ed5ac3da2 100644 (file)
@@ -45,7 +45,7 @@ interface State {
   types?: string[];
 }
 
-const PAGE_SIZE = 500;
+const PAGE_SIZE = 50;
 
 export default class App extends React.PureComponent<Props, State> {
   mounted: boolean;