]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-14110 Add "Open in IDE" button to Security Hotspots page
authorJean-Baptiste Lievremont <jeanbaptiste.lievremont@sonarsource.com>
Thu, 5 Nov 2020 14:17:27 +0000 (15:17 +0100)
committersonartech <sonartech@sonarsource.com>
Thu, 26 Nov 2020 20:06:29 +0000 (20:06 +0000)
server/sonar-web/src/main/js/app/utils/addGlobalErrorMessage.ts [new file with mode: 0644]
server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotOpenInIdeButton.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerRenderer.tsx
server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotOpenInIdeButton-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotOpenInIdeButton-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotViewerRenderer-test.tsx.snap
server/sonar-web/src/main/js/helpers/__tests__/sonarlint-test.ts [new file with mode: 0644]
server/sonar-web/src/main/js/helpers/sonarlint.ts [new file with mode: 0644]
server/sonar-web/src/main/js/types/sonarlint.ts [new file with mode: 0644]
sonar-core/src/main/resources/org/sonar/l10n/core.properties

diff --git a/server/sonar-web/src/main/js/app/utils/addGlobalErrorMessage.ts b/server/sonar-web/src/main/js/app/utils/addGlobalErrorMessage.ts
new file mode 100644 (file)
index 0000000..e722983
--- /dev/null
@@ -0,0 +1,26 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 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 globalMessages from '../../store/globalMessages';
+import getStore from './getStore';
+
+export default function addGlobalErrorMessage(message: string): void {
+  const store = getStore();
+  store.dispatch(globalMessages.addGlobalErrorMessage(message));
+}
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotOpenInIdeButton.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotOpenInIdeButton.tsx
new file mode 100644 (file)
index 0000000..2a35407
--- /dev/null
@@ -0,0 +1,74 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 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 { Button } from 'sonar-ui-common/components/controls/buttons';
+import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner';
+import { translate } from 'sonar-ui-common/helpers/l10n';
+import addGlobalErrorMessage from '../../../app/utils/addGlobalErrorMessage';
+import addGlobalSuccessMessage from '../../../app/utils/addGlobalSuccessMessage';
+import { openHotspot, probeSonarLintServers } from '../../../helpers/sonarlint';
+
+interface Props {
+  projectKey: string;
+  hotspotKey: string;
+}
+
+interface State {
+  inDiscovery: boolean;
+}
+
+export default class HotspotOpenInIdeButton extends React.PureComponent<Props, State> {
+  state = {
+    inDiscovery: false
+  };
+
+  handleOnClick = () => {
+    const { projectKey, hotspotKey } = this.props;
+    this.setState({ inDiscovery: true });
+    return probeSonarLintServers()
+      .then(ides => {
+        if (ides.length > 0) {
+          const calledPort = ides[0].port;
+          return openHotspot(calledPort, projectKey, hotspotKey);
+        } else {
+          return Promise.reject();
+        }
+      })
+      .then(() => {
+        addGlobalSuccessMessage(translate('hotspots.open_in_ide.success'));
+      })
+      .catch(() => {
+        addGlobalErrorMessage(translate('hotspots.open_in_ide.failure'));
+      })
+      .finally(() => {
+        this.setState({ inDiscovery: false });
+      });
+  };
+
+  render() {
+    return (
+      <Button onClick={this.handleOnClick}>
+        {translate('hotspots.open_in_ide.open')}
+        <DeferredSpinner loading={this.state.inDiscovery} className="spacer-left" />
+      </Button>
+    );
+  }
+}
index 9bb83fc1f6ffb1ba1b91db876047d72ffe3dea12..4014c74f88f42b95d1be59d8f435ff1d6f297235 100644 (file)
@@ -32,6 +32,7 @@ import { isLoggedIn } from '../../../helpers/users';
 import { BranchLike } from '../../../types/branch-like';
 import { Hotspot } from '../../../types/security-hotspots';
 import Assignee from './assignee/Assignee';
+import HotspotOpenInIdeButton from './HotspotOpenInIdeButton';
 import HotspotReviewHistoryAndComments from './HotspotReviewHistoryAndComments';
 import HotspotSnippetContainer from './HotspotSnippetContainer';
 import './HotspotViewer.css';
@@ -80,11 +81,19 @@ export function HotspotViewerRenderer(props: HotspotViewerRendererProps) {
             <strong className="big big-spacer-right">{hotspot.message}</strong>
             <div className="display-flex-row flex-0">
               {isLoggedIn(currentUser) && (
-                <div className="dropdown spacer-right flex-1-0-auto">
-                  <Button onClick={props.onOpenComment}>
-                    {translate('hotspots.comment.open')}
-                  </Button>
-                </div>
+                <>
+                  <div className="dropdown spacer-right flex-1-0-auto">
+                    <Button onClick={props.onOpenComment}>
+                      {translate('hotspots.comment.open')}
+                    </Button>
+                  </div>
+                  <div className="dropdown spacer-right flex-1-0-auto">
+                    <HotspotOpenInIdeButton
+                      hotspotKey={hotspot.key}
+                      projectKey={hotspot.project.key}
+                    />
+                  </div>
+                </>
               )}
               <ClipboardButton className="flex-1-0-auto" copyValue={permalink}>
                 <LinkIcon className="spacer-right" />
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotOpenInIdeButton-test.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotOpenInIdeButton-test.tsx
new file mode 100644 (file)
index 0000000..025668b
--- /dev/null
@@ -0,0 +1,45 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 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 { shallow } from 'enzyme';
+import * as React from 'react';
+import { Button } from 'sonar-ui-common/components/controls/buttons';
+import * as sonarlint from '../../../../helpers/sonarlint';
+import HotspotOpenInIdeButton from '../HotspotOpenInIdeButton';
+
+jest.mock('../../../../helpers/sonarlint');
+
+it('should render correctly', async () => {
+  const projectKey = 'my-project:key';
+  const hotspotKey = 'AXWsgE9RpggAQesHYfwm';
+
+  const wrapper = shallow(
+    <HotspotOpenInIdeButton projectKey={projectKey} hotspotKey={hotspotKey} />
+  );
+  expect(wrapper).toMatchSnapshot();
+
+  (sonarlint.probeSonarLintServers as jest.Mock).mockResolvedValue([
+    { port: 42001, ideName: 'BlueJ IDE', description: 'Hello World' }
+  ]);
+
+  wrapper.find(Button).simulate('click');
+
+  await new Promise(setImmediate);
+  expect(sonarlint.openHotspot).toBeCalledWith(42001, projectKey, hotspotKey);
+});
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotOpenInIdeButton-test.tsx.snap b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotOpenInIdeButton-test.tsx.snap
new file mode 100644 (file)
index 0000000..396feb4
--- /dev/null
@@ -0,0 +1,13 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<Button
+  onClick={[Function]}
+>
+  hotspots.open_in_ide.open
+  <DeferredSpinner
+    className="spacer-left"
+    loading={false}
+  />
+</Button>
+`;
index c4b3dc8e40dc94b6ee4d87c67ecdee9fa17828a2..ac46855ef954a66c792e4d540ea2f347862b9fc0 100644 (file)
@@ -27,6 +27,14 @@ exports[`should render correctly 1`] = `
             hotspots.comment.open
           </Button>
         </div>
+        <div
+          className="dropdown spacer-right flex-1-0-auto"
+        >
+          <HotspotOpenInIdeButton
+            hotspotKey="01fc972e-2a3c-433e-bcae-0bd7f88f5123"
+            projectKey="my-project"
+          />
+        </div>
         <ClipboardButton
           className="flex-1-0-auto"
           copyValue="http://localhost/security_hotspots?id=my-project&branch=branch-6.7&hotspots=01fc972e-2a3c-433e-bcae-0bd7f88f5123"
@@ -674,6 +682,14 @@ exports[`should render correctly: anonymous user 1`] = `
             hotspots.comment.open
           </Button>
         </div>
+        <div
+          className="dropdown spacer-right flex-1-0-auto"
+        >
+          <HotspotOpenInIdeButton
+            hotspotKey="01fc972e-2a3c-433e-bcae-0bd7f88f5123"
+            projectKey="my-project"
+          />
+        </div>
         <ClipboardButton
           className="flex-1-0-auto"
           copyValue="http://localhost/security_hotspots?id=my-project&branch=branch-6.7&hotspots=01fc972e-2a3c-433e-bcae-0bd7f88f5123"
@@ -1321,6 +1337,14 @@ exports[`should render correctly: assignee without name 1`] = `
             hotspots.comment.open
           </Button>
         </div>
+        <div
+          className="dropdown spacer-right flex-1-0-auto"
+        >
+          <HotspotOpenInIdeButton
+            hotspotKey="01fc972e-2a3c-433e-bcae-0bd7f88f5123"
+            projectKey="my-project"
+          />
+        </div>
         <ClipboardButton
           className="flex-1-0-auto"
           copyValue="http://localhost/security_hotspots?id=my-project&branch=branch-6.7&hotspots=01fc972e-2a3c-433e-bcae-0bd7f88f5123"
@@ -1968,6 +1992,14 @@ exports[`should render correctly: deleted assignee 1`] = `
             hotspots.comment.open
           </Button>
         </div>
+        <div
+          className="dropdown spacer-right flex-1-0-auto"
+        >
+          <HotspotOpenInIdeButton
+            hotspotKey="01fc972e-2a3c-433e-bcae-0bd7f88f5123"
+            projectKey="my-project"
+          />
+        </div>
         <ClipboardButton
           className="flex-1-0-auto"
           copyValue="http://localhost/security_hotspots?id=my-project&branch=branch-6.7&hotspots=01fc972e-2a3c-433e-bcae-0bd7f88f5123"
@@ -2621,6 +2653,14 @@ exports[`should render correctly: unassigned 1`] = `
             hotspots.comment.open
           </Button>
         </div>
+        <div
+          className="dropdown spacer-right flex-1-0-auto"
+        >
+          <HotspotOpenInIdeButton
+            hotspotKey="01fc972e-2a3c-433e-bcae-0bd7f88f5123"
+            projectKey="my-project"
+          />
+        </div>
         <ClipboardButton
           className="flex-1-0-auto"
           copyValue="http://localhost/security_hotspots?id=my-project&branch=branch-6.7&hotspots=01fc972e-2a3c-433e-bcae-0bd7f88f5123"
diff --git a/server/sonar-web/src/main/js/helpers/__tests__/sonarlint-test.ts b/server/sonar-web/src/main/js/helpers/__tests__/sonarlint-test.ts
new file mode 100644 (file)
index 0000000..4d2be23
--- /dev/null
@@ -0,0 +1,67 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 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 { buildPortRange, openHotspot, probeSonarLintServers } from '../sonarlint';
+
+describe('buildPortRange', () => {
+  it('should build a port range of size <size> starting at port <port>', () => {
+    expect(buildPortRange(10000, 5)).toStrictEqual([10000, 10001, 10002, 10003, 10004]);
+  });
+});
+
+describe('probeSonarLintServers', () => {
+  const sonarLintResponse = { ideName: 'BlueJ IDE', description: 'Hello World' };
+
+  window.fetch = jest.fn((input: RequestInfo) => {
+    const calledPort = new URL(input.toString()).port;
+    if (calledPort === '64120') {
+      const resp = new Response();
+      resp.json = () => Promise.resolve(sonarLintResponse);
+      return Promise.resolve(resp);
+    } else {
+      return Promise.reject('oops');
+    }
+  });
+
+  it('should probe all ports in range', async () => {
+    const results = await probeSonarLintServers();
+    expect(results).toStrictEqual([{ port: 64120, ...sonarLintResponse }]);
+  });
+});
+
+describe('openHotspot', () => {
+  it('should send request to IDE on the right port', async () => {
+    const resp = new Response();
+    window.fetch = jest.fn((input: RequestInfo) => {
+      const calledUrl = new URL(input.toString());
+      try {
+        expect(calledUrl.searchParams.get('server')).toStrictEqual('http://localhost');
+        expect(calledUrl.searchParams.get('project')).toStrictEqual('my-project:key');
+        expect(calledUrl.searchParams.get('hotspot')).toStrictEqual('my-hotspot-key');
+      } catch (error) {
+        return Promise.reject(error);
+      }
+      return Promise.resolve(resp);
+    });
+
+    const result = await openHotspot(42000, 'my-project:key', 'my-hotspot-key');
+    expect(result).toBe(resp);
+  });
+});
diff --git a/server/sonar-web/src/main/js/helpers/sonarlint.ts b/server/sonar-web/src/main/js/helpers/sonarlint.ts
new file mode 100644 (file)
index 0000000..9b69b51
--- /dev/null
@@ -0,0 +1,59 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 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 { getHostUrl } from 'sonar-ui-common/helpers/urls';
+import { Ide } from '../types/sonarlint';
+
+const SONARLINT_PORT_START = 64120;
+const SONARLINT_PORT_RANGE = 11;
+
+export async function probeSonarLintServers(): Promise<Array<Ide>> {
+  const probedPorts = buildPortRange();
+  const probeRequests = probedPorts.map(p =>
+    fetch(buildSonarLintEndpoint(p, '/status'))
+      .then(r => r.json())
+      .then(json => {
+        const { ideName, description } = json;
+        return { port: p, ideName, description } as Ide;
+      })
+      .catch(() => undefined)
+  );
+  const results = await Promise.all(probeRequests);
+  return results.filter(r => r !== undefined) as Ide[];
+}
+
+export function openHotspot(calledPort: number, projectKey: string, hotspotKey: string) {
+  const showUrl = new URL(buildSonarLintEndpoint(calledPort, '/hotspots/show'));
+  showUrl.searchParams.set('server', getHostUrl());
+  showUrl.searchParams.set('project', projectKey);
+  showUrl.searchParams.set('hotspot', hotspotKey);
+  return fetch(showUrl.toString());
+}
+
+/**
+ * @returns [ start , ... , start + size - 1 ]
+ */
+export function buildPortRange(start = SONARLINT_PORT_START, size = SONARLINT_PORT_RANGE) {
+  return Array.from(Array(size).keys()).map(p => start + p);
+}
+
+function buildSonarLintEndpoint(port: number, path: string) {
+  return `http://localhost:${port}/sonarlint/api${path}`;
+}
diff --git a/server/sonar-web/src/main/js/types/sonarlint.ts b/server/sonar-web/src/main/js/types/sonarlint.ts
new file mode 100644 (file)
index 0000000..87a2a9c
--- /dev/null
@@ -0,0 +1,25 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 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.
+ */
+
+export interface Ide {
+  port: number;
+  ideName: string;
+  description: string;
+}
index 65f0a4d9e6e017517afc91826600a7dd8e8ee5e1..6b5b98f328a1e2a1d125c6413a38ee5ccf90ee5e 100644 (file)
@@ -716,6 +716,9 @@ hotspots.review_history.comment_added=added a comment
 hotspots.comment.field=Comment:
 hotspots.comment.open=Add Comment
 hotspots.comment.submit=Comment
+hotspots.open_in_ide.open=Open in IDE
+hotspots.open_in_ide.success=Success. Switch to your IDE to see the security hotspot.
+hotspots.open_in_ide.failure=Unable to connect to your IDE to open the Security Hotspot. Please make sure you're running the latest version of SonarLint.
 
 hotspots.assignee.select_user=Select a user...
 hotspots.status.cannot_change_status=Changing a hotspot's status requires permission.
@@ -736,8 +739,8 @@ hotspot.filters.assignee.assigned_to_me=Assigned to me
 hotspot.filters.assignee.all=All
 hotspot.filters.status.to_review=To review
 hotspot.filters.status.fixed=Reviewed as fixed
-hotspot.filters.period.since_leak_period=New code 
-hotspot.filters.period.overall=Overall code 
+hotspot.filters.period.since_leak_period=New code
+hotspot.filters.period.overall=Overall code
 hotspot.filters.status.safe=Reviewed as safe
 hotspot.filters.show_all=Show all hotspots
 hotspot.section.activity=Activity:
@@ -2529,7 +2532,7 @@ keyboard_shortcuts.title=Keyboard Shortcuts
 keyboard_shortcuts.shortcut=Shortcut
 keyboard_shortcuts.action=Action
 keyboard_shortcuts.global.title=Global
-keyboard_shortcuts.global.search=Open the search bar 
+keyboard_shortcuts.global.search=Open the search bar
 keyboard_shortcuts.global.open_shortcuts=Open this panel
 keyboard_shortcuts.code_page.title=Code Page
 keyboard_shortcuts.code_page.select_files=Select files
@@ -2881,7 +2884,7 @@ overview.measures=Measures
 overview.measures.empty_explanation=Measures on New Code will appear after the second analysis of this branch.
 overview.measures.empty_link={learn_more_link} about the Clean as You Code approach.
 overview.measures.same_reference.explanation=This branch is configured to use itself as reference branch. It will never have New Code.
-overview.measures.bad_reference.explanation=This branch could not be compared to its reference branch. See the SCM or analysis report for more details. 
+overview.measures.bad_reference.explanation=This branch could not be compared to its reference branch. See the SCM or analysis report for more details.
 overview.measures.bad_setting.link=This can be fixed in the {setting_link} setting.
 overview.measures.security_hotspots_reviewed=Reviewed
 
@@ -2913,7 +2916,7 @@ overview.period.manual_baseline=Since {0}
 # New periods (MMF-1579)
 overview.period.number_of_days=From last {0} days
 overview.period.specific_analysis=Since {0}
-overview.period.reference_branch=Compared to {0} 
+overview.period.reference_branch=Compared to {0}
 
 overview.gate.ERROR=Failed
 overview.gate.WARN=Warning
@@ -3129,7 +3132,7 @@ organization.updated=Organization details have been updated.
 organization.url=Url
 organization.url.description=Url of the homepage of the organization.
 organization.binding_with_x_easy_sync=Binding an organization from SonarCloud with {0} is an easy way to keep them synchronized.
-organization.app_will_be_installed_on_x=To bind this organization to {0}, the SonarCloud application will be installed. 
+organization.app_will_be_installed_on_x=To bind this organization to {0}, the SonarCloud application will be installed.
 organization.members.page=Members
 organization.members.page.description=Add users to the organization and grant them permissions to work on the projects. See {link} documentation.
 organization.members.add=Add a member
@@ -3781,7 +3784,7 @@ maintenance.sonarqube_is_offline.text=The connection to SonarQube is lost. Pleas
 # INDEXATION
 #
 #------------------------------------------------------------------------------
-indexation.in_progress=SonarQube is reloading project data. Some projects will be unavailable until this process is complete. 
+indexation.in_progress=SonarQube is reloading project data. Some projects will be unavailable until this process is complete.
 indexation.progression={0}% complete.
 indexation.progression_with_error={0}% complete with some {link}.
 indexation.progression_with_error.link=tasks failing