]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-15910 Extract Languages from redux
authorJeremy Davis <jeremy.davis@sonarsource.com>
Wed, 26 Jan 2022 17:48:05 +0000 (18:48 +0100)
committersonartech <sonartech@sonarsource.com>
Tue, 1 Feb 2022 20:02:59 +0000 (20:02 +0000)
65 files changed:
server/sonar-web/src/main/js/api/languages.ts
server/sonar-web/src/main/js/app/components/App.tsx
server/sonar-web/src/main/js/app/components/GlobalContainer.tsx
server/sonar-web/src/main/js/app/components/__tests__/App-test.tsx
server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/GlobalContainer-test.tsx.snap
server/sonar-web/src/main/js/app/components/languages/LanguagesContext.ts [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/languages/LanguagesContextProvider.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/languages/__tests__/LanguagesContextProvider-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/languages/__tests__/withLanguagesContext-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/languages/withLanguagesContext.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/nav/component/projectInformation/__tests__/__snapshots__/ProjectInformationRenderer-test.tsx.snap
server/sonar-web/src/main/js/app/components/nav/component/projectInformation/meta/MetaQualityProfiles.tsx
server/sonar-web/src/main/js/apps/coding-rules/components/App.tsx
server/sonar-web/src/main/js/apps/coding-rules/components/BulkChange.tsx
server/sonar-web/src/main/js/apps/coding-rules/components/BulkChangeModal.tsx
server/sonar-web/src/main/js/apps/coding-rules/components/LanguageFacet.tsx
server/sonar-web/src/main/js/apps/coding-rules/components/RepositoryFacet.tsx
server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/App-test.tsx
server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/BulkChange-test.tsx
server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/BulkChangeModal-test.tsx
server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/FacetsList-test.tsx
server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/LanguageFacet-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/RepositoryFacet-test.tsx
server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/App-test.tsx.snap
server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/BulkChange-test.tsx.snap
server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/FacetsList-test.tsx.snap
server/sonar-web/src/main/js/apps/component-measures/components/__tests__/MeasureHeader-test.tsx
server/sonar-web/src/main/js/apps/issues/sidebar/LanguageFacet.tsx
server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.tsx
server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/Sidebar-test.tsx.snap
server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/__snapshots__/ProjectQualityProfilesAppRenderer-test.tsx.snap
server/sonar-web/src/main/js/apps/projectQualityProfiles/components/AddLanguageModal.tsx
server/sonar-web/src/main/js/apps/projects/components/PageSidebar.tsx
server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/PageSidebar-test.tsx.snap
server/sonar-web/src/main/js/apps/projects/components/project-card/ProjectCard.tsx
server/sonar-web/src/main/js/apps/projects/components/project-card/ProjectCardLanguages.tsx
server/sonar-web/src/main/js/apps/projects/components/project-card/ProjectCardLanguagesContainer.tsx [deleted file]
server/sonar-web/src/main/js/apps/projects/components/project-card/__tests__/ProjectCardLanguages-test.tsx
server/sonar-web/src/main/js/apps/projects/filters/LanguagesFilter.tsx
server/sonar-web/src/main/js/apps/projects/filters/LanguagesFilterContainer.tsx [deleted file]
server/sonar-web/src/main/js/apps/projects/filters/__tests__/LanguagesFilter-test.tsx
server/sonar-web/src/main/js/apps/quality-profiles/components/App.tsx [deleted file]
server/sonar-web/src/main/js/apps/quality-profiles/components/AppContainer.tsx [deleted file]
server/sonar-web/src/main/js/apps/quality-profiles/components/QualityProfilesApp.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/App-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/AppContainer-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/QualityProfilesApp-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/App-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/QualityProfilesApp-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesList.tsx
server/sonar-web/src/main/js/apps/quality-profiles/routes.ts
server/sonar-web/src/main/js/components/charts/LanguageDistribution.tsx
server/sonar-web/src/main/js/components/hoc/__tests__/withCLanguageFeature-test.tsx
server/sonar-web/src/main/js/components/hoc/withCLanguageFeature.tsx
server/sonar-web/src/main/js/components/tutorials/azure-pipelines/BranchAnalysisStepContent.tsx
server/sonar-web/src/main/js/components/tutorials/azure-pipelines/__tests__/__snapshots__/AzurePipelinesTutorial-test.tsx.snap
server/sonar-web/src/main/js/components/tutorials/bitbucket-pipelines/__tests__/__snapshots__/BitbucketPipelinesTutorial-test.tsx.snap
server/sonar-web/src/main/js/components/tutorials/github-action/__tests__/__snapshots__/GitHubActionTutorial-test.tsx.snap
server/sonar-web/src/main/js/components/tutorials/gitlabci/__tests__/__snapshots__/GitLabCITutorial-test.tsx.snap
server/sonar-web/src/main/js/components/tutorials/jenkins/__tests__/__snapshots__/JenkinsTutorial-test.tsx.snap
server/sonar-web/src/main/js/helpers/testMocks.ts
server/sonar-web/src/main/js/store/languages.ts [deleted file]
server/sonar-web/src/main/js/store/rootActions.ts
server/sonar-web/src/main/js/store/rootReducer.ts
server/sonar-web/src/main/js/types/languages.ts [new file with mode: 0644]

index 964d95d6ec79cb17e0960551907bc6d861fcec22..941c2c4c7baf88e6f257d2969cd6f5f39618f3fe 100644 (file)
@@ -19,7 +19,7 @@
  */
 import throwGlobalError from '../app/utils/throwGlobalError';
 import { getJSON } from '../helpers/request';
-import { Language } from '../types/types';
+import { Language } from '../types/languages';
 
 export function getLanguages(): Promise<Language[]> {
   return getJSON('/api/languages/list').then(r => r.languages, throwGlobalError);
index b53b6da1f8411f9b26e2c09704658dffe5b37abf..e1ba399c6059c2566fe595812a2b12a41d405ea7 100644 (file)
 import * as React from 'react';
 import { connect } from 'react-redux';
 import { lazyLoadComponent } from '../../components/lazyLoadComponent';
-import { fetchLanguages } from '../../store/rootActions';
 import { getGlobalSettingValue, Store } from '../../store/rootReducer';
 import KeyboardShortcutsModal from './KeyboardShortcutsModal';
 
 const PageTracker = lazyLoadComponent(() => import('./PageTracker'));
 
 interface Props {
-  fetchLanguages: () => void;
   enableGravatar: boolean;
   gravatarServerUrl: string;
 }
@@ -37,7 +35,6 @@ export class App extends React.PureComponent<Props> {
 
   componentDidMount() {
     this.mounted = true;
-    this.props.fetchLanguages();
     this.setScrollbarWidth();
   }
 
@@ -100,6 +97,4 @@ const mapStateToProps = (state: Store) => {
   };
 };
 
-const mapDispatchToProps = { fetchLanguages };
-
-export default connect(mapStateToProps, mapDispatchToProps)(App);
+export default connect(mapStateToProps)(App);
index fa9574be70526f9ff3ce4b062c665f7a2eadc2f7..f824e12e575b7992c08fbc4f19b93479622f3df2 100644 (file)
@@ -26,6 +26,7 @@ import GlobalFooterContainer from './GlobalFooterContainer';
 import GlobalMessagesContainer from './GlobalMessagesContainer';
 import IndexationContextProvider from './indexation/IndexationContextProvider';
 import IndexationNotification from './indexation/IndexationNotification';
+import LanguageContextProvider from './languages/LanguagesContextProvider';
 import GlobalNav from './nav/global/GlobalNav';
 import PromotionNotification from './promotion-notification/PromotionNotification';
 import StartupModal from './StartupModal';
@@ -51,11 +52,13 @@ export default function GlobalContainer(props: Props) {
               <div className="page-container">
                 <Workspace>
                   <IndexationContextProvider>
-                    <GlobalNav location={props.location} />
-                    <GlobalMessagesContainer />
-                    <IndexationNotification />
-                    <UpdateNotification dismissable={true} />
-                    {props.children}
+                    <LanguageContextProvider>
+                      <GlobalNav location={props.location} />
+                      <GlobalMessagesContainer />
+                      <IndexationNotification />
+                      <UpdateNotification dismissable={true} />
+                      {props.children}
+                    </LanguageContextProvider>
                   </IndexationContextProvider>
                 </Workspace>
               </div>
index 6787d603a70551ecf69c41cfa794483492030f5b..75b9d805b88eaf04316b1660991427c554c0da9b 100644 (file)
@@ -20,7 +20,6 @@
 import { shallow } from 'enzyme';
 import * as React from 'react';
 import { connect } from 'react-redux';
-import { fetchLanguages as realFetchLanguages } from '../../../store/rootActions';
 import { App } from '../App';
 
 jest.mock('react-redux', () => ({
@@ -40,33 +39,22 @@ it('should render correctly', () => {
   ).toMatchSnapshot('with gravatar');
 });
 
-it('should correctly fetch available languages', () => {
-  const fetchLanguages = jest.fn();
-  shallowRender({ fetchLanguages });
-  expect(fetchLanguages).toBeCalled();
-});
-
 it('should correctly set the scrollbar width as a custom property', () => {
   shallowRender();
   expect(document.body.style.getPropertyValue('--sbw')).toBe('0px');
 });
 
 describe('redux', () => {
-  it('should correctly map state and dispatch props', () => {
-    const [mapStateToProps, mapDispatchToProps] = (connect as jest.Mock).mock.calls[0];
+  it('should correctly map state props', () => {
+    const [mapStateToProps] = (connect as jest.Mock).mock.calls[0];
 
     expect(mapStateToProps({})).toEqual({
       enableGravatar: true,
       gravatarServerUrl: 'http://gravatar.com'
     });
-    expect(mapDispatchToProps).toEqual(
-      expect.objectContaining({ fetchLanguages: realFetchLanguages })
-    );
   });
 });
 
 function shallowRender(props: Partial<App['props']> = {}) {
-  return shallow<App>(
-    <App fetchLanguages={jest.fn()} enableGravatar={false} gravatarServerUrl="" {...props} />
-  );
+  return shallow<App>(<App enableGravatar={false} gravatarServerUrl="" {...props} />);
 }
index 107d16e2a176cc6ce3891a7777f363d99b9b0b02..e68741350699e21fe22877dd7e8884a8e1f07ec9 100644 (file)
@@ -17,25 +17,27 @@ exports[`should render correctly 1`] = `
           >
             <Workspace>
               <Connect(withAppState(IndexationContextProvider))>
-                <Connect(GlobalNav)
-                  location={
-                    Object {
-                      "action": "PUSH",
-                      "hash": "",
-                      "key": "key",
-                      "pathname": "/path",
-                      "query": Object {},
-                      "search": "",
-                      "state": Object {},
+                <LanguageContextProvider>
+                  <Connect(GlobalNav)
+                    location={
+                      Object {
+                        "action": "PUSH",
+                        "hash": "",
+                        "key": "key",
+                        "pathname": "/path",
+                        "query": Object {},
+                        "search": "",
+                        "state": Object {},
+                      }
                     }
-                  }
-                />
-                <Connect(GlobalMessages) />
-                <Connect(withCurrentUser(withIndexationContext(IndexationNotification))) />
-                <Connect(withCurrentUser(Connect(withAppState(UpdateNotification))))
-                  dismissable={true}
-                />
-                <ChildComponent />
+                  />
+                  <Connect(GlobalMessages) />
+                  <Connect(withCurrentUser(withIndexationContext(IndexationNotification))) />
+                  <Connect(withCurrentUser(Connect(withAppState(UpdateNotification))))
+                    dismissable={true}
+                  />
+                  <ChildComponent />
+                </LanguageContextProvider>
               </Connect(withAppState(IndexationContextProvider))>
             </Workspace>
           </div>
diff --git a/server/sonar-web/src/main/js/app/components/languages/LanguagesContext.ts b/server/sonar-web/src/main/js/app/components/languages/LanguagesContext.ts
new file mode 100644 (file)
index 0000000..677a05c
--- /dev/null
@@ -0,0 +1,23 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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 { Languages } from '../../../types/languages';
+
+export const LanguagesContext = React.createContext<Languages>({});
diff --git a/server/sonar-web/src/main/js/app/components/languages/LanguagesContextProvider.tsx b/server/sonar-web/src/main/js/app/components/languages/LanguagesContextProvider.tsx
new file mode 100644 (file)
index 0000000..9e83ad9
--- /dev/null
@@ -0,0 +1,58 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { keyBy } from 'lodash';
+import * as React from 'react';
+import { getLanguages } from '../../../api/languages';
+import { Languages } from '../../../types/languages';
+import { LanguagesContext } from './LanguagesContext';
+
+interface State {
+  languages: Languages;
+}
+
+export default class LanguageContextProvider extends React.PureComponent<{}, State> {
+  mounted = false;
+  state: State = {
+    languages: {}
+  };
+
+  componentDidMount() {
+    this.mounted = true;
+
+    this.loadData();
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+  }
+
+  loadData = async () => {
+    const languageList = await getLanguages().catch(() => []);
+    this.setState({ languages: keyBy(languageList, 'key') });
+  };
+
+  render() {
+    return (
+      <LanguagesContext.Provider value={this.state.languages}>
+        {this.props.children}
+      </LanguagesContext.Provider>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/app/components/languages/__tests__/LanguagesContextProvider-test.tsx b/server/sonar-web/src/main/js/app/components/languages/__tests__/LanguagesContextProvider-test.tsx
new file mode 100644 (file)
index 0000000..feb859a
--- /dev/null
@@ -0,0 +1,46 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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 { getLanguages } from '../../../../api/languages';
+import { waitAndUpdate } from '../../../../helpers/testUtils';
+import LanguageContextProvider from '../LanguagesContextProvider';
+
+jest.mock('../../../../api/languages', () => ({
+  getLanguages: jest.fn().mockResolvedValue({})
+}));
+
+it('should call language', async () => {
+  const languages = { c: { key: 'c', name: 'c' } };
+  (getLanguages as jest.Mock).mockResolvedValueOnce(languages);
+  const wrapper = shallowRender();
+
+  expect(getLanguages).toBeCalled();
+  await waitAndUpdate(wrapper);
+  expect(wrapper.state()).toEqual({ languages });
+});
+
+function shallowRender() {
+  return shallow<LanguageContextProvider>(
+    <LanguageContextProvider>
+      <div />
+    </LanguageContextProvider>
+  );
+}
diff --git a/server/sonar-web/src/main/js/app/components/languages/__tests__/withLanguagesContext-test.tsx b/server/sonar-web/src/main/js/app/components/languages/__tests__/withLanguagesContext-test.tsx
new file mode 100644 (file)
index 0000000..4cf9157
--- /dev/null
@@ -0,0 +1,47 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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 { Languages } from '../../../../types/languages';
+import withLanguagesContext from '../withLanguagesContext';
+
+jest.mock('../LanguagesContext', () => {
+  return {
+    LanguagesContext: {
+      Consumer: ({ children }: { children: (props: {}) => React.ReactNode }) => {
+        return children({ c: { key: 'c', name: 'c' } });
+      }
+    }
+  };
+});
+
+class Wrapped extends React.Component<{ languages: Languages }> {
+  render() {
+    return <div />;
+  }
+}
+
+const UnderTest = withLanguagesContext(Wrapped);
+
+it('should inject languages', () => {
+  const wrapper = shallow(<UnderTest />);
+  expect(wrapper.dive().type()).toBe(Wrapped);
+  expect(wrapper.dive<Wrapped>().props().languages).toEqual({ c: { key: 'c', name: 'c' } });
+});
diff --git a/server/sonar-web/src/main/js/app/components/languages/withLanguagesContext.tsx b/server/sonar-web/src/main/js/app/components/languages/withLanguagesContext.tsx
new file mode 100644 (file)
index 0000000..1a7c61b
--- /dev/null
@@ -0,0 +1,45 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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 { getWrappedDisplayName } from '../../../components/hoc/utils';
+import { Languages } from '../../../types/languages';
+import { LanguagesContext } from './LanguagesContext';
+
+export interface WithLanguagesContextProps {
+  languages: Languages;
+}
+
+export default function withLanguagesContext<P>(
+  WrappedComponent: React.ComponentType<P & WithLanguagesContextProps>
+) {
+  return class WithLanguagesContext extends React.PureComponent<
+    Omit<P, keyof WithLanguagesContextProps>
+  > {
+    static displayName = getWrappedDisplayName(WrappedComponent, 'withLanguagesContext');
+
+    render() {
+      return (
+        <LanguagesContext.Consumer>
+          {languages => <WrappedComponent languages={languages} {...(this.props as P)} />}
+        </LanguagesContext.Consumer>
+      );
+    }
+  };
+}
index 2c0dae404d9df998b10f5d7a2659da677fe49b05..b13ddca3f78a7897a41307f78ec90157a0d334ad 100644 (file)
@@ -189,7 +189,7 @@ exports[`should render a private project correctly 1`] = `
           }
         }
       />
-      <Connect(MetaQualityProfiles)
+      <withLanguagesContext(MetaQualityProfiles)
         headerClassName="big-spacer-top"
         profiles={
           Array [
@@ -448,7 +448,7 @@ exports[`should render correctly: default 1`] = `
           }
         }
       />
-      <Connect(MetaQualityProfiles)
+      <withLanguagesContext(MetaQualityProfiles)
         headerClassName="big-spacer-top"
         profiles={
           Array [
@@ -606,7 +606,7 @@ exports[`should render correctly: no badges 1`] = `
           }
         }
       />
-      <Connect(MetaQualityProfiles)
+      <withLanguagesContext(MetaQualityProfiles)
         headerClassName="big-spacer-top"
         profiles={
           Array [
@@ -759,7 +759,7 @@ exports[`should render correctly: no badges, no notifications 1`] = `
           }
         }
       />
-      <Connect(MetaQualityProfiles)
+      <withLanguagesContext(MetaQualityProfiles)
         headerClassName="big-spacer-top"
         profiles={
           Array [
@@ -907,7 +907,7 @@ exports[`should render correctly: with notifications 1`] = `
           }
         }
       />
-      <Connect(MetaQualityProfiles)
+      <withLanguagesContext(MetaQualityProfiles)
         headerClassName="big-spacer-top"
         profiles={
           Array [
@@ -1061,7 +1061,7 @@ exports[`should render with description 1`] = `
           }
         }
       />
-      <Connect(MetaQualityProfiles)
+      <withLanguagesContext(MetaQualityProfiles)
         headerClassName="big-spacer-top"
         profiles={
           Array [
index 193e31438614234627ddb0d6960b39ee8a150f20..0f0fc343ff563939cd34c5861ef39897840dd718 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
-import { connect } from 'react-redux';
 import { Link } from 'react-router';
 import { searchRules } from '../../../../../../api/rules';
 import Tooltip from '../../../../../../components/controls/Tooltip';
 import { translate, translateWithParameters } from '../../../../../../helpers/l10n';
 import { getQualityProfileUrl } from '../../../../../../helpers/urls';
-import { getLanguages, Store } from '../../../../../../store/rootReducer';
-import { ComponentQualityProfile, Dict, Languages } from '../../../../../../types/types';
+import { Languages } from '../../../../../../types/languages';
+import { ComponentQualityProfile, Dict } from '../../../../../../types/types';
+import withLanguagesContext from '../../../../languages/withLanguagesContext';
 
-interface StateProps {
-  languages: Languages;
-}
-
-interface OwnProps {
+interface Props {
   headerClassName?: string;
+  languages: Languages;
   profiles: ComponentQualityProfile[];
 }
 
@@ -40,7 +37,7 @@ interface State {
   deprecatedByKey: Dict<number>;
 }
 
-export class MetaQualityProfiles extends React.PureComponent<StateProps & OwnProps, State> {
+export class MetaQualityProfiles extends React.PureComponent<Props, State> {
   mounted = false;
   state: State = { deprecatedByKey: {} };
 
@@ -141,8 +138,4 @@ export class MetaQualityProfiles extends React.PureComponent<StateProps & OwnPro
   }
 }
 
-const mapStateToProps = (state: Store) => ({
-  languages: getLanguages(state)
-});
-
-export default connect(mapStateToProps)(MetaQualityProfiles);
+export default withLanguagesContext(MetaQualityProfiles);
index 72af2ad730758e33aff041e9fb566eed6433fe97..a49dfd37afd83640f208c9b33ed0ebb06d473786 100644 (file)
@@ -42,17 +42,9 @@ import {
 } from '../../../helpers/pages';
 import { scrollToElement } from '../../../helpers/scrolling';
 import { isLoggedIn } from '../../../helpers/users';
-import { getCurrentUser, getLanguages, Store } from '../../../store/rootReducer';
+import { getCurrentUser, Store } from '../../../store/rootReducer';
 import { SecurityStandard } from '../../../types/security';
-import {
-  CurrentUser,
-  Dict,
-  Languages,
-  Paging,
-  RawQuery,
-  Rule,
-  RuleActivation
-} from '../../../types/types';
+import { CurrentUser, Dict, Paging, RawQuery, Rule, RuleActivation } from '../../../types/types';
 import {
   shouldOpenSonarSourceSecurityFacet,
   shouldOpenStandardsChildFacet,
@@ -88,7 +80,6 @@ const LIMIT_BEFORE_LOAD_MORE = 5;
 
 interface Props extends WithRouterProps {
   currentUser: CurrentUser;
-  languages: Languages;
 }
 
 interface State {
@@ -533,7 +524,7 @@ export class App extends React.PureComponent<Props, State> {
   isFiltered = () => Object.keys(serializeQuery(this.state.query)).length > 0;
 
   renderBulkButton = () => {
-    const { currentUser, languages } = this.props;
+    const { currentUser } = this.props;
     const { canWrite, paging, query, referencedProfiles } = this.state;
     const canUpdate = canWrite || Object.values(referencedProfiles).some(p => p.actions?.edit);
 
@@ -543,12 +534,7 @@ export class App extends React.PureComponent<Props, State> {
 
     return (
       paging && (
-        <BulkChange
-          languages={languages}
-          query={query}
-          referencedProfiles={referencedProfiles}
-          total={paging.total}
-        />
+        <BulkChange query={query} referencedProfiles={referencedProfiles} total={paging.total} />
       )
     );
   };
@@ -705,8 +691,7 @@ function parseFacets(rawFacets: { property: string; values: { count: number; val
 }
 
 const mapStateToProps = (state: Store) => ({
-  currentUser: getCurrentUser(state),
-  languages: getLanguages(state)
+  currentUser: getCurrentUser(state)
 });
 
 export default withRouter(connect(mapStateToProps)(App));
index 5cc4ce9cdaa1788785886fec765bbb92809f68d0..5ddf87e1a90a3e69a730f7356b68f551b5911274 100644 (file)
@@ -24,12 +24,11 @@ import Dropdown from '../../../components/controls/Dropdown';
 import Tooltip from '../../../components/controls/Tooltip';
 import { PopupPlacement } from '../../../components/ui/popups';
 import { translate } from '../../../helpers/l10n';
-import { Dict, Languages } from '../../../types/types';
+import { Dict } from '../../../types/types';
 import { Query } from '../query';
 import BulkChangeModal from './BulkChangeModal';
 
 interface Props {
-  languages: Languages;
   query: Query;
   referencedProfiles: Dict<Profile>;
   total: number;
@@ -135,7 +134,6 @@ export default class BulkChange extends React.PureComponent<Props, State> {
         {this.state.modal && this.state.action && (
           <BulkChangeModal
             action={this.state.action}
-            languages={this.props.languages}
             onClose={this.closeModal}
             profile={this.state.profile}
             query={this.props.query}
index 5c1e9d68309cba2e0118f95c94451603856017a8..700e29367d7b5f1d030ab8d89f1a6c2b29ab0d4f 100644 (file)
  */
 import * as React from 'react';
 import { bulkActivateRules, bulkDeactivateRules, Profile } from '../../../api/quality-profiles';
+import withLanguagesContext from '../../../app/components/languages/withLanguagesContext';
 import { ResetButtonLink, SubmitButton } from '../../../components/controls/buttons';
 import Modal from '../../../components/controls/Modal';
 import SelectLegacy from '../../../components/controls/SelectLegacy';
 import { Alert } from '../../../components/ui/Alert';
 import { translate, translateWithParameters } from '../../../helpers/l10n';
 import { formatMeasure } from '../../../helpers/measures';
-import { Dict, Languages } from '../../../types/types';
+import { Languages } from '../../../types/languages';
+import { Dict } from '../../../types/types';
 import { Query, serializeQuery } from '../query';
 
 interface Props {
@@ -51,7 +53,7 @@ interface State {
   submitting: boolean;
 }
 
-export default class BulkChangeModal extends React.PureComponent<Props, State> {
+export class BulkChangeModal extends React.PureComponent<Props, State> {
   mounted = false;
 
   constructor(props: Props) {
@@ -253,3 +255,5 @@ export default class BulkChangeModal extends React.PureComponent<Props, State> {
     );
   }
 }
+
+export default withLanguagesContext(BulkChangeModal);
index 3d0df61d3393cb325bdecdab9c6f179c2daa3580..f6f34f1b1fd4c8d83c6e8b9dd3fce0d5c444c8a9 100644 (file)
  */
 import { uniqBy } from 'lodash';
 import * as React from 'react';
-import { connect } from 'react-redux';
+import withLanguagesContext from '../../../app/components/languages/withLanguagesContext';
 import ListStyleFacet from '../../../components/facet/ListStyleFacet';
 import { translate } from '../../../helpers/l10n';
 import { highlightTerm } from '../../../helpers/search';
-import { getLanguages, Store } from '../../../store/rootReducer';
+import { Language, Languages } from '../../../types/languages';
 import { BasicProps } from './Facet';
 
-interface InstalledLanguage {
-  key: string;
-  name: string;
-}
-
 interface Props extends BasicProps {
   disabled?: boolean;
-  installedLanguages: InstalledLanguage[];
+  languages: Languages;
 }
 
-class LanguageFacet extends React.PureComponent<Props> {
+export class LanguageFacet extends React.PureComponent<Props> {
   getLanguageName = (languageKey: string) => {
-    const language = this.props.installedLanguages.find(l => l.key === languageKey);
+    const language = this.props.languages[languageKey];
     return language ? language.name : languageKey;
   };
 
@@ -52,24 +47,24 @@ class LanguageFacet extends React.PureComponent<Props> {
   };
 
   getAllPossibleOptions = () => {
-    const { installedLanguages, stats = {} } = this.props;
+    const { languages, stats = {} } = this.props;
 
     // add any language that presents in the facet, but might not be installed
     // for such language we don't know their display name, so let's just use their key
     // and make sure we reference each language only once
-    return uniqBy(
-      [...installedLanguages, ...Object.keys(stats).map(key => ({ key, name: key }))],
-      language => language.key
+    return uniqBy<Language>(
+      [...Object.values(languages), ...Object.keys(stats).map(key => ({ key, name: key }))],
+      (language: Language) => language.key
     );
   };
 
-  renderSearchResult = ({ name }: InstalledLanguage, term: string) => {
+  renderSearchResult = ({ name }: Language, term: string) => {
     return highlightTerm(name, term);
   };
 
   render() {
     return (
-      <ListStyleFacet<InstalledLanguage>
+      <ListStyleFacet<Language>
         disabled={this.props.disabled}
         disabledHelper={translate('coding_rules.filters.language.inactive')}
         facetHeader={translate('coding_rules.facet.languages')}
@@ -93,8 +88,4 @@ class LanguageFacet extends React.PureComponent<Props> {
   }
 }
 
-const mapStateToProps = (state: Store) => ({
-  installedLanguages: Object.values(getLanguages(state))
-});
-
-export default connect(mapStateToProps)(LanguageFacet);
+export default withLanguagesContext(LanguageFacet);
index 636218477bba53fb4d00d2b6b8afbd590ff321e8..550ec661b1a5025d0cf4df98aa299125d402b19d 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
-import { connect } from 'react-redux';
 import { getRuleRepositories } from '../../../api/rules';
+import withLanguagesContext from '../../../app/components/languages/withLanguagesContext';
 import ListStyleFacet from '../../../components/facet/ListStyleFacet';
 import { translate } from '../../../helpers/l10n';
 import { highlightTerm } from '../../../helpers/search';
-import { getLanguages, Store } from '../../../store/rootReducer';
+import { Languages } from '../../../types/languages';
 import { Dict } from '../../../types/types';
 import { BasicProps } from './Facet';
 
 interface StateProps {
-  referencedLanguages: Dict<{ key: string; name: string }>;
+  languages: Languages;
 }
 
 interface Props extends BasicProps, StateProps {
@@ -37,8 +37,8 @@ interface Props extends BasicProps, StateProps {
 
 export class RepositoryFacet extends React.PureComponent<Props> {
   getLanguageName = (languageKey: string) => {
-    const { referencedLanguages } = this.props;
-    const language = referencedLanguages[languageKey];
+    const { languages } = this.props;
+    const language = languages[languageKey];
     return (language && language.name) || languageKey;
   };
 
@@ -107,8 +107,4 @@ export class RepositoryFacet extends React.PureComponent<Props> {
   }
 }
 
-const mapStateToProps = (state: Store): StateProps => ({
-  referencedLanguages: getLanguages(state)
-});
-
-export default connect(mapStateToProps)(RepositoryFacet);
+export default withLanguagesContext(RepositoryFacet);
index a20fb8d20504de62b3697cfff9585abeb35a61fb..99bc833fe6b89102235fd3b0d252c5dd8479d66b 100644 (file)
@@ -122,7 +122,6 @@ function shallowRender(props: Partial<App['props']> = {}) {
       currentUser={mockCurrentUser({
         isLoggedIn: true
       })}
-      languages={{ js: { key: 'js', name: 'JavaScript' } }}
       location={mockLocation()}
       params={{}}
       router={mockRouter()}
index 0804d361273d15fab83d5be0239b80cd974ea021..01ec801671687b4d347155d188fe948b5bc1dbb9 100644 (file)
@@ -47,13 +47,12 @@ it('should not a disabled button when edition is not possible', () => {
 it('should display BulkChangeModal', () => {
   const wrapper = shallowRender();
   wrapper.instance().handleActivateClick(mockEvent());
-  expect(wrapper.find('BulkChangeModal')).toMatchSnapshot();
+  expect(wrapper.find('withLanguagesContext(BulkChangeModal)').exists()).toBe(true);
 });
 
 function shallowRender(props: Partial<BulkChange['props']> = {}) {
   return shallow<BulkChange>(
     <BulkChange
-      languages={{ js: { key: 'js', name: 'JavaScript' } }}
       query={{ activation: false, profile: 'key' } as BulkChange['props']['query']}
       referencedProfiles={{ key: profile }}
       total={2}
index 7791db14c7065e02bff3aeacb0c7c37fd787d95a..2c3d43654be83391e01c33d2edfd775c7efd56d6 100644 (file)
@@ -23,7 +23,7 @@ import { bulkActivateRules, bulkDeactivateRules } from '../../../../api/quality-
 import { mockLanguage, mockQualityProfile } from '../../../../helpers/testMocks';
 import { submit, waitAndUpdate } from '../../../../helpers/testUtils';
 import { Query } from '../../query';
-import BulkChangeModal from '../BulkChangeModal';
+import { BulkChangeModal } from '../BulkChangeModal';
 
 jest.mock('../../../../api/quality-profiles', () => ({
   bulkActivateRules: jest.fn().mockResolvedValue({ failed: 0, succeeded: 2 }),
index ea7e2198dac0351d88eeea8035674cd731158fcb..1b0ca0fcb52ff28150456db6f60489fca0b5b8e5 100644 (file)
@@ -36,10 +36,10 @@ it('should correctly hide profile facets', () => {
 
 it('should correctly enable/disable the language facet', () => {
   const wrapper = shallowRender({ query: { profile: 'foo' } as Query });
-  expect(wrapper.find('Connect(LanguageFacet)').prop('disabled')).toBe(true);
+  expect(wrapper.find('withLanguagesContext(LanguageFacet)').prop('disabled')).toBe(true);
 
   wrapper.setProps({ query: {} }).update();
-  expect(wrapper.find('Connect(LanguageFacet)').prop('disabled')).toBe(false);
+  expect(wrapper.find('withLanguagesContext(LanguageFacet)').prop('disabled')).toBe(false);
 });
 
 it('should correctly enable/disable the activation severity facet', () => {
diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/LanguageFacet-test.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/LanguageFacet-test.tsx
new file mode 100644 (file)
index 0000000..1d2ce59
--- /dev/null
@@ -0,0 +1,78 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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 { mockLanguage } from '../../../../helpers/testMocks';
+import { LanguageFacet } from '../LanguageFacet';
+
+it('should handle search correctly', async () => {
+  const wrapper = shallowRender({ stats: { java: 12 } });
+  const result = await wrapper.instance().handleSearch('ja');
+
+  expect(result).toStrictEqual({
+    paging: {
+      pageIndex: 1,
+      pageSize: 2,
+      total: 2
+    },
+    results: [
+      { key: 'js', name: 'javascript' },
+      { key: 'java', name: 'java' }
+    ]
+  });
+});
+
+it('should render name correctly', () => {
+  const wrapper = shallowRender();
+
+  expect(wrapper.instance().getLanguageName('js')).toBe('javascript');
+  expect(wrapper.instance().getLanguageName('unknownKey')).toBe('unknownKey');
+});
+
+it('should render search results correctly', () => {
+  const wrapper = shallowRender();
+
+  expect(wrapper.instance().renderSearchResult({ key: 'hello', name: 'Hello' }, 'llo'))
+    .toMatchInlineSnapshot(`
+    <React.Fragment>
+      He
+      <mark>
+        llo
+      </mark>
+    </React.Fragment>
+  `);
+});
+
+function shallowRender(props: Partial<LanguageFacet['props']> = {}) {
+  return shallow<LanguageFacet>(
+    <LanguageFacet
+      languages={{
+        js: mockLanguage({ key: 'js', name: 'javascript' }),
+        c: mockLanguage({ key: 'c', name: 'c' })
+      }}
+      onChange={jest.fn()}
+      onToggle={jest.fn()}
+      open={false}
+      stats={{}}
+      values={[]}
+      {...props}
+    />
+  );
+}
index 28f8df7d4b62092bd80b2f673ef513fc8a57fb0f..455003d6c34a8a4e2e7a744bc5acdb6111a9d973 100644 (file)
@@ -75,7 +75,7 @@ it('should render search repository correctly', () => {
 function shallowRender(props: Partial<RepositoryFacet['props']> = {}) {
   return shallow<RepositoryFacet>(
     <RepositoryFacet
-      referencedLanguages={{ l: mockLanguage() }}
+      languages={{ l: mockLanguage() }}
       referencedRepositories={{
         l: mockRuleRepository(),
         noName: mockRuleRepository({ name: undefined })
index 09a0e14f6fbd016ed70e9190528f965673109fca..0fb3795dcdc3b798d94471b562db16fa2bf7810c 100644 (file)
@@ -6,14 +6,6 @@ exports[`renderBulkButton should be null when the user is not logged in 1`] = `<
 
 exports[`renderBulkButton should show bulk change button when user has edit rights on specific quality profile 1`] = `
 <BulkChange
-  languages={
-    Object {
-      "js": Object {
-        "key": "js",
-        "name": "JavaScript",
-      },
-    }
-  }
   query={
     Object {
       "activation": undefined,
@@ -78,14 +70,6 @@ exports[`renderBulkButton should show bulk change button when user has edit righ
 
 exports[`renderBulkButton should show bulk change button when user has global admin rights on quality profiles 1`] = `
 <BulkChange
-  languages={
-    Object {
-      "js": Object {
-        "key": "js",
-        "name": "JavaScript",
-      },
-    }
-  }
   query={
     Object {
       "activation": undefined,
@@ -235,14 +219,6 @@ exports[`should render correctly: loaded 1`] = `
               className="display-flex-space-between"
             >
               <BulkChange
-                languages={
-                  Object {
-                    "js": Object {
-                      "key": "js",
-                      "name": "JavaScript",
-                    },
-                  }
-                }
                 query={
                   Object {
                     "activation": undefined,
index 45021122b8d0c0d00afb6a15580414b9467c6602..3ed95693494a791186e6dfdfdbdc8683a2f62e61 100644 (file)
@@ -1,52 +1,5 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`should display BulkChangeModal 1`] = `
-<BulkChangeModal
-  action="activate"
-  languages={
-    Object {
-      "js": Object {
-        "key": "js",
-        "name": "JavaScript",
-      },
-    }
-  }
-  onClose={[Function]}
-  query={
-    Object {
-      "activation": false,
-      "profile": "key",
-    }
-  }
-  referencedProfiles={
-    Object {
-      "key": Object {
-        "actions": Object {
-          "associateProjects": true,
-          "copy": true,
-          "delete": false,
-          "edit": true,
-          "setAsDefault": true,
-        },
-        "activeDeprecatedRuleCount": 2,
-        "activeRuleCount": 10,
-        "childrenCount": 0,
-        "depth": 1,
-        "isBuiltIn": false,
-        "isDefault": false,
-        "isInherited": false,
-        "key": "key",
-        "language": "js",
-        "languageName": "JavaScript",
-        "name": "name",
-        "projectCount": 3,
-      },
-    }
-  }
-  total={2}
-/>
-`;
-
 exports[`should not a disabled button when edition is not possible 1`] = `
 <Tooltip
   overlay="coding_rules.can_not_bulk_change"
index 7b252f0158747162b8956d007bc6f0337570c08d..7a490ced59c7e842e58a3f069e980225416f426d 100644 (file)
@@ -2,7 +2,7 @@
 
 exports[`should render correctly 1`] = `
 <Fragment>
-  <Connect(LanguageFacet)
+  <withLanguagesContext(LanguageFacet)
     disabled={false}
     onChange={[MockFunction]}
     onToggle={[MockFunction]}
@@ -18,7 +18,7 @@ exports[`should render correctly 1`] = `
     onToggle={[MockFunction]}
     open={false}
   />
-  <Connect(RepositoryFacet)
+  <withLanguagesContext(RepositoryFacet)
     onChange={[MockFunction]}
     onToggle={[MockFunction]}
     open={false}
index 2bc684537e8785b66dc3405c780f147fbe39190e..443e596f7548208b11ad83553a74f707aa4e5a55 100644 (file)
@@ -96,7 +96,7 @@ it('should not render link to activity page for files', () => {
 
 it('should display secondary measure too', () => {
   const wrapper = shallow(<MeasureHeader {...PROPS} secondaryMeasure={SECONDARY} />);
-  expect(wrapper.find('Connect(LanguageDistribution)')).toHaveLength(1);
+  expect(wrapper.find('withLanguagesContext(LanguageDistribution)')).toHaveLength(1);
 });
 
 it('should work with measure without value', () => {
index 2bce40f8c5caeca1903851d594c01b7760bf6fcb..c10a038f8b736527ad0117dc4c5f7a1449fce559 100644 (file)
  */
 import { omit, uniqBy } from 'lodash';
 import * as React from 'react';
-import { connect } from 'react-redux';
+import withLanguagesContext from '../../../app/components/languages/withLanguagesContext';
 import ListStyleFacet from '../../../components/facet/ListStyleFacet';
 import { translate } from '../../../helpers/l10n';
 import { highlightTerm } from '../../../helpers/search';
-import { getLanguages, Store } from '../../../store/rootReducer';
 import { Facet, ReferencedLanguage } from '../../../types/issues';
+import { Language, Languages } from '../../../types/languages';
 import { Dict } from '../../../types/types';
 import { Query } from '../utils';
 
-interface InstalledLanguage {
-  key: string;
-  name: string;
-}
-
 interface Props {
   fetching: boolean;
-  installedLanguages: InstalledLanguage[];
-  languages: string[];
+  languages: Languages;
+  selectedLanguages: string[];
   loadSearchResultCount: (property: string, changes: Partial<Query>) => Promise<Facet>;
   onChange: (changes: Partial<Query>) => void;
   onToggle: (property: string) => void;
@@ -62,30 +57,30 @@ class LanguageFacet extends React.PureComponent<Props> {
   };
 
   getAllPossibleOptions = () => {
-    const { installedLanguages, stats = {} } = this.props;
+    const { languages, stats = {} } = this.props;
 
     // add any language that presents in the facet, but might not be installed
     // for such language we don't know their display name, so let's just use their key
     // and make sure we reference each language only once
     return uniqBy(
-      [...installedLanguages, ...Object.keys(stats).map(key => ({ key, name: key }))],
+      [...Object.values(languages), ...Object.keys(stats).map(key => ({ key, name: key }))],
       language => language.key
     );
   };
 
-  loadSearchResultCount = (languages: InstalledLanguage[]) => {
+  loadSearchResultCount = (languages: Language[]) => {
     return this.props.loadSearchResultCount('languages', {
       languages: languages.map(language => language.key)
     });
   };
 
-  renderSearchResult = ({ name }: InstalledLanguage, term: string) => {
+  renderSearchResult = ({ name }: Language, term: string) => {
     return highlightTerm(name, term);
   };
 
   render() {
     return (
-      <ListStyleFacet<InstalledLanguage>
+      <ListStyleFacet<Language>
         facetHeader={translate('issues.facet.languages')}
         fetching={this.props.fetching}
         getFacetItemText={this.getLanguageName}
@@ -103,14 +98,10 @@ class LanguageFacet extends React.PureComponent<Props> {
         renderSearchResult={this.renderSearchResult}
         searchPlaceholder={translate('search.search_for_languages')}
         stats={this.props.stats}
-        values={this.props.languages}
+        values={this.props.selectedLanguages}
       />
     );
   }
 }
 
-const mapStateToProps = (state: Store) => ({
-  installedLanguages: Object.values(getLanguages(state))
-});
-
-export default connect(mapStateToProps)(LanguageFacet);
+export default withLanguagesContext(LanguageFacet);
index fa4ded607af8fcf8813ed5d323e8f1469561a0c7..fd455980dc0c2acb6bac5fb4a098f375a839b41e 100644 (file)
@@ -207,13 +207,13 @@ export class Sidebar extends React.PureComponent<Props> {
         />
         <LanguageFacet
           fetching={this.props.loadingFacets.languages === true}
-          languages={query.languages}
           loadSearchResultCount={this.props.loadSearchResultCount}
           onChange={this.props.onFilterChange}
           onToggle={this.props.onFacetToggle}
           open={!!openFacets.languages}
           query={query}
           referencedLanguages={this.props.referencedLanguages}
+          selectedLanguages={query.languages}
           stats={facets.languages}
         />
         <RuleFacet
index 435a8ee0bcd7ebaab02265921936c9290d708014..cd84f4811067c217ffe099245bffd3083d02cee6 100644 (file)
@@ -9,7 +9,7 @@ Array [
   "StatusFacet",
   "StandardFacet",
   "injectIntl(CreationDateFacet)",
-  "Connect(LanguageFacet)",
+  "withLanguagesContext(LanguageFacet)",
   "RuleFacet",
   "TagFacet",
   "ProjectFacet",
@@ -25,7 +25,7 @@ Array [
   "StatusFacet",
   "StandardFacet",
   "injectIntl(CreationDateFacet)",
-  "Connect(LanguageFacet)",
+  "withLanguagesContext(LanguageFacet)",
   "RuleFacet",
   "TagFacet",
   "ProjectFacet",
@@ -43,7 +43,7 @@ Array [
   "StatusFacet",
   "StandardFacet",
   "injectIntl(CreationDateFacet)",
-  "Connect(LanguageFacet)",
+  "withLanguagesContext(LanguageFacet)",
   "RuleFacet",
   "TagFacet",
   "DirectoryFacet",
@@ -62,7 +62,7 @@ Array [
   "StatusFacet",
   "StandardFacet",
   "injectIntl(CreationDateFacet)",
-  "Connect(LanguageFacet)",
+  "withLanguagesContext(LanguageFacet)",
   "RuleFacet",
   "TagFacet",
   "ProjectFacet",
@@ -81,7 +81,7 @@ Array [
   "StatusFacet",
   "StandardFacet",
   "injectIntl(CreationDateFacet)",
-  "Connect(LanguageFacet)",
+  "withLanguagesContext(LanguageFacet)",
   "RuleFacet",
   "TagFacet",
   "FileFacet",
@@ -99,7 +99,7 @@ Array [
   "StatusFacet",
   "StandardFacet",
   "injectIntl(CreationDateFacet)",
-  "Connect(LanguageFacet)",
+  "withLanguagesContext(LanguageFacet)",
   "RuleFacet",
   "TagFacet",
   "ProjectFacet",
@@ -117,7 +117,7 @@ Array [
   "StatusFacet",
   "StandardFacet",
   "injectIntl(CreationDateFacet)",
-  "Connect(LanguageFacet)",
+  "withLanguagesContext(LanguageFacet)",
   "RuleFacet",
   "TagFacet",
   "ProjectFacet",
@@ -135,7 +135,7 @@ Array [
   "StatusFacet",
   "StandardFacet",
   "injectIntl(CreationDateFacet)",
-  "Connect(LanguageFacet)",
+  "withLanguagesContext(LanguageFacet)",
   "RuleFacet",
   "TagFacet",
   "ProjectFacet",
@@ -153,7 +153,7 @@ Array [
   "StatusFacet",
   "StandardFacet",
   "injectIntl(CreationDateFacet)",
-  "Connect(LanguageFacet)",
+  "withLanguagesContext(LanguageFacet)",
   "RuleFacet",
   "TagFacet",
   "DirectoryFacet",
@@ -172,7 +172,7 @@ Array [
   "StatusFacet",
   "StandardFacet",
   "injectIntl(CreationDateFacet)",
-  "Connect(LanguageFacet)",
+  "withLanguagesContext(LanguageFacet)",
   "RuleFacet",
   "TagFacet",
   "ProjectFacet",
index e17e8469e713df28b7747e06a4590dbe3fe0c827..6256fd408b1b5d6042d0f86a086d67be902fae47 100644 (file)
@@ -246,7 +246,7 @@ exports[`should render correctly: add language 1`] = `
           project_quality_profile.add_language.action
         </Button>
       </div>
-      <Connect(AddLanguageModal)
+      <withLanguagesContext(AddLanguageModal)
         onClose={[MockFunction]}
         onSubmit={[MockFunction]}
         profilesByLanguage={
index 8f6264b69cd46eb51c705a90fea4369e4146e65e..84b26dcef781b8d3bd543d4c3aaff5d2ec490001 100644 (file)
  */
 import { difference } from 'lodash';
 import * as React from 'react';
-import { connect } from 'react-redux';
 import { Link } from 'react-router';
 import { Profile } from '../../../api/quality-profiles';
+import withLanguagesContext from '../../../app/components/languages/withLanguagesContext';
 import DisableableSelectOption from '../../../components/common/DisableableSelectOption';
 import { ButtonLink, SubmitButton } from '../../../components/controls/buttons';
 import SelectLegacy from '../../../components/controls/SelectLegacy';
 import SimpleModal from '../../../components/controls/SimpleModal';
 import { translate } from '../../../helpers/l10n';
 import { getQualityProfileUrl } from '../../../helpers/urls';
-import { Store } from '../../../store/rootReducer';
-import { Dict, Languages } from '../../../types/types';
+import { Languages } from '../../../types/languages';
+import { Dict } from '../../../types/types';
 
 export interface AddLanguageModalProps {
   languages: Languages;
@@ -155,8 +155,4 @@ export function AddLanguageModal(props: AddLanguageModalProps) {
   );
 }
 
-function mapStateToProps({ languages }: Store) {
-  return { languages };
-}
-
-export default connect(mapStateToProps)(AddLanguageModal);
+export default withLanguagesContext(AddLanguageModal);
index cb8c0511bd597283061d13bddaec533f93231f6a..417b9e7af829311fe30f03eff5124f8510d32039 100644 (file)
@@ -23,7 +23,7 @@ import { translate } from '../../../helpers/l10n';
 import { RawQuery } from '../../../types/types';
 import CoverageFilter from '../filters/CoverageFilter';
 import DuplicationsFilter from '../filters/DuplicationsFilter';
-import LanguagesFilterContainer from '../filters/LanguagesFilterContainer';
+import LanguagesFilter from '../filters/LanguagesFilter';
 import MaintainabilityFilter from '../filters/MaintainabilityFilter';
 import NewCoverageFilter from '../filters/NewCoverageFilter';
 import NewDuplicationsFilter from '../filters/NewDuplicationsFilter';
@@ -157,7 +157,7 @@ export default function PageSidebar(props: PageSidebarProps) {
           />
         </>
       )}
-      <LanguagesFilterContainer
+      <LanguagesFilter
         {...facetProps}
         facet={getFacet(facets, 'languages')}
         query={query}
index 5e4876ebedaa20e6f47a55b7e463baf5c1412d1d..ed70b96f32fbc4465a656a4e8eb36ce2d21c563c 100644 (file)
@@ -44,7 +44,7 @@ exports[`should render \`leak\` view correctly 1`] = `
   <NewLinesFilter
     onQueryChange={[MockFunction]}
   />
-  <Connect(LanguagesFilter)
+  <withLanguagesContext(LanguagesFilter)
     onQueryChange={[MockFunction]}
     query={
       Object {
@@ -110,7 +110,7 @@ exports[`should render \`leak\` view correctly with no applications 1`] = `
   <NewLinesFilter
     onQueryChange={[MockFunction]}
   />
-  <Connect(LanguagesFilter)
+  <withLanguagesContext(LanguagesFilter)
     onQueryChange={[MockFunction]}
     query={
       Object {
@@ -169,7 +169,7 @@ exports[`should render correctly 1`] = `
     onQueryChange={[MockFunction]}
     value="3"
   />
-  <Connect(LanguagesFilter)
+  <withLanguagesContext(LanguagesFilter)
     onQueryChange={[MockFunction]}
     query={
       Object {
@@ -231,7 +231,7 @@ exports[`should render correctly with no applications 1`] = `
     onQueryChange={[MockFunction]}
     value="3"
   />
-  <Connect(LanguagesFilter)
+  <withLanguagesContext(LanguagesFilter)
     onQueryChange={[MockFunction]}
     query={
       Object {
index 43003296c8014f4b6cb83fcada238df31c694754..d8e0dc3f0ddd9308ef9909faf0d45f6f2130b95c 100644 (file)
@@ -38,7 +38,7 @@ import { MetricKey } from '../../../../types/metrics';
 import { CurrentUser } from '../../../../types/types';
 import { Project } from '../../types';
 import './ProjectCard.css';
-import ProjectCardLanguagesContainer from './ProjectCardLanguagesContainer';
+import ProjectCardLanguages from './ProjectCardLanguages';
 import ProjectCardMeasure from './ProjectCardMeasure';
 import ProjectCardMeasures from './ProjectCardMeasures';
 import ProjectCardQualityGate from './ProjectCardQualityGate';
@@ -176,7 +176,7 @@ function renderSecondLine(
                   <span className="spacer-left">
                     <SizeRating value={Number(measures[MetricKey.ncloc])} />
                   </span>
-                  <ProjectCardLanguagesContainer
+                  <ProjectCardLanguages
                     className="small spacer-left text-ellipsis"
                     distribution={measures[MetricKey.ncloc_language_distribution]}
                   />
index 0b3a3e390ad77357d2e1d4f8bfe3afc1984306da..295aa1cbecc649243721dfd74dc9b84bed0942e3 100644 (file)
@@ -19,8 +19,9 @@
  */
 import { sortBy } from 'lodash';
 import * as React from 'react';
+import withLanguagesContext from '../../../../app/components/languages/withLanguagesContext';
 import { translate } from '../../../../helpers/l10n';
-import { Languages } from '../../../../types/types';
+import { Languages } from '../../../../types/languages';
 
 interface Props {
   className?: string;
@@ -28,7 +29,7 @@ interface Props {
   languages: Languages;
 }
 
-export default function ProjectCardLanguages({ className, distribution, languages }: Props) {
+export function ProjectCardLanguages({ className, distribution, languages }: Props) {
   if (distribution === undefined) {
     return null;
   }
@@ -54,3 +55,5 @@ function getLanguageName(languages: Languages, key: string): string {
   const language = languages[key];
   return language != null ? language.name : key;
 }
+
+export default withLanguagesContext(ProjectCardLanguages);
diff --git a/server/sonar-web/src/main/js/apps/projects/components/project-card/ProjectCardLanguagesContainer.tsx b/server/sonar-web/src/main/js/apps/projects/components/project-card/ProjectCardLanguagesContainer.tsx
deleted file mode 100644 (file)
index e95e607..0000000
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2022 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 { connect } from 'react-redux';
-import { getLanguages, Store } from '../../../../store/rootReducer';
-import ProjectCardLanguages from './ProjectCardLanguages';
-
-const stateToProps = (state: Store) => ({
-  languages: getLanguages(state)
-});
-
-export default connect(stateToProps)(ProjectCardLanguages);
index 07e12c17dabdc8ba5f185480a65b1c3d79851212..4e0e927f18b21ae4913a721a507cc129672cb4ff 100644 (file)
@@ -19,7 +19,7 @@
  */
 import { shallow } from 'enzyme';
 import * as React from 'react';
-import ProjectCardLanguages from '../ProjectCardLanguages';
+import { ProjectCardLanguages } from '../ProjectCardLanguages';
 
 const languages = {
   java: { key: 'java', name: 'Java' },
index c571c51d95803890596c8b3f5ed3d560fc3d0563..141fd43f77b8dc5b8661158777381c263b15af7a 100644 (file)
  */
 import { difference, sortBy } from 'lodash';
 import * as React from 'react';
+import withLanguagesContext from '../../../app/components/languages/withLanguagesContext';
 import { translate } from '../../../helpers/l10n';
-import { getLanguageByKey } from '../../../store/languages';
-import { Dict, Languages, RawQuery } from '../../../types/types';
+import { Languages } from '../../../types/languages';
+import { Dict, RawQuery } from '../../../types/types';
 import { Facet } from '../types';
 import Filter from './Filter';
 import FilterHeader from './FilterHeader';
@@ -38,7 +39,7 @@ interface Props {
   value?: string[];
 }
 
-export default class LanguagesFilter extends React.Component<Props> {
+export class LanguagesFilter extends React.Component<Props> {
   getSearchOptions = () => {
     const { facet, languages } = this.props;
     let languageKeys = Object.keys(languages);
@@ -52,10 +53,7 @@ export default class LanguagesFilter extends React.Component<Props> {
     sortBy(Object.keys(facet), [(option: string) => -facet[option], (option: string) => option]);
 
   renderOption = (option: string) => (
-    <SearchableFilterOption
-      option={getLanguageByKey(this.props.languages, option)}
-      optionKey={option}
-    />
+    <SearchableFilterOption option={this.props.languages[option]} optionKey={option} />
   );
 
   render() {
@@ -83,3 +81,5 @@ export default class LanguagesFilter extends React.Component<Props> {
     );
   }
 }
+
+export default withLanguagesContext(LanguagesFilter);
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/LanguagesFilterContainer.tsx b/server/sonar-web/src/main/js/apps/projects/filters/LanguagesFilterContainer.tsx
deleted file mode 100644 (file)
index 143b4be..0000000
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2022 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 { connect } from 'react-redux';
-import { getLanguages, Store } from '../../../store/rootReducer';
-import LanguagesFilter from './LanguagesFilter';
-
-const stateToProps = (state: Store) => ({
-  languages: getLanguages(state)
-});
-
-export default connect(stateToProps)(LanguagesFilter);
index fe0086d48918ccfafdf7f87669aca9986c4a3fa1..04cfb1e88db17f874741c0e0b71a5d6b3d55221a 100644 (file)
@@ -19,7 +19,7 @@
  */
 import { shallow } from 'enzyme';
 import * as React from 'react';
-import LanguagesFilter from '../LanguagesFilter';
+import { LanguagesFilter } from '../LanguagesFilter';
 
 const languages = {
   java: { key: 'java', name: 'Java' },
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/App.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/components/App.tsx
deleted file mode 100644 (file)
index 84af30f..0000000
+++ /dev/null
@@ -1,114 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2022 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-async';
-import { Actions, getExporters, searchQualityProfiles } from '../../../api/quality-profiles';
-import Suggestions from '../../../app/components/embed-docs-modal/Suggestions';
-import { translate } from '../../../helpers/l10n';
-import { Languages } from '../../../types/types';
-import '../styles.css';
-import { Exporter, Profile } from '../types';
-import { sortProfiles } from '../utils';
-
-interface Props {
-  children: React.ReactElement<any>;
-  languages: Languages;
-}
-
-interface State {
-  actions?: Actions;
-  loading: boolean;
-  exporters?: Exporter[];
-  profiles?: Profile[];
-}
-
-export default class App extends React.PureComponent<Props, State> {
-  mounted = false;
-  state: State = { loading: true };
-
-  componentDidMount() {
-    this.mounted = true;
-    this.loadData();
-  }
-
-  componentWillUnmount() {
-    this.mounted = false;
-  }
-
-  fetchProfiles() {
-    return searchQualityProfiles();
-  }
-
-  loadData() {
-    this.setState({ loading: true });
-    Promise.all([getExporters(), this.fetchProfiles()]).then(
-      responses => {
-        if (this.mounted) {
-          const [exporters, profilesResponse] = responses;
-          this.setState({
-            actions: profilesResponse.actions,
-            exporters,
-            profiles: sortProfiles(profilesResponse.profiles),
-            loading: false
-          });
-        }
-      },
-      () => {
-        if (this.mounted) {
-          this.setState({ loading: false });
-        }
-      }
-    );
-  }
-
-  updateProfiles = () => {
-    return this.fetchProfiles().then(r => {
-      if (this.mounted) {
-        this.setState({ profiles: sortProfiles(r.profiles) });
-      }
-    });
-  };
-
-  renderChild() {
-    if (this.state.loading) {
-      return <i className="spinner" />;
-    }
-    const finalLanguages = Object.values(this.props.languages);
-
-    return React.cloneElement(this.props.children, {
-      actions: this.state.actions || {},
-      profiles: this.state.profiles || [],
-      languages: finalLanguages,
-      exporters: this.state.exporters,
-      updateProfiles: this.updateProfiles
-    });
-  }
-
-  render() {
-    return (
-      <div className="page page-limited">
-        <Suggestions suggestions="quality_profiles" />
-        <Helmet defer={false} title={translate('quality_profiles.page')} />
-
-        {this.renderChild()}
-      </div>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/AppContainer.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/components/AppContainer.tsx
deleted file mode 100644 (file)
index 5504db2..0000000
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2022 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 { connect } from 'react-redux';
-import { getLanguages, Store } from '../../../store/rootReducer';
-import App from './App';
-
-const mapStateToProps = (state: Store) => ({
-  languages: getLanguages(state)
-});
-
-export default connect(mapStateToProps)(App);
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/QualityProfilesApp.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/components/QualityProfilesApp.tsx
new file mode 100644 (file)
index 0000000..a5f66e4
--- /dev/null
@@ -0,0 +1,116 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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-async';
+import { Actions, getExporters, searchQualityProfiles } from '../../../api/quality-profiles';
+import Suggestions from '../../../app/components/embed-docs-modal/Suggestions';
+import withLanguagesContext from '../../../app/components/languages/withLanguagesContext';
+import { translate } from '../../../helpers/l10n';
+import { Languages } from '../../../types/languages';
+import '../styles.css';
+import { Exporter, Profile } from '../types';
+import { sortProfiles } from '../utils';
+
+interface Props {
+  children: React.ReactElement;
+  languages: Languages;
+}
+
+interface State {
+  actions?: Actions;
+  loading: boolean;
+  exporters?: Exporter[];
+  profiles?: Profile[];
+}
+
+export class QualityProfilesApp extends React.PureComponent<Props, State> {
+  mounted = false;
+  state: State = { loading: true };
+
+  componentDidMount() {
+    this.mounted = true;
+    this.loadData();
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+  }
+
+  fetchProfiles() {
+    return searchQualityProfiles();
+  }
+
+  loadData() {
+    this.setState({ loading: true });
+    Promise.all([getExporters(), this.fetchProfiles()]).then(
+      ([exporters, profilesResponse]) => {
+        if (this.mounted) {
+          this.setState({
+            actions: profilesResponse.actions,
+            exporters,
+            profiles: sortProfiles(profilesResponse.profiles),
+            loading: false
+          });
+        }
+      },
+      () => {
+        if (this.mounted) {
+          this.setState({ loading: false });
+        }
+      }
+    );
+  }
+
+  updateProfiles = () => {
+    return this.fetchProfiles().then(r => {
+      if (this.mounted) {
+        this.setState({ profiles: sortProfiles(r.profiles) });
+      }
+    });
+  };
+
+  renderChild() {
+    if (this.state.loading) {
+      return <i className="spinner" />;
+    }
+    const finalLanguages = Object.values(this.props.languages);
+
+    return React.cloneElement(this.props.children, {
+      actions: this.state.actions || {},
+      profiles: this.state.profiles || [],
+      languages: finalLanguages,
+      exporters: this.state.exporters,
+      updateProfiles: this.updateProfiles
+    });
+  }
+
+  render() {
+    return (
+      <div className="page page-limited">
+        <Suggestions suggestions="quality_profiles" />
+        <Helmet defer={false} title={translate('quality_profiles.page')} />
+
+        {this.renderChild()}
+      </div>
+    );
+  }
+}
+
+export default withLanguagesContext(QualityProfilesApp);
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/App-test.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/App-test.tsx
deleted file mode 100644 (file)
index 83b50ef..0000000
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2022 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 App from '../App';
-
-it('should render correctly', () => {
-  const wrapper = shallowRender();
-  expect(wrapper).toMatchSnapshot();
-});
-
-function shallowRender(props: Partial<App['props']> = {}) {
-  return shallow<App>(
-    <App languages={{}} {...props}>
-      <div />
-    </App>
-  );
-}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/AppContainer-test.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/AppContainer-test.tsx
deleted file mode 100644 (file)
index 3e2bf53..0000000
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2022 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 { connect } from 'react-redux';
-import '../AppContainer';
-
-jest.mock('react-redux', () => ({
-  connect: jest.fn(() => (a: any) => a)
-}));
-
-jest.mock('../../../../store/rootReducer', () => {
-  return {
-    getLanguages: jest.fn(() => [
-      { key: 'css', name: 'CSS' },
-      { key: 'js', name: 'JS' }
-    ])
-  };
-});
-
-describe('redux', () => {
-  it('should correctly map state and dispatch props', () => {
-    const [mapStateToProps] = (connect as jest.Mock).mock.calls[0];
-    const { languages } = mapStateToProps({});
-
-    expect(languages).toEqual([
-      { key: 'css', name: 'CSS' },
-      { key: 'js', name: 'JS' }
-    ]);
-  });
-});
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/QualityProfilesApp-test.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/QualityProfilesApp-test.tsx
new file mode 100644 (file)
index 0000000..f7e68bf
--- /dev/null
@@ -0,0 +1,92 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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 { Actions, getExporters, searchQualityProfiles } from '../../../../api/quality-profiles';
+import {
+  mockLanguage,
+  mockQualityProfile,
+  mockQualityProfileExporter
+} from '../../../../helpers/testMocks';
+import { waitAndUpdate } from '../../../../helpers/testUtils';
+import { QualityProfilesApp } from '../QualityProfilesApp';
+
+jest.mock('../../../../api/quality-profiles', () => ({
+  getExporters: jest.fn().mockResolvedValue([]),
+  searchQualityProfiles: jest.fn().mockResolvedValue({ profiles: [] })
+}));
+
+it('should render correctly', async () => {
+  const wrapper = shallowRender();
+  expect(wrapper).toMatchSnapshot('loading');
+
+  expect(getExporters).toBeCalled();
+  expect(searchQualityProfiles).toBeCalled();
+
+  await waitAndUpdate(wrapper);
+  expect(wrapper).toMatchSnapshot('full');
+});
+
+it('should render child with additional props', () => {
+  const language = mockLanguage();
+  const wrapper = shallowRender({ languages: { [language.key]: language } });
+
+  const actions: Actions = { create: true };
+  const profiles = [mockQualityProfile()];
+  const exporters = [mockQualityProfileExporter()];
+
+  wrapper.setState({ loading: false, actions, profiles, exporters });
+
+  expect(wrapper.childAt(2).props()).toEqual({
+    actions,
+    profiles,
+    languages: [language],
+    exporters,
+    updateProfiles: wrapper.instance().updateProfiles
+  });
+});
+
+it('should handle update', async () => {
+  const profile1 = mockQualityProfile({ key: 'qp1', name: 'An amazing profile' });
+  const profile2 = mockQualityProfile({ key: 'qp2', name: 'Quality Profile' });
+
+  // Mock one call for the initial load, one for the update
+  (searchQualityProfiles as jest.Mock)
+    .mockResolvedValueOnce({ profiles: [] })
+    .mockResolvedValueOnce({ profiles: [profile2, profile1] });
+
+  const wrapper = shallowRender();
+
+  await waitAndUpdate(wrapper);
+  expect(wrapper.state().profiles).toHaveLength(0);
+
+  wrapper.instance().updateProfiles();
+
+  await waitAndUpdate(wrapper);
+  expect(wrapper.state().profiles).toEqual([profile1, profile2]);
+});
+
+function shallowRender(props: Partial<QualityProfilesApp['props']> = {}) {
+  return shallow<QualityProfilesApp>(
+    <QualityProfilesApp languages={{}} {...props}>
+      <div />
+    </QualityProfilesApp>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/App-test.tsx.snap b/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/App-test.tsx.snap
deleted file mode 100644 (file)
index 6fea3f7..0000000
+++ /dev/null
@@ -1,19 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render correctly 1`] = `
-<div
-  className="page page-limited"
->
-  <Suggestions
-    suggestions="quality_profiles"
-  />
-  <Helmet
-    defer={false}
-    encodeSpecialCharacters={true}
-    title="quality_profiles.page"
-  />
-  <i
-    className="spinner"
-  />
-</div>
-`;
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/QualityProfilesApp-test.tsx.snap b/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/QualityProfilesApp-test.tsx.snap
new file mode 100644 (file)
index 0000000..e804294
--- /dev/null
@@ -0,0 +1,41 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly: full 1`] = `
+<div
+  className="page page-limited"
+>
+  <Suggestions
+    suggestions="quality_profiles"
+  />
+  <Helmet
+    defer={false}
+    encodeSpecialCharacters={true}
+    title="quality_profiles.page"
+  />
+  <div
+    actions={Object {}}
+    exporters={Array []}
+    languages={Array []}
+    profiles={Array []}
+    updateProfiles={[Function]}
+  />
+</div>
+`;
+
+exports[`should render correctly: loading 1`] = `
+<div
+  className="page page-limited"
+>
+  <Suggestions
+    suggestions="quality_profiles"
+  />
+  <Helmet
+    defer={false}
+    encodeSpecialCharacters={true}
+    title="quality_profiles.page"
+  />
+  <i
+    className="spinner"
+  />
+</div>
+`;
index 37552cc278b90cff092f5b40f3734fdb56cfd994..02fa9f9aeacff3f417994351751dbec2f97012c7 100644 (file)
@@ -23,7 +23,8 @@ import * as React from 'react';
 import HelpTooltip from '../../../components/controls/HelpTooltip';
 import { Alert } from '../../../components/ui/Alert';
 import { translate, translateWithParameters } from '../../../helpers/l10n';
-import { Dict, Language } from '../../../types/types';
+import { Language } from '../../../types/languages';
+import { Dict } from '../../../types/types';
 import { Profile } from '../types';
 import ProfilesListHeader from './ProfilesListHeader';
 import ProfilesListRow from './ProfilesListRow';
index 335c1ef1a5eaa0bf63074d522d86aeb021747a0f..6e14d1a75af6b2387cf65b658d92154334beaa52 100644 (file)
@@ -21,7 +21,7 @@ import { lazyLoadComponent } from '../../components/lazyLoadComponent';
 
 const routes = [
   {
-    component: lazyLoadComponent(() => import('./components/AppContainer')),
+    component: lazyLoadComponent(() => import('./components/QualityProfilesApp')),
     indexRoute: { component: lazyLoadComponent(() => import('./home/HomeContainer')) },
     childRoutes: [
       {
index 89659887c1f86182d498141c0e3a4973a33631a8..18dc1af38c68a4b82e0b0074072254762f4baeef 100644 (file)
  */
 import { sortBy } from 'lodash';
 import * as React from 'react';
-import { connect } from 'react-redux';
+import withLanguagesContext from '../../app/components/languages/withLanguagesContext';
 import Histogram from '../../components/charts/Histogram';
 import { translate } from '../../helpers/l10n';
 import { formatMeasure } from '../../helpers/measures';
-import { getLanguages, Store } from '../../store/rootReducer';
+import { Languages } from '../../types/languages';
 import { MetricType } from '../../types/metrics';
-import { Languages } from '../../types/types';
 
 interface LanguageDistributionProps {
   distribution: string;
@@ -74,8 +73,4 @@ function cutLanguageName(name: string) {
   return name.length > 10 ? `${name.substr(0, 7)}...` : name;
 }
 
-const mapStateToProps = (state: Store) => ({
-  languages: getLanguages(state)
-});
-
-export default connect(mapStateToProps)(LanguageDistribution);
+export default withLanguagesContext(LanguageDistribution);
index 92de9876b56c2398645d411ae213aa1607af212e..0a68721016e82cb65d124d24b256ee21565ca00c 100644 (file)
  */
 import { shallow } from 'enzyme';
 import * as React from 'react';
-import { mockStore } from '../../../helpers/testMocks';
 import { withCLanguageFeature } from '../withCLanguageFeature';
 
+jest.mock('../../../app/components/languages/LanguagesContext', () => {
+  return {
+    LanguagesContext: {
+      Consumer: ({ children }: { children: (props: {}) => React.ReactNode }) => {
+        return children({ c: { key: 'c', name: 'c' } });
+      }
+    }
+  };
+});
+
 class X extends React.Component<{ hasCLanguageFeature: boolean }> {
   render() {
     return <div />;
@@ -31,9 +40,7 @@ class X extends React.Component<{ hasCLanguageFeature: boolean }> {
 const UnderTest = withCLanguageFeature(X);
 
 it('should pass if C Language feature is available', () => {
-  const wrapper = shallow(<UnderTest />, {
-    context: { store: mockStore({ languages: { c: {} } }) }
-  });
+  const wrapper = shallow(<UnderTest />);
   expect(wrapper.dive().type()).toBe(X);
   expect(wrapper.dive<X>().props().hasCLanguageFeature).toBe(true);
 });
index baa36501b28d59dd30c27280dcd035d2b4fd1160..4b9150b82eff50f973bbc4f312df2b3e8bccc5bf 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
-import { connect } from 'react-redux';
-import { getLanguages, Store } from '../../store/rootReducer';
+import { LanguagesContext } from '../../app/components/languages/LanguagesContext';
 import { getWrappedDisplayName } from './utils';
 
 export function withCLanguageFeature<P>(
   WrappedComponent: React.ComponentType<P & { hasCLanguageFeature: boolean }>
 ) {
-  class Wrapper extends React.Component<P & { hasCLanguageFeature: boolean }> {
+  class Wrapper extends React.Component<Omit<P, 'hasCLanguageFeature'>> {
     static displayName = getWrappedDisplayName(WrappedComponent, 'withCLanguageFeature');
 
     render() {
-      return <WrappedComponent {...this.props} />;
-    }
-  }
+      return (
+        <LanguagesContext.Consumer>
+          {languages => {
+            const hasCLanguageFeature = languages['c'] !== undefined;
 
-  function mapStateToProps(state: Store) {
-    const languages = getLanguages(state);
-    const hasCLanguageFeature = languages['c'] !== undefined;
-
-    return { hasCLanguageFeature };
+            return (
+              <WrappedComponent {...(this.props as P)} hasCLanguageFeature={hasCLanguageFeature} />
+            );
+          }}
+        </LanguagesContext.Consumer>
+      );
+    }
   }
 
-  return connect(mapStateToProps)(Wrapper);
+  return Wrapper;
 }
index 1888ea898ddce7fbc511c91d4d04d08a1298df57..2a05bd465d57d92805ec05755a82e3306e5f7281 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
-import { connect } from 'react-redux';
+import withLanguagesContext from '../../../app/components/languages/withLanguagesContext';
 import { translate } from '../../../helpers/l10n';
-import { getLanguages, Store } from '../../../store/rootReducer';
-import { Component, Languages } from '../../../types/types';
+import { Languages } from '../../../types/languages';
+import { Component } from '../../../types/types';
 import RenderOptions from '../components/RenderOptions';
 import { BuildTools } from '../types';
 import AnalysisCommand from './commands/AnalysisCommand';
@@ -29,6 +29,7 @@ import AnalysisCommand from './commands/AnalysisCommand';
 export interface BranchesAnalysisStepProps {
   languages: Languages;
   component: Component;
+
   onStepValidationChange: (isValid: boolean) => void;
 }
 
@@ -66,8 +67,4 @@ export function BranchAnalysisStepContent(props: BranchesAnalysisStepProps) {
   );
 }
 
-const mapStateToProps = (state: Store) => ({
-  languages: getLanguages(state)
-});
-
-export default connect(mapStateToProps)(BranchAnalysisStepContent);
+export default withLanguagesContext(BranchAnalysisStepContent);
index 3a94c11f96174b9f9083aab575bca958a4bf0bb6..8150d91badf896f6b87ece76b03cc6286a9777df 100644 (file)
@@ -181,7 +181,7 @@ exports[`should render correctly 4`] = `
       className="boxed-group-inner"
     >
       <div>
-        <Connect(BranchAnalysisStepContent)
+        <withLanguagesContext(BranchAnalysisStepContent)
           component={
             Object {
               "breadcrumbs": Array [],
index d4841cd4a6c578343fb552aacc414e2aaef1ba86..0384a6cc607a0998ae7c9f9024823c0a58b364c6 100644 (file)
@@ -107,7 +107,7 @@ exports[`should render correctly: repo variable step content 1`] = `
 `;
 
 exports[`should render correctly: yaml file step content 1`] = `
-<Connect(withCLanguageFeature(YamlFileStep))>
+<withCLanguageFeature(YamlFileStep)>
   [Function]
-</Connect(withCLanguageFeature(YamlFileStep))>
+</withCLanguageFeature(YamlFileStep)>
 `;
index 1294b75715028cddc22d9b92cd05953b53a03db6..7c34cbefba159a99ef594223f302feca7411bfc8 100644 (file)
@@ -107,7 +107,7 @@ exports[`should render correctly: secrets step content 1`] = `
 `;
 
 exports[`should render correctly: yaml file step content 1`] = `
-<Connect(withCLanguageFeature(YamlFileStep))>
+<withCLanguageFeature(YamlFileStep)>
   [Function]
-</Connect(withCLanguageFeature(YamlFileStep))>
+</withCLanguageFeature(YamlFileStep)>
 `;
index 67069e385ff12ac14e522c3aca6bb167d45d6a57..6082843f8d0146e9e76ce99da01efa0c4bd680d3 100644 (file)
@@ -11,7 +11,7 @@ exports[`should render correctly 1`] = `
       onboarding.tutorial.with.gitlab_ci.title
     </h1>
   </div>
-  <Connect(withCLanguageFeature(ProjectKeyStep))
+  <withCLanguageFeature(ProjectKeyStep)
     component={
       Object {
         "breadcrumbs": Array [],
index 7858029d190f29b9d6227cf59c829e5b332e1d61..eed4fce8e5d7f615e3ef0d6496572273627c6688 100644 (file)
@@ -45,7 +45,7 @@ exports[`should render correctly: branches not enabled 1`] = `
       }
     }
   />
-  <Connect(withCLanguageFeature(JenkinsfileStep))
+  <withCLanguageFeature(JenkinsfileStep)
     baseUrl=""
     component={
       Object {
@@ -137,7 +137,7 @@ exports[`should render correctly: default 1`] = `
       }
     }
   />
-  <Connect(withCLanguageFeature(JenkinsfileStep))
+  <withCLanguageFeature(JenkinsfileStep)
     baseUrl=""
     component={
       Object {
index 153a47f321ac9a075f0253e860319538ba743bb2..ddbc2c8e952c3bf5a8d2df942b6c7c1a30e26e67 100644 (file)
@@ -22,6 +22,7 @@ import { InjectedRouter } from 'react-router';
 import { createStore, Store } from 'redux';
 import { DocumentationEntry } from '../apps/documentation/utils';
 import { Exporter, Profile } from '../apps/quality-profiles/types';
+import { Language } from '../types/languages';
 import { DumpStatus, DumpTask } from '../types/project-dump';
 import { TaskStatuses } from '../types/tasks';
 import {
@@ -36,7 +37,6 @@ import {
   HealthType,
   IdentityProvider,
   Issue,
-  Language,
   LoggedInUser,
   Measure,
   MeasureEnhanced,
diff --git a/server/sonar-web/src/main/js/store/languages.ts b/server/sonar-web/src/main/js/store/languages.ts
deleted file mode 100644 (file)
index f4820b3..0000000
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2022 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-import { keyBy } from 'lodash';
-import { Languages } from '../types/types';
-import { ActionType } from './utils/actions';
-
-export function receiveLanguages(languages: Array<{ key: string; name: string }>) {
-  return { type: 'RECEIVE_LANGUAGES', languages };
-}
-
-type Action = ActionType<typeof receiveLanguages, 'RECEIVE_LANGUAGES'>;
-
-export default function(state: Languages = {}, action: Action): Languages {
-  if (action.type === 'RECEIVE_LANGUAGES') {
-    return keyBy(action.languages, 'key');
-  }
-
-  return state;
-}
-
-export function getLanguages(state: Languages) {
-  return state;
-}
-
-export function getLanguageByKey(state: Languages, key: string) {
-  return state[key];
-}
index 4713ea1efc763cb9e112795ef1e4c0a6c1c6918f..af0e6465e7df655f0f889281949da5b1bb1e8d7c 100644 (file)
@@ -20,7 +20,6 @@
 import { InjectedRouter } from 'react-router';
 import { Dispatch } from 'redux';
 import * as auth from '../api/auth';
-import { getLanguages } from '../api/languages';
 import { getAllMetrics } from '../api/metrics';
 import { getQualityGateProjectStatus } from '../api/quality-gates';
 import { getBranchLikeQuery } from '../helpers/branch-like';
@@ -30,20 +29,8 @@ import { Status } from '../types/types';
 import { requireAuthorization as requireAuthorizationAction } from './appState';
 import { registerBranchStatusAction } from './branches';
 import { addGlobalErrorMessage } from './globalMessages';
-import { receiveLanguages } from './languages';
 import { receiveMetrics } from './metrics';
 
-export function fetchLanguages() {
-  return (dispatch: Dispatch) => {
-    getLanguages().then(
-      languages => dispatch(receiveLanguages(languages)),
-      () => {
-        /* do nothing */
-      }
-    );
-  };
-}
-
 export function fetchMetrics() {
   return (dispatch: Dispatch) => {
     getAllMetrics().then(
index a120940f9998c7c95672c0ee953158f9fc07fee0..373266b0fe2d3362573da89da61a79c68329deef 100644 (file)
 import { combineReducers } from 'redux';
 import settingsApp, * as fromSettingsApp from '../apps/settings/store/rootReducer';
 import { BranchLike } from '../types/branch-like';
-import { AppState, CurrentUserSettingNames, Languages } from '../types/types';
+import { AppState, CurrentUserSettingNames } from '../types/types';
 import appState from './appState';
 import branches, * as fromBranches from './branches';
 import globalMessages, * as fromGlobalMessages from './globalMessages';
-import languages, * as fromLanguages from './languages';
 import metrics, * as fromMetrics from './metrics';
 import users, * as fromUsers from './users';
 
@@ -32,7 +31,7 @@ export type Store = {
   appState: AppState;
   branches: fromBranches.State;
   globalMessages: fromGlobalMessages.State;
-  languages: Languages;
+
   metrics: fromMetrics.State;
   users: fromUsers.State;
 
@@ -44,7 +43,6 @@ export default combineReducers<Store>({
   appState,
   branches,
   globalMessages,
-  languages,
   metrics,
   users,
 
@@ -60,10 +58,6 @@ export function getGlobalMessages(state: Store) {
   return fromGlobalMessages.getGlobalMessages(state.globalMessages);
 }
 
-export function getLanguages(state: Store) {
-  return fromLanguages.getLanguages(state.languages);
-}
-
 export function getCurrentUserSetting(state: Store, key: CurrentUserSettingNames) {
   return fromUsers.getCurrentUserSetting(state.users, key);
 }
diff --git a/server/sonar-web/src/main/js/types/languages.ts b/server/sonar-web/src/main/js/types/languages.ts
new file mode 100644 (file)
index 0000000..43cd319
--- /dev/null
@@ -0,0 +1,28 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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 { Dict } from './types';
+
+export interface Language {
+  key: string;
+  name: string;
+}
+
+export type Languages = Dict<Language>;