From ee0cd4ea0df3e71d1fceced8255ce5a01c98cc4e Mon Sep 17 00:00:00 2001 From: Pascal Mugnier Date: Mon, 16 Apr 2018 08:29:08 +0200 Subject: [PATCH] SONAR-10580 Local storage references should not use window.localStorage (#145) --- .../main/js/app/components/RecentHistory.js | 26 +++--- .../src/main/js/apps/issues/utils.ts | 17 +--- .../apps/overview/components/OverviewApp.tsx | 15 +++- .../js/apps/portfolio/components/Activity.tsx | 15 +++- .../components/__tests__/Activity-test.tsx | 3 +- .../components/ProjectActivityAppContainer.js | 17 ++-- .../components/ProjectActivityGraphs.js | 15 ++-- .../src/main/js/apps/projectActivity/utils.js | 3 + .../apps/projects/components/AllProjects.tsx | 26 +++--- .../components/DefaultPageSelector.tsx | 17 +++- .../projects/components/FavoriteFilter.tsx | 23 ++--- .../components/__tests__/AllProjects-test.tsx | 40 ++++----- .../__tests__/DefaultPageSelector-test.tsx | 13 ++- .../__tests__/FavoriteFilter-test.tsx | 16 ++-- .../src/main/js/apps/projects/routes.ts | 5 +- .../src/main/js/apps/projects/utils.ts | 4 + .../components/preview-graph/PreviewGraph.js | 14 ++-- .../js/components/workspace/Workspace.tsx | 11 +-- server/sonar-web/src/main/js/helpers/l10n.ts | 31 ++++--- .../sonar-web/src/main/js/helpers/storage.ts | 83 +++---------------- 20 files changed, 180 insertions(+), 214 deletions(-) diff --git a/server/sonar-web/src/main/js/app/components/RecentHistory.js b/server/sonar-web/src/main/js/app/components/RecentHistory.js index 6015e4d7576..0967b38c07a 100644 --- a/server/sonar-web/src/main/js/app/components/RecentHistory.js +++ b/server/sonar-web/src/main/js/app/components/RecentHistory.js @@ -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( diff --git a/server/sonar-web/src/main/js/apps/issues/utils.ts b/server/sonar-web/src/main/js/apps/issues/utils.ts index 0423bc1ee64..82df6728898 100644 --- a/server/sonar-web/src/main/js/apps/issues/utils.ts +++ b/server/sonar-web/src/main/js/apps/issues/utils.ts @@ -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, diff --git a/server/sonar-web/src/main/js/apps/overview/components/OverviewApp.tsx b/server/sonar-web/src/main/js/apps/overview/components/OverviewApp.tsx index 091826a44a6..4a0fce06905 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/OverviewApp.tsx +++ b/server/sonar-web/src/main/js/apps/overview/components/OverviewApp.tsx @@ -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 { 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, []); } diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/Activity.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/Activity.tsx index 01d14bb1807..d022f92f0fb 100644 --- a/server/sonar-web/src/main/js/apps/portfolio/components/Activity.tsx +++ b/server/sonar-web/src/main/js/apps/portfolio/components/Activity.tsx @@ -18,13 +18,18 @@ * 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 { 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, []); } diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/Activity-test.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/Activity-test.tsx index 0b5fb1f1584..fadb3f26884 100644 --- a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/Activity-test.tsx +++ b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/Activity-test.tsx @@ -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', () => ({ diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppContainer.js b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppContainer.js index 54aa9929ecc..49c5efe09f3 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppContainer.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppContainer.js @@ -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} diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphs.js b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphs.js index 43057590919..e390d843f89 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphs.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphs.js @@ -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} diff --git a/server/sonar-web/src/main/js/apps/projectActivity/utils.js b/server/sonar-web/src/main/js/apps/projectActivity/utils.js index ad2e53d848a..bb09445e470 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/utils.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/utils.js @@ -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); diff --git a/server/sonar-web/src/main/js/apps/projects/components/AllProjects.tsx b/server/sonar-web/src/main/js/apps/projects/components/AllProjects.tsx index dc9836331d8..cd9547a11b4 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/AllProjects.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/AllProjects.tsx @@ -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 { mounted = false; @@ -159,14 +163,14 @@ export default class AllProjects extends React.PureComponent { 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 { 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) { diff --git a/server/sonar-web/src/main/js/apps/projects/components/DefaultPageSelector.tsx b/server/sonar-web/src/main/js/apps/projects/components/DefaultPageSelector.tsx index ea4fbe1a357..07119e134ed 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/DefaultPageSelector.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/DefaultPageSelector.tsx @@ -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 { + 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 { 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 {
+ className="button" + id="favorite-projects" + onClick={this.handleSaveFavorite} + to={{ pathname: pathnameForFavorite, query: this.props.query }}> {translate('my_favorites')} + className="button" + id="all-projects" + onClick={this.handleSaveAll} + to={{ pathname: pathnameForAll, query: this.props.query }}> {translate('all')}
diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/AllProjects-test.tsx b/server/sonar-web/src/main/js/apps/projects/components/__tests__/AllProjects-test.tsx index cc642e972f4..e3fac302fb4 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/AllProjects-test.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/AllProjects-test.tsx @@ -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; beforeEach(() => { - (getView as jest.Mock).mockImplementation(() => null); - (saveSort as jest.Mock).mockClear(); - (saveView as jest.Mock).mockClear(); - (saveVisualization as jest.Mock).mockClear(); + (get as jest.Mock).mockImplementation(() => null); + (save as jest.Mock).mockClear(); fetchProjects.mockClear(); }); @@ -106,7 +100,9 @@ it('fetches projects', () => { }); it('redirects to the saved search', () => { - (getView as jest.Mock).mockImplementation(() => 'leak'); + (get as jest.Mock).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('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()) { diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/DefaultPageSelector-test.tsx b/server/sonar-web/src/main/js/apps/projects/components/__tests__/DefaultPageSelector-test.tsx index 18bdfd94ba0..f840055a266 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/DefaultPageSelector-test.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/DefaultPageSelector-test.tsx @@ -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; -const isAllSet = require('../../../../helpers/storage').isAllSet as jest.Mock; +const get = require('../../../../helpers/storage').get as jest.Mock; const searchProjects = require('../../../../api/components').searchProjects as jest.Mock; 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(); diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/FavoriteFilter-test.tsx b/server/sonar-web/src/main/js/apps/projects/components/__tests__/FavoriteFilter-test.tsx index 4565f21b769..069e87572f6 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/FavoriteFilter-test.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/FavoriteFilter-test.tsx @@ -19,22 +19,20 @@ */ /* 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).mockClear(); - (saveFavorite as jest.Mock).mockClear(); + (save as jest.Mock).mockClear(); }); it('renders for logged in user', () => { @@ -44,9 +42,9 @@ it('renders for logged in user', () => { it('saves last selection', () => { const wrapper = shallow(); 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', () => { ); 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', () => { diff --git a/server/sonar-web/src/main/js/apps/projects/routes.ts b/server/sonar-web/src/main/js/apps/projects/routes.ts index 45b50868b80..873d933ce07 100644 --- a/server/sonar-web/src/main/js/apps/projects/routes.ts +++ b/server/sonar-web/src/main/js/apps/projects/routes.ts @@ -20,14 +20,15 @@ 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'); } }, diff --git a/server/sonar-web/src/main/js/apps/projects/utils.ts b/server/sonar-web/src/main/js/apps/projects/utils.ts index b59516613d6..419ef569f9f 100644 --- a/server/sonar-web/src/main/js/apps/projects/utils.ts +++ b/server/sonar-web/src/main/js/apps/projects/utils.ts @@ -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' }, diff --git a/server/sonar-web/src/main/js/components/preview-graph/PreviewGraph.js b/server/sonar-web/src/main/js/components/preview-graph/PreviewGraph.js index ff32a9df3c6..6c1c233c93f 100644 --- a/server/sonar-web/src/main/js/components/preview-graph/PreviewGraph.js +++ b/server/sonar-web/src/main/js/components/preview-graph/PreviewGraph.js @@ -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, diff --git a/server/sonar-web/src/main/js/components/workspace/Workspace.tsx b/server/sonar-web/src/main/js/components/workspace/Workspace.tsx index a5dcce0d75b..65950494d58 100644 --- a/server/sonar-web/src/main/js/components/workspace/Workspace.tsx +++ b/server/sonar-web/src/main/js/components/workspace/Workspace.tsx @@ -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) => { diff --git a/server/sonar-web/src/main/js/helpers/l10n.ts b/server/sonar-web/src/main/js/helpers/l10n.ts index 28ea4d6c93c..19014bcb8b0 100644 --- a/server/sonar-web/src/main/js/helpers/l10n.ts +++ b/server/sonar-web/src/main/js/helpers/l10n.ts @@ -19,6 +19,11 @@ */ 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 { 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 { 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) { diff --git a/server/sonar-web/src/main/js/helpers/storage.ts b/server/sonar-web/src/main/js/helpers/storage.ts index b533d353735..d4cb6082856 100644 --- a/server/sonar-web/src/main/js/helpers/storage.ts +++ b/server/sonar-web/src/main/js/helpers/storage.ts @@ -17,18 +17,8 @@ * 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; + } } -- 2.39.5