]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-10580 Local storage references should not use window.localStorage (#145)
authorPascal Mugnier <pascal.mugnier@sonarsource.com>
Mon, 16 Apr 2018 06:29:08 +0000 (08:29 +0200)
committerSonarTech <sonartech@sonarsource.com>
Mon, 16 Apr 2018 18:20:47 +0000 (20:20 +0200)
20 files changed:
server/sonar-web/src/main/js/app/components/RecentHistory.js
server/sonar-web/src/main/js/apps/issues/utils.ts
server/sonar-web/src/main/js/apps/overview/components/OverviewApp.tsx
server/sonar-web/src/main/js/apps/portfolio/components/Activity.tsx
server/sonar-web/src/main/js/apps/portfolio/components/__tests__/Activity-test.tsx
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppContainer.js
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphs.js
server/sonar-web/src/main/js/apps/projectActivity/utils.js
server/sonar-web/src/main/js/apps/projects/components/AllProjects.tsx
server/sonar-web/src/main/js/apps/projects/components/DefaultPageSelector.tsx
server/sonar-web/src/main/js/apps/projects/components/FavoriteFilter.tsx
server/sonar-web/src/main/js/apps/projects/components/__tests__/AllProjects-test.tsx
server/sonar-web/src/main/js/apps/projects/components/__tests__/DefaultPageSelector-test.tsx
server/sonar-web/src/main/js/apps/projects/components/__tests__/FavoriteFilter-test.tsx
server/sonar-web/src/main/js/apps/projects/routes.ts
server/sonar-web/src/main/js/apps/projects/utils.ts
server/sonar-web/src/main/js/components/preview-graph/PreviewGraph.js
server/sonar-web/src/main/js/components/workspace/Workspace.tsx
server/sonar-web/src/main/js/helpers/l10n.ts
server/sonar-web/src/main/js/helpers/storage.ts

index 6015e4d757675a90c24b2ca0ceb77a1a00d5bc1a..0967b38c07a27c80a5e0fdd4b8b547b72497183f 100644 (file)
@@ -18,7 +18,9 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 // @flow
-const STORAGE_KEY = 'sonar_recent_history';
+import { get, remove, save } from '../../helpers/storage';
+
+const RECENT_HISTORY = 'sonar_recent_history';
 const HISTORY_LIMIT = 10;
 
 /*::
@@ -32,33 +34,25 @@ type History = Array<{
 
 export default class RecentHistory {
   static get() /*: History */ {
-    if (!window.localStorage) {
-      return [];
-    }
-    let history = window.localStorage.getItem(STORAGE_KEY);
+    const history = get(RECENT_HISTORY);
     if (history == null) {
-      history = [];
+      return [];
     } else {
       try {
-        history = JSON.parse(history);
+        return JSON.parse(history);
       } catch (e) {
-        RecentHistory.clear();
-        history = [];
+        remove(RECENT_HISTORY);
+        return [];
       }
     }
-    return history;
   }
 
   static set(newHistory /*: History */) /*: void */ {
-    if (window.localStorage) {
-      window.localStorage.setItem(STORAGE_KEY, JSON.stringify(newHistory));
-    }
+    save(RECENT_HISTORY, JSON.stringify(newHistory));
   }
 
   static clear() /*: void */ {
-    if (window.localStorage) {
-      window.localStorage.removeItem(STORAGE_KEY);
-    }
+    remove(RECENT_HISTORY);
   }
 
   static add(
index 0423bc1ee648e8c361d9525f2dd7b1067e4bf363..82df6728898422c395c2bab12887dab9617a0445 100644 (file)
@@ -21,6 +21,7 @@ import { searchMembers } from '../../api/organizations';
 import { searchUsers } from '../../api/users';
 import { Issue } from '../../app/types';
 import { formatMeasure } from '../../helpers/measures';
+import { get, save } from '../../helpers/storage';
 import {
   queriesEqual,
   cleanQuery,
@@ -63,6 +64,7 @@ export interface Query {
 
 // allow sorting by CREATION_DATE only
 const parseAsSort = (sort: string) => (sort === 'CREATION_DATE' ? 'CREATION_DATE' : '');
+const ISSUES_DEFAULT = 'sonarqube.issues.default';
 
 export function parseQuery(query: RawQuery): Query {
   return {
@@ -208,26 +210,15 @@ export const searchAssignees = (query: string, organization?: string) => {
       );
 };
 
-const LOCALSTORAGE_KEY = 'sonarqube.issues.default';
 const LOCALSTORAGE_MY = 'my';
 const LOCALSTORAGE_ALL = 'all';
 
 export const isMySet = () => {
-  const setting = window.localStorage.getItem(LOCALSTORAGE_KEY);
-  return setting === LOCALSTORAGE_MY;
-};
-
-const save = (value: string) => {
-  try {
-    window.localStorage.setItem(LOCALSTORAGE_KEY, value);
-  } catch (e) {
-    // usually that means the storage is full
-    // just do nothing in this case
-  }
+  return get(ISSUES_DEFAULT) === LOCALSTORAGE_MY;
 };
 
 export const saveMyIssues = (myIssues: boolean) =>
-  save(myIssues ? LOCALSTORAGE_MY : LOCALSTORAGE_ALL);
+  save(ISSUES_DEFAULT, myIssues ? LOCALSTORAGE_MY : LOCALSTORAGE_ALL);
 
 export function getLocations(
   { flows, secondaryLocations }: Pick<Issue, 'flows' | 'secondaryLocations'>,
index 091826a44a6c2079bc7758f7aef147608a4eb735..4a0fce0690500ef0fb579778bfd0487a95f07128 100644 (file)
@@ -33,9 +33,14 @@ import { getAllTimeMachineData, History } from '../../../api/time-machine';
 import { parseDate } from '../../../helpers/dates';
 import { enhanceMeasuresWithMetrics, MeasureEnhanced } from '../../../helpers/measures';
 import { getLeakPeriod, Period } from '../../../helpers/periods';
-import { getCustomGraph, getGraph } from '../../../helpers/storage';
+import { get } from '../../../helpers/storage';
 import { METRICS, HISTORY_METRICS_LIST } from '../utils';
-import { DEFAULT_GRAPH, getDisplayedHistoryMetrics } from '../../projectActivity/utils';
+import {
+  DEFAULT_GRAPH,
+  getDisplayedHistoryMetrics,
+  PROJECT_ACTIVITY_GRAPH,
+  PROJECT_ACTIVITY_GRAPH_CUSTOM
+} from '../../projectActivity/utils';
 import { isSameBranchLike, getBranchLikeQuery } from '../../../helpers/branches';
 import { fetchMetrics } from '../../../store/rootActions';
 import { getMetrics } from '../../../store/rootReducer';
@@ -118,7 +123,11 @@ export class OverviewApp extends React.PureComponent<Props, State> {
   loadHistory = () => {
     const { branchLike, component } = this.props;
 
-    let graphMetrics = getDisplayedHistoryMetrics(getGraph(), getCustomGraph());
+    const customGraphs = get(PROJECT_ACTIVITY_GRAPH_CUSTOM);
+    let graphMetrics = getDisplayedHistoryMetrics(
+      get(PROJECT_ACTIVITY_GRAPH) || 'issues',
+      customGraphs ? customGraphs.split(',') : []
+    );
     if (!graphMetrics || graphMetrics.length <= 0) {
       graphMetrics = getDisplayedHistoryMetrics(DEFAULT_GRAPH, []);
     }
index 01d14bb18078bd29974e6d3ec7731c8204f18784..d022f92f0fbe23f8e31cd69ac5f515eca14e8916 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
-import { getDisplayedHistoryMetrics, DEFAULT_GRAPH } from '../../projectActivity/utils';
+import {
+  getDisplayedHistoryMetrics,
+  DEFAULT_GRAPH,
+  PROJECT_ACTIVITY_GRAPH,
+  PROJECT_ACTIVITY_GRAPH_CUSTOM
+} from '../../projectActivity/utils';
 import PreviewGraph from '../../../components/preview-graph/PreviewGraph';
 import { getAllTimeMachineData, History } from '../../../api/time-machine';
 import { Metric } from '../../../app/types';
 import { parseDate } from '../../../helpers/dates';
 import { translate } from '../../../helpers/l10n';
-import { getCustomGraph, getGraph } from '../../../helpers/storage';
+import { get } from '../../../helpers/storage';
 
 const AnyPreviewGraph = PreviewGraph as any;
 
@@ -60,7 +65,11 @@ export default class Activity extends React.PureComponent<Props> {
   fetchHistory = () => {
     const { component } = this.props;
 
-    let graphMetrics: string[] = getDisplayedHistoryMetrics(getGraph(), getCustomGraph());
+    const customGraphs = get(PROJECT_ACTIVITY_GRAPH_CUSTOM);
+    let graphMetrics = getDisplayedHistoryMetrics(
+      get(PROJECT_ACTIVITY_GRAPH) || 'issues',
+      customGraphs ? customGraphs.split(',') : []
+    );
     if (!graphMetrics || graphMetrics.length <= 0) {
       graphMetrics = getDisplayedHistoryMetrics(DEFAULT_GRAPH, []);
     }
index 0b5fb1f158482e6b5658132090d21757d0671548..fadb3f26884831204017cfdf516b536857a4ac3d 100644 (file)
@@ -19,8 +19,7 @@
  */
 /* eslint-disable import/first, import/order */
 jest.mock('../../../../helpers/storage', () => ({
-  getCustomGraph: () => ['coverage'],
-  getGraph: () => 'custom'
+  get: (key: string) => (key === 'sonarqube.project_activity.graph.custom' ? 'coverage' : 'custom')
 }));
 
 jest.mock('../../../../api/time-machine', () => ({
index 54aa9929ecc45f5c1f968a92f7738f69d36a42be..49c5efe09f332f6beccdcc10a808195d8482548d 100644 (file)
@@ -28,13 +28,15 @@ import * as api from '../../../api/projectActivity';
 import * as actions from '../actions';
 import { getBranchLikeQuery } from '../../../helpers/branches';
 import { parseDate } from '../../../helpers/dates';
-import { getCustomGraph, getGraph } from '../../../helpers/storage';
+import { get } from '../../../helpers/storage';
 import {
   customMetricsChanged,
   DEFAULT_GRAPH,
   getHistoryMetrics,
   isCustomGraph,
   parseQuery,
+  PROJECT_ACTIVITY_GRAPH,
+  PROJECT_ACTIVITY_GRAPH_CUSTOM,
   serializeQuery,
   serializeUrlQuery
 } from '../utils';
@@ -95,9 +97,10 @@ export default class ProjectActivityAppContainer extends React.PureComponent {
   componentDidMount() {
     this.mounted = true;
     if (this.shouldRedirect()) {
-      const newQuery = { ...this.state.query, graph: getGraph() };
+      const newQuery = { ...this.state.query, graph: get(PROJECT_ACTIVITY_GRAPH) || 'issues' };
       if (isCustomGraph(newQuery.graph)) {
-        newQuery.customMetrics = getCustomGraph();
+        const customGraphs = get(PROJECT_ACTIVITY_GRAPH_CUSTOM);
+        newQuery.customMetrics = customGraphs ? customGraphs.split(',') : [];
       }
       this.context.router.replace({
         pathname: this.props.location.pathname,
@@ -312,8 +315,10 @@ export default class ProjectActivityAppContainer extends React.PureComponent {
       key => key !== 'id' && locationQuery[key] !== ''
     );
 
-    const graph = getGraph();
-    const emptyCustomGraph = isCustomGraph(graph) && getCustomGraph().length <= 0;
+    const customGraphs = get(PROJECT_ACTIVITY_GRAPH_CUSTOM);
+    const graph = get(PROJECT_ACTIVITY_GRAPH) || 'issues';
+    const emptyCustomGraph =
+      isCustomGraph(graph) && customGraphs && customGraphs.split(',').length <= 0;
 
     // if there is no filter, but there are saved preferences in the localStorage
     // also don't redirect to custom if there is no metrics selected for it
@@ -336,8 +341,8 @@ export default class ProjectActivityAppContainer extends React.PureComponent {
         deleteEvent={this.deleteEvent}
         graphLoading={!this.state.initialized || this.state.graphLoading}
         initializing={!this.state.initialized}
-        metrics={this.state.metrics}
         measuresHistory={this.state.measuresHistory}
+        metrics={this.state.metrics}
         project={this.props.component}
         query={this.state.query}
         updateQuery={this.updateQuery}
index 430575909198f51700b12f1fdba1c4ca33ba35e9..e390d843f891b8c3e98c06f648aca3fcf934d4eb 100644 (file)
@@ -23,7 +23,7 @@ import { debounce, findLast, maxBy, minBy, sortBy } from 'lodash';
 import ProjectActivityGraphsHeader from './ProjectActivityGraphsHeader';
 import GraphsZoom from './GraphsZoom';
 import GraphsHistory from './GraphsHistory';
-import { getCustomGraph, saveCustomGraph, saveGraph } from '../../../helpers/storage';
+import { get, save } from '../../../helpers/storage';
 import {
   datesQueryChanged,
   generateSeries,
@@ -31,6 +31,8 @@ import {
   getSeriesMetricType,
   historyQueryChanged,
   isCustomGraph,
+  PROJECT_ACTIVITY_GRAPH,
+  PROJECT_ACTIVITY_GRAPH_CUSTOM,
   splitSeriesInGraphs
 } from '../utils';
 /*:: import type { RawQuery } from '../../../helpers/query'; */
@@ -148,20 +150,21 @@ export default class ProjectActivityGraphs extends React.PureComponent {
 
   addCustomMetric = (metric /*: string */) => {
     const customMetrics = [...this.props.query.customMetrics, metric];
-    saveCustomGraph(customMetrics);
+    save(PROJECT_ACTIVITY_GRAPH_CUSTOM, customMetrics.join(','));
     this.props.updateQuery({ customMetrics });
   };
 
   removeCustomMetric = (removedMetric /*: string */) => {
     const customMetrics = this.props.query.customMetrics.filter(metric => metric !== removedMetric);
-    saveCustomGraph(customMetrics);
+    save(PROJECT_ACTIVITY_GRAPH_CUSTOM, customMetrics.join(','));
     this.props.updateQuery({ customMetrics });
   };
 
   updateGraph = (graph /*: string */) => {
-    saveGraph(graph);
+    save(PROJECT_ACTIVITY_GRAPH, graph);
     if (isCustomGraph(graph) && this.props.query.customMetrics.length <= 0) {
-      this.props.updateQuery({ graph, customMetrics: getCustomGraph() });
+      const customGraphs = get(PROJECT_ACTIVITY_GRAPH_CUSTOM);
+      this.props.updateQuery({ graph, customMetrics: customGraphs ? customGraphs.split(',') : [] });
     } else {
       this.props.updateQuery({ graph, customMetrics: [] });
     }
@@ -210,9 +213,9 @@ export default class ProjectActivityGraphs extends React.PureComponent {
           analyses={this.props.analyses}
           eventFilter={query.category}
           graph={query.graph}
-          graphs={this.state.graphs}
           graphEndDate={graphEndDate}
           graphStartDate={graphStartDate}
+          graphs={this.state.graphs}
           leakPeriodDate={leakPeriodDate}
           loading={loading}
           measuresHistory={this.props.measuresHistory}
index ad2e53d848aee691ffbec4af1947e1b14182f9cd..bb09445e4704956df927b6fabd87ec08b5f82c01 100644 (file)
@@ -53,6 +53,9 @@ export const GRAPHS_METRICS = {
   duplications: GRAPHS_METRICS_DISPLAYED['duplications'].concat(['duplicated_lines_density'])
 };
 
+export const PROJECT_ACTIVITY_GRAPH = 'sonarqube.project_activity.graph';
+export const PROJECT_ACTIVITY_GRAPH_CUSTOM = 'sonarqube.project_activity.graph.custom';
+
 export const datesQueryChanged = (prevQuery /*: Query */, nextQuery /*: Query */) =>
   !isEqual(prevQuery.from, nextQuery.from) || !isEqual(prevQuery.to, nextQuery.to);
 
index dc9836331d8932d24ded51ad250ce17376619414..cd9547a11b494b0e5f66f844fb4e37a24e285e3f 100644 (file)
@@ -30,7 +30,7 @@ import handleRequiredAuthentication from '../../../app/utils/handleRequiredAuthe
 import ScreenPositionHelper from '../../../components/common/ScreenPositionHelper';
 import ListFooter from '../../../components/controls/ListFooter';
 import { translate } from '../../../helpers/l10n';
-import * as storage from '../../../helpers/storage';
+import { get, save } from '../../../helpers/storage';
 import { RawQuery } from '../../../helpers/query';
 import '../styles.css';
 import { Project, Facets } from '../types';
@@ -56,6 +56,10 @@ interface State {
   total?: number;
 }
 
+const PROJECTS_SORT = 'sonarqube.projects.sort';
+const PROJECTS_VIEW = 'sonarqube.projects.view';
+const PROJECTS_VISUALIZATION = 'sonarqube.projects.visualization';
+
 export default class AllProjects extends React.PureComponent<Props, State> {
   mounted = false;
 
@@ -159,14 +163,14 @@ export default class AllProjects extends React.PureComponent<Props, State> {
       view?: string;
       visualization?: string;
     } = {};
-    if (storage.getSort(storageOptionsSuffix)) {
-      options.sort = storage.getSort(storageOptionsSuffix) || undefined;
+    if (get(PROJECTS_SORT, storageOptionsSuffix)) {
+      options.sort = get(PROJECTS_SORT, storageOptionsSuffix) || undefined;
     }
-    if (storage.getView(storageOptionsSuffix)) {
-      options.view = storage.getView(storageOptionsSuffix) || undefined;
+    if (get(PROJECTS_VIEW, storageOptionsSuffix)) {
+      options.view = get(PROJECTS_VIEW, storageOptionsSuffix) || undefined;
     }
-    if (storage.getVisualization(storageOptionsSuffix)) {
-      options.visualization = storage.getVisualization(storageOptionsSuffix) || undefined;
+    if (get(PROJECTS_VISUALIZATION, storageOptionsSuffix)) {
+      options.visualization = get(PROJECTS_VISUALIZATION, storageOptionsSuffix) || undefined;
     }
     return options;
   };
@@ -194,15 +198,15 @@ export default class AllProjects extends React.PureComponent<Props, State> {
       this.updateLocationQuery(query);
     }
 
-    storage.saveSort(query.sort, storageOptionsSuffix);
-    storage.saveView(query.view, storageOptionsSuffix);
-    storage.saveVisualization(visualization, storageOptionsSuffix);
+    save(PROJECTS_SORT, query.sort, storageOptionsSuffix);
+    save(PROJECTS_VIEW, query.view, storageOptionsSuffix);
+    save(PROJECTS_VISUALIZATION, visualization, storageOptionsSuffix);
   };
 
   handleSortChange = (sort: string, desc: boolean) => {
     const asString = (desc ? '-' : '') + sort;
     this.updateLocationQuery({ sort: asString });
-    storage.saveSort(asString, this.props.storageOptionsSuffix);
+    save(PROJECTS_SORT, asString, this.props.storageOptionsSuffix);
   };
 
   handleQueryChange(initialMount: boolean) {
index ea4fbe1a357cb73943d808a5ada82eb72d699ee1..07119e134ede03df0fa0916b7699a2304b2f6492 100644 (file)
@@ -20,7 +20,8 @@
 import * as React from 'react';
 import * as PropTypes from 'prop-types';
 import AllProjectsContainer from './AllProjectsContainer';
-import { isFavoriteSet, isAllSet } from '../../../helpers/storage';
+import { PROJECTS_DEFAULT_FILTER, PROJECTS_FAVORITE, PROJECTS_ALL } from '../utils';
+import { get } from '../../../helpers/storage';
 import { searchProjects } from '../../../api/components';
 import { CurrentUser, isLoggedIn } from '../../../app/types';
 
@@ -73,6 +74,16 @@ export default class DefaultPageSelector extends React.PureComponent<Props, Stat
     }
   }
 
+  isFavoriteSet = (): boolean => {
+    const setting = get(PROJECTS_DEFAULT_FILTER);
+    return setting === PROJECTS_FAVORITE;
+  };
+
+  isAllSet = (): boolean => {
+    const setting = get(PROJECTS_DEFAULT_FILTER);
+    return setting === PROJECTS_ALL;
+  };
+
   defineIfShouldBeRedirected() {
     if (Object.keys(this.props.location.query).length > 0) {
       // show ALL projects when there are some filters
@@ -85,10 +96,10 @@ export default class DefaultPageSelector extends React.PureComponent<Props, Stat
       } else {
         this.setState({ shouldBeRedirected: false, shouldForceSorting: undefined });
       }
-    } else if (isFavoriteSet()) {
+    } else if (this.isFavoriteSet()) {
       // show FAVORITE projects if "favorite" setting is explicitly set
       this.setState({ shouldBeRedirected: true, shouldForceSorting: undefined });
-    } else if (isAllSet()) {
+    } else if (this.isAllSet()) {
       // show ALL projects if "all" setting is explicitly set
       this.setState({ shouldBeRedirected: false, shouldForceSorting: undefined });
     } else {
index 11583197e64aa437056fa13377e4e531a53c9862..3b9a46cbb7b531d003c258b88d8c8382eb9681df 100644 (file)
@@ -21,8 +21,9 @@ import * as React from 'react';
 import { IndexLink, Link } from 'react-router';
 import { translate } from '../../../helpers/l10n';
 import { CurrentUser, isLoggedIn } from '../../../app/types';
-import { saveAll, saveFavorite } from '../../../helpers/storage';
+import { save } from '../../../helpers/storage';
 import { RawQuery } from '../../../helpers/query';
+import { PROJECTS_DEFAULT_FILTER, PROJECTS_FAVORITE, PROJECTS_ALL } from '../utils';
 
 interface Props {
   currentUser: CurrentUser;
@@ -33,13 +34,13 @@ interface Props {
 export default class FavoriteFilter extends React.PureComponent<Props> {
   handleSaveFavorite = () => {
     if (!this.props.organization) {
-      saveFavorite();
+      save(PROJECTS_DEFAULT_FILTER, PROJECTS_FAVORITE);
     }
   };
 
   handleSaveAll = () => {
     if (!this.props.organization) {
-      saveAll();
+      save(PROJECTS_DEFAULT_FILTER, PROJECTS_ALL);
     }
   };
 
@@ -60,19 +61,19 @@ export default class FavoriteFilter extends React.PureComponent<Props> {
       <header className="page-header text-center">
         <div className="button-group">
           <Link
-            id="favorite-projects"
-            to={{ pathname: pathnameForFavorite, query: this.props.query }}
-            className="button"
             activeClassName="button-active"
-            onClick={this.handleSaveFavorite}>
+            className="button"
+            id="favorite-projects"
+            onClick={this.handleSaveFavorite}
+            to={{ pathname: pathnameForFavorite, query: this.props.query }}>
             {translate('my_favorites')}
           </Link>
           <IndexLink
-            id="all-projects"
-            to={{ pathname: pathnameForAll, query: this.props.query }}
-            className="button"
             activeClassName="button-active"
-            onClick={this.handleSaveAll}>
+            className="button"
+            id="all-projects"
+            onClick={this.handleSaveAll}
+            to={{ pathname: pathnameForAll, query: this.props.query }}>
             {translate('all')}
           </IndexLink>
         </div>
index cc642e972f48286aef57a8f89ee6aa588f558801..e3fac302fb46ef54ba55158df9c9dda7ee181d03 100644 (file)
@@ -21,7 +21,7 @@
 import * as React from 'react';
 import { mount, shallow } from 'enzyme';
 import AllProjects, { Props } from '../AllProjects';
-import { getView, saveSort, saveView, saveVisualization } from '../../../../helpers/storage';
+import { get, save } from '../../../../helpers/storage';
 
 jest.mock('../ProjectsList', () => ({
   // eslint-disable-next-line
@@ -51,21 +51,15 @@ jest.mock('../../utils', () => {
 });
 
 jest.mock('../../../../helpers/storage', () => ({
-  getSort: () => null,
-  getView: jest.fn(() => null),
-  getVisualization: () => null,
-  saveSort: jest.fn(),
-  saveView: jest.fn(),
-  saveVisualization: jest.fn()
+  get: jest.fn(() => null),
+  save: jest.fn()
 }));
 
 const fetchProjects = require('../../utils').fetchProjects as jest.Mock<any>;
 
 beforeEach(() => {
-  (getView as jest.Mock<any>).mockImplementation(() => null);
-  (saveSort as jest.Mock<any>).mockClear();
-  (saveView as jest.Mock<any>).mockClear();
-  (saveVisualization as jest.Mock<any>).mockClear();
+  (get as jest.Mock<any>).mockImplementation(() => null);
+  (save as jest.Mock<any>).mockClear();
   fetchProjects.mockClear();
 });
 
@@ -106,7 +100,9 @@ it('fetches projects', () => {
 });
 
 it('redirects to the saved search', () => {
-  (getView as jest.Mock<any>).mockImplementation(() => 'leak');
+  (get as jest.Mock<any>).mockImplementation(
+    (key: string) => (key === 'sonarqube.projects.view' ? 'leak' : null)
+  );
   const replace = jest.fn();
   mountRender({}, jest.fn(), replace);
   expect(replace).lastCalledWith({ pathname: '/projects', query: { view: 'leak' } });
@@ -117,7 +113,7 @@ it('changes sort', () => {
   const wrapper = shallowRender({}, push);
   wrapper.find('PageHeader').prop<Function>('onSortChange')('size', false);
   expect(push).lastCalledWith({ pathname: '/projects', query: { sort: 'size' } });
-  expect(saveSort).lastCalledWith('size', undefined);
+  expect(save).lastCalledWith('sonarqube.projects.sort', 'size', undefined);
 });
 
 it('changes perspective to leak', () => {
@@ -128,9 +124,9 @@ it('changes perspective to leak', () => {
     pathname: '/projects',
     query: { view: 'leak', visualization: undefined }
   });
-  expect(saveSort).lastCalledWith(undefined, undefined);
-  expect(saveView).lastCalledWith('leak', undefined);
-  expect(saveVisualization).lastCalledWith(undefined, undefined);
+  expect(save).toHaveBeenCalledWith('sonarqube.projects.sort', undefined, undefined);
+  expect(save).toHaveBeenCalledWith('sonarqube.projects.view', 'leak', undefined);
+  expect(save).toHaveBeenCalledWith('sonarqube.projects.visualization', undefined, undefined);
 });
 
 it('updates sorting when changing perspective from leak', () => {
@@ -144,9 +140,9 @@ it('updates sorting when changing perspective from leak', () => {
     pathname: '/projects',
     query: { sort: 'coverage', view: undefined, visualization: undefined }
   });
-  expect(saveSort).lastCalledWith('coverage', undefined);
-  expect(saveView).lastCalledWith(undefined, undefined);
-  expect(saveVisualization).lastCalledWith(undefined, undefined);
+  expect(save).toHaveBeenCalledWith('sonarqube.projects.sort', 'coverage', undefined);
+  expect(save).toHaveBeenCalledWith('sonarqube.projects.view', undefined, undefined);
+  expect(save).toHaveBeenCalledWith('sonarqube.projects.visualization', undefined, undefined);
 });
 
 it('changes perspective to risk visualization', () => {
@@ -160,9 +156,9 @@ it('changes perspective to risk visualization', () => {
     pathname: '/projects',
     query: { view: 'visualizations', visualization: 'risk' }
   });
-  expect(saveSort).lastCalledWith(undefined, undefined);
-  expect(saveView).lastCalledWith('visualizations', undefined);
-  expect(saveVisualization).lastCalledWith('risk', undefined);
+  expect(save).toHaveBeenCalledWith('sonarqube.projects.sort', undefined, undefined);
+  expect(save).toHaveBeenCalledWith('sonarqube.projects.view', 'visualizations', undefined);
+  expect(save).toHaveBeenCalledWith('sonarqube.projects.visualization', 'risk', undefined);
 });
 
 function mountRender(props: any = {}, push: Function = jest.fn(), replace: Function = jest.fn()) {
index 18bdfd94ba01a5334e30571811532f791ac0c9fc..f840055a266597c05912f0af5c8ce70daa189096 100644 (file)
@@ -26,8 +26,7 @@ jest.mock('../AllProjectsContainer', () => ({
 }));
 
 jest.mock('../../../../helpers/storage', () => ({
-  isFavoriteSet: jest.fn(),
-  isAllSet: jest.fn()
+  get: jest.fn()
 }));
 
 jest.mock('../../../../api/components', () => ({
@@ -40,13 +39,11 @@ import DefaultPageSelector from '../DefaultPageSelector';
 import { CurrentUser } from '../../../../app/types';
 import { doAsync } from '../../../../helpers/testUtils';
 
-const isFavoriteSet = require('../../../../helpers/storage').isFavoriteSet as jest.Mock<any>;
-const isAllSet = require('../../../../helpers/storage').isAllSet as jest.Mock<any>;
+const get = require('../../../../helpers/storage').get as jest.Mock<any>;
 const searchProjects = require('../../../../api/components').searchProjects as jest.Mock<any>;
 
 beforeEach(() => {
-  isFavoriteSet.mockImplementation(() => false).mockClear();
-  isAllSet.mockImplementation(() => false).mockClear();
+  get.mockImplementation(() => '').mockClear();
 });
 
 it('shows all projects with existing filter', () => {
@@ -62,14 +59,14 @@ it('shows all projects sorted by analysis date for anonymous', () => {
 });
 
 it('shows favorite projects', () => {
-  isFavoriteSet.mockImplementation(() => true);
+  get.mockImplementation(() => 'favorite');
   const replace = jest.fn();
   mountRender(undefined, undefined, replace);
   expect(replace).lastCalledWith({ pathname: '/projects/favorite', query: {} });
 });
 
 it('shows all projects', () => {
-  isAllSet.mockImplementation(() => true);
+  get.mockImplementation(() => 'all');
   const replace = jest.fn();
   mountRender(undefined, undefined, replace);
   expect(replace).not.toBeCalled();
index 4565f21b769a61c9c989e997616c638d1bb1627f..069e87572f6785d739e8da754915e52d1ebb2d9b 100644 (file)
  */
 /* eslint-disable import/first */
 jest.mock('../../../../helpers/storage', () => ({
-  saveAll: jest.fn(),
-  saveFavorite: jest.fn()
+  save: jest.fn()
 }));
 
 import * as React from 'react';
 import { shallow } from 'enzyme';
 import FavoriteFilter from '../FavoriteFilter';
-import { saveAll, saveFavorite } from '../../../../helpers/storage';
+import { save } from '../../../../helpers/storage';
 import { click } from '../../../../helpers/testUtils';
 
 const currentUser = { isLoggedIn: true };
 const query = { size: 1 };
 
 beforeEach(() => {
-  (saveAll as jest.Mock<any>).mockClear();
-  (saveFavorite as jest.Mock<any>).mockClear();
+  (save as jest.Mock<any>).mockClear();
 });
 
 it('renders for logged in user', () => {
@@ -44,9 +42,9 @@ it('renders for logged in user', () => {
 it('saves last selection', () => {
   const wrapper = shallow(<FavoriteFilter currentUser={currentUser} query={query} />);
   click(wrapper.find('#favorite-projects'));
-  expect(saveFavorite).toBeCalled();
+  expect(save).toBeCalledWith('sonarqube.projects.default', 'favorite');
   click(wrapper.find('#all-projects'));
-  expect(saveAll).toBeCalled();
+  expect(save).toBeCalledWith('sonarqube.projects.default', 'all');
 });
 
 it('handles organization', () => {
@@ -62,9 +60,9 @@ it('does not save last selection with organization', () => {
     <FavoriteFilter currentUser={currentUser} organization={{ key: 'org' }} query={query} />
   );
   click(wrapper.find('#favorite-projects'));
-  expect(saveFavorite).not.toBeCalled();
+  expect(save).not.toBeCalled();
   click(wrapper.find('#all-projects'));
-  expect(saveAll).not.toBeCalled();
+  expect(save).not.toBeCalled();
 });
 
 it('does not render for anonymous', () => {
index 45b50868b80df1cdaddd34515519dd8d3ba73242..873d933ce071c4b58d691a0127956af85713ac90 100644 (file)
 import { RouterState, RedirectFunction } from 'react-router';
 import DefaultPageSelectorContainer from './components/DefaultPageSelectorContainer';
 import FavoriteProjectsContainer from './components/FavoriteProjectsContainer';
-import { saveAll } from '../../helpers/storage';
+import { PROJECTS_DEFAULT_FILTER, PROJECTS_ALL } from './utils';
+import { save } from '../../helpers/storage';
 
 const routes = [
   { indexRoute: { component: DefaultPageSelectorContainer } },
   {
     path: 'all',
     onEnter(_: RouterState, replace: RedirectFunction) {
-      saveAll();
+      save(PROJECTS_DEFAULT_FILTER, PROJECTS_ALL);
       replace('/projects');
     }
   },
index b59516613d6c4ad892a804236aa282090ba621a6..419ef569f9f49e7ddfbd2daffc7be58c1da0c31b 100644 (file)
@@ -31,6 +31,10 @@ interface SortingOption {
   value: string;
 }
 
+export const PROJECTS_DEFAULT_FILTER = 'sonarqube.projects.default';
+export const PROJECTS_FAVORITE = 'favorite';
+export const PROJECTS_ALL = 'all';
+
 export const SORTING_METRICS: SortingOption[] = [
   { value: 'name' },
   { value: 'analysis_date' },
index ff32a9df3c69f44137dfd9d775db69c591070a40..6c1c233c93f29652332a0975c361f5aac2325677 100644 (file)
@@ -30,9 +30,11 @@ import {
   generateSeries,
   getSeriesMetricType,
   hasHistoryDataValue,
+  PROJECT_ACTIVITY_GRAPH,
+  PROJECT_ACTIVITY_GRAPH_CUSTOM,
   splitSeriesInGraphs
 } from '../../apps/projectActivity/utils';
-import { getCustomGraph, getGraph } from '../../helpers/storage';
+import { get } from '../../helpers/storage';
 import { formatMeasure, getShortType } from '../../helpers/measures';
 import { getBranchLikeQuery } from '../../helpers/branches';
 /*:: import type { Serie } from '../charts/AdvancedTimeline'; */
@@ -73,8 +75,9 @@ export default class PreviewGraph extends React.PureComponent {
 
   constructor(props /*: Props */) {
     super(props);
-    const graph = getGraph();
-    const customMetrics = getCustomGraph();
+    const customGraphs = get(PROJECT_ACTIVITY_GRAPH_CUSTOM);
+    const graph = get(PROJECT_ACTIVITY_GRAPH) || 'issues';
+    const customMetrics = customGraphs ? customGraphs.split(',') : [];
     const series = splitSeriesInGraphs(
       this.getSeries(props.history, graph, customMetrics, props.metrics),
       MAX_GRAPH_NB,
@@ -92,8 +95,9 @@ export default class PreviewGraph extends React.PureComponent {
 
   componentWillReceiveProps(nextProps /*: Props */) {
     if (nextProps.history !== this.props.history || nextProps.metrics !== this.props.metrics) {
-      const graph = getGraph();
-      const customMetrics = getCustomGraph();
+      const customGraphs = get(PROJECT_ACTIVITY_GRAPH_CUSTOM);
+      const graph = get(PROJECT_ACTIVITY_GRAPH) || 'issues';
+      const customMetrics = customGraphs ? customGraphs.split(',') : [];
       const series = splitSeriesInGraphs(
         this.getSeries(nextProps.history, graph, customMetrics, nextProps.metrics),
         MAX_GRAPH_NB,
index a5dcce0d75b9da4ce0cc7ab13d8d365bcaeea0b8..65950494d588cda5f0b4d8afa6671e68c74c0bc6 100644 (file)
@@ -23,9 +23,11 @@ import { omit, uniqBy } from 'lodash';
 import { WorkspaceContext, ComponentDescriptor, RuleDescriptor } from './context';
 import WorkspaceNav from './WorkspaceNav';
 import WorkspacePortal from './WorkspacePortal';
+import { get, save } from '../../helpers/storage';
 import { lazyLoad } from '../lazyLoad';
 import './styles.css';
 
+const WORKSPACE = 'sonarqube-workspace';
 const WorkspaceRuleViewer = lazyLoad(() => import('./WorkspaceRuleViewer'));
 const WorkspaceComponentViewer = lazyLoad(() => import('./WorkspaceComponentViewer'));
 
@@ -41,7 +43,6 @@ const MIN_HEIGHT = 0.05;
 const MAX_HEIGHT = 0.85;
 const INITIAL_HEIGHT = 300;
 
-const STORAGE_KEY = 'sonarqube-workspace';
 const TYPE_KEY = '__type__';
 
 export default class Workspace extends React.PureComponent<{}, State> {
@@ -76,7 +77,7 @@ export default class Workspace extends React.PureComponent<{}, State> {
 
   loadWorkspace = () => {
     try {
-      const data: any[] = JSON.parse(window.localStorage.getItem(STORAGE_KEY) || '');
+      const data: any[] = JSON.parse(get(WORKSPACE) || '');
       const components: ComponentDescriptor[] = data.filter(x => x[TYPE_KEY] === 'component');
       const rules: RuleDescriptor[] = data.filter(x => x[TYPE_KEY] === 'rule');
       return { components, rules };
@@ -92,11 +93,7 @@ export default class Workspace extends React.PureComponent<{}, State> {
       ...this.state.components.map(x => omit({ ...x, [TYPE_KEY]: 'component' }, 'line')),
       ...this.state.rules.map(x => ({ ...x, [TYPE_KEY]: 'rule' }))
     ];
-    try {
-      window.localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
-    } catch {
-      // fail silently
-    }
+    save(WORKSPACE, JSON.stringify(data));
   };
 
   openComponent = (component: ComponentDescriptor) => {
index 28ea4d6c93c29ef878d3c2f32d446398ba8d7831..19014bcb8b052b1341898e0249b4e6ffefa1e928 100644 (file)
  */
 import { getJSON } from './request';
 import { toNotSoISOString } from './dates';
+import { save, get } from './storage';
+
+const L10_TIMESTAMP = 'l10n.timestamp';
+const L10_LOCALE = 'l10n.locale';
+const L10_BUNDLE = 'l10n.bundle';
 
 interface LanguageBundle {
   [name: string]: string;
@@ -77,7 +82,7 @@ function getPreferredLanguage(): string | undefined {
 }
 
 function checkCachedBundle(): boolean {
-  const cached = localStorage.getItem('l10n.bundle');
+  const cached = get(L10_BUNDLE);
 
   if (!cached) {
     return false;
@@ -98,14 +103,14 @@ function getL10nBundle(params: BundleRequestParams): Promise<BundleRequestRespon
 
 export function requestMessages(): Promise<string> {
   const browserLocale = getPreferredLanguage();
-  const cachedLocale = localStorage.getItem('l10n.locale');
+  const cachedLocale = get(L10_LOCALE);
   const params: BundleRequestParams = {};
 
   if (browserLocale) {
     params.locale = browserLocale;
 
     if (cachedLocale && browserLocale.startsWith(cachedLocale)) {
-      const bundleTimestamp = localStorage.getItem('l10n.timestamp');
+      const bundleTimestamp = get(L10_TIMESTAMP);
       if (bundleTimestamp !== null && checkCachedBundle()) {
         params.ts = bundleTimestamp;
       }
@@ -114,20 +119,16 @@ export function requestMessages(): Promise<string> {
 
   return getL10nBundle(params).then(
     ({ effectiveLocale, messages }: BundleRequestResponse) => {
-      try {
-        const currentTimestamp = toNotSoISOString(new Date());
-        localStorage.setItem('l10n.timestamp', currentTimestamp);
-        localStorage.setItem('l10n.locale', effectiveLocale);
-        localStorage.setItem('l10n.bundle', JSON.stringify(messages));
-      } catch (e) {
-        // do nothing
-      }
+      const currentTimestamp = toNotSoISOString(new Date());
+      save(L10_TIMESTAMP, currentTimestamp);
+      save(L10_LOCALE, effectiveLocale);
+      save(L10_BUNDLE, JSON.stringify(messages));
       resetBundle(messages);
       return effectiveLocale;
     },
     ({ response }) => {
       if (response && response.status === 304) {
-        resetBundle(JSON.parse(localStorage.getItem('l10n.bundle') || '{}'));
+        resetBundle(JSON.parse(get(L10_BUNDLE) || '{}') as LanguageBundle);
       } else {
         throw new Error('Unexpected status code: ' + response.status);
       }
@@ -180,11 +181,7 @@ export function getLocalizedMetricDomain(domainName: string) {
 }
 
 export function getCurrentLocale() {
-  // check `window && window.localStorage` for tests
-  return (
-    (window && window.localStorage && window.localStorage.getItem('l10n.locale')) ||
-    DEFAULT_LANGUAGE
-  );
+  return get(L10_LOCALE) || DEFAULT_LANGUAGE;
 }
 
 export function getShortMonthName(index: number) {
index b533d353735cf05e980a70e900664edc8eb2c751..d4cb608285649fcec8035a8839080a71fdc422dd 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-const PROJECTS_DEFAULT_FILTER = 'sonarqube.projects.default';
-const PROJECTS_FAVORITE = 'favorite';
-const PROJECTS_ALL = 'all';
 
-const PROJECTS_VIEW = 'sonarqube.projects.view';
-const PROJECTS_VISUALIZATION = 'sonarqube.projects.visualization';
-const PROJECTS_SORT = 'sonarqube.projects.sort';
-
-const PROJECT_ACTIVITY_GRAPH = 'sonarqube.project_activity.graph';
-const PROJECT_ACTIVITY_GRAPH_CUSTOM = 'sonarqube.project_activity.graph.custom';
-
-function save(key: string, value?: string, suffix?: string): void {
+export function save(key: string, value?: string, suffix?: string): void {
   try {
     const finalKey = suffix ? `${key}.${suffix}` : key;
     if (value) {
@@ -42,65 +32,18 @@ function save(key: string, value?: string, suffix?: string): void {
   }
 }
 
-function get(key: string, suffix?: string): string | null {
-  return window.localStorage.getItem(suffix ? `${key}.${suffix}` : key);
-}
-
-export function saveFavorite(): void {
-  save(PROJECTS_DEFAULT_FILTER, PROJECTS_FAVORITE);
-}
-
-export function isFavoriteSet(): boolean {
-  const setting = get(PROJECTS_DEFAULT_FILTER);
-  return setting === PROJECTS_FAVORITE;
-}
-
-export function saveAll(): void {
-  save(PROJECTS_DEFAULT_FILTER, PROJECTS_ALL);
-}
-
-export function isAllSet(): boolean {
-  const setting = get(PROJECTS_DEFAULT_FILTER);
-  return setting === PROJECTS_ALL;
-}
-
-export function saveView(view?: string, suffix?: string): void {
-  save(PROJECTS_VIEW, view, suffix);
-}
-
-export function getView(suffix?: string): string | null {
-  return get(PROJECTS_VIEW, suffix);
-}
-
-export function saveVisualization(visualization?: string, suffix?: string): void {
-  save(PROJECTS_VISUALIZATION, visualization, suffix);
-}
-
-export function getVisualization(suffix?: string): string | null {
-  return get(PROJECTS_VISUALIZATION, suffix);
-}
-
-export function saveSort(sort?: string, suffix?: string): void {
-  save(PROJECTS_SORT, sort, suffix);
-}
-
-export function getSort(suffix?: string): string | null {
-  return get(PROJECTS_SORT, suffix);
-}
-
-export function saveCustomGraph(metrics?: string[]): void {
-  save(PROJECT_ACTIVITY_GRAPH_CUSTOM, metrics ? metrics.join(',') : '');
-}
-
-export function getCustomGraph(): string[] {
-  const customGraphs = get(PROJECT_ACTIVITY_GRAPH_CUSTOM);
-  return customGraphs ? customGraphs.split(',') : [];
-}
-
-export function saveGraph(graph?: string): void {
-  save(PROJECT_ACTIVITY_GRAPH, graph);
+export function remove(key: string, suffix?: string): void {
+  try {
+    window.localStorage.removeItem(suffix ? `${key}.${suffix}` : key);
+  } catch {
+    // Fail silently
+  }
 }
 
-export function getGraph(): string {
-  return get(PROJECT_ACTIVITY_GRAPH) || 'issues';
+export function get(key: string, suffix?: string): string | null {
+  try {
+    return window.localStorage.getItem(suffix ? `${key}.${suffix}` : key);
+  } catch {
+    return null;
+  }
 }